From a1b8ef006c5d57816fb66ee7581d510e91f7dd98 Mon Sep 17 00:00:00 2001
From: Patrick Kollitsch <davidsneighbourdev+gh@gmail.com>
Date: Thu, 16 Apr 2026 22:37:39 +0000
Subject: [PATCH] fix: remove environments, add default mounts
---
/dev/null | 11
scripts/test-hugo-quickstart.ts | 617 +++++++++++++++++++++++++++++++++++++++++++++++++++
package-lock.json | 15
config/_default/module.toml | 26 ++
tsconfig.json | 44 +++
package.json | 6
6 files changed, 698 insertions(+), 21 deletions(-)
diff --git a/config/_default/module.toml b/config/_default/module.toml
index 8caba23..05b1329 100644
--- a/config/_default/module.toml
+++ b/config/_default/module.toml
@@ -7,4 +7,28 @@
[[mounts]]
source = 'assets'
-target = 'assets'
\ No newline at end of file
+target = 'assets'
+
+[[mounts]]
+source = 'archetypes'
+target = 'archetypes'
+
+[[mounts]]
+source = 'content'
+target = 'content'
+
+[[mounts]]
+source = 'data'
+target = 'data'
+
+[[mounts]]
+source = 'i18n'
+target = 'i18n'
+
+[[mounts]]
+source = 'layouts'
+target = 'layouts'
+
+[[mounts]]
+source = 'static'
+target = 'static'
\ No newline at end of file
diff --git a/config/documentation/hugo.toml b/config/documentation/hugo.toml
deleted file mode 100644
index 6d4b741..0000000
--- a/config/documentation/hugo.toml
+++ /dev/null
@@ -1,5 +0,0 @@
-baseURL = "https://ananke-documentation.netlify.app/"
-languageCode = "en-us"
-title = "Ananke Documentation"
-contentDir = "../docs"
-enableRobotsTXT = true
diff --git a/config/documentation/languages.toml b/config/documentation/languages.toml
deleted file mode 100644
index 063b091..0000000
--- a/config/documentation/languages.toml
+++ /dev/null
@@ -1,4 +0,0 @@
-[en]
-contentDir = 'docs/en'
-label = 'English'
-weight = 10
\ No newline at end of file
diff --git a/config/documentation/markup.toml b/config/documentation/markup.toml
deleted file mode 100644
index 51938db..0000000
--- a/config/documentation/markup.toml
+++ /dev/null
@@ -1,7 +0,0 @@
-[highlight]
-anchorLineNos = true
-codeFences = true
-noClasses = false
-
-[goldmark.renderer]
-unsafe = true # meh, no like, but required for contributors list
\ No newline at end of file
diff --git a/config/documentation/module.toml b/config/documentation/module.toml
deleted file mode 100644
index 22e8883..0000000
--- a/config/documentation/module.toml
+++ /dev/null
@@ -1,17 +0,0 @@
-[hugoVersion]
-min = "0.160.0"
-# see below
-replacements = ["github.com/theNewDynamic/gohugo-theme-ananke/v2 -> ../../"]
-
-[[mounts]]
-source = 'node_modules'
-target = 'assets'
-
-[[mounts]]
-source = 'assets'
-target = 'assets'
-
-[[imports]]
-# @todo I have the feeling that this is not required when we run the site inside
-# of the theme itself. Test and remove if not required.
-path = "github.com/theNewDynamic/gohugo-theme-ananke/v2"
diff --git a/config/documentation/params.toml b/config/documentation/params.toml
deleted file mode 100644
index 2e29e60..0000000
--- a/config/documentation/params.toml
+++ /dev/null
@@ -1,11 +0,0 @@
-description = "Documentation for the GoHugo Ananke theme."
-custom_css = ["fixes.css", "highlighting.css", "fonts.css"]
-
-[ananke]
-show_recent_posts = false
-
-[ananke.home]
-content_alignment = "left" # options: left, center, right
-
-[ananke.pages]
-show_date = false
diff --git a/package-lock.json b/package-lock.json
index 24456f3..83d1068 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -19,6 +19,7 @@
"@davidsneighbour/release-config": "2026.0.4",
"@davidsneighbour/tools": "2026.0.4",
"@github/markdownlint-github": "0.8.0",
+ "@types/node": "25.6.0",
"dotenv": "17.4.2",
"markdownlint": "0.40.0",
"markdownlint-cli2": "0.22.0",
@@ -2264,13 +2265,13 @@
"license": "MIT"
},
"node_modules/@types/node": {
- "version": "25.5.2",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.2.tgz",
- "integrity": "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==",
+ "version": "25.6.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz",
+ "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "undici-types": "~7.18.0"
+ "undici-types": "~7.19.0"
}
},
"node_modules/@types/normalize-package-data": {
@@ -10156,9 +10157,9 @@
"license": "MIT"
},
"node_modules/undici-types": {
- "version": "7.18.2",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
- "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
+ "version": "7.19.2",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz",
+ "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
"dev": true,
"license": "MIT"
},
diff --git a/package.json b/package.json
index 2dfe17a..1539087 100644
--- a/package.json
+++ b/package.json
@@ -35,6 +35,7 @@
"@davidsneighbour/release-config": "2026.0.4",
"@davidsneighbour/tools": "2026.0.4",
"@github/markdownlint-github": "0.8.0",
+ "@types/node": "25.6.0",
"dotenv": "17.4.2",
"markdownlint": "0.40.0",
"markdownlint-cli2": "0.22.0",
@@ -51,7 +52,8 @@
"release": "wireit",
"release:pre": "wireit",
"server": "wireit",
- "update:docs": "wireit"
+ "update:docs": "wireit",
+ "test:quickstart": "node scripts/test-hugo-quickstart.ts"
},
"wireit": {
"release": {
@@ -76,4 +78,4 @@
"node_modules"
]
}
-}
\ No newline at end of file
+}
diff --git a/scripts/test-hugo-quickstart.ts b/scripts/test-hugo-quickstart.ts
new file mode 100644
index 0000000..03ea8bc
--- /dev/null
+++ b/scripts/test-hugo-quickstart.ts
@@ -0,0 +1,617 @@
+#!/usr/bin/env node
+
+import { spawn } from "node:child_process";
+import { mkdtemp, readFile, rm, access } from "node:fs/promises";
+import { constants } from "node:fs";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+
+interface CommandResult {
+ code: number | null;
+ signal: NodeJS.Signals | null;
+ stdout: string;
+ stderr: string;
+ combined: string;
+ durationMs: number;
+}
+
+interface StepDefinition {
+ name: string;
+ command: string;
+ args: string[];
+ cwd: string;
+ expectedFiles?: string[];
+}
+
+interface RoutineOptions {
+ projectName: string;
+ themeRepo: string;
+ themeDir: string;
+ themeName: string;
+ configFile: string;
+ keepOnSuccess: boolean;
+ keepOnFailure: boolean;
+ runContentSmokeTest: boolean;
+ verbose: boolean;
+}
+
+interface StepReport {
+ step: string;
+ commandLine: string;
+ cwd: string;
+ result: CommandResult;
+}
+
+interface HtmlAssertion {
+ description: string;
+ test: (html: string) => boolean;
+}
+
+const DEFAULT_OPTIONS: RoutineOptions = {
+ projectName: "quickstart",
+ themeRepo: "https://github.com/theNewDynamic/gohugo-theme-ananke.git",
+ themeDir: "themes/ananke",
+ themeName: "ananke",
+ configFile: "hugo.toml",
+ keepOnSuccess: false,
+ keepOnFailure: true,
+ runContentSmokeTest: true,
+ verbose: true,
+};
+
+/**
+ * Print CLI help.
+ */
+function printHelp(): void {
+ console.log(`
+Usage:
+ node scripts/test-hugo-quickstart.ts [options]
+
+Options:
+ --project-name=<name> Hugo project folder name inside the temp directory
+ --theme-repo=<url> Git URL for the theme submodule
+ --theme-dir=<path> Theme target directory inside the project
+ --theme-name=<name> Theme name written into hugo.toml
+ --config-file=<file> Hugo config file to update
+ --keep-on-success Do not delete the temp directory when the test passes
+ --no-keep-on-failure Delete the temp directory when the test fails
+ --no-content-smoke-test Skip "hugo new foo.md"
+ --quiet Reduce step logging
+ --help Show this help
+
+Examples:
+ node scripts/test-hugo-quickstart.ts
+ node scripts/test-hugo-quickstart.ts --keep-on-success
+ node scripts/test-hugo-quickstart.ts --project-name=quickstart-test
+`.trim());
+}
+
+/**
+ * Parse CLI arguments into routine options.
+ *
+ * @param argv Raw CLI arguments after the executable and script path.
+ * @returns Parsed routine options.
+ * @throws Error when an unknown argument is passed.
+ */
+function parseArgs(argv: string[]): RoutineOptions {
+ const options: RoutineOptions = { ...DEFAULT_OPTIONS };
+
+ for (const arg of argv) {
+ if (arg === "--help") {
+ printHelp();
+ process.exit(0);
+ }
+
+ if (arg === "--keep-on-success") {
+ options.keepOnSuccess = true;
+ continue;
+ }
+
+ if (arg === "--no-keep-on-failure") {
+ options.keepOnFailure = false;
+ continue;
+ }
+
+ if (arg === "--no-content-smoke-test") {
+ options.runContentSmokeTest = false;
+ continue;
+ }
+
+ if (arg === "--quiet") {
+ options.verbose = false;
+ continue;
+ }
+
+ if (arg.startsWith("--project-name=")) {
+ options.projectName = arg.slice("--project-name=".length);
+ continue;
+ }
+
+ if (arg.startsWith("--theme-repo=")) {
+ options.themeRepo = arg.slice("--theme-repo=".length);
+ continue;
+ }
+
+ if (arg.startsWith("--theme-dir=")) {
+ options.themeDir = arg.slice("--theme-dir=".length);
+ continue;
+ }
+
+ if (arg.startsWith("--theme-name=")) {
+ options.themeName = arg.slice("--theme-name=".length);
+ continue;
+ }
+
+ if (arg.startsWith("--config-file=")) {
+ options.configFile = arg.slice("--config-file=".length);
+ continue;
+ }
+
+ throw new Error(`Unknown argument: ${arg}`);
+ }
+
+ return options;
+}
+
+/**
+ * Format a command for human-readable logging.
+ *
+ * @param command Executable name.
+ * @param args Executable arguments.
+ * @returns Full command line.
+ */
+function formatCommand(command: string, args: string[]): string {
+ return [command, ...args]
+ .map((part) => (/\s/.test(part) ? JSON.stringify(part) : part))
+ .join(" ");
+}
+
+/**
+ * Run a command and capture stdout/stderr.
+ *
+ * @param command Executable name.
+ * @param args Executable arguments.
+ * @param cwd Working directory.
+ * @returns Command execution result.
+ */
+async function runCommand(
+ command: string,
+ args: string[],
+ cwd: string,
+): Promise<CommandResult> {
+ const started = Date.now();
+
+ return new Promise<CommandResult>((resolve, reject) => {
+ const child = spawn(command, args, {
+ cwd,
+ env: process.env,
+ stdio: ["ignore", "pipe", "pipe"],
+ });
+
+ let stdout = "";
+ let stderr = "";
+
+ child.stdout.on("data", (chunk: Buffer | string) => {
+ stdout += chunk.toString();
+ });
+
+ child.stderr.on("data", (chunk: Buffer | string) => {
+ stderr += chunk.toString();
+ });
+
+ child.on("error", (error: Error) => {
+ reject(error);
+ });
+
+ child.on("close", (code, signal) => {
+ const durationMs = Date.now() - started;
+ const combined = [stdout, stderr].filter(Boolean).join(stdout && stderr ? "\n" : "");
+
+ resolve({
+ code,
+ signal,
+ stdout,
+ stderr,
+ combined,
+ durationMs,
+ });
+ });
+ });
+}
+
+/**
+ * Ensure a file exists.
+ *
+ * @param filePath Absolute file path.
+ */
+async function assertFileExists(filePath: string): Promise<void> {
+ await access(filePath, constants.F_OK);
+}
+
+/**
+ * Execute one quick-start step and validate success.
+ *
+ * @param step Step definition.
+ * @returns Step report.
+ * @throws Error when the command fails or an expected file is missing.
+ */
+async function executeStep(step: StepDefinition): Promise<StepReport> {
+ const result = await runCommand(step.command, step.args, step.cwd);
+ const commandLine = formatCommand(step.command, step.args);
+
+ if (result.code !== 0) {
+ const details = [
+ `Step failed: ${step.name}`,
+ `Command: ${commandLine}`,
+ `Working directory: ${step.cwd}`,
+ `Exit code: ${String(result.code)}`,
+ result.signal ? `Signal: ${result.signal}` : "",
+ result.stdout ? `STDOUT:\n${result.stdout}` : "",
+ result.stderr ? `STDERR:\n${result.stderr}` : "",
+ ]
+ .filter(Boolean)
+ .join("\n\n");
+
+ throw new Error(details);
+ }
+
+ if (step.expectedFiles) {
+ for (const relativePath of step.expectedFiles) {
+ const absolutePath = join(step.cwd, relativePath);
+
+ try {
+ await assertFileExists(absolutePath);
+ } catch (error: unknown) {
+ const message =
+ error instanceof Error ? error.message : "Unknown file assertion error";
+
+ throw new Error(
+ [
+ `Step failed: ${step.name}`,
+ `Command: ${commandLine}`,
+ `Working directory: ${step.cwd}`,
+ `Expected file missing: ${absolutePath}`,
+ `Details: ${message}`,
+ result.stdout ? `STDOUT:\n${result.stdout}` : "",
+ result.stderr ? `STDERR:\n${result.stderr}` : "",
+ ]
+ .filter(Boolean)
+ .join("\n\n"),
+ );
+ }
+ }
+ }
+
+ return {
+ step: step.name,
+ commandLine,
+ cwd: step.cwd,
+ result,
+ };
+}
+
+/**
+ * Escape a string for safe use in a regular expression.
+ *
+ * @param value Raw string.
+ * @returns Escaped string.
+ */
+function escapeRegExp(value: string): string {
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+}
+
+/**
+ * Read a text file as UTF-8.
+ *
+ * @param filePath Absolute file path.
+ * @returns File contents.
+ */
+async function readTextFile(filePath: string): Promise<string> {
+ return readFile(filePath, "utf8");
+}
+
+/**
+ * Assert that Hugo config contains the expected theme assignment.
+ *
+ * @param configPath Absolute config path.
+ * @param themeName Expected theme name.
+ * @throws Error when the config does not contain the expected theme.
+ */
+async function assertThemeConfigured(
+ configPath: string,
+ themeName: string,
+): Promise<void> {
+ const config = await readTextFile(configPath);
+ const themePattern = new RegExp(
+ String.raw`^\s*theme\s*=\s*['"]${escapeRegExp(themeName)}['"]\s*$`,
+ "m",
+ );
+
+ if (!themePattern.test(config)) {
+ throw new Error(
+ [
+ "Strict assertion failed: theme configuration missing or incorrect.",
+ `Config file: ${configPath}`,
+ `Expected: theme = '${themeName}'`,
+ "Actual file contents:",
+ config,
+ ].join("\n\n"),
+ );
+ }
+}
+
+/**
+ * Return the strict homepage assertions.
+ *
+ * These are intentionally a mix of generic HTML checks and theme-oriented checks.
+ * The goal is not to pin every byte of output, but to prove that the theme actually
+ * rendered a plausible Ananke homepage.
+ *
+ * @returns List of homepage assertions.
+ */
+function getHomepageAssertions(): HtmlAssertion[] {
+ return [
+ {
+ description: "homepage contains an HTML document root",
+ test: (html: string): boolean => /<html\b/i.test(html),
+ },
+ {
+ description: "homepage contains a document title",
+ test: (html: string): boolean => /<title>[\s\S]*?<\/title>/i.test(html),
+ },
+ {
+ description: "homepage contains a body element",
+ test: (html: string): boolean => /<body\b/i.test(html),
+ },
+ {
+ description: "homepage contains at least one stylesheet reference",
+ test: (html: string): boolean =>
+ /<link\b[^>]*rel=["']stylesheet["'][^>]*>/i.test(html),
+ },
+ {
+ description: "homepage contains at least one navigation-related landmark",
+ test: (html: string): boolean =>
+ /<(nav|header)\b/i.test(html),
+ },
+ {
+ description: "homepage contains theme-generated CSS class markers",
+ test: (html: string): boolean =>
+ /\b(ma[0-9]|pa[0-9]|bg-black|near-white|sans-serif)\b/i.test(html),
+ },
+ {
+ description: "homepage contains a main content area",
+ test: (html: string): boolean =>
+ /<(main|article|section)\b/i.test(html),
+ },
+ ];
+}
+
+/**
+ * Assert that the generated homepage looks like a real themed render and not just
+ * an empty or broken output file.
+ *
+ * @param homepagePath Absolute path to `public/index.html`.
+ * @throws Error when one or more strict assertions fail.
+ */
+async function assertHomepageLooksValid(homepagePath: string): Promise<void> {
+ const html = await readTextFile(homepagePath);
+ const failures: string[] = [];
+
+ if (html.trim().length === 0) {
+ throw new Error(
+ [
+ "Strict assertion failed: generated homepage is empty.",
+ `Homepage file: ${homepagePath}`,
+ ].join("\n\n"),
+ );
+ }
+
+ for (const assertion of getHomepageAssertions()) {
+ if (!assertion.test(html)) {
+ failures.push(`- ${assertion.description}`);
+ }
+ }
+
+ if (failures.length > 0) {
+ throw new Error(
+ [
+ "Strict assertion failed: generated homepage did not match expected render checks.",
+ `Homepage file: ${homepagePath}`,
+ "Failed assertions:",
+ ...failures,
+ ].join("\n"),
+ );
+ }
+}
+
+/**
+ * Assert that generated sample content contains frontmatter and basic metadata.
+ *
+ * @param contentPath Absolute path to the created content file.
+ * @throws Error when the content file does not look like a valid Hugo content file.
+ */
+async function assertGeneratedContentLooksValid(contentPath: string): Promise<void> {
+ const content = await readTextFile(contentPath);
+ const failures: string[] = [];
+
+ if (!/^---\s*$/m.test(content) && !/^\+\+\+\s*$/m.test(content)) {
+ failures.push("- generated content does not appear to contain frontmatter delimiters");
+ }
+
+ if (!/^\s*title\s*:/m.test(content) && !/^\s*title\s*=/m.test(content)) {
+ failures.push("- generated content does not contain a title field");
+ }
+
+ if (!/^\s*date\s*:/m.test(content) && !/^\s*date\s*=/m.test(content)) {
+ failures.push("- generated content does not contain a date field");
+ }
+
+ if (failures.length > 0) {
+ throw new Error(
+ [
+ "Strict assertion failed: generated content file did not match expected Hugo structure.",
+ `Content file: ${contentPath}`,
+ "Failed assertions:",
+ ...failures,
+ "",
+ "Actual file contents:",
+ content,
+ ].join("\n"),
+ );
+ }
+}
+
+/**
+ * Run the Hugo quick-start verification routine.
+ *
+ * @param options Runtime options.
+ * @returns Process exit code.
+ */
+async function runRoutine(options: RoutineOptions): Promise<number> {
+ const sandboxRoot = await mkdtemp(join(tmpdir(), "hugo-quickstart-"));
+ const projectRoot = join(sandboxRoot, options.projectName);
+
+ const reports: StepReport[] = [];
+
+ const steps: StepDefinition[] = [
+ {
+ name: "Create Hugo project",
+ command: "hugo",
+ args: ["new", "project", options.projectName],
+ cwd: sandboxRoot,
+ expectedFiles: [join(options.projectName, options.configFile)],
+ },
+ {
+ name: "Initialise Git repository",
+ command: "git",
+ args: ["init"],
+ cwd: projectRoot,
+ expectedFiles: [".git"],
+ },
+ {
+ name: "Add theme as Git submodule",
+ command: "git",
+ args: ["submodule", "add", options.themeRepo, options.themeDir],
+ cwd: projectRoot,
+ expectedFiles: [options.themeDir, ".gitmodules"],
+ },
+ {
+ name: "Configure theme in Hugo config",
+ command: "bash",
+ args: ["-lc", `printf "\\ntheme = '${options.themeName}'\\n" >> ${JSON.stringify(options.configFile)}`],
+ cwd: projectRoot,
+ expectedFiles: [options.configFile],
+ },
+ {
+ name: "Build site",
+ command: "hugo",
+ args: [],
+ cwd: projectRoot,
+ expectedFiles: ["public/index.html"],
+ },
+ ];
+
+ if (options.runContentSmokeTest) {
+ steps.push({
+ name: "Create sample content",
+ command: "hugo",
+ args: ["new", "foo.md"],
+ cwd: projectRoot,
+ expectedFiles: ["content/foo.md"],
+ });
+ }
+
+ try {
+ console.log(`Test root: ${sandboxRoot}`);
+ console.log(`Project root: ${projectRoot}`);
+
+ for (const step of steps) {
+ if (options.verbose) {
+ console.log(`\n[RUN] ${step.name}`);
+ console.log(` ${formatCommand(step.command, step.args)}`);
+ }
+
+ const report = await executeStep(step);
+ reports.push(report);
+
+ if (options.verbose) {
+ console.log(
+ `[OK ] ${step.name} (${report.result.durationMs} ms, exit ${String(report.result.code)})`,
+ );
+
+ const trimmedOutput = report.result.combined.trim();
+ if (trimmedOutput) {
+ console.log(trimmedOutput);
+ }
+ }
+ }
+
+ const configPath = join(projectRoot, options.configFile);
+ const homepagePath = join(projectRoot, "public/index.html");
+
+ console.log("\n[RUN] Strict config assertion");
+ await assertThemeConfigured(configPath, options.themeName);
+ console.log("[OK ] Strict config assertion");
+
+ console.log("\n[RUN] Strict homepage assertion");
+ await assertHomepageLooksValid(homepagePath);
+ console.log("[OK ] Strict homepage assertion");
+
+ if (options.runContentSmokeTest) {
+ const contentPath = join(projectRoot, "content/foo.md");
+
+ console.log("\n[RUN] Strict generated content assertion");
+ await assertGeneratedContentLooksValid(contentPath);
+ console.log("[OK ] Strict generated content assertion");
+ }
+
+ console.log("\nResult: PASS");
+
+ if (options.keepOnSuccess) {
+ console.log(`Keeping successful test directory: ${projectRoot}`);
+ } else {
+ await rm(sandboxRoot, { recursive: true, force: true });
+ console.log(`Deleted successful test directory: ${sandboxRoot}`);
+ }
+
+ return 0;
+ } catch (error: unknown) {
+ const message = error instanceof Error ? error.message : "Unknown error";
+
+ console.error("\nResult: FAIL");
+ console.error(message);
+
+ if (reports.length > 0) {
+ console.error("\nCompleted command steps before failure:");
+ for (const report of reports) {
+ console.error(`- ${report.step}`);
+ }
+ }
+
+ if (options.keepOnFailure) {
+ console.error(`\nKept failing test directory for inspection: ${projectRoot}`);
+ } else {
+ await rm(sandboxRoot, { recursive: true, force: true });
+ console.error(`\nDeleted failing test directory: ${sandboxRoot}`);
+ }
+
+ return 1;
+ }
+}
+
+/**
+ * Main entry point.
+ */
+async function main(): Promise<void> {
+ try {
+ const options = parseArgs(process.argv.slice(2));
+ const exitCode = await runRoutine(options);
+ process.exit(exitCode);
+ } catch (error: unknown) {
+ const message = error instanceof Error ? error.message : "Unknown fatal error";
+ console.error(`Fatal error: ${message}`);
+ process.exit(1);
+ }
+}
+
+await main();
\ No newline at end of file
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..cec4a3a
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,44 @@
+{
+ // Visit https://aka.ms/tsconfig to read more about this file
+ "compilerOptions": {
+ // File Layout
+ // "rootDir": "./src",
+ // "outDir": "./dist",
+
+ // Environment Settings
+ // See also https://aka.ms/tsconfig/module
+ "module": "nodenext",
+ "target": "esnext",
+ "types": [],
+ // For nodejs:
+ // "lib": ["esnext"],
+ // "types": ["node"],
+ // and npm install -D @types/node
+
+ // Other Outputs
+ "sourceMap": true,
+ "declaration": true,
+ "declarationMap": true,
+
+ // Stricter Typechecking Options
+ "noUncheckedIndexedAccess": true,
+ "exactOptionalPropertyTypes": true,
+
+ // Style Options
+ // "noImplicitReturns": true,
+ // "noImplicitOverride": true,
+ // "noUnusedLocals": true,
+ // "noUnusedParameters": true,
+ // "noFallthroughCasesInSwitch": true,
+ // "noPropertyAccessFromIndexSignature": true,
+
+ // Recommended Options
+ "strict": true,
+ "jsx": "react-jsx",
+ "verbatimModuleSyntax": true,
+ "isolatedModules": true,
+ "noUncheckedSideEffectImports": true,
+ "moduleDetection": "force",
+ "skipLibCheck": true,
+ }
+}
--
Gitblit v1.10.0