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