From c67d0ded7f6370380394b5ae8d7acecb1751435b Mon Sep 17 00:00:00 2001 From: Koji Wakayama Date: Sat, 22 Aug 2026 22:05:07 +0200 Subject: [PATCH 1/8] fix(init): accept an existing directory unless a scaffold file would be overwritten `veryfront init app` refused any existing `app/`, including an empty one or a fresh clone holding only `.git`, with "Directory already exists". Every mainstream scaffolder accepts those, and `mkdir app && veryfront init app` is the first thing many developers type. A conflict is now a file the scaffold would write over, not the directory existing. `createProject` is the single authority: the named path uses the same `findExistingPaths` check the current-directory path already used, and both directory-existence checks in `initCommand` are gone. The refusal names the files and points at `--force`: Directory "app" already contains README.md. Use --force to overwrite. `.gitignore` is merged rather than replaced, so it never conflicts. The interactive wizard now runs before a refusal for a taken name; the message it ends on says exactly which files are in the way. The `vf_create_project` MCP tool keeps its own directory check and message; aligning it is a separate change. Tests: empty directory and unrelated-file cases at the `createProject`, `initCommand`, and subprocess levels; the conflict message for a named directory; existing expectations updated from "already exists" to the file-level message. API reference pins regenerated with CI's Deno. --- cli/commands/init/init-command.test.ts | 2 +- cli/commands/init/init-command.ts | 34 +---------- .../init/init-deploy.integration.test.ts | 6 +- cli/commands/init/init.integration.test.ts | 22 ++++++- cli/shared/project-creation.test.ts | 58 ++++++++++++++++++- cli/shared/project-creation.ts | 23 +++----- docs/api-reference/veryfront/scaffold.md | 12 ++-- 7 files changed, 99 insertions(+), 58 deletions(-) diff --git a/cli/commands/init/init-command.test.ts b/cli/commands/init/init-command.test.ts index 8bfdb8f0c2..70429cbc12 100644 --- a/cli/commands/init/init-command.test.ts +++ b/cli/commands/init/init-command.test.ts @@ -205,7 +205,7 @@ describe("initCommand target directory", () => { quiet: true, }), Error, - `Directory "${name}" already exists`, + `Directory "${name}" already contains README.md`, ); assertEquals(await Deno.readTextFile(keepsake), "keep me\n"); diff --git a/cli/commands/init/init-command.ts b/cli/commands/init/init-command.ts index 683cf60b58..7a0b036ed7 100644 --- a/cli/commands/init/init-command.ts +++ b/cli/commands/init/init-command.ts @@ -6,11 +6,9 @@ import { cliLogger as logger, isVerbose } from "#cli/utils"; import { brand, dim } from "#cli/ui"; import { createTransientSpinner } from "../../ui/progress.ts"; -import { join } from "veryfront/platform/path"; import { createError, toError } from "veryfront/errors"; import type { InitOptions, InitRuntime, InitTemplate } from "./types.ts"; import { cwd } from "veryfront/platform"; -import { createFileSystem } from "veryfront/platform"; import { getDlxCommand, getInstallCommand, getRunCommand } from "../../utils/package-manager.ts"; import { createProject, type ProjectCreationObserver } from "../../shared/project-creation.ts"; import { validateProjectName } from "../../shared/project-name.ts"; @@ -141,22 +139,6 @@ export async function initCommand( } } - // Refuse an existing directory before entering the wizard. This has to be an - // error rather than a printed message: `veryfront init x && cd x` must stop - // here, not carry on into a directory that was never scaffolded. - if (name && !options.force) { - const fs = createFileSystem(); - if (await fs.exists(join(parentDir, name))) { - throw toError( - createError({ - type: "config", - message: - `Directory "${name}" already exists. Choose a different name or use --force to overwrite.`, - }), - ); - } - } - let wizardRuntime: InitRuntime = "node"; if (shouldRunWizard(options)) { const wizardResult = await runInteractiveWizard(name, options.runtime); @@ -174,19 +156,9 @@ export async function initCommand( } const runtime: InitRuntime = options.runtime ?? wizardRuntime; - const projectDir = projectName ? join(parentDir, projectName) : parentDir; - if (projectName && !options.force) { - const fs = createFileSystem(); - if (await fs.exists(projectDir)) { - throw toError( - createError({ - type: "config", - message: - `Directory "${projectName}" already exists. Choose a different name or use --force to overwrite.`, - }), - ); - } - } + // Whether the target can be written to is `createProject`'s call: it knows + // which files the template ships, so it refuses exactly the files it would + // overwrite rather than any directory that happens to exist. let installSpinner: ReturnType | null = null; const installObserver: ProjectCreationObserver = { diff --git a/cli/commands/init/init-deploy.integration.test.ts b/cli/commands/init/init-deploy.integration.test.ts index 0dafd1df5f..dedd1500f0 100644 --- a/cli/commands/init/init-deploy.integration.test.ts +++ b/cli/commands/init/init-deploy.integration.test.ts @@ -98,8 +98,9 @@ describe("init command integration", () => { }); describe("validation", () => { - it("should reject existing directories without --force", async () => { + it("should reject a directory holding files the template writes without --force", async () => { await mkdir(projectDir); + await writeTextFile(join(projectDir, "README.md"), "mine"); const result = await runInitCommand(projectName, [ "-t", @@ -107,7 +108,8 @@ describe("init command integration", () => { "--skip-install", "--skip-env-prompt", ]); - assertEquals(result.code !== 0 || (result.stderr ?? "").includes("already exists"), true); + assertEquals(result.code !== 0, true); + assertEquals((result.stderr ?? "").includes("already contains README.md"), true); }); it("should overwrite with --force flag", async () => { diff --git a/cli/commands/init/init.integration.test.ts b/cli/commands/init/init.integration.test.ts index 1c6fad3b6f..8f7bb00b5b 100644 --- a/cli/commands/init/init.integration.test.ts +++ b/cli/commands/init/init.integration.test.ts @@ -712,17 +712,35 @@ describe("init command integration", () => { }); describe("existing directory", () => { - it("should show error when directory already exists", async () => { + it("should show error when the directory holds files the template writes", async () => { const dirName = `exists-${randomSuffix()}`; const dirPath = join(TEST_DIR, dirName); await Deno.mkdir(dirPath); + await Deno.writeTextFile(join(dirPath, "README.md"), "mine\n"); try { const result = await runInitCommand([dirName, "-t", "minimal", "--skip-install"]); const output = (result.stdout ?? "") + (result.stderr ?? ""); - assertEquals(output.includes("already exists"), true); + assertEquals(result.code === 0, false); + assertEquals(output.includes("already contains README.md"), true); assertEquals(output.includes("Stack trace"), false); + assertEquals(await Deno.readTextFile(join(dirPath, "README.md")), "mine\n"); + } finally { + await remove(dirPath, { recursive: true }).catch(() => {}); + } + }); + + it("should scaffold into an existing empty directory", async () => { + const dirName = `empty-${randomSuffix()}`; + const dirPath = join(TEST_DIR, dirName); + await Deno.mkdir(dirPath); + + try { + const result = await runInitCommand([dirName, "-t", "minimal", "--skip-install"]); + + assertEquals(result.code, 0); + assertEquals(await exists(join(dirPath, "app", "page.tsx")), true); } finally { await remove(dirPath, { recursive: true }).catch(() => {}); } diff --git a/cli/shared/project-creation.test.ts b/cli/shared/project-creation.test.ts index 4259671198..7710dafa77 100644 --- a/cli/shared/project-creation.test.ts +++ b/cli/shared/project-creation.test.ts @@ -104,7 +104,7 @@ describe("createProject", () => { await assertRejects( () => createProject({ ...request, conflictPolicy: "fail" }), Error, - 'Directory "contract-project" already exists', + 'Directory "contract-project" already contains', ); const overwritten = await createProject({ @@ -671,3 +671,59 @@ describe("createProject into the current directory", () => { } }); }); + +describe("createProject into an existing named directory", () => { + it("scaffolds into an existing empty directory", async () => { + const parentDir = await makeTempDir({ prefix: "veryfront-create-empty-named-" }); + + try { + // `mkdir app && veryfront init app`, or a freshly cloned empty repo. + await Deno.mkdir(join(parentDir, "contract-project")); + + const result = await createProject(baseRequest(parentDir)); + + assertEquals(result.projectDir, join(parentDir, "contract-project")); + assertEquals(await exists(join(parentDir, "contract-project", "app", "page.tsx")), true); + } finally { + await remove(parentDir, { recursive: true }).catch(() => {}); + } + }); + + it("scaffolds beside files the template does not write", async () => { + const parentDir = await makeTempDir({ prefix: "veryfront-create-beside-named-" }); + const projectDir = join(parentDir, "contract-project"); + + try { + await Deno.mkdir(join(projectDir, ".git"), { recursive: true }); + await Deno.writeTextFile(join(projectDir, "LICENSE"), "MIT\n"); + + await createProject(baseRequest(parentDir)); + + assertEquals(await exists(join(projectDir, "app", "page.tsx")), true); + assertEquals(await Deno.readTextFile(join(projectDir, "LICENSE")), "MIT\n"); + } finally { + await remove(parentDir, { recursive: true }).catch(() => {}); + } + }); + + it("names the files it would overwrite, not just the directory", async () => { + const parentDir = await makeTempDir({ prefix: "veryfront-create-conflict-named-" }); + const projectDir = join(parentDir, "contract-project"); + + try { + await Deno.mkdir(projectDir); + await Deno.writeTextFile(join(projectDir, "README.md"), "mine\n"); + + await assertRejects( + () => createProject(baseRequest(parentDir)), + Error, + 'Directory "contract-project" already contains README.md', + ); + + assertEquals(await Deno.readTextFile(join(projectDir, "README.md")), "mine\n"); + assertEquals(await exists(join(projectDir, "app")), false); + } finally { + await remove(parentDir, { recursive: true }).catch(() => {}); + } + }); +}); diff --git a/cli/shared/project-creation.ts b/cli/shared/project-creation.ts index b62dff1be6..ccebe7bf47 100644 --- a/cli/shared/project-creation.ts +++ b/cli/shared/project-creation.ts @@ -517,29 +517,22 @@ export async function createProject( const projectDir = projectName === undefined ? request.parentDir : join(request.parentDir, projectName); - const fs = createFileSystem(); validateIntegrationsOrThrow(request.integrations); - if ( - projectName !== undefined && - request.conflictPolicy === "fail" && - await fs.exists(projectDir) - ) { - throw createConfigError(`Directory "${projectName}" already exists`); - } - const assembly = await assembleScaffold(request); - // A named project gets a fresh directory, checked above. Without a name the - // scaffold lands in `parentDir` itself, which always exists, so the conflict - // is any file the scaffold would write over - a `package.json` with the - // author's scripts, a `README.md` - and those are refused the same way. - if (projectName === undefined && request.conflictPolicy === "fail") { + // A conflict is a file the scaffold would write over - a `package.json` with + // the author's scripts, a `README.md` - not the directory existing. So an + // empty directory, a fresh clone holding only `.git`, or the working + // directory itself (the no-name case) all scaffold, and a `--force` is asked + // for only when something would actually be replaced. + if (request.conflictPolicy === "fail") { const conflicts = await findExistingPaths(projectDir, scaffoldWritePaths(assembly, request)); if (conflicts.length) { + const where = projectName === undefined ? "Directory" : `Directory "${projectName}"`; throw createConfigError( - `Directory already contains ${conflicts.join(", ")}. Use --force to overwrite.`, + `${where} already contains ${conflicts.join(", ")}. Use --force to overwrite.`, ); } } diff --git a/docs/api-reference/veryfront/scaffold.md b/docs/api-reference/veryfront/scaffold.md index ae3934603b..8e062f8317 100644 --- a/docs/api-reference/veryfront/scaffold.md +++ b/docs/api-reference/veryfront/scaffold.md @@ -38,20 +38,20 @@ for (const file of files) { | Name | Description | Source | | --------------------------- | --------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -| `SCAFFOLD_TEMPLATE_ALIASES` | Slugs other product surfaces use for a template this CLI names differently. | [source](https://github.com/veryfront/veryfront-code/blob/main/cli/shared/project-creation.ts#L604) | +| `SCAFFOLD_TEMPLATE_ALIASES` | Slugs other product surfaces use for a template this CLI names differently. | [source](https://github.com/veryfront/veryfront-code/blob/main/cli/shared/project-creation.ts#L597) | ### Functions | Name | Description | Source | | ------------------------- | ----------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -| `listScaffoldTemplates` | Every template slug a caller may ask for, canonical names and aliases. | [source](https://github.com/veryfront/veryfront-code/blob/main/cli/shared/project-creation.ts#L619) | -| `materializeScaffold` | Produce the complete contents of a new project without touching a disk. | [source](https://github.com/veryfront/veryfront-code/blob/main/cli/shared/project-creation.ts#L658) | -| `resolveScaffoldTemplate` | Canonical starter template for a slug, or `null` when nothing matches. | [source](https://github.com/veryfront/veryfront-code/blob/main/cli/shared/project-creation.ts#L611) | +| `listScaffoldTemplates` | Every template slug a caller may ask for, canonical names and aliases. | [source](https://github.com/veryfront/veryfront-code/blob/main/cli/shared/project-creation.ts#L612) | +| `materializeScaffold` | Produce the complete contents of a new project without touching a disk. | [source](https://github.com/veryfront/veryfront-code/blob/main/cli/shared/project-creation.ts#L651) | +| `resolveScaffoldTemplate` | Canonical starter template for a slug, or `null` when nothing matches. | [source](https://github.com/veryfront/veryfront-code/blob/main/cli/shared/project-creation.ts#L604) | ### Types | Name | Description | Source | | ---------------------------- | --------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -| `MaterializedScaffold` | A new project: every file it starts with, plus anything worth telling the author. | [source](https://github.com/veryfront/veryfront-code/blob/main/cli/shared/project-creation.ts#L640) | -| `MaterializeScaffoldRequest` | What to build: which starter, under what name, for which runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/cli/shared/project-creation.ts#L624) | +| `MaterializedScaffold` | A new project: every file it starts with, plus anything worth telling the author. | [source](https://github.com/veryfront/veryfront-code/blob/main/cli/shared/project-creation.ts#L633) | +| `MaterializeScaffoldRequest` | What to build: which starter, under what name, for which runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/cli/shared/project-creation.ts#L617) | | `TemplateFile` | | [source](https://github.com/veryfront/veryfront-code/blob/main/templates/types.ts#L17) | From 865e20004caf223f3bafea742dacf4a71aba348a Mon Sep 17 00:00:00 2001 From: Kentaro Wakayama Date: Sun, 23 Aug 2026 01:05:26 +0200 Subject: [PATCH 2/8] fix(init): refuse a file or a link where the scaffold needs a directory Accepting an existing target directory means the scaffold now meets states the old "directory already exists" check never let it reach. One of them wrote outside the project. `findExistingPaths` asks whether `app/page.tsx` exists. When `app` is a regular file, that path cannot resolve, so the check reports no conflict. `writeScaffoldFiles` then writes the root files (`README.md`, `AGENTS.md`) and fails on `ensureDir("app")` with a raw stat error, leaving a half scaffold behind. When `app` is a link to another directory, nothing fails at all: `veryfront init app` exits 0, prints "app ready", and leaves `page.tsx`, `layout.tsx` and `about/page.mdx` in the link target instead of the project you named. `createProject` now checks every directory the scaffold has to create, before it writes anything, and refuses when one is already a file or a link: Directory "app" already contains app as a file or a link, and the scaffold needs a directory there. Move it aside or use a different name. The check runs whatever the conflict policy is. `--force` says you accept your own files being replaced, not the scaffold writing somewhere else. Every segment is checked, not just the first, so a real `app/` with a file at `app/about` is caught before `app/page.tsx` is written. A real directory that is already there is never blocked: it is exactly what the scaffold is about to create. The current-directory path had the same hole, so `cd repo && veryfront init` with a linked `app/` wrote outside the repo too. The check covers both paths because it sits in `createProject`. Tests: a file and a link at a scaffold directory, for the named path, the current-directory path, and under `--force`, plus a block one level down at `app/about`, each asserting nothing was written through or beside it; and an existing real `app/` that must still scaffold. Every refusal test fails without the check. --- cli/shared/project-creation.test.ts | 138 +++++++++++++++++++++++ cli/shared/project-creation.ts | 53 ++++++++- docs/api-reference/veryfront/scaffold.md | 12 +- 3 files changed, 195 insertions(+), 8 deletions(-) diff --git a/cli/shared/project-creation.test.ts b/cli/shared/project-creation.test.ts index 7710dafa77..08b9fd918f 100644 --- a/cli/shared/project-creation.test.ts +++ b/cli/shared/project-creation.test.ts @@ -727,3 +727,141 @@ describe("createProject into an existing named directory", () => { } }); }); + +describe("createProject when something blocks a scaffold directory", () => { + it("refuses a file where the scaffold needs a directory, before writing anything", async () => { + const parentDir = await makeTempDir({ prefix: "veryfront-create-blocked-file-" }); + const projectDir = join(parentDir, "contract-project"); + + try { + await Deno.mkdir(projectDir); + // `app/page.tsx` cannot resolve through a regular `app`, so the conflict + // check sees nothing and the scaffold used to write README.md and + // AGENTS.md before failing on the directory it could not create. + await Deno.writeTextFile(join(projectDir, "app"), "mine\n"); + + await assertRejects( + () => createProject(baseRequest(parentDir)), + Error, + 'Directory "contract-project" already contains app as a file or a link', + ); + + assertEquals(await Deno.readTextFile(join(projectDir, "app")), "mine\n"); + assertEquals(await exists(join(projectDir, "README.md")), false); + assertEquals(await exists(join(projectDir, "AGENTS.md")), false); + } finally { + await remove(parentDir, { recursive: true }).catch(() => {}); + } + }); + + it("refuses a link where the scaffold needs a directory, and writes nothing through it", async () => { + const parentDir = await makeTempDir({ prefix: "veryfront-create-blocked-link-" }); + const projectDir = join(parentDir, "contract-project"); + const outside = join(parentDir, "outside"); + + try { + await Deno.mkdir(projectDir); + await Deno.mkdir(outside); + await Deno.symlink(outside, join(projectDir, "app")); + + await assertRejects( + () => createProject(baseRequest(parentDir)), + Error, + 'Directory "contract-project" already contains app as a file or a link', + ); + + // The scaffold would otherwise report success and leave page.tsx, + // layout.tsx and about/page.mdx outside the project it named. + assertEquals(await exists(join(outside, "page.tsx")), false); + assertEquals(await exists(join(outside, "layout.tsx")), false); + assertEquals(await exists(join(projectDir, "README.md")), false); + } finally { + await remove(parentDir, { recursive: true }).catch(() => {}); + } + }); + + it("refuses a blocked directory in the current-directory path too", async () => { + const parentDir = await makeTempDir({ prefix: "veryfront-create-blocked-cwd-" }); + const outside = await makeTempDir({ prefix: "veryfront-create-blocked-target-" }); + + try { + await Deno.symlink(outside, join(parentDir, "app")); + + await assertRejects( + () => createProject({ ...baseRequest(parentDir), name: undefined }), + Error, + "Directory already contains app as a file or a link", + ); + + assertEquals(await exists(join(outside, "page.tsx")), false); + } finally { + await remove(parentDir, { recursive: true }).catch(() => {}); + await remove(outside, { recursive: true }).catch(() => {}); + } + }); + + it("refuses under --force as well, because force overwrites files it does not redirect writes", async () => { + const parentDir = await makeTempDir({ prefix: "veryfront-create-blocked-force-" }); + const projectDir = join(parentDir, "contract-project"); + const outside = join(parentDir, "outside"); + + try { + await Deno.mkdir(projectDir); + await Deno.mkdir(outside); + await Deno.symlink(outside, join(projectDir, "app")); + + await assertRejects( + () => createProject({ ...baseRequest(parentDir), conflictPolicy: "overwrite" }), + Error, + 'Directory "contract-project" already contains app as a file or a link', + ); + + assertEquals(await exists(join(outside, "page.tsx")), false); + } finally { + await remove(parentDir, { recursive: true }).catch(() => {}); + } + }); + + it("refuses a block nested below a directory that is genuinely there", async () => { + const parentDir = await makeTempDir({ prefix: "veryfront-create-blocked-nested-" }); + const projectDir = join(parentDir, "contract-project"); + + try { + // `app/` is a real directory, so only the second segment is in the way. + // Checking the first segment alone would let the scaffold write + // app/page.tsx and app/layout.tsx before failing on app/about. + await Deno.mkdir(join(projectDir, "app"), { recursive: true }); + await Deno.writeTextFile(join(projectDir, "app", "about"), "mine\n"); + + await assertRejects( + () => createProject(baseRequest(parentDir)), + Error, + 'Directory "contract-project" already contains app/about as a file or a link', + ); + + assertEquals(await Deno.readTextFile(join(projectDir, "app", "about")), "mine\n"); + assertEquals(await exists(join(projectDir, "app", "page.tsx")), false); + assertEquals(await exists(join(projectDir, "README.md")), false); + } finally { + await remove(parentDir, { recursive: true }).catch(() => {}); + } + }); + + it("scaffolds normally when the directories it needs are absent or already directories", async () => { + const parentDir = await makeTempDir({ prefix: "veryfront-create-blocked-clear-" }); + const projectDir = join(parentDir, "contract-project"); + + try { + // A real `app/` directory is not in the way, it is exactly what the + // scaffold is about to create. + await Deno.mkdir(join(projectDir, "app"), { recursive: true }); + + await createProject(baseRequest(parentDir)); + + assertEquals(await exists(join(projectDir, "app", "page.tsx")), true); + assertEquals(await exists(join(projectDir, "README.md")), true); + } finally { + await remove(parentDir, { recursive: true }).catch(() => {}); + } + }); +}); diff --git a/cli/shared/project-creation.ts b/cli/shared/project-creation.ts index ccebe7bf47..f4a4d8df4e 100644 --- a/cli/shared/project-creation.ts +++ b/cli/shared/project-creation.ts @@ -504,6 +504,44 @@ async function findExistingPaths(dir: string, paths: string[]): Promise { + const fs = createFileSystem(); + // `lstat` reports a link as "not a directory", which is exactly the answer + // this needs. It is optional only for virtual filesystems that have no + // links of their own; every runtime this CLI scaffolds on provides it, and + // `stat` still catches a plain file in the way if one ever does not. + const describe = fs.lstat?.bind(fs) ?? fs.stat.bind(fs); + const blocked = new Set(); + + for (const path of paths) { + const segments = path.split("/").slice(0, -1); + for (let depth = 1; depth <= segments.length; depth++) { + const ancestor = segments.slice(0, depth).join("/"); + if (blocked.has(ancestor)) break; + let info: Awaited>; + try { + info = await describe(join(dir, ancestor)); + } catch { + break; // Nothing there yet, so nothing below it either. + } + if (!info.isDirectory) { + blocked.add(ancestor); + break; + } + } + } + + return [...blocked].sort(); +} + export async function createProject( request: CreateProjectRequest, dependencies: CreateProjectDependencies = {}, @@ -521,6 +559,18 @@ export async function createProject( validateIntegrationsOrThrow(request.integrations); const assembly = await assembleScaffold(request); + const writePaths = scaffoldWritePaths(assembly, request); + const where = projectName === undefined ? "Directory" : `Directory "${projectName}"`; + + // Checked whatever the conflict policy is: `--force` says you accept your + // files being replaced, not the scaffold writing somewhere else entirely. + const blocked = await findBlockedDirectories(projectDir, writePaths); + if (blocked.length) { + throw createConfigError( + `${where} already contains ${blocked.join(", ")} as a file or a link, ` + + `and the scaffold needs a directory there. Move it aside or use a different name.`, + ); + } // A conflict is a file the scaffold would write over - a `package.json` with // the author's scripts, a `README.md` - not the directory existing. So an @@ -528,9 +578,8 @@ export async function createProject( // directory itself (the no-name case) all scaffold, and a `--force` is asked // for only when something would actually be replaced. if (request.conflictPolicy === "fail") { - const conflicts = await findExistingPaths(projectDir, scaffoldWritePaths(assembly, request)); + const conflicts = await findExistingPaths(projectDir, writePaths); if (conflicts.length) { - const where = projectName === undefined ? "Directory" : `Directory "${projectName}"`; throw createConfigError( `${where} already contains ${conflicts.join(", ")}. Use --force to overwrite.`, ); diff --git a/docs/api-reference/veryfront/scaffold.md b/docs/api-reference/veryfront/scaffold.md index 8e062f8317..6ab1b5b16a 100644 --- a/docs/api-reference/veryfront/scaffold.md +++ b/docs/api-reference/veryfront/scaffold.md @@ -38,20 +38,20 @@ for (const file of files) { | Name | Description | Source | | --------------------------- | --------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -| `SCAFFOLD_TEMPLATE_ALIASES` | Slugs other product surfaces use for a template this CLI names differently. | [source](https://github.com/veryfront/veryfront-code/blob/main/cli/shared/project-creation.ts#L597) | +| `SCAFFOLD_TEMPLATE_ALIASES` | Slugs other product surfaces use for a template this CLI names differently. | [source](https://github.com/veryfront/veryfront-code/blob/main/cli/shared/project-creation.ts#L646) | ### Functions | Name | Description | Source | | ------------------------- | ----------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -| `listScaffoldTemplates` | Every template slug a caller may ask for, canonical names and aliases. | [source](https://github.com/veryfront/veryfront-code/blob/main/cli/shared/project-creation.ts#L612) | -| `materializeScaffold` | Produce the complete contents of a new project without touching a disk. | [source](https://github.com/veryfront/veryfront-code/blob/main/cli/shared/project-creation.ts#L651) | -| `resolveScaffoldTemplate` | Canonical starter template for a slug, or `null` when nothing matches. | [source](https://github.com/veryfront/veryfront-code/blob/main/cli/shared/project-creation.ts#L604) | +| `listScaffoldTemplates` | Every template slug a caller may ask for, canonical names and aliases. | [source](https://github.com/veryfront/veryfront-code/blob/main/cli/shared/project-creation.ts#L661) | +| `materializeScaffold` | Produce the complete contents of a new project without touching a disk. | [source](https://github.com/veryfront/veryfront-code/blob/main/cli/shared/project-creation.ts#L700) | +| `resolveScaffoldTemplate` | Canonical starter template for a slug, or `null` when nothing matches. | [source](https://github.com/veryfront/veryfront-code/blob/main/cli/shared/project-creation.ts#L653) | ### Types | Name | Description | Source | | ---------------------------- | --------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -| `MaterializedScaffold` | A new project: every file it starts with, plus anything worth telling the author. | [source](https://github.com/veryfront/veryfront-code/blob/main/cli/shared/project-creation.ts#L633) | -| `MaterializeScaffoldRequest` | What to build: which starter, under what name, for which runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/cli/shared/project-creation.ts#L617) | +| `MaterializedScaffold` | A new project: every file it starts with, plus anything worth telling the author. | [source](https://github.com/veryfront/veryfront-code/blob/main/cli/shared/project-creation.ts#L682) | +| `MaterializeScaffoldRequest` | What to build: which starter, under what name, for which runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/cli/shared/project-creation.ts#L666) | | `TemplateFile` | | [source](https://github.com/veryfront/veryfront-code/blob/main/templates/types.ts#L17) | From 090585fd1d0bf2edc73f3680946b415272b5534f Mon Sep 17 00:00:00 2001 From: Kentaro Wakayama Date: Sun, 23 Aug 2026 01:33:05 +0200 Subject: [PATCH 3/8] fix(init): refuse a link at a scaffold path, and keep the TUI on fresh directories Two more places where accepting an existing directory let a write land somewhere it was never asked to go. A link at the scaffold path itself escaped the preflight, which only walked the directories above it. `findExistingPaths` resolves a dangling link to nothing and reports it absent, so `proj/README.md -> ../outside.md` made `veryfront init proj` exit 0, print "proj ready", and write the README to `outside.md` outside the project. The check now walks every segment, including the last, and refuses a link anywhere along the path: Directory "proj" already contains README.md as a file or a link the scaffold cannot write through. Move it aside or use a different name. A real file at a scaffold path is deliberately not refused here. It resolves fine and stays the ordinary conflict pointing at `--force`, pinned by a test so this cannot drift into refusing any directory with a file in it. The named target being a link is still allowed on purpose. `ln -s /mnt/big/app app && veryfront init app` puts the project on another volume and every file is reachable at the path you named. Only a link you did not name can surprise you. The TUI is the second caller of `createProject` with a fail policy, and it relied on the directory check this branch removed. It reserves a new remote slug, then scaffolds into `projects/`, then writes the link for that slug. With the check gone it would adopt an existing `projects/` that holds none of the template files, and repoint a directory that is already another project. It now refuses before scaffolding. The constraint belongs in that caller, not in `createProject`: `veryfront init` accepting a directory that exists is the point of this branch, and the TUI wanting a fresh one is the opposite requirement. Tests: a dangling link at a scaffold path, the same under `--force`, a real file at a scaffold path that must stay an overwritable conflict, and a TUI slug whose directory already exists and is linked elsewhere. All fail without these changes. --- cli/app/operations/project-creation.test.ts | 69 +++++++++++++++++++++ cli/app/operations/project-creation.ts | 15 ++++- cli/shared/project-creation.test.ts | 67 +++++++++++++++++++- cli/shared/project-creation.ts | 53 ++++++++++------ docs/api-reference/veryfront/scaffold.md | 12 ++-- 5 files changed, 188 insertions(+), 28 deletions(-) diff --git a/cli/app/operations/project-creation.test.ts b/cli/app/operations/project-creation.test.ts index fe2a54f114..ecb760373d 100644 --- a/cli/app/operations/project-creation.test.ts +++ b/cli/app/operations/project-creation.test.ts @@ -89,4 +89,73 @@ describe("TUI project creation", () => { await Deno.remove(configHome, { recursive: true }); } }); + + it("refuses a slug whose directory already exists rather than adopting it", async () => { + const originalFetch = globalThis.fetch; + const envKeys = ["VERYFRONT_API_URL", "VERYFRONT_API_BASE_URL", "XDG_CONFIG_HOME"]; + const savedEnv = envKeys.map((key) => Deno.env.get(key)); + const workDir = await Deno.makeTempDir(); + const configHome = await Deno.makeTempDir(); + const existingDir = join(workDir, "projects", "my-app"); + + try { + await Deno.mkdir(join(configHome, "veryfront"), { recursive: true }); + await Deno.writeTextFile(join(configHome, "veryfront", "token"), TOKEN); + Deno.env.set("VERYFRONT_API_URL", API_URL); + Deno.env.delete("VERYFRONT_API_BASE_URL"); + Deno.env.set("XDG_CONFIG_HOME", configHome); + _resetEnvironmentConfig(); + + // A directory already linked to a different project, holding nothing the + // template writes. `veryfront init` scaffolds into a directory like this + // on purpose; this caller must not, because it would repoint the link. + await Deno.mkdir(join(existingDir, ".veryfront"), { recursive: true }); + await Deno.writeTextFile( + join(existingDir, ".veryfront", "project.json"), + '{"projectId":"proj_someone_else"}\n', + ); + await Deno.writeTextFile(join(existingDir, "notes.txt"), "mine\n"); + + globalThis.fetch = ((input: string | URL | Request, init?: RequestInit) => { + const request = input instanceof Request ? input : new Request(input, init); + const url = new URL(request.url); + if (request.method === "POST" && url.pathname === "/projects") { + return Promise.resolve(Response.json({ id: "proj_new", slug: "my-app" })); + } + if (request.method === "GET" && url.pathname === "/projects") { + return Promise.resolve(Response.json({ data: [], page_info: {} })); + } + throw new Error(`Unexpected request: ${request.method} ${url.pathname}`); + }) as typeof fetch; + + const state = await withCwd(workDir, () => + createProject( + { state: createInitialState(), render: () => {} }, + "My App", + "minimal", + )); + + assertEquals( + state.logs.some((entry) => entry.message.includes("projects/my-app already exists")), + true, + ); + // The existing link and the existing file are both untouched, and no + // scaffold file landed in the directory. + assertEquals( + (await Deno.readTextFile(join(existingDir, ".veryfront", "project.json"))).trim(), + '{"projectId":"proj_someone_else"}', + ); + assertEquals(await Deno.readTextFile(join(existingDir, "notes.txt")), "mine\n"); + assertEquals( + await Deno.stat(join(existingDir, "README.md")).then(() => true, () => false), + false, + ); + } finally { + globalThis.fetch = originalFetch; + envKeys.forEach((key, index) => restoreEnv(key, savedEnv[index])); + _resetEnvironmentConfig(); + await Deno.remove(workDir, { recursive: true }); + await Deno.remove(configHome, { recursive: true }); + } + }); }); diff --git a/cli/app/operations/project-creation.ts b/cli/app/operations/project-creation.ts index c10e479d03..cd68d6cab5 100644 --- a/cli/app/operations/project-creation.ts +++ b/cli/app/operations/project-creation.ts @@ -5,7 +5,7 @@ * including remote project registration and local scaffolding. */ -import { cwd } from "veryfront/platform"; +import { createFileSystem, cwd } from "veryfront/platform"; import { join } from "veryfront/platform/path"; import type { AppState } from "../state.ts"; import { addLog, setProjects, setRemoteProjects } from "../state.ts"; @@ -49,6 +49,19 @@ export async function createProject( const reserved = await reserveProjectSlug(normalizedSlug, token); const slug = reserved.slug; + // `veryfront init` deliberately scaffolds into a directory that is already + // there. This caller must not: it has just reserved a brand new remote + // slug, and `resolveOrCreateProject` below writes the link for it. Adopting + // an existing `projects/` would point a directory that is already + // someone else's project at the project just reserved. + const projectDir = join(cwd(), "projects", slug); + if (await createFileSystem().exists(projectDir)) { + return addLog( + "error", + `projects/${slug} already exists. Remove it or choose a different name.`, + )(state); + } + const creation = await createSharedProject({ name: slug, parentDir: join(cwd(), "projects"), diff --git a/cli/shared/project-creation.test.ts b/cli/shared/project-creation.test.ts index 08b9fd918f..b3ecdef6de 100644 --- a/cli/shared/project-creation.test.ts +++ b/cli/shared/project-creation.test.ts @@ -728,7 +728,7 @@ describe("createProject into an existing named directory", () => { }); }); -describe("createProject when something blocks a scaffold directory", () => { +describe("createProject when a path cannot be written through", () => { it("refuses a file where the scaffold needs a directory, before writing anything", async () => { const parentDir = await makeTempDir({ prefix: "veryfront-create-blocked-file-" }); const projectDir = join(parentDir, "contract-project"); @@ -847,6 +847,71 @@ describe("createProject when something blocks a scaffold directory", () => { } }); + it("refuses a link at a scaffold path itself, dangling or not", async () => { + const parentDir = await makeTempDir({ prefix: "veryfront-create-leaf-link-" }); + const projectDir = join(parentDir, "contract-project"); + const outside = join(parentDir, "outside.md"); + + try { + await Deno.mkdir(projectDir); + // A dangling link resolves to nothing, so `findExistingPaths` reports it + // absent and the write follows it out of the project. + await Deno.symlink(outside, join(projectDir, "README.md")); + + await assertRejects( + () => createProject(baseRequest(parentDir)), + Error, + 'Directory "contract-project" already contains README.md as a file or a link', + ); + + assertEquals(await exists(outside), false); + assertEquals(await exists(join(projectDir, "AGENTS.md")), false); + } finally { + await remove(parentDir, { recursive: true }).catch(() => {}); + } + }); + + it("refuses a link at a scaffold path under --force as well", async () => { + const parentDir = await makeTempDir({ prefix: "veryfront-create-leaf-force-" }); + const projectDir = join(parentDir, "contract-project"); + const outside = join(parentDir, "outside.md"); + + try { + await Deno.mkdir(projectDir); + await Deno.symlink(outside, join(projectDir, "README.md")); + + await assertRejects( + () => createProject({ ...baseRequest(parentDir), conflictPolicy: "overwrite" }), + Error, + 'Directory "contract-project" already contains README.md as a file or a link', + ); + + assertEquals(await exists(outside), false); + } finally { + await remove(parentDir, { recursive: true }).catch(() => {}); + } + }); + + it("still reports a real file at a scaffold path as an overwritable conflict", async () => { + const parentDir = await makeTempDir({ prefix: "veryfront-create-leaf-file-" }); + const projectDir = join(parentDir, "contract-project"); + + try { + await Deno.mkdir(projectDir); + await Deno.writeTextFile(join(projectDir, "README.md"), "mine\n"); + + // A real file resolves fine, so it stays a conflict pointing at --force + // rather than the refusal above. + await assertRejects( + () => createProject(baseRequest(parentDir)), + Error, + 'Directory "contract-project" already contains README.md. Use --force to overwrite.', + ); + } finally { + await remove(parentDir, { recursive: true }).catch(() => {}); + } + }); + it("scaffolds normally when the directories it needs are absent or already directories", async () => { const parentDir = await makeTempDir({ prefix: "veryfront-create-blocked-clear-" }); const projectDir = join(parentDir, "contract-project"); diff --git a/cli/shared/project-creation.ts b/cli/shared/project-creation.ts index f4a4d8df4e..a8295fcb34 100644 --- a/cli/shared/project-creation.ts +++ b/cli/shared/project-creation.ts @@ -505,35 +505,48 @@ async function findExistingPaths(dir: string, paths: string[]): Promise ../elsewhere` makes `app/page.tsx` + * resolve outside the project, and a dangling `README.md -> ../outside.md` + * resolves to nothing at all, so both are reported absent and the write then + * follows the link out of the project. + * - a regular file where a directory has to go. `app/page.tsx` cannot resolve + * through a file named `app`, so the write stops halfway through instead. + * + * A real file sitting at a scaffold path is not listed here. That one resolves + * fine and is the conflict `findExistingPaths` reports. */ -async function findBlockedDirectories(dir: string, paths: string[]): Promise { +async function findUnwritablePaths(dir: string, paths: string[]): Promise { const fs = createFileSystem(); - // `lstat` reports a link as "not a directory", which is exactly the answer - // this needs. It is optional only for virtual filesystems that have no - // links of their own; every runtime this CLI scaffolds on provides it, and - // `stat` still catches a plain file in the way if one ever does not. + // `lstat` is what makes a link visible: `stat` follows it and reports the + // target. It is optional only for virtual filesystems that have no links of + // their own; every runtime this CLI scaffolds on provides it, and `stat` + // still catches a plain file in the way if one ever does not. const describe = fs.lstat?.bind(fs) ?? fs.stat.bind(fs); const blocked = new Set(); for (const path of paths) { - const segments = path.split("/").slice(0, -1); + const segments = path.split("/"); for (let depth = 1; depth <= segments.length; depth++) { - const ancestor = segments.slice(0, depth).join("/"); - if (blocked.has(ancestor)) break; + const prefix = segments.slice(0, depth).join("/"); + if (blocked.has(prefix)) break; let info: Awaited>; try { - info = await describe(join(dir, ancestor)); + info = await describe(join(dir, prefix)); } catch { break; // Nothing there yet, so nothing below it either. } - if (!info.isDirectory) { - blocked.add(ancestor); + if (info.isSymlink) { + blocked.add(prefix); + break; + } + // The last segment is the file itself, and a real file there is a + // conflict rather than something to refuse outright. + if (depth < segments.length && !info.isDirectory) { + blocked.add(prefix); break; } } @@ -564,11 +577,11 @@ export async function createProject( // Checked whatever the conflict policy is: `--force` says you accept your // files being replaced, not the scaffold writing somewhere else entirely. - const blocked = await findBlockedDirectories(projectDir, writePaths); - if (blocked.length) { + const unwritable = await findUnwritablePaths(projectDir, writePaths); + if (unwritable.length) { throw createConfigError( - `${where} already contains ${blocked.join(", ")} as a file or a link, ` + - `and the scaffold needs a directory there. Move it aside or use a different name.`, + `${where} already contains ${unwritable.join(", ")} as a file or a link ` + + `the scaffold cannot write through. Move it aside or use a different name.`, ); } diff --git a/docs/api-reference/veryfront/scaffold.md b/docs/api-reference/veryfront/scaffold.md index 6ab1b5b16a..a486432414 100644 --- a/docs/api-reference/veryfront/scaffold.md +++ b/docs/api-reference/veryfront/scaffold.md @@ -38,20 +38,20 @@ for (const file of files) { | Name | Description | Source | | --------------------------- | --------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -| `SCAFFOLD_TEMPLATE_ALIASES` | Slugs other product surfaces use for a template this CLI names differently. | [source](https://github.com/veryfront/veryfront-code/blob/main/cli/shared/project-creation.ts#L646) | +| `SCAFFOLD_TEMPLATE_ALIASES` | Slugs other product surfaces use for a template this CLI names differently. | [source](https://github.com/veryfront/veryfront-code/blob/main/cli/shared/project-creation.ts#L659) | ### Functions | Name | Description | Source | | ------------------------- | ----------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -| `listScaffoldTemplates` | Every template slug a caller may ask for, canonical names and aliases. | [source](https://github.com/veryfront/veryfront-code/blob/main/cli/shared/project-creation.ts#L661) | -| `materializeScaffold` | Produce the complete contents of a new project without touching a disk. | [source](https://github.com/veryfront/veryfront-code/blob/main/cli/shared/project-creation.ts#L700) | -| `resolveScaffoldTemplate` | Canonical starter template for a slug, or `null` when nothing matches. | [source](https://github.com/veryfront/veryfront-code/blob/main/cli/shared/project-creation.ts#L653) | +| `listScaffoldTemplates` | Every template slug a caller may ask for, canonical names and aliases. | [source](https://github.com/veryfront/veryfront-code/blob/main/cli/shared/project-creation.ts#L674) | +| `materializeScaffold` | Produce the complete contents of a new project without touching a disk. | [source](https://github.com/veryfront/veryfront-code/blob/main/cli/shared/project-creation.ts#L713) | +| `resolveScaffoldTemplate` | Canonical starter template for a slug, or `null` when nothing matches. | [source](https://github.com/veryfront/veryfront-code/blob/main/cli/shared/project-creation.ts#L666) | ### Types | Name | Description | Source | | ---------------------------- | --------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -| `MaterializedScaffold` | A new project: every file it starts with, plus anything worth telling the author. | [source](https://github.com/veryfront/veryfront-code/blob/main/cli/shared/project-creation.ts#L682) | -| `MaterializeScaffoldRequest` | What to build: which starter, under what name, for which runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/cli/shared/project-creation.ts#L666) | +| `MaterializedScaffold` | A new project: every file it starts with, plus anything worth telling the author. | [source](https://github.com/veryfront/veryfront-code/blob/main/cli/shared/project-creation.ts#L695) | +| `MaterializeScaffoldRequest` | What to build: which starter, under what name, for which runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/cli/shared/project-creation.ts#L679) | | `TemplateFile` | | [source](https://github.com/veryfront/veryfront-code/blob/main/templates/types.ts#L17) | From 53734a211eefca5931fd95761984891ab48946ea Mon Sep 17 00:00:00 2001 From: Koji Wakayama Date: Sun, 23 Aug 2026 05:36:54 +0200 Subject: [PATCH 4/8] fix(cli): classify user-input mistakes instead of reporting unknown-error A bad flag, an invalid project name, a missing positional, or a target that already exists all rendered as "[unknown-error] Unknown/unclassified error" with "Check logs for more details", because those sites threw plain Errors or `createError({ type: "config" })`, which carry no registry slug. Exit codes were split between 1 and 2 by a message-prefix heuristic. Two registered errors now cover them: - `invalid-argument` (existing, exit 2) for anything the caller typed wrong. Its title drops "function" - it has always been the CLI's usage error too (`login` already used it) and the docs catalog describes it that way. `parseArgsOrThrow` now throws it, which classifies every command's argument-parse failure at once (`dev --port abc`, ...), as do `parseRuntime`, project-name validation, invalid integrations, and the `generate` usage error. - `already-exists` (new, 409, exit 1) for writing over something that is there: `init` into a directory holding scaffold files, and `generate` onto an existing file. Its suggestion is command-agnostic because `generate` has no --force; `init` keeps the --force hint in its detail. Exit code 2 is the style guide's "invalid usage" code; the router heuristic stays as the fallback for sites not yet converted. Tests pin the slug and exit code at the unit level (registry, parseRuntime, parseArgsOrThrow, generate, createProject) and at the process level (init integration: exit 2 with [invalid-argument], exit 1 with [already-exists], never unknown-error). Error reference and API reference regenerated. Closes veryfront/veryfront-issue-inbox#740. --- cli/commands/generate/command.ts | 12 ++-- cli/commands/generate/generate.test.ts | 22 +++++++- cli/commands/generate/handler.test.ts | 13 ++++- cli/commands/generate/handler.ts | 13 +++-- cli/commands/init/init-command.ts | 4 +- cli/commands/init/init.integration.test.ts | 20 +++---- cli/commands/init/runtime.test.ts | 11 ++++ cli/commands/init/runtime.ts | 8 ++- cli/router.test.ts | 2 +- cli/shared/args.test.ts | 21 ++++++- cli/shared/args.ts | 8 ++- cli/shared/project-creation.test.ts | 37 ++++++++++++ cli/shared/project-creation.ts | 31 ++++++---- docs/api-reference/veryfront/errors.md | 56 +++++++++---------- docs/api-reference/veryfront/index.client.md | 8 +-- docs/api-reference/veryfront/index.md | 8 +-- docs/api-reference/veryfront/scaffold.md | 12 ++-- docs/api-reference/veryfront/security.md | 4 +- docs/guides/errors.md | 10 +++- src/errors/catalog/general-errors.test.ts | 5 +- src/errors/catalog/general-errors.ts | 11 ++++ src/errors/error-registry.test.ts | 22 +++++++- src/errors/error-registry/general.ts | 18 +++++- src/errors/index.ts | 1 + src/server/handlers/dev/dashboard/api.test.ts | 4 +- 25 files changed, 262 insertions(+), 99 deletions(-) diff --git a/cli/commands/generate/command.ts b/cli/commands/generate/command.ts index 9bc732044f..93065df421 100644 --- a/cli/commands/generate/command.ts +++ b/cli/commands/generate/command.ts @@ -1,6 +1,6 @@ import { getConfig } from "veryfront/config"; import { cliLogger } from "#cli/utils"; -import { createError, toError } from "veryfront/errors"; +import { ALREADY_EXISTS, createError, toError } from "veryfront/errors"; import { parseExtensionManifest } from "veryfront/extensions"; import { exists, join, readTextFile } from "veryfront/fs"; import { generateIntegration } from "./integration-generator.ts"; @@ -123,12 +123,10 @@ export async function generateCommand( }); if (!result.success) { - throw toError( - createError({ - type: "config", - message: result.message, - }), - ); + throw ALREADY_EXISTS.create({ + detail: result.message, + context: { paths: result.files.map((file) => file.path) }, + }); } for (const file of result.files) cliLogger.info(`Created ${file.path}`); diff --git a/cli/commands/generate/generate.test.ts b/cli/commands/generate/generate.test.ts index c7b38b52e7..9d0624475e 100644 --- a/cli/commands/generate/generate.test.ts +++ b/cli/commands/generate/generate.test.ts @@ -3,7 +3,8 @@ import "#veryfront/schemas/_test-setup.ts"; * Tests for generate command */ -import { assertEquals } from "#veryfront/testing/assert.ts"; +import { assertEquals, assertRejects } from "#veryfront/testing/assert.ts"; +import { VeryfrontError } from "veryfront/errors"; import { describe, it } from "#veryfront/testing/bdd.ts"; import { generateCommand } from "./index.ts"; @@ -22,3 +23,22 @@ describe("generate command", () => { }); }); }); + +describe("generateCommand conflicts", () => { + it("refuses to overwrite an existing file with an already-exists error", async () => { + const projectDir = await Deno.makeTempDir({ prefix: "veryfront-generate-conflict-" }); + + try { + await generateCommand(projectDir, "tool", "calculator"); + + const error = await assertRejects(() => generateCommand(projectDir, "tool", "calculator")); + + assertEquals(error instanceof VeryfrontError, true); + assertEquals((error as VeryfrontError).slug, "already-exists"); + assertEquals((error as VeryfrontError).exitCode, 1); + assertEquals((error as VeryfrontError).detail?.includes("tools/calculator.ts"), true); + } finally { + await Deno.remove(projectDir, { recursive: true }).catch(() => {}); + } + }); +}); diff --git a/cli/commands/generate/handler.test.ts b/cli/commands/generate/handler.test.ts index a7a2f70a18..8cf8742523 100644 --- a/cli/commands/generate/handler.test.ts +++ b/cli/commands/generate/handler.test.ts @@ -3,7 +3,8 @@ import "#veryfront/schemas/_test-setup.ts"; * Tests for generate command handler */ -import { assertEquals } from "#veryfront/testing/assert.ts"; +import { assertEquals, assertRejects } from "#veryfront/testing/assert.ts"; +import { VeryfrontError } from "veryfront/errors"; import { describe, it } from "#veryfront/testing/bdd.ts"; import { handleGenerateCommand, parseGenerateArgs } from "./handler.ts"; import type { ParsedArgs } from "#cli/shared/types"; @@ -107,3 +108,13 @@ describe("commands/generate/handler", () => { }); }); }); + +describe("commands/generate/handler usage errors", () => { + it("rejects missing arguments as a registered usage error", async () => { + const error = await assertRejects(() => handleGenerateCommand({ _: [] })); + + assertEquals(error instanceof VeryfrontError, true); + assertEquals((error as VeryfrontError).slug, "invalid-argument"); + assertEquals((error as VeryfrontError).exitCode, 2); + }); +}); diff --git a/cli/commands/generate/handler.ts b/cli/commands/generate/handler.ts index 86b2be9ec2..b5e85d79ee 100644 --- a/cli/commands/generate/handler.ts +++ b/cli/commands/generate/handler.ts @@ -2,6 +2,7 @@ * Generate command handler */ +import { INVALID_ARGUMENT } from "veryfront/errors"; import { defineSchema, lazySchema } from "veryfront/schemas"; import { generateCommand } from "./index.ts"; import { showHeader } from "#cli/utils"; @@ -30,11 +31,11 @@ export async function handleGenerateCommand(args: ParsedArgs): Promise { showHeader(); const result = parseGenerateArgs(args); if (!result.success) { - throw new Error( - `Invalid arguments. Usage: veryfront generate \n\nValid types: ${ + throw INVALID_ARGUMENT.create({ + detail: `Invalid arguments. Usage: veryfront generate \n\nValid types: ${ VALID_TYPES.join(", ") }`, - ); + }); } const { type, name } = result.data; @@ -45,11 +46,11 @@ export async function handleGenerateCommand(args: ParsedArgs): Promise { } if (!type || !name) { - throw new Error( - `Invalid arguments. Usage: veryfront generate \n\nValid types: ${ + throw INVALID_ARGUMENT.create({ + detail: `Invalid arguments. Usage: veryfront generate \n\nValid types: ${ VALID_TYPES.join(", ") }`, - ); + }); } await generateCommand(cwd(), type, name); diff --git a/cli/commands/init/init-command.ts b/cli/commands/init/init-command.ts index 7a0b036ed7..89c80ccd5d 100644 --- a/cli/commands/init/init-command.ts +++ b/cli/commands/init/init-command.ts @@ -6,7 +6,7 @@ import { cliLogger as logger, isVerbose } from "#cli/utils"; import { brand, dim } from "#cli/ui"; import { createTransientSpinner } from "../../ui/progress.ts"; -import { createError, toError } from "veryfront/errors"; +import { INVALID_ARGUMENT } from "veryfront/errors"; import type { InitOptions, InitRuntime, InitTemplate } from "./types.ts"; import { cwd } from "veryfront/platform"; import { getDlxCommand, getInstallCommand, getRunCommand } from "../../utils/package-manager.ts"; @@ -135,7 +135,7 @@ export async function initCommand( if (name) { const nameError = validateProjectName(name); if (nameError) { - throw toError(createError({ type: "config", message: nameError })); + throw INVALID_ARGUMENT.create({ detail: nameError }); } } diff --git a/cli/commands/init/init.integration.test.ts b/cli/commands/init/init.integration.test.ts index 8f7bb00b5b..6fce590640 100644 --- a/cli/commands/init/init.integration.test.ts +++ b/cli/commands/init/init.integration.test.ts @@ -687,16 +687,14 @@ describe("init command integration", () => { "--skip-install", "--skip-env-prompt", ]); - // Non-zero exit; the project directory must not exist. - assertEquals(result.code !== 0, true); + // Usage exit code; the project directory must not exist. + assertEquals(result.code, 2); assertEquals(await exists(projectDir), false); - // The error message should surface the validator. - assertEquals( - ((result.stdout ?? "") + (result.stderr ?? "")).includes( - "Invalid runtime value", - ), - true, - ); + // A classified usage error that surfaces the validator, not unknown-error. + const output = (result.stdout ?? "") + (result.stderr ?? ""); + assertEquals(output.includes("[invalid-argument]"), true); + assertEquals(output.includes("Invalid runtime value"), true); + assertEquals(output.includes("unknown-error"), false); }); }); @@ -722,8 +720,10 @@ describe("init command integration", () => { const result = await runInitCommand([dirName, "-t", "minimal", "--skip-install"]); const output = (result.stdout ?? "") + (result.stderr ?? ""); - assertEquals(result.code === 0, false); + assertEquals(result.code, 1); + assertEquals(output.includes("[already-exists]"), true); assertEquals(output.includes("already contains README.md"), true); + assertEquals(output.includes("unknown-error"), false); assertEquals(output.includes("Stack trace"), false); assertEquals(await Deno.readTextFile(join(dirPath, "README.md")), "mine\n"); } finally { diff --git a/cli/commands/init/runtime.test.ts b/cli/commands/init/runtime.test.ts index f67a2a26da..368e1944b8 100644 --- a/cli/commands/init/runtime.test.ts +++ b/cli/commands/init/runtime.test.ts @@ -3,6 +3,7 @@ import "#veryfront/schemas/_test-setup.ts"; import { assertEquals, assertThrows } from "#veryfront/testing/assert.ts"; import { describe, it } from "#veryfront/testing/bdd.ts"; +import { VeryfrontError } from "veryfront/errors"; import { parseRuntime } from "./runtime.ts"; describe("parseRuntime", () => { @@ -46,3 +47,13 @@ describe("parseRuntime", () => { } }); }); + +describe("parseRuntime error classification", () => { + it("throws a registered usage error, not an unclassified one", () => { + const error = assertThrows(() => parseRuntime("rust")); + + assertEquals(error instanceof VeryfrontError, true); + assertEquals((error as VeryfrontError).slug, "invalid-argument"); + assertEquals((error as VeryfrontError).exitCode, 2); + }); +}); diff --git a/cli/commands/init/runtime.ts b/cli/commands/init/runtime.ts index f420b5c786..259ad024b9 100644 --- a/cli/commands/init/runtime.ts +++ b/cli/commands/init/runtime.ts @@ -1,3 +1,4 @@ +import { INVALID_ARGUMENT } from "veryfront/errors"; import type { InitRuntime } from "./types.ts"; const VALID_RUNTIMES: readonly InitRuntime[] = ["node", "bun", "deno"]; @@ -14,8 +15,9 @@ export function parseRuntime(value: unknown): InitRuntime { ) { return value as InitRuntime; } - throw new Error( - `Invalid runtime value: ${JSON.stringify(value)}. ` + + throw INVALID_ARGUMENT.create({ + detail: `Invalid runtime value: ${JSON.stringify(value)}. ` + `Must be one of: ${VALID_RUNTIMES.join(", ")}.`, - ); + context: { value, allowed: VALID_RUNTIMES }, + }); } diff --git a/cli/router.test.ts b/cli/router.test.ts index 87b049837d..8eff8bba02 100644 --- a/cli/router.test.ts +++ b/cli/router.test.ts @@ -599,7 +599,7 @@ describe("cli/router helpers", () => { assertEquals(parsed.command, "serve"); assertEquals(parsed.error.code, "USAGE_ERROR"); assertEquals(parsed.error.slug, "invalid-arguments"); - assertEquals(parsed.error.registrySlug, "unknown-error"); + assertEquals(parsed.error.registrySlug, "invalid-argument"); } finally { restoreAll(); } diff --git a/cli/shared/args.test.ts b/cli/shared/args.test.ts index eed6456ad5..89a35ddaa7 100644 --- a/cli/shared/args.test.ts +++ b/cli/shared/args.test.ts @@ -1,5 +1,6 @@ import "#veryfront/schemas/_test-setup.ts"; -import { assertEquals } from "#veryfront/testing/assert.ts"; +import { assertEquals, assertThrows } from "#veryfront/testing/assert.ts"; +import { VeryfrontError } from "veryfront/errors"; import { describe, it } from "#veryfront/testing/bdd.ts"; import { register, tryResolve } from "veryfront/extensions/contracts"; import type { SchemaValidator } from "veryfront/extensions/schema"; @@ -13,6 +14,7 @@ import { extractArg, extractArgs, GLOBAL_BOOLEAN_FLAGS, + parseArgsOrThrow, parseCliArgs, } from "./args.ts"; import { COMMANDS } from "../help/command-definitions.ts"; @@ -370,3 +372,20 @@ describe("cli/shared/args", () => { }); }); }); + +describe("parseArgsOrThrow", () => { + it("throws a registered usage error naming the command and the problem", () => { + const failing = () => ({ + success: false as const, + error: Object.assign(new Error("expected number, received NaN"), { issues: [] }), + }); + + const error = assertThrows(() => parseArgsOrThrow(failing, "dev", { _: [] })); + + assertEquals(error instanceof VeryfrontError, true); + const vfError = error as VeryfrontError; + assertEquals(vfError.slug, "invalid-argument"); + assertEquals(vfError.exitCode, 2); + assertEquals(vfError.detail, "Invalid dev arguments: expected number, received NaN"); + }); +}); diff --git a/cli/shared/args.ts b/cli/shared/args.ts index 528e457bbf..f5bf6be1fc 100644 --- a/cli/shared/args.ts +++ b/cli/shared/args.ts @@ -6,6 +6,7 @@ * @module cli/shared/args */ +import { INVALID_ARGUMENT } from "veryfront/errors"; import type { Schema } from "veryfront/extensions/schema"; import { COMMANDS } from "../help/command-definitions.ts"; import { suggestCommand } from "./suggest.ts"; @@ -203,9 +204,10 @@ export function parseArgsOrThrow( ): T { const result = parser(args); if (!result.success) { - throw new Error( - `Invalid ${commandName} arguments: ${result.error.message}`, - ); + throw INVALID_ARGUMENT.create({ + detail: `Invalid ${commandName} arguments: ${result.error.message}`, + context: { command: commandName, issues: result.error.issues }, + }); } return result.data; } diff --git a/cli/shared/project-creation.test.ts b/cli/shared/project-creation.test.ts index b3ecdef6de..3c0645b57a 100644 --- a/cli/shared/project-creation.test.ts +++ b/cli/shared/project-creation.test.ts @@ -930,3 +930,40 @@ describe("createProject when a path cannot be written through", () => { } }); }); + +describe("createProject error classification", () => { + it("rejects a bad project name as a usage error", async () => { + const parentDir = await makeTempDir({ prefix: "veryfront-create-name-class-" }); + + try { + const error = await assertRejects(() => + createProject({ ...baseRequest(parentDir), name: "nested/name" }) + ); + + assertInstanceOf(error, VeryfrontError); + assertEquals(error.slug, "invalid-argument"); + assertEquals(error.exitCode, 2); + } finally { + await remove(parentDir, { recursive: true }).catch(() => {}); + } + }); + + it("rejects files it would overwrite as already-exists", async () => { + const parentDir = await makeTempDir({ prefix: "veryfront-create-exists-class-" }); + + try { + await Deno.writeTextFile(join(parentDir, "README.md"), "mine\n"); + + const error = await assertRejects(() => + createProject({ ...baseRequest(parentDir), name: undefined }) + ); + + assertInstanceOf(error, VeryfrontError); + assertEquals(error.slug, "already-exists"); + assertEquals(error.exitCode, 1); + assertEquals(error.detail?.includes("--force"), true); + } finally { + await remove(parentDir, { recursive: true }).catch(() => {}); + } + }); +}); diff --git a/cli/shared/project-creation.ts b/cli/shared/project-creation.ts index a8295fcb34..7cf1dbe277 100644 --- a/cli/shared/project-creation.ts +++ b/cli/shared/project-creation.ts @@ -1,4 +1,10 @@ -import { createError, TEMPLATE_NOT_FOUND, toError } from "veryfront/errors"; +import { + ALREADY_EXISTS, + createError, + INVALID_ARGUMENT, + TEMPLATE_NOT_FOUND, + toError, +} from "veryfront/errors"; import { cliLogger as logger } from "#cli/utils"; import { createFileSystem } from "veryfront/platform"; import { join } from "veryfront/platform/path"; @@ -190,7 +196,10 @@ function validateIntegrationsOrThrow(integrations: IntegrationName[]): void { for (const error of validation.errors) logger.error(error); - throw createConfigError("Invalid integrations specified"); + throw INVALID_ARGUMENT.create({ + detail: "Invalid integrations specified", + context: { integrations }, + }); } function dedupeEnvVars(envVars: EnvVarConfig[]): EnvVarConfig[] { @@ -562,7 +571,7 @@ export async function createProject( const projectName = request.name; if (projectName !== undefined) { const nameError = validateProjectName(projectName); - if (nameError) throw createConfigError(nameError); + if (nameError) throw INVALID_ARGUMENT.create({ detail: nameError }); } const projectDir = projectName === undefined @@ -579,10 +588,11 @@ export async function createProject( // files being replaced, not the scaffold writing somewhere else entirely. const unwritable = await findUnwritablePaths(projectDir, writePaths); if (unwritable.length) { - throw createConfigError( - `${where} already contains ${unwritable.join(", ")} as a file or a link ` + + throw ALREADY_EXISTS.create({ + detail: `${where} already contains ${unwritable.join(", ")} as a file or a link ` + `the scaffold cannot write through. Move it aside or use a different name.`, - ); + context: { projectDir, unwritable }, + }); } // A conflict is a file the scaffold would write over - a `package.json` with @@ -593,9 +603,10 @@ export async function createProject( if (request.conflictPolicy === "fail") { const conflicts = await findExistingPaths(projectDir, writePaths); if (conflicts.length) { - throw createConfigError( - `${where} already contains ${conflicts.join(", ")}. Use --force to overwrite.`, - ); + throw ALREADY_EXISTS.create({ + detail: `${where} already contains ${conflicts.join(", ")}. Use --force to overwrite.`, + context: { projectDir, conflicts }, + }); } } @@ -724,7 +735,7 @@ export async function materializeScaffold( if (request.projectName !== undefined) { const nameError = validateProjectName(request.projectName); - if (nameError) throw createConfigError(nameError); + if (nameError) throw INVALID_ARGUMENT.create({ detail: nameError }); } const integrations = request.integrations ?? []; diff --git a/docs/api-reference/veryfront/errors.md b/docs/api-reference/veryfront/errors.md index 37b9b4b34a..ff9b636f62 100644 --- a/docs/api-reference/veryfront/errors.md +++ b/docs/api-reference/veryfront/errors.md @@ -43,6 +43,7 @@ throw INVALID_WIDGET.create({ detail: "The widget id is malformed." }); | `AGENT_INTENT_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/agent.ts#L27) | | `AGENT_NOT_FOUND` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/agent.ts#L11) | | `AGENT_TIMEOUT` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/agent.ts#L19) | +| `ALREADY_EXISTS` | Writing would replace something that is already there. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L58) | | `API_CLIENT_ERROR` | API client request/response errors (replaces VeryfrontAPIError) | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/server.ts#L93) | | `API_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/runtime.ts#L52) | | `API_ROUTE_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/route.ts#L43) | @@ -96,9 +97,9 @@ throw INVALID_WIDGET.create({ detail: "The widget id is malformed." }); | `HYDRATION_MISMATCH` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/runtime.ts#L3) | | `IMPORT_MAP_INVALID` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/config.ts#L45) | | `IMPORT_RESOLUTION_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/module.ts#L11) | -| `INITIALIZATION_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L60) | -| `INPUT_VALIDATION_FAILED` | HTTP request input validation failures (replaces ValidationError) | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L86) | -| `INVALID_ARGUMENT` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L43) | +| `INITIALIZATION_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L75) | +| `INPUT_VALIDATION_FAILED` | HTTP request input validation failures (replaces ValidationError) | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L101) | +| `INVALID_ARGUMENT` | A value the caller supplied is not acceptable: a CLI flag, a positional argument, a config field, or a function argument. Exit code 2 is the CLI's "invalid usage" code, so a script can tell a typo from a failed run. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L48) | | `INVALID_IMPORT` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/module.ts#L27) | | `INVALID_ROUTE_FILE` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/route.ts#L11) | | `INVALID_USE_CLIENT` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/boundary.ts#L27) | @@ -117,9 +118,9 @@ throw INVALID_WIDGET.create({ detail: "The widget id is malformed." }); | `MIDDLEWARE_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/runtime.ts#L60) | | `MODULE_ERROR_CATALOG` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/catalog/module-errors.ts#L4) | | `MODULE_NOT_FOUND` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/module.ts#L3) | -| `NESTED_CWD_SCOPE` | A scope that owns the process working directory was opened inside another one. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L103) | +| `NESTED_CWD_SCOPE` | A scope that owns the process working directory was opened inside another one. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L118) | | `NETWORK_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/server.ts#L84) | -| `NOT_SUPPORTED` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L68) | +| `NOT_SUPPORTED` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L83) | | `ORCHESTRATION_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/agent.ts#L35) | | `PAGE_NOT_FOUND` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/runtime.ts#L44) | | `PERMISSION_DENIED` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L19) | @@ -129,7 +130,7 @@ throw INVALID_WIDGET.create({ detail: "The widget id is malformed." }); | `PROBLEM_JSON_CONTENT_TYPE` | Content-Type header for RFC 9457 responses | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/http-error.ts#L32) | | `PRODUCTION_BUILD_REQUIRED` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/deploy.ts#L27) | | `PROJECT_EXECUTION_UNAVAILABLE` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/server.ts#L52) | -| `PROJECT_SOURCE_EMPTY` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L94) | +| `PROJECT_SOURCE_EMPTY` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L109) | | `PUSH_CONFLICT` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/deploy.ts#L84) | | `PUSH_RECEIPT_MISSING` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/deploy.ts#L76) | | `RAG_STORE_CORRUPT` | Persisted RAG index is malformed or failed structural validation. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/server.ts#L138) | @@ -149,7 +150,7 @@ throw INVALID_WIDGET.create({ detail: "The widget id is malformed." }); | `RSC_PAYLOAD_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/boundary.ts#L43) | | `RUNTIME_ERROR_CATALOG` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/catalog/runtime-errors.ts#L4) | | `SCHEDULE_CONFIG_INVALID` | Schedule definition validation failures (required fields, cron, concurrencyPolicy, target) | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/config.ts#L96) | -| `SECURITY_VIOLATION` | Path traversal / secure-fs violations (replaces SecurityError) | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L77) | +| `SECURITY_VIOLATION` | Path traversal / secure-fs violations (replaces SecurityError) | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L92) | | `SEMAPHORE_TIMEOUT` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/server.ts#L60) | | `SERVER_ERROR_CATALOG` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/catalog/server-errors.ts#L4) | | `SERVER_ONLY_IN_CLIENT` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/boundary.ts#L11) | @@ -162,7 +163,7 @@ throw INVALID_WIDGET.create({ detail: "The widget id is malformed." }); | `SSR_OUTPUT_LIMIT_EXCEEDED` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/boundary.ts#L51) | | `SYNC_STATE_INVALID` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/deploy.ts#L93) | | `TEMPLATE_NOT_FOUND` | `veryfront init --template ` (and `npm create veryfront -- --template`) was given a name that is not in the starter catalog. The detail carries the list of valid names so a wrong guess is self-correcting. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/config.ts#L120) | -| `TIMEOUT_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L52) | +| `TIMEOUT_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L67) | | `TOKEN_STORAGE_ERROR` | Token storage adapter failures (replaces TokenStorageError) | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/server.ts#L102) | | `TOOL_ID_CONFLICT` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/agent.ts#L51) | | `TRIGGER_CONFIG_INVALID` | Trigger ID format and input serialization validation failures | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/config.ts#L106) | @@ -260,31 +261,28 @@ These import paths group focused functionality under this module. Each is a sepa ### `veryfront/errors/general` ```ts -import { - AUTHENTICATION_REQUIRED, - FILE_NOT_FOUND, - GENERAL_REGISTRY, -} from "veryfront/errors/general"; +import { ALREADY_EXISTS, AUTHENTICATION_REQUIRED, FILE_NOT_FOUND } from "veryfront/errors/general"; ``` #### Components -| Name | Description | Source | -| ------------------------- | ------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------- | -| `AUTHENTICATION_REQUIRED` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L11) | -| `FILE_NOT_FOUND` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L27) | -| `GENERAL_REGISTRY` | Registry fragment for GENERAL errors (slug → definition). | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L121) | -| `INITIALIZATION_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L60) | -| `INPUT_VALIDATION_FAILED` | HTTP request input validation failures (replaces ValidationError) | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L86) | -| `INVALID_ARGUMENT` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L43) | -| `NESTED_CWD_SCOPE` | A scope that owns the process working directory was opened inside another one. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L103) | -| `NOT_SUPPORTED` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L68) | -| `PERMISSION_DENIED` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L19) | -| `PROJECT_SOURCE_EMPTY` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L94) | -| `RESOURCE_NOT_FOUND` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L35) | -| `SECURITY_VIOLATION` | Path traversal / secure-fs violations (replaces SecurityError) | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L77) | -| `TIMEOUT_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L52) | -| `UNKNOWN_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L3) | +| Name | Description | Source | +| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | +| `ALREADY_EXISTS` | Writing would replace something that is already there. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L58) | +| `AUTHENTICATION_REQUIRED` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L11) | +| `FILE_NOT_FOUND` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L27) | +| `GENERAL_REGISTRY` | Registry fragment for GENERAL errors (slug → definition). | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L136) | +| `INITIALIZATION_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L75) | +| `INPUT_VALIDATION_FAILED` | HTTP request input validation failures (replaces ValidationError) | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L101) | +| `INVALID_ARGUMENT` | A value the caller supplied is not acceptable: a CLI flag, a positional argument, a config field, or a function argument. Exit code 2 is the CLI's "invalid usage" code, so a script can tell a typo from a failed run. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L48) | +| `NESTED_CWD_SCOPE` | A scope that owns the process working directory was opened inside another one. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L118) | +| `NOT_SUPPORTED` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L83) | +| `PERMISSION_DENIED` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L19) | +| `PROJECT_SOURCE_EMPTY` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L109) | +| `RESOURCE_NOT_FOUND` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L35) | +| `SECURITY_VIOLATION` | Path traversal / secure-fs violations (replaces SecurityError) | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L92) | +| `TIMEOUT_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L67) | +| `UNKNOWN_ERROR` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L3) | ### `veryfront/errors/module` diff --git a/docs/api-reference/veryfront/index.client.md b/docs/api-reference/veryfront/index.client.md index ceed09778c..6261e90641 100644 --- a/docs/api-reference/veryfront/index.client.md +++ b/docs/api-reference/veryfront/index.client.md @@ -31,10 +31,10 @@ export function GET() { ### Components -| Name | Description | Source | -| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | -| `CommonSchemas` | Lazy-getter object that preserves the `CommonSchemas.email` call shape. Each access returns the cached `Schema` (memoized inside `defineSchema`), so chained calls like `CommonSchemas.email.parse(x)` work as before. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/schemas/common.ts#L91) | -| `INPUT_VALIDATION_FAILED` | HTTP request input validation failures (replaces ValidationError) | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L86) | +| Name | Description | Source | +| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | +| `CommonSchemas` | Lazy-getter object that preserves the `CommonSchemas.email` call shape. Each access returns the cached `Schema` (memoized inside `defineSchema`), so chained calls like `CommonSchemas.email.parse(x)` work as before. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/schemas/common.ts#L91) | +| `INPUT_VALIDATION_FAILED` | HTTP request input validation failures (replaces ValidationError) | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L101) | ### Functions diff --git a/docs/api-reference/veryfront/index.md b/docs/api-reference/veryfront/index.md index 9311677cf5..a6582420eb 100644 --- a/docs/api-reference/veryfront/index.md +++ b/docs/api-reference/veryfront/index.md @@ -49,10 +49,10 @@ export function getServerData(ctx: DataContext) { ### Components -| Name | Description | Source | -| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | -| `CommonSchemas` | Lazy-getter object that preserves the `CommonSchemas.email` call shape. Each access returns the cached `Schema` (memoized inside `defineSchema`), so chained calls like `CommonSchemas.email.parse(x)` work as before. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/schemas/common.ts#L91) | -| `INPUT_VALIDATION_FAILED` | HTTP request input validation failures (replaces ValidationError) | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L86) | +| Name | Description | Source | +| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | +| `CommonSchemas` | Lazy-getter object that preserves the `CommonSchemas.email` call shape. Each access returns the cached `Schema` (memoized inside `defineSchema`), so chained calls like `CommonSchemas.email.parse(x)` work as before. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/schemas/common.ts#L91) | +| `INPUT_VALIDATION_FAILED` | HTTP request input validation failures (replaces ValidationError) | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L101) | ### Functions diff --git a/docs/api-reference/veryfront/scaffold.md b/docs/api-reference/veryfront/scaffold.md index a486432414..31f8d3c1cf 100644 --- a/docs/api-reference/veryfront/scaffold.md +++ b/docs/api-reference/veryfront/scaffold.md @@ -38,20 +38,20 @@ for (const file of files) { | Name | Description | Source | | --------------------------- | --------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -| `SCAFFOLD_TEMPLATE_ALIASES` | Slugs other product surfaces use for a template this CLI names differently. | [source](https://github.com/veryfront/veryfront-code/blob/main/cli/shared/project-creation.ts#L659) | +| `SCAFFOLD_TEMPLATE_ALIASES` | Slugs other product surfaces use for a template this CLI names differently. | [source](https://github.com/veryfront/veryfront-code/blob/main/cli/shared/project-creation.ts#L670) | ### Functions | Name | Description | Source | | ------------------------- | ----------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -| `listScaffoldTemplates` | Every template slug a caller may ask for, canonical names and aliases. | [source](https://github.com/veryfront/veryfront-code/blob/main/cli/shared/project-creation.ts#L674) | -| `materializeScaffold` | Produce the complete contents of a new project without touching a disk. | [source](https://github.com/veryfront/veryfront-code/blob/main/cli/shared/project-creation.ts#L713) | -| `resolveScaffoldTemplate` | Canonical starter template for a slug, or `null` when nothing matches. | [source](https://github.com/veryfront/veryfront-code/blob/main/cli/shared/project-creation.ts#L666) | +| `listScaffoldTemplates` | Every template slug a caller may ask for, canonical names and aliases. | [source](https://github.com/veryfront/veryfront-code/blob/main/cli/shared/project-creation.ts#L685) | +| `materializeScaffold` | Produce the complete contents of a new project without touching a disk. | [source](https://github.com/veryfront/veryfront-code/blob/main/cli/shared/project-creation.ts#L724) | +| `resolveScaffoldTemplate` | Canonical starter template for a slug, or `null` when nothing matches. | [source](https://github.com/veryfront/veryfront-code/blob/main/cli/shared/project-creation.ts#L677) | ### Types | Name | Description | Source | | ---------------------------- | --------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | -| `MaterializedScaffold` | A new project: every file it starts with, plus anything worth telling the author. | [source](https://github.com/veryfront/veryfront-code/blob/main/cli/shared/project-creation.ts#L695) | -| `MaterializeScaffoldRequest` | What to build: which starter, under what name, for which runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/cli/shared/project-creation.ts#L679) | +| `MaterializedScaffold` | A new project: every file it starts with, plus anything worth telling the author. | [source](https://github.com/veryfront/veryfront-code/blob/main/cli/shared/project-creation.ts#L706) | +| `MaterializeScaffoldRequest` | What to build: which starter, under what name, for which runtime. | [source](https://github.com/veryfront/veryfront-code/blob/main/cli/shared/project-creation.ts#L690) | | `TemplateFile` | | [source](https://github.com/veryfront/veryfront-code/blob/main/templates/types.ts#L17) | diff --git a/docs/api-reference/veryfront/security.md b/docs/api-reference/veryfront/security.md index 7aac4214da..825da23772 100644 --- a/docs/api-reference/veryfront/security.md +++ b/docs/api-reference/veryfront/security.md @@ -41,9 +41,9 @@ applySecurityHeaders(response.headers, false, generateNonce(), null); | `DEFAULT_CORS_HEADERS` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/security/http/cors/constants.ts#L9) | | `DEFAULT_CORS_METHODS` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/security/http/cors/constants.ts#L1) | | `DEFAULT_LIMITS` | Framework-owned request limits. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/security/input-validation/types.ts#L21) | -| `INPUT_VALIDATION_FAILED` | HTTP request input validation failures (replaces ValidationError) | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L86) | +| `INPUT_VALIDATION_FAILED` | HTTP request input validation failures (replaces ValidationError) | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L101) | | `PathValidationError` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/security/path-validation/types.ts#L39) | -| `SECURITY_VIOLATION` | Path traversal / secure-fs violations (replaces SecurityError) | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L77) | +| `SECURITY_VIOLATION` | Path traversal / secure-fs violations (replaces SecurityError) | [source](https://github.com/veryfront/veryfront-code/blob/main/src/errors/error-registry/general.ts#L92) | | `SERVER_PERMISSIONS` | SERVER - CLI server (dev, production, proxy, MCP, split-mode). Also used by build and test tasks that need equivalent access. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/security/deno-permissions.ts#L14) | | `ValidationPresets` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/security/path-validation/presets.ts#L45) | | `WORKFLOW_RUN_PERMISSIONS` | WORKFLOW_RUN - `ProcessRunExecutor` (RESTRICTED). Runs user-authored code - no `--allow-run`, `--allow-ffi`, or `--allow-sys`. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/security/deno-permissions.ts#L37) | diff --git a/docs/guides/errors.md b/docs/guides/errors.md index 6ebdc34a8f..a563ddb7df 100644 --- a/docs/guides/errors.md +++ b/docs/guides/errors.md @@ -846,12 +846,20 @@ Requested resource not found. ### invalid-argument -Invalid function argument. +Invalid argument. - **HTTP status:** 400 - **CLI exit code:** 2 - **What to do:** Check argument types and values +### already-exists + +Target already exists. + +- **HTTP status:** 409 +- **CLI exit code:** 1 +- **What to do:** Choose a different name, or remove the existing target first + ### timeout-error Operation timed out. diff --git a/src/errors/catalog/general-errors.test.ts b/src/errors/catalog/general-errors.test.ts index e17faa28f7..17f0a9f027 100644 --- a/src/errors/catalog/general-errors.test.ts +++ b/src/errors/catalog/general-errors.test.ts @@ -12,6 +12,7 @@ describe("errors/catalog/general-errors", () => { "file-not-found", "resource-not-found", "invalid-argument", + "already-exists", "timeout-error", ]; @@ -35,8 +36,8 @@ describe("errors/catalog/general-errors", () => { } }); - it("should have 6 entries", () => { - assertEquals(Object.keys(GENERAL_ERROR_CATALOG).length, 6); + it("should have 7 entries", () => { + assertEquals(Object.keys(GENERAL_ERROR_CATALOG).length, 7); }); it("unknown-error should suggest running veryfront doctor", () => { diff --git a/src/errors/catalog/general-errors.ts b/src/errors/catalog/general-errors.ts index c5987e2f90..c189efa290 100644 --- a/src/errors/catalog/general-errors.ts +++ b/src/errors/catalog/general-errors.ts @@ -58,6 +58,17 @@ export const GENERAL_ERROR_CATALOG: PartialErrorCatalog = Object.freeze({ ], ), + "already-exists": createSimpleError( + "already-exists", + "Target already exists", + "The command would overwrite a file or directory that is already there.", + [ + "Choose a different name", + "Remove the existing file or directory first", + "Commands that can overwrite, such as 'veryfront init', accept --force", + ], + ), + "timeout-error": createSimpleError( "timeout-error", "Operation timed out", diff --git a/src/errors/error-registry.test.ts b/src/errors/error-registry.test.ts index 40acdb36a3..add03357f1 100644 --- a/src/errors/error-registry.test.ts +++ b/src/errors/error-registry.test.ts @@ -29,9 +29,9 @@ describe("error-registry", () => { assertEquals(slugs.length, uniqueSlugs.size, "Duplicate slugs detected"); }); - it("should have 120 registered errors", () => { + it("should have 121 registered errors", () => { const slugs = getAllSlugs(); - assertEquals(slugs.length, 120); + assertEquals(slugs.length, 121); }); it("registers every local integration boundary error", () => { @@ -347,7 +347,7 @@ describe("error-registry", () => { DEV: 5, DEPLOY: 16, AGENT: 9, - GENERAL: 13, + GENERAL: 14, }; for ( @@ -478,3 +478,19 @@ describe("error-registry", () => { }); }); }); + +describe("CLI usage errors", () => { + it("maps a bad argument to invalid-argument with the usage exit code", () => { + const error = getErrorBySlug("invalid-argument"); + assertEquals(error.title, "Invalid argument"); + assertEquals(error.exitCode, 2); + }); + + it("registers already-exists for targets that would be overwritten", () => { + const error = getErrorBySlug("already-exists"); + assertEquals(error.category, "GENERAL"); + assertEquals(error.status, 409); + assertEquals(error.exitCode, 1); + assertEquals(error.suggestion?.includes("different name"), true); + }); +}); diff --git a/src/errors/error-registry/general.ts b/src/errors/error-registry/general.ts index aaba25a956..270a1508f9 100644 --- a/src/errors/error-registry/general.ts +++ b/src/errors/error-registry/general.ts @@ -40,15 +40,30 @@ export const RESOURCE_NOT_FOUND = defineError({ suggestion: "Verify the referenced resource ID or name exists", }); +/** + * A value the caller supplied is not acceptable: a CLI flag, a positional + * argument, a config field, or a function argument. Exit code 2 is the CLI's + * "invalid usage" code, so a script can tell a typo from a failed run. + */ export const INVALID_ARGUMENT = defineError({ slug: "invalid-argument", category: "GENERAL", status: 400, - title: "Invalid function argument", + title: "Invalid argument", suggestion: "Check argument types and values", exitCode: 2, }); +/** Writing would replace something that is already there. */ +export const ALREADY_EXISTS = defineError({ + slug: "already-exists", + category: "GENERAL", + status: 409, + title: "Target already exists", + suggestion: "Choose a different name, or remove the existing target first", + exitCode: 1, +}); + export const TIMEOUT_ERROR = defineError({ slug: "timeout-error", category: "GENERAL", @@ -125,6 +140,7 @@ export const GENERAL_REGISTRY = { "file-not-found": FILE_NOT_FOUND, "resource-not-found": RESOURCE_NOT_FOUND, "invalid-argument": INVALID_ARGUMENT, + "already-exists": ALREADY_EXISTS, "timeout-error": TIMEOUT_ERROR, "initialization-error": INITIALIZATION_ERROR, "not-supported": NOT_SUPPORTED, diff --git a/src/errors/index.ts b/src/errors/index.ts index 5dffc7d70b..15274900cf 100644 --- a/src/errors/index.ts +++ b/src/errors/index.ts @@ -34,6 +34,7 @@ export { AGENT_INTENT_ERROR, AGENT_NOT_FOUND, AGENT_TIMEOUT, + ALREADY_EXISTS, API_CLIENT_ERROR, API_ERROR, API_ROUTE_ERROR, diff --git a/src/server/handlers/dev/dashboard/api.test.ts b/src/server/handlers/dev/dashboard/api.test.ts index 4d4f4c0cb6..b88a7caf37 100644 --- a/src/server/handlers/dev/dashboard/api.test.ts +++ b/src/server/handlers/dev/dashboard/api.test.ts @@ -214,7 +214,7 @@ describe("Dashboard API - GET endpoints", () => { assertEquals("errors" in body, true); assertEquals("categories" in body, true); assertEquals("count" in body, true); - assertEquals(body.count, 66); + assertEquals(body.count, 67); assertEquals(body.categories, { config: 7, build: 9, @@ -225,7 +225,7 @@ describe("Dashboard API - GET endpoints", () => { dev: 5, rsc: 6, deployment: 4, - general: 6, + general: 7, }); const errorsByCode = new Map( From 93737ce15e6affb902f43809259da6cf5f962668 Mon Sep 17 00:00:00 2001 From: Kentaro Wakayama Date: Sun, 23 Aug 2026 08:49:59 +0200 Subject: [PATCH 5/8] chore(rsc): regenerate the committed client bundles for the new error titles The RSC client bundles inline the error registry, so renaming `invalid-argument` and adding `already-exists` left `rsc-bundles.generated.ts` holding the old "Invalid function argument" title. `deno task typecheck` runs `generate:manifests:check` first and failed on it, which the required `ci (typecheck)` job would have caught once this PR targets main. --- src/server/services/rsc/endpoints/rsc-bundles.generated.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server/services/rsc/endpoints/rsc-bundles.generated.ts b/src/server/services/rsc/endpoints/rsc-bundles.generated.ts index 79a8e53e2e..f2dc470e5f 100644 --- a/src/server/services/rsc/endpoints/rsc-bundles.generated.ts +++ b/src/server/services/rsc/endpoints/rsc-bundles.generated.ts @@ -7,7 +7,7 @@ */ export const CLIENT_BOOT_BUNDLE: string = - 'var ct=Object.defineProperty;var lt=(e,t,n)=>t in e?ct(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n;var m=(e,t,n)=>lt(e,typeof t!="symbol"?t+"":t,n);var Ar=Array.prototype.at,Tr=Array.prototype.filter,dt=Array.prototype.join,Cr=Array.prototype.map,wr=Array.prototype.pop,ut=Array.prototype.push,Dr=Array.prototype.sort,Ae=Reflect.apply;function z(e,t){return Ae(dt,e,[t])}function M(e,t){Ae(ut,e,[t])}var Ir=Set.prototype.has,Or=String.prototype.charCodeAt,Mr=String.prototype.includes,Pr=String.prototype.indexOf,Hr=String.prototype.lastIndexOf,Lr=String.prototype.slice,Ur=String.prototype.split,$r=String.prototype.startsWith;var S=Object.getOwnPropertyDescriptor,kr=S(URL.prototype,"origin").get,vr=S(URL.prototype,"pathname").get,Fr=S(URL.prototype,"protocol").get,Br=S(URL.prototype,"hostname").get,Vr=S(URL.prototype,"port").get,zr=S(URL.prototype,"search").get,jr=S(URL.prototype,"hash").get;var ft="3.2.3",gt=Object.entries;function pt(e){let t=[];if(e?.external?.length&&M(t,`external=${z(e.external,",")}`),M(t,`target=${e?.target??"es2022"}`),e?.deps){let n=[],r=gt(e.deps);for(let o=0;ot||n?.(r,...o)}debug(t,...n){this.log(0,console.debug,`[${this.prefix}] DEBUG: ${t}`,...n)}info(t,...n){this.log(1,console.log,`[${this.prefix}] ${t}`,...n)}warn(t,...n){this.log(2,console.warn,`[${this.prefix}] WARN: ${t}`,...n)}error(t,...n){this.log(3,console.error,`[${this.prefix}] ERROR: ${t}`,...n)}};function Dt(){if(typeof window>"u")return 2;let e=globalThis;return e.__VERYFRONT_DEV__||e.__RSC_DEV__?e.__VERYFRONT_DEBUG__||e.__RSC_DEBUG__?0:1:2}var W=Dt(),u=new C("RSC",W),xo=new C("PREFETCH",W),So=new C("HYDRATE",W),bo=new C("VERYFRONT",W);var Nt="veryfront-hydration-data";function ue(e){try{let t=[...e.querySelectorAll(`[id="${Nt}"]`)];if(t.length!==1)return null;let n=e.body;if(!n)return null;let r=t[0];return n.firstElementChild!==r&&r.parentElement!==n||r.tagName?.toLowerCase()!=="script"||r.getAttribute("type")?.trim().toLowerCase()!=="application/json"?null:r}catch{return null}}function w(e=document){try{let t=ue(e);return t?JSON.parse(t.textContent||"{}"):null}catch(t){return u.debug("hydration data parse failed",t),null}}function K(e,t){if(!t?.startsWith("on:"))return!1;try{let n=ue(e);if(!n)return!1;let r=JSON.parse(n.textContent||"{}");return r.dependencyPinningCacheKey=t,n.textContent=JSON.stringify(r),!0}catch(n){return u.debug("hydration dependency snapshot seed failed",n),!1}}function Y(e){return e?.clientModuleStrategy?e.clientModuleStrategy:e?.dev?"fs":"rsc-module"}function It(e,t){if(!t)return e;let n=e.includes("?")?"&":"?";return`${e}${n}v=${encodeURIComponent(t)}`}function X(e,t){if(!t?.startsWith("on:"))return e;let n=e.indexOf("#"),r=n===-1?"":e.slice(n),o=n===-1?e:e.slice(0,n),i=o.indexOf("?"),s=i===-1?o:o.slice(0,i),a=new URLSearchParams(i===-1?"":o.slice(i+1));a.set("pins",t);let l=a.toString();return`${s}${l?`?${l}`:""}${r}`}function Ot(e,t){return It(`${Ie}${ce(e)}.js`,t)}function Mt(e,t,n){let r=t?`&v=${encodeURIComponent(t)}`:"";return X(`${H}module?rel=${encodeURIComponent(e)}${r}`,n)}function U(e){let t=e?.dependencyPinningCacheKey;return t?.startsWith("on:")?{[G]:t}:{}}function Pt(e){return e.replace(/^\\/+_vf_modules\\//,"").replace(/^\\/+/,"").replace(/\\.js$/,"")}var Ht=/\\.(tsx|ts|jsx|mdx|js)$/;function Lt(e){let t=Pt(e),n=[e,t];return Ht.test(t)||n.push(`${t}.tsx`,`${t}.ts`,`${t}.jsx`,`${t}.mdx`,`${t}.js`),Array.from(new Set(n))}function Ut(e,t){if(!e)return null;for(let n of Lt(t)){let r=e[n];if(r)return r}return null}function J(e){if(e.strategy==="fs"){let n=e.absPath??e.rel;return n?X(Ot(n,e.version),e.dependencyPinningCacheKey):null}let t=Ut(e.releaseAssetModules,e.rel);return t||Mt(e.rel,e.version,e.dependencyPinningCacheKey)}function q(e=document,t=P){let n=le(e);return{react:j("react",n)?"react":Ce(t),reactDomClient:j("react-dom/client",n)?"react-dom/client":we(t)}}function Oe(e=document){let t=le(e);return j("veryfront/router",t)?"veryfront/router":null}var Z={reset:"\\x1B[0m",dim:"\\x1B[2m",gray:"\\x1B[90m",red:"\\x1B[31m",green:"\\x1B[32m",yellow:"\\x1B[33m",blue:"\\x1B[34m",magenta:"\\x1B[35m",cyan:"\\x1B[36m"},Ho={debug:Z.gray,info:Z.green,warn:Z.yellow,error:Z.red};var y="[REDACTED]",p=Reflect.apply,$t=Array.prototype.pop,kt=Array.prototype.push;var Uo=Array.prototype,$o=BigInt.prototype.toString,Ue=Map,vt=Map.prototype.delete,Ft=Map.prototype.get,Bt=Map.prototype.keys,Vt=Map.prototype.set;var _=Object.getOwnPropertyDescriptor,zt=Object.getPrototypeOf,ko=Object.hasOwn,vo=Object.prototype,jt=Set,Gt=decodeURIComponent,T=URL,Fo=Number.isFinite,Bo=Number.isInteger,fe=RegExp.prototype.exec,Wt=_(RegExp.prototype,"global").get,Kt=_(RegExp.prototype,"unicode").get,Yt=String.prototype.charCodeAt,Xt=String.prototype.includes,Jt=String.prototype.indexOf,Me=String.prototype.slice,$e=String.prototype.startsWith,ke=String.prototype.toLowerCase,qt=Set.prototype.add,Vo=Set.prototype.delete,Zt=Set.prototype.has,Qt=zt(new Ue().keys()).next,en=_(Map.prototype,"size").get,zo=_(T.prototype,"host").get,jo=_(T.prototype,"origin").get,tn=_(T.prototype,"password").get,Go=_(T.prototype,"pathname").get,Wo=_(T.prototype,"protocol").get,nn=_(T.prototype,"username").get,rn=/[^a-z0-9]/g,on=/([a-z0-9])([A-Z])/g,sn=/([A-Z])([A-Z][a-z])/g,an=/\\b(?:sk-[A-Za-z0-9._-]{8,}|gh[po]_[A-Za-z0-9._-]{8,}|xox[baprs]-[A-Za-z0-9._-]{8,}|eyJ[A-Za-z0-9._-]{8,})\\b/g;function R(e,t,n){let r=p(Wt,t,[]),o=p(Kt,t,[]),i=0,s=!1,a="";t.lastIndex=0;try{for(;;){let l=p(fe,t,[e]);if(l===null)break;let c=l[0],d=l.index;if(a+=A(e,i,d),a+=typeof n=="string"?n:n(l),i=d+c.length,s=!0,!r)break;c.length===0&&(t.lastIndex=cn(e,d,o))}}finally{t.lastIndex=0}return s?a+A(e,i):e}function ge(e){let t=p(ke,e,[]);return R(t,rn,"")}function D(e,t){return p(Yt,e,[t])}function cn(e,t,n){let r=t+1;if(!n||r>=e.length)return r;let o=D(e,t);if(o<55296||o>56319)return r;let i=D(e,r);return i>=56320&&i<=57343?t+2:r}function A(e,t,n){return n===void 0?p(Me,e,[t]):p(Me,e,[t,n])}function ln(e){let t=[],n=0;for(let r=0;r<=e.length;r++){let o=r===e.length?-1:D(e,r);o>=97&&o<=122||o>=48&&o<=57||(r>n&&(t[t.length]=A(e,n,r)),n=r+1)}return t}var Q=["password","passwd","pwd","passphrase","secret","clientsecret","token","apikey","accesskey","privatekey","credential","authheader","authorization","cookie","bearer","jwt","connectionstring","signature","sessionid","sid","otp","mfa","pin","salt","xsrf","csrf"],dn=512,un=128,$=new Ue;var fn=256;function gn(e){let t=e.length<=un;if(t){let o=p(Ft,$,[e]);if(o!==void 0)return o}let n=ge(e),r=n==="auth";for(let o=0;!r&&o=dn){let i=p(Bt,$,[]),s=p(Qt,i,[]).value;s!==void 0&&p(vt,$,[s])}p(Vt,$,[e,r])}return r}var Pe=["access_token","accesstoken","refresh_token","api_key","apikey","code","token","secret","client_secret","password","passwd","pwd","state","sig","signature","auth","x-amz-credential","x-amz-signature","x-amz-security-token","x-goog-credential","x-goog-signature"],ve=new jt;for(let e=0;e=65&&t<=90||t>=97&&t<=122}function Fe(e){return En(e)||e==="_"||e==="$"}function Rn(e){if(!e)return!1;let t=D(e,0);return Fe(e)||t>=48&&t<=57||e==="."||e==="-"}function Be(e,t){let n=t,r=e[n]===\'"\'||e[n]==="\'"?e[n++]:"";if(!Fe(e[n]))return!1;for(n++;Rn(e[n]);)n++;if(r){if(e[n]!==r)return!1;n++}for(;e[n]===" "||e[n]==="\t";)n++;return e[n]===":"||e[n]==="="}function Ve(e){return e==="\\r"||e===`\n`||e==="}"||e==="]"||hn(e)}function ze(e,t){let n=t;for(;n=e.length||Be(e,n)}function _n(e,t){let n=t,r=!0;if(p($e,e,[y,t])){let d=t+y.length;if(He(e,d))return{end:d,replacement:y};n=d,r=!1}let o=r&&(e[n]===\'"\'||e[n]==="\'"||e[n]==="`")?e[n]:"",i=!1,s=()=>o?`${o}${y}${i?o:""}`:y,a=[],l="",c=-1;for(let d=n;d0&&(f==="}"||f==="]")){if(a[a.length-1]!==f)return{end:e.length,replacement:s()};if(p($t,a,[]),d++,a.length===0&&He(e,d))return{end:d,replacement:s()};continue}if(a.length>0||!Ve(f)){d++;continue}let E=d;if(d=ze(e,d),d>=e.length||Be(e,d))return{end:E,replacement:s()}}return{end:e.length,replacement:s()}}function Le(e,t,n,r){let o=0,i="";for(let s=p(fe,t,[e]);s;s=p(fe,t,[e])){let a=s[n];if(!xn(a))continue;let l=t.lastIndex,c=r===void 0?void 0:s[r],d=l+y.length;if((c==="?"||c==="&"||c===";")&&p($e,e,[y,l])&&e[d]==="#")continue;let f=_n(e,l);i+=A(e,o,s.index),i+=s[0],i+=f.replacement,o=f.end,t.lastIndex=f.end}return o===0?e:i+A(e,o)}function xn(e){if(e.length>fn)return!0;let t=R(e,sn,i=>`${i[1]} ${i[2]}`),n=R(t,on,i=>`${i[1]} ${i[2]}`),r=p(ke,n,[]),o=ln(r);for(let i=0;i{let r=n[1],o=n[2],i=p(Jt,o,[":"]);if(i===-1)return`${r}${y}@`;let s=A(o,0,i);return`${r}${s}:${y}@`});return t=R(t,yn,n=>{let r=n[1],o=n[2],i=n[3];return Sn(r,o,i)?n[0]:`${r}${o}:${y}@`}),t=R(t,/([?#&;])([-a-z0-9_.%\\[\\]]+)=([^&#;\\s]*)/gi,n=>{let r=n[1],o=n[2],i=bn(o);return p(Zt,ve,[ge(i)])||gn(i)?`${r}${o}=${y}`:n[0]}),t=R(t,/(^|[^a-z0-9_-])((?:set-cookie|cookie)\\s*:\\s*)[^\\r\\n]*/gi,n=>`${n[1]}${n[2]}${y}`),t=R(t,/\\b(authorization\\s*[:=]\\s*)[^\\r\\n]*/gi,n=>`${n[1]}${y}`),t=R(t,/\\b(bearer|basic)(\\s+)(?:"[^"\\r\\n]*"|\'[^\'\\r\\n]*\'|[a-z0-9._~+/=-]+)/gi,n=>`${n[1]}${n[2]}${y}`),t=R(t,an,y),t=Le(t,/(["\'])([_$a-z][a-z0-9_.$-]*)\\1(\\s*[:=]\\s*)/gi,2),t=Le(t,/(^|[^a-z0-9_.$-])([_$a-z][a-z0-9_.$-]*)(\\s*[:=]\\s*)/gi,2,1),t}var An=2048;var qo=64*1024,Tn=256,Cn="https://veryfront.com/docs/code/guides/errors#",je="...[truncated]",ye="unknown-error";function Ge(e,t){if(e.length<=t)return e;let n=Math.max(0,t-je.length);return`${wn(e,n)}${je}`}function wn(e,t){let n=e.slice(0,t),r=n.charCodeAt(n.length-1);return r>=55296&&r<=56319&&(n=n.slice(0,-1)),n}function Dn(e){let t="";for(let n=0;n=55296&&r<=56319){let o=e.charCodeAt(n+1);o>=56320&&o<=57343?(t+=e.slice(n,n+2),n++):t+="\\uFFFD";continue}t+=r>=56320&&r<=57343?"\\uFFFD":e.charAt(n)}return t}function N(e){return typeof e!="string"?y:Ge(pe(e),An)}function Nn(e){let t=typeof e=="string"?pe(e):ye,n=Ge(t||ye,Tn),r=Dn(n);return r==="."||r===".."?ye:r}function ee(e){let t=encodeURIComponent(Nn(e));return`${Cn}${t}`}var Ye=Reflect.apply,In=Object.freeze,On=Object.getOwnPropertyDescriptors,We=Number.isFinite,Xe=new WeakSet,Mn=WeakSet.prototype.add,Pn=WeakSet.prototype.has,Hn=new Set(["CONFIG","BUILD","RUNTIME","ROUTE","MODULE","SERVER","BOUNDARY","DEV","DEPLOY","AGENT","GENERAL"]);function h(e){let t={...e},n={...t,create(r){let o=r?.message,i=r?.detail,s=r?.cause,a=r?.instance,l=r?.context,c=r?.status??t.status;return new me(o||i||t.title,{slug:t.slug,category:t.category,status:c,title:t.title,suggestion:t.suggestion,exitCode:t.exitCode,detail:i,cause:s,instance:a,context:l})}};return In(n)}var me=class extends Error{constructor(n,r){super(n);m(this,"slug");m(this,"category");m(this,"status");m(this,"title");m(this,"suggestion");m(this,"exitCode");m(this,"detail");m(this,"cause");m(this,"instance");m(this,"context");Ye(Mn,Xe,[this]),this.name="VeryfrontError",this.slug=r.slug,this.category=r.category,this.status=r.status,this.title=r.title,this.suggestion=r.suggestion,this.exitCode=r.exitCode,this.detail=r.detail,this.cause=r.cause,this.instance=r.instance,this.context=r.context}toRFC9457(){let n=Ke(this);return n?{type:ee(n.slug),title:N(n.title),status:n.status,detail:n.detail===void 0?void 0:N(n.detail),instance:n.instance===void 0?void 0:N(n.instance),category:n.category,suggestion:n.suggestion===void 0?void 0:N(n.suggestion),cause:typeof n.cause=="string"?N(n.cause):void 0}:{type:ee("unknown-error"),title:"Unknown/unclassified error",status:500,category:"GENERAL"}}getDocsUrl(){let n=Ke(this);return ee(n?.slug??"unknown-error")}};function Je(e){return typeof e=="object"&&e!==null&&Ye(Pn,Xe,[e])===!0}function Ke(e){return Je(e)?Ln(e):null}function Ln(e){try{if(!Je(e))return null;let t=On(e),n=oe=>{let O=t[oe];return O&&"value"in O?O.value:void 0},r=n("slug"),o=n("category"),i=n("status"),s=n("title"),a=n("message"),l=n("suggestion"),c=n("exitCode"),d=n("detail"),f=n("cause"),E=n("instance"),F=n("context"),x=n("stack");return typeof r!="string"||!Hn.has(o)||typeof i!="number"||!We(i)||typeof s!="string"||typeof a!="string"||l!==void 0&&typeof l!="string"||c!==void 0&&(typeof c!="number"||!We(c))||d!==void 0&&typeof d!="string"||E!==void 0&&typeof E!="string"||x!==void 0&&typeof x!="string"?null:{slug:r,category:o,status:i,title:s,message:a,suggestion:l,exitCode:c,detail:d,cause:f,instance:E,context:F,stack:x}}catch{return null}}var ri=h({slug:"unknown-error",category:"GENERAL",status:500,title:"Unknown/unclassified error",suggestion:"Check logs for more details"}),oi=h({slug:"authentication-required",category:"GENERAL",status:401,title:"Authentication required",suggestion:"Set VERYFRONT_API_TOKEN or run \'veryfront login\'"}),ii=h({slug:"permission-denied",category:"GENERAL",status:403,title:"File/resource permission denied",suggestion:"Check file permissions and access rights"}),si=h({slug:"file-not-found",category:"GENERAL",status:404,title:"File not found",suggestion:"Verify the file path exists"}),ai=h({slug:"resource-not-found",category:"GENERAL",status:404,title:"Requested resource not found",suggestion:"Verify the referenced resource ID or name exists"}),ci=h({slug:"invalid-argument",category:"GENERAL",status:400,title:"Invalid function argument",suggestion:"Check argument types and values",exitCode:2}),li=h({slug:"timeout-error",category:"GENERAL",status:408,title:"Operation timed out",suggestion:"Increase timeout or optimize the operation"}),di=h({slug:"initialization-error",category:"GENERAL",status:500,title:"Initialization failed",suggestion:"Check initialization requirements and dependencies"}),ui=h({slug:"not-supported",category:"GENERAL",status:501,title:"Feature not supported",suggestion:"Check documentation for supported features"}),qe=h({slug:"security-violation",category:"GENERAL",status:403,title:"Security violation detected",suggestion:"Check for path traversal or unauthorized access attempts"}),fi=h({slug:"input-validation-failed",category:"GENERAL",status:400,title:"Input validation failed",suggestion:"Check request input against validation rules"}),gi=h({slug:"project-source-empty",category:"GENERAL",status:400,title:"Project source is empty",suggestion:"Add project files or run \'veryfront init\'"}),pi=h({slug:"nested-cwd-scope",category:"GENERAL",status:500,title:"Working directory scope nested inside another",suggestion:"Do the inner work directly in the outer scope\'s callback instead of opening a second one"});var Un=[{source:String.raw`]*>[\\s\\S]*?<\\/script>`,flags:"gi",name:"inline script"},{source:String.raw`javascript:`,flags:"gi",name:"javascript: URL"},{source:String.raw`\\bon\\w+\\s*=`,flags:"gi",name:"event handler attribute"},{source:String.raw`data:\\s*text\\/html`,flags:"gi",name:"data: HTML URL"}];function $n(){return Un.map(({source:e,flags:t,name:n})=>({pattern:new RegExp(e,t),name:n}))}function kn(){let e=globalThis;return e.__VERYFRONT_DEV__===!0||e.Deno?.env?.get?.("VERYFRONT_ENV")==="development"}function k(e,t={}){let{allowInlineScripts:n=!1,strict:r=!1,warn:o=!0}=t;for(let{pattern:i,name:s}of $n())if(!(n&&s==="inline script")&&(i.lastIndex=0,!!i.test(e)&&(o&&console.warn(`[Security] Suspicious ${s} detected in server HTML`),r||!kn())))throw qe.create({detail:`Potentially unsafe HTML: ${s} detected`});return e}function v(e,t){let n=t==="root"?L:`rsc-slot-${t}`,r=e.getElementById(n);if(r)return r;let o=e.createElement("div");return o.id=n,e.body.appendChild(o),o}function vn(e,t){if(t.type!=="slot")return;let n=v(e,t.id);n.innerHTML=k(String(t.html??""))}function Ze(e,t){let n=t.split(`\n`),r=n.pop()??"";for(let o of n){let i=o.trim();if(!i)continue;let s;try{s=JSON.parse(i)}catch(l){u.debug("[client-dom] malformed NDJSON line",{line:i,error:l instanceof Error?l.message:String(l)});continue}if(!s||typeof s!="object")continue;let a=s;if(a.type==="slot"){vn(e,a);try{Vn(e,a.id||"root")}catch(l){u.debug("[client-dom] hydration optional failed",l)}}}return r}function Fn(e){return new Promise((t,n)=>{let r=()=>n(new DOMException("aborted","AbortError"));if(e.aborted){r();return}e.addEventListener("abort",r,{once:!0})})}async function Qe(e,t=document,n){let r="body"in e?e:null,o=r?.body??e;if(!o)return;r&&K(t,r.headers.get(G));let i=o.getReader(),s=new TextDecoder,a="",l=!1;try{for(;;){if(n?.aborted)throw new DOMException("aborted","AbortError");let c=i.read(),{done:d,value:f}=n?await Promise.race([c,Fn(n)]):await c;if(d){l=!0;break}a+=s.decode(f,{stream:!0}),a=Ze(t,a)}a&&Ze(t,`${a}\n`)}catch(c){throw c instanceof Error&&c.name==="AbortError"||u.debug("[client-dom] consumeNdjsonStream error",c),c}finally{try{await i.cancel()}catch(c){l||u.debug("[client-dom] reader.cancel failed",c)}try{i.releaseLock()}catch(c){u.debug("[client-dom] reader.releaseLock failed",c)}if(typeof o.cancel=="function")try{await o.cancel()}catch(c){u.debug("[client-dom] stream.cancel failed",c)}if(typeof r?.body?.cancel=="function")try{await r.body.cancel()}catch(c){u.debug("[client-dom] response.body.cancel failed",c)}}}function Bn(e,t){let n=v(e,t),r=[],o=i=>{let s=i;s.dataset?.clientRef&&r.push(s);for(let a of i.children)o(a)};return o(n),r}function Vn(e,t){let n=Bn(e,t);for(let r of n){let o=r.dataset?.clientRef;o&&(r.dataset.hydrated="true",u.debug("[client-dom] marked for hydration",o))}}var zn=new Set(["server","client","html","fragment"]);function et(e){if(!e)return[];try{let t=JSON.parse(e);return Gn(t)?t.nodes:[]}catch{return[]}}async function Ee(e,t,n){return await Promise.all(e.map(r=>jn(r,t,n)))}async function jn(e,t,n){if(e.type==="html")return e.text??e.html??"";let r=await Ee(e.children??[],t,n);if(e.type==="fragment"||e.type==="server"&&!e.component)return t.createElement(t.Fragment,{},...r);if(e.type==="server")return t.createElement(e.component,e.props??{},...r);let o=await n(e.component);return o?t.createElement(o,e.props??{},...r):null}function Gn(e){return!he(e)||e.version!==1||!Array.isArray(e.nodes)?!1:e.nodes.every(t=>tt(t,0))}function tt(e,t){return t>100||!he(e)||!zn.has(e.type)||e.type==="html"&&typeof e.html!="string"&&typeof e.text!="string"||e.type==="client"&&typeof e.component!="string"||e.type==="server"&&e.component!==void 0&&typeof e.component!="string"||e.props!==void 0&&!he(e.props)?!1:e.children===void 0?!0:Array.isArray(e.children)&&e.children.every(n=>tt(n,t+1))}function he(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function Wn(e){if(!e)return{};let t={};for(let[n,r]of Object.entries(e))t[n]=Array.isArray(r)?r.join("/"):r;return t}async function te(e,t,n=document){try{let r=Oe(n);if(!r)return e;let i=(await import(r)).wrapForHydration;return typeof i!="function"?e:i(e,{params:Wn(t?.params),frontmatter:t?.frontmatter??{},data:t?.props??{}})}catch(r){return u.debug("router provider wrap failed",r),e}}var Kn="Unknown dependency snapshot",Yn="export default null; // Unknown dependency snapshot",Re="__VF_DEPENDENCY_SNAPSHOT_RECOVERY_STARTED__";function Xn(){return globalThis}async function Jn(e){if(e.status!==409)return!1;try{let t=(await e.clone().text()).trim();return t===Kn||t===Yn}catch{return!1}}async function I(e,t=()=>globalThis.location.reload()){if(!await Jn(e))return!1;let n=Xn();if(n[Re])return!0;n[Re]=!0;try{t()}catch{return delete n[Re],!1}return!0}async function ne(e,t=globalThis.fetch,n=()=>globalThis.location.reload()){try{let r=new URL(e,"http://veryfront.local").searchParams.getAll("pins");if(r.length!==1||!r[0]?.startsWith("on:"))return!1;let o=await t(e,{cache:"no-store"});return await I(o,n)}catch{return!1}}var qn=100;function Zn(e,t){if(globalThis.__VF_CLIENT_MOD_CACHE??(globalThis.__VF_CLIENT_MOD_CACHE=new Map),globalThis.__VF_CLIENT_MOD_CACHE.size>=qn){let n=globalThis.__VF_CLIENT_MOD_CACHE.keys().next().value;n&&globalThis.__VF_CLIENT_MOD_CACHE.delete(n)}globalThis.__VF_CLIENT_MOD_CACHE.set(e,t)}function nt(e){let t=e.match(/^\\/app\\/(.+)#([\\w$.-]+)$/);if(t)return{rel:`/${t[1]||""}`,exportName:t[2]||"default"};let n=e.match(/^(\\/_veryfront\\/[^#]+)#([\\w$.-]+)$/);return n?{moduleUrl:n[1],exportName:n[2]||"default"}:(u.debug("hydrate: unrecognised client ref format, skipping",{ref:e}),null)}function Qn(e){let t=e.dataset?.rscProps;if(!t)return{};try{let n=JSON.parse(t);return n&&typeof n=="object"&&!Array.isArray(n)?n:{}}catch(n){return u.debug("hydrate: invalid client boundary props, using empty props",n),{}}}function er(e){return et(e.dataset?.rscChildren)}function tr(e){return"/_veryfront/rsc/manifest"}function nr(e){return U(e)}async function rr(e=document){try{let t=w(e),n=await fetch(tr(t),{headers:nr(t)});return n.ok?await n.json():(await I(n),null)}catch{return null}}async function rt(e,t,n,r={}){let o=or(e,t,n,r.releaseAssetModules),i=t.moduleUrl??t.rel;if(!i)return null;let s=`${i}#${e.hash??""}`;try{let a=globalThis.__VF_CLIENT_MOD_CACHE?.get(s);if(a)return a}catch(a){u.debug("hydrate: cache get failed",a)}if(!o)return null;try{let a=await(r.importModule??(l=>import(l)))(o);try{Zn(s,a)}catch(l){u.debug("hydrate: cache set failed",l)}return a}catch(a){return u.debug("hydrate: failed to import module",{moduleUrl:o,error:a}),await(r.recoverSnapshotFailure??ne)(o),null}}function or(e,t,n,r){if(t.moduleUrl)return X(t.moduleUrl,e.dependencyPinningCacheKey);if(!t.rel)return null;let o=e.graphIds?.client.find(i=>i.rel===t.rel)?.path;return J({strategy:n,rel:t.rel,absPath:o,version:e.hash,dependencyPinningCacheKey:e.dependencyPinningCacheKey,releaseAssetModules:r})}function ir(e){let t=Array.from(e.querySelectorAll("[data-client-ref]")),n=new Set(t);return t.filter(r=>{let o=r.parentElement;for(;o;){if(n.has(o))return!1;o=o.parentElement}return!0})}async function ot(e=document){let t=null;try{t=await rr(e)}catch(c){u.debug("hydrate: fetch manifest failed",c)}if(!t){u.debug("hydrate: no manifest");return}let n=ir(e);try{let c=globalThis.__VF_MANIFEST_HASH;if(!n.some(f=>f.dataset?.hydrated!=="true")&&c&&t.hash&&c===t.hash)return}catch(c){u.debug("hydrate: hmr hash read failed",c)}if(n.length===0){try{globalThis.__VF_MANIFEST_HASH=t.hash??""}catch(c){u.debug("hydrate: set hash failed",c)}return}let r=w(e),o=Y(r),i=r?.releaseAssetModules;try{if(globalThis.__VF_TEST_MODE__){globalThis.__VF_HYDRATE_CALLED=!0,globalThis.__VF_MANIFEST_HASH=t.hash??"";return}}catch(c){u.debug("hydrate: test mode flags failed",c)}let s=q(e,r?.reactVersion),[{default:a},{createRoot:l}]=await Promise.all([import(s.react),import(s.reactDomClient)]);for(let c of n){let d=c.dataset?.clientRef??"";if(!d||c.dataset?.hydrated==="true")continue;let f=nt(d);if(!f)continue;let E=await rt(t,f,o,{releaseAssetModules:i});if(!E)continue;let F=E[f.exportName]??E.default;if(typeof F=="function")try{let x=l(c),oe=Qn(c),O=er(c),it=await Ee(O,{Fragment:a.Fragment,createElement(B,ie,...V){return a.createElement(B,ie,...V)}},async B=>{let ie=t.modules.find(at=>at.id===B),V=t.components?.[B],Se=ie?.clientRef??(V?`${V}#default`:void 0);if(!Se)return null;let se=nt(Se);if(!se)return null;let ae=await rt(t,se,o,{releaseAssetModules:i});if(!ae)return null;let be=ae[se.exportName]??ae.default;return typeof be=="function"?be:null}),st=await te(a.createElement(F,oe,...it),r,e);x.render(st),c.dataset.hydrated="true"}catch(x){u.warn("hydrate: render failed",x)}}try{globalThis.__VF_MANIFEST_HASH=t.hash??""}catch(c){u.debug("hydrate: set hash failed (post)",c)}}var _e="data-vf-react-head-owner";var sr=2*1024*1024,Li=sr*2;var Ui=64*1024,$i=1024*1024,ki=1024*1024;var vi=new TextEncoder;async function ar(){let e=w(document),t=q(document,e?.reactVersion),[n,r]=await Promise.all([import(t.react),import(t.reactDomClient)]);return{React:n,ReactDOM:r}}var cr=new Set(["SCRIPT","STYLE","NOSCRIPT","TEMPLATE"]);function xe(e){let t=e.getAttribute("style")??"";return e.hasAttribute("data-veryfront-head")||e.hasAttribute("hidden")||/(?:^|;)\\s*display\\s*:\\s*none(?:\\s*;|$)/i.test(t)||cr.has(e.tagName.toUpperCase())}function lr(e,t){return e.find(n=>n.tagName.toUpperCase()==="DIV"&&!!n.getAttribute("class")?.trim()&&!xe(n))??t}function dr(e,t){return e===t}function ur(e,t){let n=document.createElement("div");n.setAttribute("data-veryfront-hydration-root","page");let r=e.find(o=>!xe(o));r?.parentNode===t?t.insertBefore(n,r):t.appendChild(n);for(let o of e)!xe(o)&&o.parentNode===t&&n.appendChild(o);return n}function fr(e,t){for(let n of e){let r=[...n.hasAttribute(_e)?[n]:[],...n.querySelectorAll(`[${_e}]`)];for(let o of r)t.contains(o)||o.remove()}}function gr(e,t,n=document){return!!t?.pagePath&&typeof e?.__veryfrontRenderPage=="function"&&!!n.getElementById("root")}function pr(e,t){return t?.pagePath?!1:!!e.getElementById(L)}function yr(e=import.meta.url){try{return new URL(e,"http://veryfront.local").searchParams.get("hydrate")==="1"}catch{return!1}}function mr(e){return e==="rsc-module"}function hr(e,t){return e?e.startsWith("?")?e:`?${e}`:""}function Er(e,t,n){return J({strategy:t,rel:e,releaseAssetModules:n?.releaseAssetModules,dependencyPinningCacheKey:n?.dependencyPinningCacheKey})}async function Rr(e,t){try{let n=await fetch(H+"stream"+e,{headers:U(t)});if(!n.ok)return await I(n)?"snapshot-conflict":"failure";if(!n.body)return"failure";let r=new AbortController;return addEventListener("pagehide",()=>r.abort(),{once:!0}),await Qe(n,document,r.signal),"success"}catch(n){return u.debug("tryStream failed",n),"failure"}}async function re(){try{await ot(document)}catch(e){u.debug("hydration failed",e)}}async function _r(e,t,n){try{let{React:r,ReactDOM:o}=await ar(),i=Er(e,t,n);if(!i)return!1;u.debug("Loading component from:",i);let s;try{s=await import(i)}catch(E){throw await ne(i),E}let a=s.default;if(typeof a!="function")return u.debug("Page component is not a function"),!1;let l=Array.from(document.body.children),c=lr(l,document.body),d=dr(c,document.body)?ur(l,document.body):c;fr(l,d);let f=await te(r.createElement(a,{}),n);return mr(t)?o.createRoot(d).render(f):o.hydrateRoot(d,f,{identifierPrefix:"vf",onRecoverableError:()=>{}}),u.debug("Page component hydrated successfully"),!0}catch(r){return u.error("Page hydration failed",r),!1}}async function xr(e,t){try{let n=await fetch(H+"payload"+e,{headers:U(t)});if(!n.ok)return await I(n)?"snapshot-conflict":"failure";let r=await n.json();if(K(document,r?.dependencyPinningCacheKey),r?.slots){for(let[o,i]of Object.entries(r.slots))v(document,o).innerHTML=k(String(i||""));return"success"}return v(document,L).innerHTML=k(String(r?.html||"")),"success"}catch(n){return u.debug("payload fetch failed",n),"failure"}}async function Sr(){try{let e=w(document),t=hr(globalThis.window?.location.search??"",e?.dependencyPinningCacheKey);if(yr()){await re();return}let n=e?.pagePath,r=Y(e);if(n){if(gr(globalThis.window,e,document)){u.debug("Page renderer owns hydration");return}u.debug("Found page component in hydration data:",n),await _r(n,r,e)&&u.debug("Client component hydrated successfully");return}if(!pr(document,e))return;let o=await Rr(t,e);if(o==="snapshot-conflict")return;if(o==="success"){await re();return}let i=await xr(t,e);if(i==="snapshot-conflict")return;if(i==="success"){await re();return}await re()}catch(e){u.error("boot failed",e)}}if(typeof document<"u"){let e=()=>{Sr()};document.readyState==="loading"?document.addEventListener("DOMContentLoaded",e,{once:!0}):e()}export{Sr as boot,Er as buildPageHydrationModuleUrl,hr as buildRSCTransportQuery,fr as retireAbandonedHeadOwnerMarkers,lr as selectHydrationRoot,pr as shouldAttemptRSCTransport,yr as shouldHydrateOnly,mr as shouldRenderPageComponent,gr as shouldUsePageRendererHydration,dr as shouldWrapPageHydrationRoot};\n'; + 'var ct=Object.defineProperty;var lt=(e,t,n)=>t in e?ct(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n;var h=(e,t,n)=>lt(e,typeof t!="symbol"?t+"":t,n);var Ar=Array.prototype.at,Tr=Array.prototype.filter,dt=Array.prototype.join,Cr=Array.prototype.map,wr=Array.prototype.pop,ut=Array.prototype.push,Dr=Array.prototype.sort,Ae=Reflect.apply;function z(e,t){return Ae(dt,e,[t])}function M(e,t){Ae(ut,e,[t])}var Ir=Set.prototype.has,Or=String.prototype.charCodeAt,Mr=String.prototype.includes,Pr=String.prototype.indexOf,Hr=String.prototype.lastIndexOf,Lr=String.prototype.slice,Ur=String.prototype.split,$r=String.prototype.startsWith;var S=Object.getOwnPropertyDescriptor,kr=S(URL.prototype,"origin").get,vr=S(URL.prototype,"pathname").get,Fr=S(URL.prototype,"protocol").get,Br=S(URL.prototype,"hostname").get,Vr=S(URL.prototype,"port").get,zr=S(URL.prototype,"search").get,jr=S(URL.prototype,"hash").get;var ft="3.2.3",gt=Object.entries;function pt(e){let t=[];if(e?.external?.length&&M(t,`external=${z(e.external,",")}`),M(t,`target=${e?.target??"es2022"}`),e?.deps){let n=[],r=gt(e.deps);for(let o=0;ot||n?.(r,...o)}debug(t,...n){this.log(0,console.debug,`[${this.prefix}] DEBUG: ${t}`,...n)}info(t,...n){this.log(1,console.log,`[${this.prefix}] ${t}`,...n)}warn(t,...n){this.log(2,console.warn,`[${this.prefix}] WARN: ${t}`,...n)}error(t,...n){this.log(3,console.error,`[${this.prefix}] ERROR: ${t}`,...n)}};function Dt(){if(typeof window>"u")return 2;let e=globalThis;return e.__VERYFRONT_DEV__||e.__RSC_DEV__?e.__VERYFRONT_DEBUG__||e.__RSC_DEBUG__?0:1:2}var W=Dt(),u=new C("RSC",W),xo=new C("PREFETCH",W),So=new C("HYDRATE",W),bo=new C("VERYFRONT",W);var Nt="veryfront-hydration-data";function ue(e){try{let t=[...e.querySelectorAll(`[id="${Nt}"]`)];if(t.length!==1)return null;let n=e.body;if(!n)return null;let r=t[0];return n.firstElementChild!==r&&r.parentElement!==n||r.tagName?.toLowerCase()!=="script"||r.getAttribute("type")?.trim().toLowerCase()!=="application/json"?null:r}catch{return null}}function w(e=document){try{let t=ue(e);return t?JSON.parse(t.textContent||"{}"):null}catch(t){return u.debug("hydration data parse failed",t),null}}function K(e,t){if(!t?.startsWith("on:"))return!1;try{let n=ue(e);if(!n)return!1;let r=JSON.parse(n.textContent||"{}");return r.dependencyPinningCacheKey=t,n.textContent=JSON.stringify(r),!0}catch(n){return u.debug("hydration dependency snapshot seed failed",n),!1}}function Y(e){return e?.clientModuleStrategy?e.clientModuleStrategy:e?.dev?"fs":"rsc-module"}function It(e,t){if(!t)return e;let n=e.includes("?")?"&":"?";return`${e}${n}v=${encodeURIComponent(t)}`}function X(e,t){if(!t?.startsWith("on:"))return e;let n=e.indexOf("#"),r=n===-1?"":e.slice(n),o=n===-1?e:e.slice(0,n),s=o.indexOf("?"),i=s===-1?o:o.slice(0,s),a=new URLSearchParams(s===-1?"":o.slice(s+1));a.set("pins",t);let l=a.toString();return`${i}${l?`?${l}`:""}${r}`}function Ot(e,t){return It(`${Ie}${ce(e)}.js`,t)}function Mt(e,t,n){let r=t?`&v=${encodeURIComponent(t)}`:"";return X(`${H}module?rel=${encodeURIComponent(e)}${r}`,n)}function U(e){let t=e?.dependencyPinningCacheKey;return t?.startsWith("on:")?{[G]:t}:{}}function Pt(e){return e.replace(/^\\/+_vf_modules\\//,"").replace(/^\\/+/,"").replace(/\\.js$/,"")}var Ht=/\\.(tsx|ts|jsx|mdx|js)$/;function Lt(e){let t=Pt(e),n=[e,t];return Ht.test(t)||n.push(`${t}.tsx`,`${t}.ts`,`${t}.jsx`,`${t}.mdx`,`${t}.js`),Array.from(new Set(n))}function Ut(e,t){if(!e)return null;for(let n of Lt(t)){let r=e[n];if(r)return r}return null}function J(e){if(e.strategy==="fs"){let n=e.absPath??e.rel;return n?X(Ot(n,e.version),e.dependencyPinningCacheKey):null}let t=Ut(e.releaseAssetModules,e.rel);return t||Mt(e.rel,e.version,e.dependencyPinningCacheKey)}function q(e=document,t=P){let n=le(e);return{react:j("react",n)?"react":Ce(t),reactDomClient:j("react-dom/client",n)?"react-dom/client":we(t)}}function Oe(e=document){let t=le(e);return j("veryfront/router",t)?"veryfront/router":null}var Z={reset:"\\x1B[0m",dim:"\\x1B[2m",gray:"\\x1B[90m",red:"\\x1B[31m",green:"\\x1B[32m",yellow:"\\x1B[33m",blue:"\\x1B[34m",magenta:"\\x1B[35m",cyan:"\\x1B[36m"},Ho={debug:Z.gray,info:Z.green,warn:Z.yellow,error:Z.red};var y="[REDACTED]",p=Reflect.apply,$t=Array.prototype.pop,kt=Array.prototype.push;var Uo=Array.prototype,$o=BigInt.prototype.toString,Ue=Map,vt=Map.prototype.delete,Ft=Map.prototype.get,Bt=Map.prototype.keys,Vt=Map.prototype.set;var _=Object.getOwnPropertyDescriptor,zt=Object.getPrototypeOf,ko=Object.hasOwn,vo=Object.prototype,jt=Set,Gt=decodeURIComponent,T=URL,Fo=Number.isFinite,Bo=Number.isInteger,fe=RegExp.prototype.exec,Wt=_(RegExp.prototype,"global").get,Kt=_(RegExp.prototype,"unicode").get,Yt=String.prototype.charCodeAt,Xt=String.prototype.includes,Jt=String.prototype.indexOf,Me=String.prototype.slice,$e=String.prototype.startsWith,ke=String.prototype.toLowerCase,qt=Set.prototype.add,Vo=Set.prototype.delete,Zt=Set.prototype.has,Qt=zt(new Ue().keys()).next,en=_(Map.prototype,"size").get,zo=_(T.prototype,"host").get,jo=_(T.prototype,"origin").get,tn=_(T.prototype,"password").get,Go=_(T.prototype,"pathname").get,Wo=_(T.prototype,"protocol").get,nn=_(T.prototype,"username").get,rn=/[^a-z0-9]/g,on=/([a-z0-9])([A-Z])/g,sn=/([A-Z])([A-Z][a-z])/g,an=/\\b(?:sk-[A-Za-z0-9._-]{8,}|gh[po]_[A-Za-z0-9._-]{8,}|xox[baprs]-[A-Za-z0-9._-]{8,}|eyJ[A-Za-z0-9._-]{8,})\\b/g;function R(e,t,n){let r=p(Wt,t,[]),o=p(Kt,t,[]),s=0,i=!1,a="";t.lastIndex=0;try{for(;;){let l=p(fe,t,[e]);if(l===null)break;let c=l[0],d=l.index;if(a+=A(e,s,d),a+=typeof n=="string"?n:n(l),s=d+c.length,i=!0,!r)break;c.length===0&&(t.lastIndex=cn(e,d,o))}}finally{t.lastIndex=0}return i?a+A(e,s):e}function ge(e){let t=p(ke,e,[]);return R(t,rn,"")}function D(e,t){return p(Yt,e,[t])}function cn(e,t,n){let r=t+1;if(!n||r>=e.length)return r;let o=D(e,t);if(o<55296||o>56319)return r;let s=D(e,r);return s>=56320&&s<=57343?t+2:r}function A(e,t,n){return n===void 0?p(Me,e,[t]):p(Me,e,[t,n])}function ln(e){let t=[],n=0;for(let r=0;r<=e.length;r++){let o=r===e.length?-1:D(e,r);o>=97&&o<=122||o>=48&&o<=57||(r>n&&(t[t.length]=A(e,n,r)),n=r+1)}return t}var Q=["password","passwd","pwd","passphrase","secret","clientsecret","token","apikey","accesskey","privatekey","credential","authheader","authorization","cookie","bearer","jwt","connectionstring","signature","sessionid","sid","otp","mfa","pin","salt","xsrf","csrf"],dn=512,un=128,$=new Ue;var fn=256;function gn(e){let t=e.length<=un;if(t){let o=p(Ft,$,[e]);if(o!==void 0)return o}let n=ge(e),r=n==="auth";for(let o=0;!r&&o=dn){let s=p(Bt,$,[]),i=p(Qt,s,[]).value;i!==void 0&&p(vt,$,[i])}p(Vt,$,[e,r])}return r}var Pe=["access_token","accesstoken","refresh_token","api_key","apikey","code","token","secret","client_secret","password","passwd","pwd","state","sig","signature","auth","x-amz-credential","x-amz-signature","x-amz-security-token","x-goog-credential","x-goog-signature"],ve=new jt;for(let e=0;e=65&&t<=90||t>=97&&t<=122}function Fe(e){return En(e)||e==="_"||e==="$"}function Rn(e){if(!e)return!1;let t=D(e,0);return Fe(e)||t>=48&&t<=57||e==="."||e==="-"}function Be(e,t){let n=t,r=e[n]===\'"\'||e[n]==="\'"?e[n++]:"";if(!Fe(e[n]))return!1;for(n++;Rn(e[n]);)n++;if(r){if(e[n]!==r)return!1;n++}for(;e[n]===" "||e[n]==="\t";)n++;return e[n]===":"||e[n]==="="}function Ve(e){return e==="\\r"||e===`\n`||e==="}"||e==="]"||hn(e)}function ze(e,t){let n=t;for(;n=e.length||Be(e,n)}function _n(e,t){let n=t,r=!0;if(p($e,e,[y,t])){let d=t+y.length;if(He(e,d))return{end:d,replacement:y};n=d,r=!1}let o=r&&(e[n]===\'"\'||e[n]==="\'"||e[n]==="`")?e[n]:"",s=!1,i=()=>o?`${o}${y}${s?o:""}`:y,a=[],l="",c=-1;for(let d=n;d0&&(f==="}"||f==="]")){if(a[a.length-1]!==f)return{end:e.length,replacement:i()};if(p($t,a,[]),d++,a.length===0&&He(e,d))return{end:d,replacement:i()};continue}if(a.length>0||!Ve(f)){d++;continue}let E=d;if(d=ze(e,d),d>=e.length||Be(e,d))return{end:E,replacement:i()}}return{end:e.length,replacement:i()}}function Le(e,t,n,r){let o=0,s="";for(let i=p(fe,t,[e]);i;i=p(fe,t,[e])){let a=i[n];if(!xn(a))continue;let l=t.lastIndex,c=r===void 0?void 0:i[r],d=l+y.length;if((c==="?"||c==="&"||c===";")&&p($e,e,[y,l])&&e[d]==="#")continue;let f=_n(e,l);s+=A(e,o,i.index),s+=i[0],s+=f.replacement,o=f.end,t.lastIndex=f.end}return o===0?e:s+A(e,o)}function xn(e){if(e.length>fn)return!0;let t=R(e,sn,s=>`${s[1]} ${s[2]}`),n=R(t,on,s=>`${s[1]} ${s[2]}`),r=p(ke,n,[]),o=ln(r);for(let s=0;s{let r=n[1],o=n[2],s=p(Jt,o,[":"]);if(s===-1)return`${r}${y}@`;let i=A(o,0,s);return`${r}${i}:${y}@`});return t=R(t,yn,n=>{let r=n[1],o=n[2],s=n[3];return Sn(r,o,s)?n[0]:`${r}${o}:${y}@`}),t=R(t,/([?#&;])([-a-z0-9_.%\\[\\]]+)=([^&#;\\s]*)/gi,n=>{let r=n[1],o=n[2],s=bn(o);return p(Zt,ve,[ge(s)])||gn(s)?`${r}${o}=${y}`:n[0]}),t=R(t,/(^|[^a-z0-9_-])((?:set-cookie|cookie)\\s*:\\s*)[^\\r\\n]*/gi,n=>`${n[1]}${n[2]}${y}`),t=R(t,/\\b(authorization\\s*[:=]\\s*)[^\\r\\n]*/gi,n=>`${n[1]}${y}`),t=R(t,/\\b(bearer|basic)(\\s+)(?:"[^"\\r\\n]*"|\'[^\'\\r\\n]*\'|[a-z0-9._~+/=-]+)/gi,n=>`${n[1]}${n[2]}${y}`),t=R(t,an,y),t=Le(t,/(["\'])([_$a-z][a-z0-9_.$-]*)\\1(\\s*[:=]\\s*)/gi,2),t=Le(t,/(^|[^a-z0-9_.$-])([_$a-z][a-z0-9_.$-]*)(\\s*[:=]\\s*)/gi,2,1),t}var An=2048;var qo=64*1024,Tn=256,Cn="https://veryfront.com/docs/code/guides/errors#",je="...[truncated]",ye="unknown-error";function Ge(e,t){if(e.length<=t)return e;let n=Math.max(0,t-je.length);return`${wn(e,n)}${je}`}function wn(e,t){let n=e.slice(0,t),r=n.charCodeAt(n.length-1);return r>=55296&&r<=56319&&(n=n.slice(0,-1)),n}function Dn(e){let t="";for(let n=0;n=55296&&r<=56319){let o=e.charCodeAt(n+1);o>=56320&&o<=57343?(t+=e.slice(n,n+2),n++):t+="\\uFFFD";continue}t+=r>=56320&&r<=57343?"\\uFFFD":e.charAt(n)}return t}function N(e){return typeof e!="string"?y:Ge(pe(e),An)}function Nn(e){let t=typeof e=="string"?pe(e):ye,n=Ge(t||ye,Tn),r=Dn(n);return r==="."||r===".."?ye:r}function ee(e){let t=encodeURIComponent(Nn(e));return`${Cn}${t}`}var Ye=Reflect.apply,In=Object.freeze,On=Object.getOwnPropertyDescriptors,We=Number.isFinite,Xe=new WeakSet,Mn=WeakSet.prototype.add,Pn=WeakSet.prototype.has,Hn=new Set(["CONFIG","BUILD","RUNTIME","ROUTE","MODULE","SERVER","BOUNDARY","DEV","DEPLOY","AGENT","GENERAL"]);function m(e){let t={...e},n={...t,create(r){let o=r?.message,s=r?.detail,i=r?.cause,a=r?.instance,l=r?.context,c=r?.status??t.status;return new me(o||s||t.title,{slug:t.slug,category:t.category,status:c,title:t.title,suggestion:t.suggestion,exitCode:t.exitCode,detail:s,cause:i,instance:a,context:l})}};return In(n)}var me=class extends Error{constructor(n,r){super(n);h(this,"slug");h(this,"category");h(this,"status");h(this,"title");h(this,"suggestion");h(this,"exitCode");h(this,"detail");h(this,"cause");h(this,"instance");h(this,"context");Ye(Mn,Xe,[this]),this.name="VeryfrontError",this.slug=r.slug,this.category=r.category,this.status=r.status,this.title=r.title,this.suggestion=r.suggestion,this.exitCode=r.exitCode,this.detail=r.detail,this.cause=r.cause,this.instance=r.instance,this.context=r.context}toRFC9457(){let n=Ke(this);return n?{type:ee(n.slug),title:N(n.title),status:n.status,detail:n.detail===void 0?void 0:N(n.detail),instance:n.instance===void 0?void 0:N(n.instance),category:n.category,suggestion:n.suggestion===void 0?void 0:N(n.suggestion),cause:typeof n.cause=="string"?N(n.cause):void 0}:{type:ee("unknown-error"),title:"Unknown/unclassified error",status:500,category:"GENERAL"}}getDocsUrl(){let n=Ke(this);return ee(n?.slug??"unknown-error")}};function Je(e){return typeof e=="object"&&e!==null&&Ye(Pn,Xe,[e])===!0}function Ke(e){return Je(e)?Ln(e):null}function Ln(e){try{if(!Je(e))return null;let t=On(e),n=oe=>{let O=t[oe];return O&&"value"in O?O.value:void 0},r=n("slug"),o=n("category"),s=n("status"),i=n("title"),a=n("message"),l=n("suggestion"),c=n("exitCode"),d=n("detail"),f=n("cause"),E=n("instance"),F=n("context"),x=n("stack");return typeof r!="string"||!Hn.has(o)||typeof s!="number"||!We(s)||typeof i!="string"||typeof a!="string"||l!==void 0&&typeof l!="string"||c!==void 0&&(typeof c!="number"||!We(c))||d!==void 0&&typeof d!="string"||E!==void 0&&typeof E!="string"||x!==void 0&&typeof x!="string"?null:{slug:r,category:o,status:s,title:i,message:a,suggestion:l,exitCode:c,detail:d,cause:f,instance:E,context:F,stack:x}}catch{return null}}var rs=m({slug:"unknown-error",category:"GENERAL",status:500,title:"Unknown/unclassified error",suggestion:"Check logs for more details"}),os=m({slug:"authentication-required",category:"GENERAL",status:401,title:"Authentication required",suggestion:"Set VERYFRONT_API_TOKEN or run \'veryfront login\'"}),ss=m({slug:"permission-denied",category:"GENERAL",status:403,title:"File/resource permission denied",suggestion:"Check file permissions and access rights"}),is=m({slug:"file-not-found",category:"GENERAL",status:404,title:"File not found",suggestion:"Verify the file path exists"}),as=m({slug:"resource-not-found",category:"GENERAL",status:404,title:"Requested resource not found",suggestion:"Verify the referenced resource ID or name exists"}),cs=m({slug:"invalid-argument",category:"GENERAL",status:400,title:"Invalid argument",suggestion:"Check argument types and values",exitCode:2}),ls=m({slug:"already-exists",category:"GENERAL",status:409,title:"Target already exists",suggestion:"Choose a different name, or remove the existing target first",exitCode:1}),ds=m({slug:"timeout-error",category:"GENERAL",status:408,title:"Operation timed out",suggestion:"Increase timeout or optimize the operation"}),us=m({slug:"initialization-error",category:"GENERAL",status:500,title:"Initialization failed",suggestion:"Check initialization requirements and dependencies"}),fs=m({slug:"not-supported",category:"GENERAL",status:501,title:"Feature not supported",suggestion:"Check documentation for supported features"}),qe=m({slug:"security-violation",category:"GENERAL",status:403,title:"Security violation detected",suggestion:"Check for path traversal or unauthorized access attempts"}),gs=m({slug:"input-validation-failed",category:"GENERAL",status:400,title:"Input validation failed",suggestion:"Check request input against validation rules"}),ps=m({slug:"project-source-empty",category:"GENERAL",status:400,title:"Project source is empty",suggestion:"Add project files or run \'veryfront init\'"}),ys=m({slug:"nested-cwd-scope",category:"GENERAL",status:500,title:"Working directory scope nested inside another",suggestion:"Do the inner work directly in the outer scope\'s callback instead of opening a second one"});var Un=[{source:String.raw`]*>[\\s\\S]*?<\\/script>`,flags:"gi",name:"inline script"},{source:String.raw`javascript:`,flags:"gi",name:"javascript: URL"},{source:String.raw`\\bon\\w+\\s*=`,flags:"gi",name:"event handler attribute"},{source:String.raw`data:\\s*text\\/html`,flags:"gi",name:"data: HTML URL"}];function $n(){return Un.map(({source:e,flags:t,name:n})=>({pattern:new RegExp(e,t),name:n}))}function kn(){let e=globalThis;return e.__VERYFRONT_DEV__===!0||e.Deno?.env?.get?.("VERYFRONT_ENV")==="development"}function k(e,t={}){let{allowInlineScripts:n=!1,strict:r=!1,warn:o=!0}=t;for(let{pattern:s,name:i}of $n())if(!(n&&i==="inline script")&&(s.lastIndex=0,!!s.test(e)&&(o&&console.warn(`[Security] Suspicious ${i} detected in server HTML`),r||!kn())))throw qe.create({detail:`Potentially unsafe HTML: ${i} detected`});return e}function v(e,t){let n=t==="root"?L:`rsc-slot-${t}`,r=e.getElementById(n);if(r)return r;let o=e.createElement("div");return o.id=n,e.body.appendChild(o),o}function vn(e,t){if(t.type!=="slot")return;let n=v(e,t.id);n.innerHTML=k(String(t.html??""))}function Ze(e,t){let n=t.split(`\n`),r=n.pop()??"";for(let o of n){let s=o.trim();if(!s)continue;let i;try{i=JSON.parse(s)}catch(l){u.debug("[client-dom] malformed NDJSON line",{line:s,error:l instanceof Error?l.message:String(l)});continue}if(!i||typeof i!="object")continue;let a=i;if(a.type==="slot"){vn(e,a);try{Vn(e,a.id||"root")}catch(l){u.debug("[client-dom] hydration optional failed",l)}}}return r}function Fn(e){return new Promise((t,n)=>{let r=()=>n(new DOMException("aborted","AbortError"));if(e.aborted){r();return}e.addEventListener("abort",r,{once:!0})})}async function Qe(e,t=document,n){let r="body"in e?e:null,o=r?.body??e;if(!o)return;r&&K(t,r.headers.get(G));let s=o.getReader(),i=new TextDecoder,a="",l=!1;try{for(;;){if(n?.aborted)throw new DOMException("aborted","AbortError");let c=s.read(),{done:d,value:f}=n?await Promise.race([c,Fn(n)]):await c;if(d){l=!0;break}a+=i.decode(f,{stream:!0}),a=Ze(t,a)}a&&Ze(t,`${a}\n`)}catch(c){throw c instanceof Error&&c.name==="AbortError"||u.debug("[client-dom] consumeNdjsonStream error",c),c}finally{try{await s.cancel()}catch(c){l||u.debug("[client-dom] reader.cancel failed",c)}try{s.releaseLock()}catch(c){u.debug("[client-dom] reader.releaseLock failed",c)}if(typeof o.cancel=="function")try{await o.cancel()}catch(c){u.debug("[client-dom] stream.cancel failed",c)}if(typeof r?.body?.cancel=="function")try{await r.body.cancel()}catch(c){u.debug("[client-dom] response.body.cancel failed",c)}}}function Bn(e,t){let n=v(e,t),r=[],o=s=>{let i=s;i.dataset?.clientRef&&r.push(i);for(let a of s.children)o(a)};return o(n),r}function Vn(e,t){let n=Bn(e,t);for(let r of n){let o=r.dataset?.clientRef;o&&(r.dataset.hydrated="true",u.debug("[client-dom] marked for hydration",o))}}var zn=new Set(["server","client","html","fragment"]);function et(e){if(!e)return[];try{let t=JSON.parse(e);return Gn(t)?t.nodes:[]}catch{return[]}}async function Ee(e,t,n){return await Promise.all(e.map(r=>jn(r,t,n)))}async function jn(e,t,n){if(e.type==="html")return e.text??e.html??"";let r=await Ee(e.children??[],t,n);if(e.type==="fragment"||e.type==="server"&&!e.component)return t.createElement(t.Fragment,{},...r);if(e.type==="server")return t.createElement(e.component,e.props??{},...r);let o=await n(e.component);return o?t.createElement(o,e.props??{},...r):null}function Gn(e){return!he(e)||e.version!==1||!Array.isArray(e.nodes)?!1:e.nodes.every(t=>tt(t,0))}function tt(e,t){return t>100||!he(e)||!zn.has(e.type)||e.type==="html"&&typeof e.html!="string"&&typeof e.text!="string"||e.type==="client"&&typeof e.component!="string"||e.type==="server"&&e.component!==void 0&&typeof e.component!="string"||e.props!==void 0&&!he(e.props)?!1:e.children===void 0?!0:Array.isArray(e.children)&&e.children.every(n=>tt(n,t+1))}function he(e){return typeof e=="object"&&e!==null&&!Array.isArray(e)}function Wn(e){if(!e)return{};let t={};for(let[n,r]of Object.entries(e))t[n]=Array.isArray(r)?r.join("/"):r;return t}async function te(e,t,n=document){try{let r=Oe(n);if(!r)return e;let s=(await import(r)).wrapForHydration;return typeof s!="function"?e:s(e,{params:Wn(t?.params),frontmatter:t?.frontmatter??{},data:t?.props??{}})}catch(r){return u.debug("router provider wrap failed",r),e}}var Kn="Unknown dependency snapshot",Yn="export default null; // Unknown dependency snapshot",Re="__VF_DEPENDENCY_SNAPSHOT_RECOVERY_STARTED__";function Xn(){return globalThis}async function Jn(e){if(e.status!==409)return!1;try{let t=(await e.clone().text()).trim();return t===Kn||t===Yn}catch{return!1}}async function I(e,t=()=>globalThis.location.reload()){if(!await Jn(e))return!1;let n=Xn();if(n[Re])return!0;n[Re]=!0;try{t()}catch{return delete n[Re],!1}return!0}async function ne(e,t=globalThis.fetch,n=()=>globalThis.location.reload()){try{let r=new URL(e,"http://veryfront.local").searchParams.getAll("pins");if(r.length!==1||!r[0]?.startsWith("on:"))return!1;let o=await t(e,{cache:"no-store"});return await I(o,n)}catch{return!1}}var qn=100;function Zn(e,t){if(globalThis.__VF_CLIENT_MOD_CACHE??(globalThis.__VF_CLIENT_MOD_CACHE=new Map),globalThis.__VF_CLIENT_MOD_CACHE.size>=qn){let n=globalThis.__VF_CLIENT_MOD_CACHE.keys().next().value;n&&globalThis.__VF_CLIENT_MOD_CACHE.delete(n)}globalThis.__VF_CLIENT_MOD_CACHE.set(e,t)}function nt(e){let t=e.match(/^\\/app\\/(.+)#([\\w$.-]+)$/);if(t)return{rel:`/${t[1]||""}`,exportName:t[2]||"default"};let n=e.match(/^(\\/_veryfront\\/[^#]+)#([\\w$.-]+)$/);return n?{moduleUrl:n[1],exportName:n[2]||"default"}:(u.debug("hydrate: unrecognised client ref format, skipping",{ref:e}),null)}function Qn(e){let t=e.dataset?.rscProps;if(!t)return{};try{let n=JSON.parse(t);return n&&typeof n=="object"&&!Array.isArray(n)?n:{}}catch(n){return u.debug("hydrate: invalid client boundary props, using empty props",n),{}}}function er(e){return et(e.dataset?.rscChildren)}function tr(e){return"/_veryfront/rsc/manifest"}function nr(e){return U(e)}async function rr(e=document){try{let t=w(e),n=await fetch(tr(t),{headers:nr(t)});return n.ok?await n.json():(await I(n),null)}catch{return null}}async function rt(e,t,n,r={}){let o=or(e,t,n,r.releaseAssetModules),s=t.moduleUrl??t.rel;if(!s)return null;let i=`${s}#${e.hash??""}`;try{let a=globalThis.__VF_CLIENT_MOD_CACHE?.get(i);if(a)return a}catch(a){u.debug("hydrate: cache get failed",a)}if(!o)return null;try{let a=await(r.importModule??(l=>import(l)))(o);try{Zn(i,a)}catch(l){u.debug("hydrate: cache set failed",l)}return a}catch(a){return u.debug("hydrate: failed to import module",{moduleUrl:o,error:a}),await(r.recoverSnapshotFailure??ne)(o),null}}function or(e,t,n,r){if(t.moduleUrl)return X(t.moduleUrl,e.dependencyPinningCacheKey);if(!t.rel)return null;let o=e.graphIds?.client.find(s=>s.rel===t.rel)?.path;return J({strategy:n,rel:t.rel,absPath:o,version:e.hash,dependencyPinningCacheKey:e.dependencyPinningCacheKey,releaseAssetModules:r})}function sr(e){let t=Array.from(e.querySelectorAll("[data-client-ref]")),n=new Set(t);return t.filter(r=>{let o=r.parentElement;for(;o;){if(n.has(o))return!1;o=o.parentElement}return!0})}async function ot(e=document){let t=null;try{t=await rr(e)}catch(c){u.debug("hydrate: fetch manifest failed",c)}if(!t){u.debug("hydrate: no manifest");return}let n=sr(e);try{let c=globalThis.__VF_MANIFEST_HASH;if(!n.some(f=>f.dataset?.hydrated!=="true")&&c&&t.hash&&c===t.hash)return}catch(c){u.debug("hydrate: hmr hash read failed",c)}if(n.length===0){try{globalThis.__VF_MANIFEST_HASH=t.hash??""}catch(c){u.debug("hydrate: set hash failed",c)}return}let r=w(e),o=Y(r),s=r?.releaseAssetModules;try{if(globalThis.__VF_TEST_MODE__){globalThis.__VF_HYDRATE_CALLED=!0,globalThis.__VF_MANIFEST_HASH=t.hash??"";return}}catch(c){u.debug("hydrate: test mode flags failed",c)}let i=q(e,r?.reactVersion),[{default:a},{createRoot:l}]=await Promise.all([import(i.react),import(i.reactDomClient)]);for(let c of n){let d=c.dataset?.clientRef??"";if(!d||c.dataset?.hydrated==="true")continue;let f=nt(d);if(!f)continue;let E=await rt(t,f,o,{releaseAssetModules:s});if(!E)continue;let F=E[f.exportName]??E.default;if(typeof F=="function")try{let x=l(c),oe=Qn(c),O=er(c),st=await Ee(O,{Fragment:a.Fragment,createElement(B,se,...V){return a.createElement(B,se,...V)}},async B=>{let se=t.modules.find(at=>at.id===B),V=t.components?.[B],Se=se?.clientRef??(V?`${V}#default`:void 0);if(!Se)return null;let ie=nt(Se);if(!ie)return null;let ae=await rt(t,ie,o,{releaseAssetModules:s});if(!ae)return null;let be=ae[ie.exportName]??ae.default;return typeof be=="function"?be:null}),it=await te(a.createElement(F,oe,...st),r,e);x.render(it),c.dataset.hydrated="true"}catch(x){u.warn("hydrate: render failed",x)}}try{globalThis.__VF_MANIFEST_HASH=t.hash??""}catch(c){u.debug("hydrate: set hash failed (post)",c)}}var _e="data-vf-react-head-owner";var ir=2*1024*1024,Us=ir*2;var $s=64*1024,ks=1024*1024,vs=1024*1024;var Fs=new TextEncoder;async function ar(){let e=w(document),t=q(document,e?.reactVersion),[n,r]=await Promise.all([import(t.react),import(t.reactDomClient)]);return{React:n,ReactDOM:r}}var cr=new Set(["SCRIPT","STYLE","NOSCRIPT","TEMPLATE"]);function xe(e){let t=e.getAttribute("style")??"";return e.hasAttribute("data-veryfront-head")||e.hasAttribute("hidden")||/(?:^|;)\\s*display\\s*:\\s*none(?:\\s*;|$)/i.test(t)||cr.has(e.tagName.toUpperCase())}function lr(e,t){return e.find(n=>n.tagName.toUpperCase()==="DIV"&&!!n.getAttribute("class")?.trim()&&!xe(n))??t}function dr(e,t){return e===t}function ur(e,t){let n=document.createElement("div");n.setAttribute("data-veryfront-hydration-root","page");let r=e.find(o=>!xe(o));r?.parentNode===t?t.insertBefore(n,r):t.appendChild(n);for(let o of e)!xe(o)&&o.parentNode===t&&n.appendChild(o);return n}function fr(e,t){for(let n of e){let r=[...n.hasAttribute(_e)?[n]:[],...n.querySelectorAll(`[${_e}]`)];for(let o of r)t.contains(o)||o.remove()}}function gr(e,t,n=document){return!!t?.pagePath&&typeof e?.__veryfrontRenderPage=="function"&&!!n.getElementById("root")}function pr(e,t){return t?.pagePath?!1:!!e.getElementById(L)}function yr(e=import.meta.url){try{return new URL(e,"http://veryfront.local").searchParams.get("hydrate")==="1"}catch{return!1}}function mr(e){return e==="rsc-module"}function hr(e,t){return e?e.startsWith("?")?e:`?${e}`:""}function Er(e,t,n){return J({strategy:t,rel:e,releaseAssetModules:n?.releaseAssetModules,dependencyPinningCacheKey:n?.dependencyPinningCacheKey})}async function Rr(e,t){try{let n=await fetch(H+"stream"+e,{headers:U(t)});if(!n.ok)return await I(n)?"snapshot-conflict":"failure";if(!n.body)return"failure";let r=new AbortController;return addEventListener("pagehide",()=>r.abort(),{once:!0}),await Qe(n,document,r.signal),"success"}catch(n){return u.debug("tryStream failed",n),"failure"}}async function re(){try{await ot(document)}catch(e){u.debug("hydration failed",e)}}async function _r(e,t,n){try{let{React:r,ReactDOM:o}=await ar(),s=Er(e,t,n);if(!s)return!1;u.debug("Loading component from:",s);let i;try{i=await import(s)}catch(E){throw await ne(s),E}let a=i.default;if(typeof a!="function")return u.debug("Page component is not a function"),!1;let l=Array.from(document.body.children),c=lr(l,document.body),d=dr(c,document.body)?ur(l,document.body):c;fr(l,d);let f=await te(r.createElement(a,{}),n);return mr(t)?o.createRoot(d).render(f):o.hydrateRoot(d,f,{identifierPrefix:"vf",onRecoverableError:()=>{}}),u.debug("Page component hydrated successfully"),!0}catch(r){return u.error("Page hydration failed",r),!1}}async function xr(e,t){try{let n=await fetch(H+"payload"+e,{headers:U(t)});if(!n.ok)return await I(n)?"snapshot-conflict":"failure";let r=await n.json();if(K(document,r?.dependencyPinningCacheKey),r?.slots){for(let[o,s]of Object.entries(r.slots))v(document,o).innerHTML=k(String(s||""));return"success"}return v(document,L).innerHTML=k(String(r?.html||"")),"success"}catch(n){return u.debug("payload fetch failed",n),"failure"}}async function Sr(){try{let e=w(document),t=hr(globalThis.window?.location.search??"",e?.dependencyPinningCacheKey);if(yr()){await re();return}let n=e?.pagePath,r=Y(e);if(n){if(gr(globalThis.window,e,document)){u.debug("Page renderer owns hydration");return}u.debug("Found page component in hydration data:",n),await _r(n,r,e)&&u.debug("Client component hydrated successfully");return}if(!pr(document,e))return;let o=await Rr(t,e);if(o==="snapshot-conflict")return;if(o==="success"){await re();return}let s=await xr(t,e);if(s==="snapshot-conflict")return;if(s==="success"){await re();return}await re()}catch(e){u.error("boot failed",e)}}if(typeof document<"u"){let e=()=>{Sr()};document.readyState==="loading"?document.addEventListener("DOMContentLoaded",e,{once:!0}):e()}export{Sr as boot,Er as buildPageHydrationModuleUrl,hr as buildRSCTransportQuery,fr as retireAbandonedHeadOwnerMarkers,lr as selectHydrationRoot,pr as shouldAttemptRSCTransport,yr as shouldHydrateOnly,mr as shouldRenderPageComponent,gr as shouldUsePageRendererHydration,dr as shouldWrapPageHydrationRoot};\n'; export const CLIENT_DOM_BUNDLE: string = - 'var yt=Object.defineProperty;var ht=(t,n,e)=>n in t?yt(t,n,{enumerable:!0,configurable:!0,writable:!0,value:e}):t[n]=e;var m=(t,n,e)=>ht(t,typeof n!="symbol"?n+"":n,e);var O={reset:"\\x1B[0m",dim:"\\x1B[2m",gray:"\\x1B[90m",red:"\\x1B[31m",green:"\\x1B[32m",yellow:"\\x1B[33m",blue:"\\x1B[34m",magenta:"\\x1B[35m",cyan:"\\x1B[36m"},Me={debug:O.gray,info:O.green,warn:O.yellow,error:O.red};var p="[REDACTED]",g=Reflect.apply,xt=Array.prototype.pop,Rt=Array.prototype.push;var He=Array.prototype,je=BigInt.prototype.toString,v=Map,St=Map.prototype.delete,_t=Map.prototype.get,bt=Map.prototype.keys,At=Map.prototype.set;var x=Object.getOwnPropertyDescriptor,Tt=Object.getPrototypeOf,ze=Object.hasOwn,Ve=Object.prototype,It=Set,Ct=decodeURIComponent,S=URL,Fe=Number.isFinite,Ge=Number.isInteger,w=RegExp.prototype.exec,Ot=x(RegExp.prototype,"global").get,Nt=x(RegExp.prototype,"unicode").get,Dt=String.prototype.charCodeAt,Ut=String.prototype.includes,Lt=String.prototype.indexOf,V=String.prototype.slice,W=String.prototype.startsWith,Y=String.prototype.toLowerCase,$t=Set.prototype.add,Be=Set.prototype.delete,wt=Set.prototype.has,Pt=Tt(new v().keys()).next,Mt=x(Map.prototype,"size").get,ve=x(S.prototype,"host").get,We=x(S.prototype,"origin").get,kt=x(S.prototype,"password").get,Ye=x(S.prototype,"pathname").get,Ke=x(S.prototype,"protocol").get,Ht=x(S.prototype,"username").get,jt=/[^a-z0-9]/g,zt=/([a-z0-9])([A-Z])/g,Vt=/([A-Z])([A-Z][a-z])/g,Ft=/\\b(?:sk-[A-Za-z0-9._-]{8,}|gh[po]_[A-Za-z0-9._-]{8,}|xox[baprs]-[A-Za-z0-9._-]{8,}|eyJ[A-Za-z0-9._-]{8,})\\b/g;function h(t,n,e){let r=g(Ot,n,[]),o=g(Nt,n,[]),s=0,i=!1,a="";n.lastIndex=0;try{for(;;){let u=g(w,n,[t]);if(u===null)break;let c=u[0],l=u.index;if(a+=R(t,s,l),a+=typeof e=="string"?e:e(u),s=l+c.length,i=!0,!r)break;c.length===0&&(n.lastIndex=Gt(t,l,o))}}finally{n.lastIndex=0}return i?a+R(t,s):t}function P(t){let n=g(Y,t,[]);return h(n,jt,"")}function b(t,n){return g(Dt,t,[n])}function Gt(t,n,e){let r=n+1;if(!e||r>=t.length)return r;let o=b(t,n);if(o<55296||o>56319)return r;let s=b(t,r);return s>=56320&&s<=57343?n+2:r}function R(t,n,e){return e===void 0?g(V,t,[n]):g(V,t,[n,e])}function Bt(t){let n=[],e=0;for(let r=0;r<=t.length;r++){let o=r===t.length?-1:b(t,r);o>=97&&o<=122||o>=48&&o<=57||(r>e&&(n[n.length]=R(t,e,r)),e=r+1)}return n}var N=["password","passwd","pwd","passphrase","secret","clientsecret","token","apikey","accesskey","privatekey","credential","authheader","authorization","cookie","bearer","jwt","connectionstring","signature","sessionid","sid","otp","mfa","pin","salt","xsrf","csrf"],vt=512,Wt=128,C=new v;var Yt=256;function Kt(t){let n=t.length<=Wt;if(n){let o=g(_t,C,[t]);if(o!==void 0)return o}let e=P(t),r=e==="auth";for(let o=0;!r&&o=vt){let s=g(bt,C,[]),i=g(Pt,s,[]).value;i!==void 0&&g(St,C,[i])}g(At,C,[t,r])}return r}var F=["access_token","accesstoken","refresh_token","api_key","apikey","code","token","secret","client_secret","password","passwd","pwd","state","sig","signature","auth","x-amz-credential","x-amz-signature","x-amz-security-token","x-goog-credential","x-goog-signature"],K=new It;for(let t=0;t=65&&n<=90||n>=97&&n<=122}function X(t){return Qt(t)||t==="_"||t==="$"}function te(t){if(!t)return!1;let n=b(t,0);return X(t)||n>=48&&n<=57||t==="."||t==="-"}function J(t,n){let e=n,r=t[e]===\'"\'||t[e]==="\'"?t[e++]:"";if(!X(t[e]))return!1;for(e++;te(t[e]);)e++;if(r){if(t[e]!==r)return!1;e++}for(;t[e]===" "||t[e]==="\t";)e++;return t[e]===":"||t[e]==="="}function q(t){return t==="\\r"||t===`\n`||t==="}"||t==="]"||Zt(t)}function Z(t,n){let e=n;for(;e=t.length||J(t,e)}function ee(t,n){let e=n,r=!0;if(g(W,t,[p,n])){let l=n+p.length;if(G(t,l))return{end:l,replacement:p};e=l,r=!1}let o=r&&(t[e]===\'"\'||t[e]==="\'"||t[e]==="`")?t[e]:"",s=!1,i=()=>o?`${o}${p}${s?o:""}`:p,a=[],u="",c=-1;for(let l=e;l0&&(f==="}"||f==="]")){if(a[a.length-1]!==f)return{end:t.length,replacement:i()};if(g(xt,a,[]),l++,a.length===0&&G(t,l))return{end:l,replacement:i()};continue}if(a.length>0||!q(f)){l++;continue}let I=l;if(l=Z(t,l),l>=t.length||J(t,l))return{end:I,replacement:i()}}return{end:t.length,replacement:i()}}function B(t,n,e,r){let o=0,s="";for(let i=g(w,n,[t]);i;i=g(w,n,[t])){let a=i[e];if(!ne(a))continue;let u=n.lastIndex,c=r===void 0?void 0:i[r],l=u+p.length;if((c==="?"||c==="&"||c===";")&&g(W,t,[p,u])&&t[l]==="#")continue;let f=ee(t,u);s+=R(t,o,i.index),s+=i[0],s+=f.replacement,o=f.end,n.lastIndex=f.end}return o===0?t:s+R(t,o)}function ne(t){if(t.length>Yt)return!0;let n=h(t,Vt,s=>`${s[1]} ${s[2]}`),e=h(n,zt,s=>`${s[1]} ${s[2]}`),r=g(Y,e,[]),o=Bt(r);for(let s=0;s{let r=e[1],o=e[2],s=g(Lt,o,[":"]);if(s===-1)return`${r}${p}@`;let i=R(o,0,s);return`${r}${i}:${p}@`});return n=h(n,Jt,e=>{let r=e[1],o=e[2],s=e[3];return re(r,o,s)?e[0]:`${r}${o}:${p}@`}),n=h(n,/([?#&;])([-a-z0-9_.%\\[\\]]+)=([^&#;\\s]*)/gi,e=>{let r=e[1],o=e[2],s=oe(o);return g(wt,K,[P(s)])||Kt(s)?`${r}${o}=${p}`:e[0]}),n=h(n,/(^|[^a-z0-9_-])((?:set-cookie|cookie)\\s*:\\s*)[^\\r\\n]*/gi,e=>`${e[1]}${e[2]}${p}`),n=h(n,/\\b(authorization\\s*[:=]\\s*)[^\\r\\n]*/gi,e=>`${e[1]}${p}`),n=h(n,/\\b(bearer|basic)(\\s+)(?:"[^"\\r\\n]*"|\'[^\'\\r\\n]*\'|[a-z0-9._~+/=-]+)/gi,e=>`${e[1]}${e[2]}${p}`),n=h(n,Ft,p),n=B(n,/(["\'])([_$a-z][a-z0-9_.$-]*)\\1(\\s*[:=]\\s*)/gi,2),n=B(n,/(^|[^a-z0-9_.$-])([_$a-z][a-z0-9_.$-]*)(\\s*[:=]\\s*)/gi,2,1),n}var se=2048;var Qe=64*1024,ie=256,ae="https://veryfront.com/docs/code/guides/errors#",Q="...[truncated]",k="unknown-error";function tt(t,n){if(t.length<=n)return t;let e=Math.max(0,n-Q.length);return`${ce(t,e)}${Q}`}function ce(t,n){let e=t.slice(0,n),r=e.charCodeAt(e.length-1);return r>=55296&&r<=56319&&(e=e.slice(0,-1)),e}function ue(t){let n="";for(let e=0;e=55296&&r<=56319){let o=t.charCodeAt(e+1);o>=56320&&o<=57343?(n+=t.slice(e,e+2),e++):n+="\\uFFFD";continue}n+=r>=56320&&r<=57343?"\\uFFFD":t.charAt(e)}return n}function A(t){return typeof t!="string"?p:tt(M(t),se)}function le(t){let n=typeof t=="string"?M(t):k,e=tt(n||k,ie),r=ue(e);return r==="."||r===".."?k:r}function D(t){let n=encodeURIComponent(le(t));return`${ae}${n}`}var rt=Reflect.apply,de=Object.freeze,ge=Object.getOwnPropertyDescriptors,et=Number.isFinite,ot=new WeakSet,fe=WeakSet.prototype.add,pe=WeakSet.prototype.has,me=new Set(["CONFIG","BUILD","RUNTIME","ROUTE","MODULE","SERVER","BOUNDARY","DEV","DEPLOY","AGENT","GENERAL"]);function E(t){let n={...t},e={...n,create(r){let o=r?.message,s=r?.detail,i=r?.cause,a=r?.instance,u=r?.context,c=r?.status??n.status;return new H(o||s||n.title,{slug:n.slug,category:n.category,status:c,title:n.title,suggestion:n.suggestion,exitCode:n.exitCode,detail:s,cause:i,instance:a,context:u})}};return de(e)}var H=class extends Error{constructor(e,r){super(e);m(this,"slug");m(this,"category");m(this,"status");m(this,"title");m(this,"suggestion");m(this,"exitCode");m(this,"detail");m(this,"cause");m(this,"instance");m(this,"context");rt(fe,ot,[this]),this.name="VeryfrontError",this.slug=r.slug,this.category=r.category,this.status=r.status,this.title=r.title,this.suggestion=r.suggestion,this.exitCode=r.exitCode,this.detail=r.detail,this.cause=r.cause,this.instance=r.instance,this.context=r.context}toRFC9457(){let e=nt(this);return e?{type:D(e.slug),title:A(e.title),status:e.status,detail:e.detail===void 0?void 0:A(e.detail),instance:e.instance===void 0?void 0:A(e.instance),category:e.category,suggestion:e.suggestion===void 0?void 0:A(e.suggestion),cause:typeof e.cause=="string"?A(e.cause):void 0}:{type:D("unknown-error"),title:"Unknown/unclassified error",status:500,category:"GENERAL"}}getDocsUrl(){let e=nt(this);return D(e?.slug??"unknown-error")}};function st(t){return typeof t=="object"&&t!==null&&rt(pe,ot,[t])===!0}function nt(t){return st(t)?Ee(t):null}function Ee(t){try{if(!st(t))return null;let n=ge(t),e=Et=>{let $=n[Et];return $&&"value"in $?$.value:void 0},r=e("slug"),o=e("category"),s=e("status"),i=e("title"),a=e("message"),u=e("suggestion"),c=e("exitCode"),l=e("detail"),f=e("cause"),I=e("instance"),mt=e("context"),L=e("stack");return typeof r!="string"||!me.has(o)||typeof s!="number"||!et(s)||typeof i!="string"||typeof a!="string"||u!==void 0&&typeof u!="string"||c!==void 0&&(typeof c!="number"||!et(c))||l!==void 0&&typeof l!="string"||I!==void 0&&typeof I!="string"||L!==void 0&&typeof L!="string"?null:{slug:r,category:o,status:s,title:i,message:a,suggestion:u,exitCode:c,detail:l,cause:f,instance:I,context:mt,stack:L}}catch{return null}}var sn=E({slug:"unknown-error",category:"GENERAL",status:500,title:"Unknown/unclassified error",suggestion:"Check logs for more details"}),an=E({slug:"authentication-required",category:"GENERAL",status:401,title:"Authentication required",suggestion:"Set VERYFRONT_API_TOKEN or run \'veryfront login\'"}),cn=E({slug:"permission-denied",category:"GENERAL",status:403,title:"File/resource permission denied",suggestion:"Check file permissions and access rights"}),un=E({slug:"file-not-found",category:"GENERAL",status:404,title:"File not found",suggestion:"Verify the file path exists"}),ln=E({slug:"resource-not-found",category:"GENERAL",status:404,title:"Requested resource not found",suggestion:"Verify the referenced resource ID or name exists"}),dn=E({slug:"invalid-argument",category:"GENERAL",status:400,title:"Invalid function argument",suggestion:"Check argument types and values",exitCode:2}),gn=E({slug:"timeout-error",category:"GENERAL",status:408,title:"Operation timed out",suggestion:"Increase timeout or optimize the operation"}),fn=E({slug:"initialization-error",category:"GENERAL",status:500,title:"Initialization failed",suggestion:"Check initialization requirements and dependencies"}),pn=E({slug:"not-supported",category:"GENERAL",status:501,title:"Feature not supported",suggestion:"Check documentation for supported features"}),it=E({slug:"security-violation",category:"GENERAL",status:403,title:"Security violation detected",suggestion:"Check for path traversal or unauthorized access attempts"}),mn=E({slug:"input-validation-failed",category:"GENERAL",status:400,title:"Input validation failed",suggestion:"Check request input against validation rules"}),En=E({slug:"project-source-empty",category:"GENERAL",status:400,title:"Project source is empty",suggestion:"Add project files or run \'veryfront init\'"}),yn=E({slug:"nested-cwd-scope",category:"GENERAL",status:500,title:"Working directory scope nested inside another",suggestion:"Do the inner work directly in the outer scope\'s callback instead of opening a second one"});var ye=[{source:String.raw`]*>[\\s\\S]*?<\\/script>`,flags:"gi",name:"inline script"},{source:String.raw`javascript:`,flags:"gi",name:"javascript: URL"},{source:String.raw`\\bon\\w+\\s*=`,flags:"gi",name:"event handler attribute"},{source:String.raw`data:\\s*text\\/html`,flags:"gi",name:"data: HTML URL"}];function he(){return ye.map(({source:t,flags:n,name:e})=>({pattern:new RegExp(t,n),name:e}))}function xe(){let t=globalThis;return t.__VERYFRONT_DEV__===!0||t.Deno?.env?.get?.("VERYFRONT_ENV")==="development"}function at(t,n={}){let{allowInlineScripts:e=!1,strict:r=!1,warn:o=!0}=n;for(let{pattern:s,name:i}of he())if(!(e&&i==="inline script")&&(s.lastIndex=0,!!s.test(t)&&(o&&console.warn(`[Security] Suspicious ${i} detected in server HTML`),r||!xe())))throw it.create({detail:`Potentially unsafe HTML: ${i} detected`});return t}var T=class{constructor(n,e){m(this,"prefix",n);m(this,"level",e)}log(n,e,r,...o){this.level>n||e?.(r,...o)}debug(n,...e){this.log(0,console.debug,`[${this.prefix}] DEBUG: ${n}`,...e)}info(n,...e){this.log(1,console.log,`[${this.prefix}] ${n}`,...e)}warn(n,...e){this.log(2,console.warn,`[${this.prefix}] WARN: ${n}`,...e)}error(n,...e){this.log(3,console.error,`[${this.prefix}] ERROR: ${n}`,...e)}};function Re(){if(typeof window>"u")return 2;let t=globalThis;return t.__VERYFRONT_DEV__||t.__RSC_DEV__?t.__VERYFRONT_DEBUG__||t.__RSC_DEBUG__?0:1:2}var U=Re(),y=new T("RSC",U),Sn=new T("PREFETCH",U),_n=new T("HYDRATE",U),bn=new T("VERYFRONT",U);var In=Object.freeze({IPV4:"127.0.0.1",IPV6:"::1",HOSTNAME:"localhost"});var Se=5e3,_e=1e4,Nn=16*1024*1024,be=5e3;var Ae=100;var Te=Object.freeze([5,10,25,50,75,100,250,500,750,1e3,2500,5e3,7500,1e4]),Dn=Object.freeze([1,5,10,25,50,100,250,500,1e3,2500,5e3,1e4]),Un=Object.freeze({server:Object.freeze({port:3e3,hostname:"0.0.0.0"}),timeouts:Object.freeze({default:Se,api:3e4,ssr:_e,hmr:3e4,sandbox:be}),cache:Object.freeze({jit:Object.freeze({maxSize:Ae,tempDirPrefix:"vf-bundle-"})}),metrics:Object.freeze({ssrBoundaries:Te})});var d="/_veryfront",j={RSC:`${d}/rsc/`,FS:`${d}/fs/`,MODULES:`${d}/modules/`,PAGES:`${d}/pages/`,DATA:`${d}/data/`,LIB:`${d}/lib/`,CHUNKS:`${d}/chunks/`,CLIENT:`${d}/client/`},ut={HMR_RUNTIME:`${d}/hmr-runtime.js`,HMR:`${d}/hmr.js`,ERROR_OVERLAY:`${d}/error-overlay.js`,DEV_LOADER:`${d}/dev-loader.js`,CLIENT_LOG:`${d}/log`,CLIENT_JS:`${d}/client.js`,ROUTER_JS:`${d}/router.js`,PREFETCH_JS:`${d}/prefetch.js`,MANIFEST_JSON:`${d}/manifest.json`,APP_JS:`${d}/app.js`,RSC_CLIENT:`${d}/rsc/client.js`,RSC_MANIFEST:`${d}/rsc/manifest`,RSC_STREAM:`${d}/rsc/stream`,RSC_PAYLOAD:`${d}/rsc/payload`,RSC_RENDER:`${d}/rsc/render`,RSC_PAGE:`${d}/rsc/page`,RSC_MODULE:`${d}/rsc/module`,RSC_DOM:`${d}/rsc/dom.js`,LIB_CHAT_REACT:`${d}/lib/chat/react.js`,LIB_CHAT_COMPONENTS:`${d}/lib/chat/components.js`,LIB_CHAT_PRIMITIVES:`${d}/lib/chat/primitives.js`};var Ie={ROOT:".veryfront",CACHE:".veryfront/cache",KV:".veryfront/kv",LOGS:".veryfront/logs",TMP:".veryfront/tmp"},$n=Ie.CACHE;var wn={HMR_RUNTIME:ut.HMR_RUNTIME,ERROR_OVERLAY:ut.ERROR_OVERLAY};var Ce=j.RSC,Oe=j.FS;var lt="rsc-root",z="x-veryfront-dependency-pins";var jn=Array.prototype.at,zn=Array.prototype.filter,Vn=Array.prototype.join,Fn=Array.prototype.map,Gn=Array.prototype.pop,Bn=Array.prototype.push,vn=Array.prototype.sort;var Yn=Set.prototype.has,Kn=String.prototype.charCodeAt,Xn=String.prototype.includes,Jn=String.prototype.indexOf,qn=String.prototype.lastIndexOf,Zn=String.prototype.slice,Qn=String.prototype.split,tr=String.prototype.startsWith;var _=Object.getOwnPropertyDescriptor,er=_(URL.prototype,"origin").get,nr=_(URL.prototype,"pathname").get,rr=_(URL.prototype,"protocol").get,or=_(URL.prototype,"hostname").get,sr=_(URL.prototype,"port").get,ir=_(URL.prototype,"search").get,ar=_(URL.prototype,"hash").get;var Tr=Object.freeze({react:"","react-dom":"","react-dom/client":"","react-dom/server":"","react/jsx-runtime":"","react/jsx-dev-runtime":""});var De="veryfront-hydration-data";function dt(t){try{let n=[...t.querySelectorAll(`[id="${De}"]`)];if(n.length!==1)return null;let e=t.body;if(!e)return null;let r=n[0];return e.firstElementChild!==r&&r.parentElement!==e||r.tagName?.toLowerCase()!=="script"||r.getAttribute("type")?.trim().toLowerCase()!=="application/json"?null:r}catch{return null}}function gt(t,n){if(!n?.startsWith("on:"))return!1;try{let e=dt(t);if(!e)return!1;let r=JSON.parse(e.textContent||"{}");return r.dependencyPinningCacheKey=n,e.textContent=JSON.stringify(r),!0}catch(e){return y.debug("hydration dependency snapshot seed failed",e),!1}}function pt(t,n){let e=n==="root"?lt:`rsc-slot-${n}`,r=t.getElementById(e);if(r)return r;let o=t.createElement("div");return o.id=e,t.body.appendChild(o),o}function Ue(t,n){if(n.type!=="slot")return;let e=pt(t,n.id);e.innerHTML=at(String(n.html??""))}function ft(t,n){let e=n.split(`\n`),r=e.pop()??"";for(let o of e){let s=o.trim();if(!s)continue;let i;try{i=JSON.parse(s)}catch(u){y.debug("[client-dom] malformed NDJSON line",{line:s,error:u instanceof Error?u.message:String(u)});continue}if(!i||typeof i!="object")continue;let a=i;if(a.type==="slot"){Ue(t,a);try{we(t,a.id||"root")}catch(u){y.debug("[client-dom] hydration optional failed",u)}}}return r}function Le(t){return new Promise((n,e)=>{let r=()=>e(new DOMException("aborted","AbortError"));if(t.aborted){r();return}t.addEventListener("abort",r,{once:!0})})}async function vr(t,n=document,e){let r="body"in t?t:null,o=r?.body??t;if(!o)return;r&>(n,r.headers.get(z));let s=o.getReader(),i=new TextDecoder,a="",u=!1;try{for(;;){if(e?.aborted)throw new DOMException("aborted","AbortError");let c=s.read(),{done:l,value:f}=e?await Promise.race([c,Le(e)]):await c;if(l){u=!0;break}a+=i.decode(f,{stream:!0}),a=ft(n,a)}a&&ft(n,`${a}\n`)}catch(c){throw c instanceof Error&&c.name==="AbortError"||y.debug("[client-dom] consumeNdjsonStream error",c),c}finally{try{await s.cancel()}catch(c){u||y.debug("[client-dom] reader.cancel failed",c)}try{s.releaseLock()}catch(c){y.debug("[client-dom] reader.releaseLock failed",c)}if(typeof o.cancel=="function")try{await o.cancel()}catch(c){y.debug("[client-dom] stream.cancel failed",c)}if(typeof r?.body?.cancel=="function")try{await r.body.cancel()}catch(c){y.debug("[client-dom] response.body.cancel failed",c)}}}function $e(t,n){let e=pt(t,n),r=[],o=s=>{let i=s;i.dataset?.clientRef&&r.push(i);for(let a of s.children)o(a)};return o(e),r}function we(t,n){let e=$e(t,n);for(let r of e){let o=r.dataset?.clientRef;o&&(r.dataset.hydrated="true",y.debug("[client-dom] marked for hydration",o))}}export{vr as consumeNdjsonStream,pt as getContainer};\n'; + 'var yt=Object.defineProperty;var xt=(t,n,e)=>n in t?yt(t,n,{enumerable:!0,configurable:!0,writable:!0,value:e}):t[n]=e;var E=(t,n,e)=>xt(t,typeof n!="symbol"?n+"":n,e);var O={reset:"\\x1B[0m",dim:"\\x1B[2m",gray:"\\x1B[90m",red:"\\x1B[31m",green:"\\x1B[32m",yellow:"\\x1B[33m",blue:"\\x1B[34m",magenta:"\\x1B[35m",cyan:"\\x1B[36m"},Me={debug:O.gray,info:O.green,warn:O.yellow,error:O.red};var p="[REDACTED]",g=Reflect.apply,ht=Array.prototype.pop,Rt=Array.prototype.push;var He=Array.prototype,je=BigInt.prototype.toString,v=Map,St=Map.prototype.delete,_t=Map.prototype.get,bt=Map.prototype.keys,At=Map.prototype.set;var h=Object.getOwnPropertyDescriptor,Tt=Object.getPrototypeOf,ze=Object.hasOwn,Ve=Object.prototype,Ct=Set,It=decodeURIComponent,S=URL,Fe=Number.isFinite,Ge=Number.isInteger,w=RegExp.prototype.exec,Ot=h(RegExp.prototype,"global").get,Nt=h(RegExp.prototype,"unicode").get,Dt=String.prototype.charCodeAt,Ut=String.prototype.includes,Lt=String.prototype.indexOf,V=String.prototype.slice,W=String.prototype.startsWith,Y=String.prototype.toLowerCase,$t=Set.prototype.add,Be=Set.prototype.delete,wt=Set.prototype.has,Pt=Tt(new v().keys()).next,Mt=h(Map.prototype,"size").get,ve=h(S.prototype,"host").get,We=h(S.prototype,"origin").get,kt=h(S.prototype,"password").get,Ye=h(S.prototype,"pathname").get,Ke=h(S.prototype,"protocol").get,Ht=h(S.prototype,"username").get,jt=/[^a-z0-9]/g,zt=/([a-z0-9])([A-Z])/g,Vt=/([A-Z])([A-Z][a-z])/g,Ft=/\\b(?:sk-[A-Za-z0-9._-]{8,}|gh[po]_[A-Za-z0-9._-]{8,}|xox[baprs]-[A-Za-z0-9._-]{8,}|eyJ[A-Za-z0-9._-]{8,})\\b/g;function x(t,n,e){let r=g(Ot,n,[]),o=g(Nt,n,[]),s=0,i=!1,a="";n.lastIndex=0;try{for(;;){let u=g(w,n,[t]);if(u===null)break;let c=u[0],l=u.index;if(a+=R(t,s,l),a+=typeof e=="string"?e:e(u),s=l+c.length,i=!0,!r)break;c.length===0&&(n.lastIndex=Gt(t,l,o))}}finally{n.lastIndex=0}return i?a+R(t,s):t}function P(t){let n=g(Y,t,[]);return x(n,jt,"")}function b(t,n){return g(Dt,t,[n])}function Gt(t,n,e){let r=n+1;if(!e||r>=t.length)return r;let o=b(t,n);if(o<55296||o>56319)return r;let s=b(t,r);return s>=56320&&s<=57343?n+2:r}function R(t,n,e){return e===void 0?g(V,t,[n]):g(V,t,[n,e])}function Bt(t){let n=[],e=0;for(let r=0;r<=t.length;r++){let o=r===t.length?-1:b(t,r);o>=97&&o<=122||o>=48&&o<=57||(r>e&&(n[n.length]=R(t,e,r)),e=r+1)}return n}var N=["password","passwd","pwd","passphrase","secret","clientsecret","token","apikey","accesskey","privatekey","credential","authheader","authorization","cookie","bearer","jwt","connectionstring","signature","sessionid","sid","otp","mfa","pin","salt","xsrf","csrf"],vt=512,Wt=128,I=new v;var Yt=256;function Kt(t){let n=t.length<=Wt;if(n){let o=g(_t,I,[t]);if(o!==void 0)return o}let e=P(t),r=e==="auth";for(let o=0;!r&&o=vt){let s=g(bt,I,[]),i=g(Pt,s,[]).value;i!==void 0&&g(St,I,[i])}g(At,I,[t,r])}return r}var F=["access_token","accesstoken","refresh_token","api_key","apikey","code","token","secret","client_secret","password","passwd","pwd","state","sig","signature","auth","x-amz-credential","x-amz-signature","x-amz-security-token","x-goog-credential","x-goog-signature"],K=new Ct;for(let t=0;t=65&&n<=90||n>=97&&n<=122}function X(t){return Qt(t)||t==="_"||t==="$"}function te(t){if(!t)return!1;let n=b(t,0);return X(t)||n>=48&&n<=57||t==="."||t==="-"}function J(t,n){let e=n,r=t[e]===\'"\'||t[e]==="\'"?t[e++]:"";if(!X(t[e]))return!1;for(e++;te(t[e]);)e++;if(r){if(t[e]!==r)return!1;e++}for(;t[e]===" "||t[e]==="\t";)e++;return t[e]===":"||t[e]==="="}function q(t){return t==="\\r"||t===`\n`||t==="}"||t==="]"||Zt(t)}function Z(t,n){let e=n;for(;e=t.length||J(t,e)}function ee(t,n){let e=n,r=!0;if(g(W,t,[p,n])){let l=n+p.length;if(G(t,l))return{end:l,replacement:p};e=l,r=!1}let o=r&&(t[e]===\'"\'||t[e]==="\'"||t[e]==="`")?t[e]:"",s=!1,i=()=>o?`${o}${p}${s?o:""}`:p,a=[],u="",c=-1;for(let l=e;l0&&(f==="}"||f==="]")){if(a[a.length-1]!==f)return{end:t.length,replacement:i()};if(g(ht,a,[]),l++,a.length===0&&G(t,l))return{end:l,replacement:i()};continue}if(a.length>0||!q(f)){l++;continue}let C=l;if(l=Z(t,l),l>=t.length||J(t,l))return{end:C,replacement:i()}}return{end:t.length,replacement:i()}}function B(t,n,e,r){let o=0,s="";for(let i=g(w,n,[t]);i;i=g(w,n,[t])){let a=i[e];if(!ne(a))continue;let u=n.lastIndex,c=r===void 0?void 0:i[r],l=u+p.length;if((c==="?"||c==="&"||c===";")&&g(W,t,[p,u])&&t[l]==="#")continue;let f=ee(t,u);s+=R(t,o,i.index),s+=i[0],s+=f.replacement,o=f.end,n.lastIndex=f.end}return o===0?t:s+R(t,o)}function ne(t){if(t.length>Yt)return!0;let n=x(t,Vt,s=>`${s[1]} ${s[2]}`),e=x(n,zt,s=>`${s[1]} ${s[2]}`),r=g(Y,e,[]),o=Bt(r);for(let s=0;s{let r=e[1],o=e[2],s=g(Lt,o,[":"]);if(s===-1)return`${r}${p}@`;let i=R(o,0,s);return`${r}${i}:${p}@`});return n=x(n,Jt,e=>{let r=e[1],o=e[2],s=e[3];return re(r,o,s)?e[0]:`${r}${o}:${p}@`}),n=x(n,/([?#&;])([-a-z0-9_.%\\[\\]]+)=([^&#;\\s]*)/gi,e=>{let r=e[1],o=e[2],s=oe(o);return g(wt,K,[P(s)])||Kt(s)?`${r}${o}=${p}`:e[0]}),n=x(n,/(^|[^a-z0-9_-])((?:set-cookie|cookie)\\s*:\\s*)[^\\r\\n]*/gi,e=>`${e[1]}${e[2]}${p}`),n=x(n,/\\b(authorization\\s*[:=]\\s*)[^\\r\\n]*/gi,e=>`${e[1]}${p}`),n=x(n,/\\b(bearer|basic)(\\s+)(?:"[^"\\r\\n]*"|\'[^\'\\r\\n]*\'|[a-z0-9._~+/=-]+)/gi,e=>`${e[1]}${e[2]}${p}`),n=x(n,Ft,p),n=B(n,/(["\'])([_$a-z][a-z0-9_.$-]*)\\1(\\s*[:=]\\s*)/gi,2),n=B(n,/(^|[^a-z0-9_.$-])([_$a-z][a-z0-9_.$-]*)(\\s*[:=]\\s*)/gi,2,1),n}var se=2048;var Qe=64*1024,ie=256,ae="https://veryfront.com/docs/code/guides/errors#",Q="...[truncated]",k="unknown-error";function tt(t,n){if(t.length<=n)return t;let e=Math.max(0,n-Q.length);return`${ce(t,e)}${Q}`}function ce(t,n){let e=t.slice(0,n),r=e.charCodeAt(e.length-1);return r>=55296&&r<=56319&&(e=e.slice(0,-1)),e}function ue(t){let n="";for(let e=0;e=55296&&r<=56319){let o=t.charCodeAt(e+1);o>=56320&&o<=57343?(n+=t.slice(e,e+2),e++):n+="\\uFFFD";continue}n+=r>=56320&&r<=57343?"\\uFFFD":t.charAt(e)}return n}function A(t){return typeof t!="string"?p:tt(M(t),se)}function le(t){let n=typeof t=="string"?M(t):k,e=tt(n||k,ie),r=ue(e);return r==="."||r===".."?k:r}function D(t){let n=encodeURIComponent(le(t));return`${ae}${n}`}var rt=Reflect.apply,de=Object.freeze,ge=Object.getOwnPropertyDescriptors,et=Number.isFinite,ot=new WeakSet,fe=WeakSet.prototype.add,pe=WeakSet.prototype.has,me=new Set(["CONFIG","BUILD","RUNTIME","ROUTE","MODULE","SERVER","BOUNDARY","DEV","DEPLOY","AGENT","GENERAL"]);function m(t){let n={...t},e={...n,create(r){let o=r?.message,s=r?.detail,i=r?.cause,a=r?.instance,u=r?.context,c=r?.status??n.status;return new H(o||s||n.title,{slug:n.slug,category:n.category,status:c,title:n.title,suggestion:n.suggestion,exitCode:n.exitCode,detail:s,cause:i,instance:a,context:u})}};return de(e)}var H=class extends Error{constructor(e,r){super(e);E(this,"slug");E(this,"category");E(this,"status");E(this,"title");E(this,"suggestion");E(this,"exitCode");E(this,"detail");E(this,"cause");E(this,"instance");E(this,"context");rt(fe,ot,[this]),this.name="VeryfrontError",this.slug=r.slug,this.category=r.category,this.status=r.status,this.title=r.title,this.suggestion=r.suggestion,this.exitCode=r.exitCode,this.detail=r.detail,this.cause=r.cause,this.instance=r.instance,this.context=r.context}toRFC9457(){let e=nt(this);return e?{type:D(e.slug),title:A(e.title),status:e.status,detail:e.detail===void 0?void 0:A(e.detail),instance:e.instance===void 0?void 0:A(e.instance),category:e.category,suggestion:e.suggestion===void 0?void 0:A(e.suggestion),cause:typeof e.cause=="string"?A(e.cause):void 0}:{type:D("unknown-error"),title:"Unknown/unclassified error",status:500,category:"GENERAL"}}getDocsUrl(){let e=nt(this);return D(e?.slug??"unknown-error")}};function st(t){return typeof t=="object"&&t!==null&&rt(pe,ot,[t])===!0}function nt(t){return st(t)?Ee(t):null}function Ee(t){try{if(!st(t))return null;let n=ge(t),e=Et=>{let $=n[Et];return $&&"value"in $?$.value:void 0},r=e("slug"),o=e("category"),s=e("status"),i=e("title"),a=e("message"),u=e("suggestion"),c=e("exitCode"),l=e("detail"),f=e("cause"),C=e("instance"),mt=e("context"),L=e("stack");return typeof r!="string"||!me.has(o)||typeof s!="number"||!et(s)||typeof i!="string"||typeof a!="string"||u!==void 0&&typeof u!="string"||c!==void 0&&(typeof c!="number"||!et(c))||l!==void 0&&typeof l!="string"||C!==void 0&&typeof C!="string"||L!==void 0&&typeof L!="string"?null:{slug:r,category:o,status:s,title:i,message:a,suggestion:u,exitCode:c,detail:l,cause:f,instance:C,context:mt,stack:L}}catch{return null}}var sn=m({slug:"unknown-error",category:"GENERAL",status:500,title:"Unknown/unclassified error",suggestion:"Check logs for more details"}),an=m({slug:"authentication-required",category:"GENERAL",status:401,title:"Authentication required",suggestion:"Set VERYFRONT_API_TOKEN or run \'veryfront login\'"}),cn=m({slug:"permission-denied",category:"GENERAL",status:403,title:"File/resource permission denied",suggestion:"Check file permissions and access rights"}),un=m({slug:"file-not-found",category:"GENERAL",status:404,title:"File not found",suggestion:"Verify the file path exists"}),ln=m({slug:"resource-not-found",category:"GENERAL",status:404,title:"Requested resource not found",suggestion:"Verify the referenced resource ID or name exists"}),dn=m({slug:"invalid-argument",category:"GENERAL",status:400,title:"Invalid argument",suggestion:"Check argument types and values",exitCode:2}),gn=m({slug:"already-exists",category:"GENERAL",status:409,title:"Target already exists",suggestion:"Choose a different name, or remove the existing target first",exitCode:1}),fn=m({slug:"timeout-error",category:"GENERAL",status:408,title:"Operation timed out",suggestion:"Increase timeout or optimize the operation"}),pn=m({slug:"initialization-error",category:"GENERAL",status:500,title:"Initialization failed",suggestion:"Check initialization requirements and dependencies"}),mn=m({slug:"not-supported",category:"GENERAL",status:501,title:"Feature not supported",suggestion:"Check documentation for supported features"}),it=m({slug:"security-violation",category:"GENERAL",status:403,title:"Security violation detected",suggestion:"Check for path traversal or unauthorized access attempts"}),En=m({slug:"input-validation-failed",category:"GENERAL",status:400,title:"Input validation failed",suggestion:"Check request input against validation rules"}),yn=m({slug:"project-source-empty",category:"GENERAL",status:400,title:"Project source is empty",suggestion:"Add project files or run \'veryfront init\'"}),xn=m({slug:"nested-cwd-scope",category:"GENERAL",status:500,title:"Working directory scope nested inside another",suggestion:"Do the inner work directly in the outer scope\'s callback instead of opening a second one"});var ye=[{source:String.raw`]*>[\\s\\S]*?<\\/script>`,flags:"gi",name:"inline script"},{source:String.raw`javascript:`,flags:"gi",name:"javascript: URL"},{source:String.raw`\\bon\\w+\\s*=`,flags:"gi",name:"event handler attribute"},{source:String.raw`data:\\s*text\\/html`,flags:"gi",name:"data: HTML URL"}];function xe(){return ye.map(({source:t,flags:n,name:e})=>({pattern:new RegExp(t,n),name:e}))}function he(){let t=globalThis;return t.__VERYFRONT_DEV__===!0||t.Deno?.env?.get?.("VERYFRONT_ENV")==="development"}function at(t,n={}){let{allowInlineScripts:e=!1,strict:r=!1,warn:o=!0}=n;for(let{pattern:s,name:i}of xe())if(!(e&&i==="inline script")&&(s.lastIndex=0,!!s.test(t)&&(o&&console.warn(`[Security] Suspicious ${i} detected in server HTML`),r||!he())))throw it.create({detail:`Potentially unsafe HTML: ${i} detected`});return t}var T=class{constructor(n,e){E(this,"prefix",n);E(this,"level",e)}log(n,e,r,...o){this.level>n||e?.(r,...o)}debug(n,...e){this.log(0,console.debug,`[${this.prefix}] DEBUG: ${n}`,...e)}info(n,...e){this.log(1,console.log,`[${this.prefix}] ${n}`,...e)}warn(n,...e){this.log(2,console.warn,`[${this.prefix}] WARN: ${n}`,...e)}error(n,...e){this.log(3,console.error,`[${this.prefix}] ERROR: ${n}`,...e)}};function Re(){if(typeof window>"u")return 2;let t=globalThis;return t.__VERYFRONT_DEV__||t.__RSC_DEV__?t.__VERYFRONT_DEBUG__||t.__RSC_DEBUG__?0:1:2}var U=Re(),y=new T("RSC",U),_n=new T("PREFETCH",U),bn=new T("HYDRATE",U),An=new T("VERYFRONT",U);var In=Object.freeze({IPV4:"127.0.0.1",IPV6:"::1",HOSTNAME:"localhost"});var Se=5e3,_e=1e4,Dn=16*1024*1024,be=5e3;var Ae=100;var Te=Object.freeze([5,10,25,50,75,100,250,500,750,1e3,2500,5e3,7500,1e4]),Un=Object.freeze([1,5,10,25,50,100,250,500,1e3,2500,5e3,1e4]),Ln=Object.freeze({server:Object.freeze({port:3e3,hostname:"0.0.0.0"}),timeouts:Object.freeze({default:Se,api:3e4,ssr:_e,hmr:3e4,sandbox:be}),cache:Object.freeze({jit:Object.freeze({maxSize:Ae,tempDirPrefix:"vf-bundle-"})}),metrics:Object.freeze({ssrBoundaries:Te})});var d="/_veryfront",j={RSC:`${d}/rsc/`,FS:`${d}/fs/`,MODULES:`${d}/modules/`,PAGES:`${d}/pages/`,DATA:`${d}/data/`,LIB:`${d}/lib/`,CHUNKS:`${d}/chunks/`,CLIENT:`${d}/client/`},ut={HMR_RUNTIME:`${d}/hmr-runtime.js`,HMR:`${d}/hmr.js`,ERROR_OVERLAY:`${d}/error-overlay.js`,DEV_LOADER:`${d}/dev-loader.js`,CLIENT_LOG:`${d}/log`,CLIENT_JS:`${d}/client.js`,ROUTER_JS:`${d}/router.js`,PREFETCH_JS:`${d}/prefetch.js`,MANIFEST_JSON:`${d}/manifest.json`,APP_JS:`${d}/app.js`,RSC_CLIENT:`${d}/rsc/client.js`,RSC_MANIFEST:`${d}/rsc/manifest`,RSC_STREAM:`${d}/rsc/stream`,RSC_PAYLOAD:`${d}/rsc/payload`,RSC_RENDER:`${d}/rsc/render`,RSC_PAGE:`${d}/rsc/page`,RSC_MODULE:`${d}/rsc/module`,RSC_DOM:`${d}/rsc/dom.js`,LIB_CHAT_REACT:`${d}/lib/chat/react.js`,LIB_CHAT_COMPONENTS:`${d}/lib/chat/components.js`,LIB_CHAT_PRIMITIVES:`${d}/lib/chat/primitives.js`};var Ce={ROOT:".veryfront",CACHE:".veryfront/cache",KV:".veryfront/kv",LOGS:".veryfront/logs",TMP:".veryfront/tmp"},wn=Ce.CACHE;var Pn={HMR_RUNTIME:ut.HMR_RUNTIME,ERROR_OVERLAY:ut.ERROR_OVERLAY};var Ie=j.RSC,Oe=j.FS;var lt="rsc-root",z="x-veryfront-dependency-pins";var zn=Array.prototype.at,Vn=Array.prototype.filter,Fn=Array.prototype.join,Gn=Array.prototype.map,Bn=Array.prototype.pop,vn=Array.prototype.push,Wn=Array.prototype.sort;var Kn=Set.prototype.has,Xn=String.prototype.charCodeAt,Jn=String.prototype.includes,qn=String.prototype.indexOf,Zn=String.prototype.lastIndexOf,Qn=String.prototype.slice,tr=String.prototype.split,er=String.prototype.startsWith;var _=Object.getOwnPropertyDescriptor,nr=_(URL.prototype,"origin").get,rr=_(URL.prototype,"pathname").get,or=_(URL.prototype,"protocol").get,sr=_(URL.prototype,"hostname").get,ir=_(URL.prototype,"port").get,ar=_(URL.prototype,"search").get,cr=_(URL.prototype,"hash").get;var Cr=Object.freeze({react:"","react-dom":"","react-dom/client":"","react-dom/server":"","react/jsx-runtime":"","react/jsx-dev-runtime":""});var De="veryfront-hydration-data";function dt(t){try{let n=[...t.querySelectorAll(`[id="${De}"]`)];if(n.length!==1)return null;let e=t.body;if(!e)return null;let r=n[0];return e.firstElementChild!==r&&r.parentElement!==e||r.tagName?.toLowerCase()!=="script"||r.getAttribute("type")?.trim().toLowerCase()!=="application/json"?null:r}catch{return null}}function gt(t,n){if(!n?.startsWith("on:"))return!1;try{let e=dt(t);if(!e)return!1;let r=JSON.parse(e.textContent||"{}");return r.dependencyPinningCacheKey=n,e.textContent=JSON.stringify(r),!0}catch(e){return y.debug("hydration dependency snapshot seed failed",e),!1}}function pt(t,n){let e=n==="root"?lt:`rsc-slot-${n}`,r=t.getElementById(e);if(r)return r;let o=t.createElement("div");return o.id=e,t.body.appendChild(o),o}function Ue(t,n){if(n.type!=="slot")return;let e=pt(t,n.id);e.innerHTML=at(String(n.html??""))}function ft(t,n){let e=n.split(`\n`),r=e.pop()??"";for(let o of e){let s=o.trim();if(!s)continue;let i;try{i=JSON.parse(s)}catch(u){y.debug("[client-dom] malformed NDJSON line",{line:s,error:u instanceof Error?u.message:String(u)});continue}if(!i||typeof i!="object")continue;let a=i;if(a.type==="slot"){Ue(t,a);try{we(t,a.id||"root")}catch(u){y.debug("[client-dom] hydration optional failed",u)}}}return r}function Le(t){return new Promise((n,e)=>{let r=()=>e(new DOMException("aborted","AbortError"));if(t.aborted){r();return}t.addEventListener("abort",r,{once:!0})})}async function Wr(t,n=document,e){let r="body"in t?t:null,o=r?.body??t;if(!o)return;r&>(n,r.headers.get(z));let s=o.getReader(),i=new TextDecoder,a="",u=!1;try{for(;;){if(e?.aborted)throw new DOMException("aborted","AbortError");let c=s.read(),{done:l,value:f}=e?await Promise.race([c,Le(e)]):await c;if(l){u=!0;break}a+=i.decode(f,{stream:!0}),a=ft(n,a)}a&&ft(n,`${a}\n`)}catch(c){throw c instanceof Error&&c.name==="AbortError"||y.debug("[client-dom] consumeNdjsonStream error",c),c}finally{try{await s.cancel()}catch(c){u||y.debug("[client-dom] reader.cancel failed",c)}try{s.releaseLock()}catch(c){y.debug("[client-dom] reader.releaseLock failed",c)}if(typeof o.cancel=="function")try{await o.cancel()}catch(c){y.debug("[client-dom] stream.cancel failed",c)}if(typeof r?.body?.cancel=="function")try{await r.body.cancel()}catch(c){y.debug("[client-dom] response.body.cancel failed",c)}}}function $e(t,n){let e=pt(t,n),r=[],o=s=>{let i=s;i.dataset?.clientRef&&r.push(i);for(let a of s.children)o(a)};return o(e),r}function we(t,n){let e=$e(t,n);for(let r of e){let o=r.dataset?.clientRef;o&&(r.dataset.hydrated="true",y.debug("[client-dom] marked for hydration",o))}}export{Wr as consumeNdjsonStream,pt as getContainer};\n'; From a18f8b59f483c15648a969057a831d999bcf48ec Mon Sep 17 00:00:00 2001 From: Kentaro Wakayama Date: Sun, 23 Aug 2026 10:12:29 +0200 Subject: [PATCH 6/8] test(errors): count 122 registered errors after merging main The registry count assertion is an exact number, so two PRs each adding one error are individually correct and wrong together. This branch adds already-exists and main added one more while it waited, giving 122. Caught by the merge queue rather than by either branch's own CI: each passed alone. --- src/errors/error-registry.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/errors/error-registry.test.ts b/src/errors/error-registry.test.ts index 7879542e55..7c3f2f635a 100644 --- a/src/errors/error-registry.test.ts +++ b/src/errors/error-registry.test.ts @@ -29,9 +29,9 @@ describe("error-registry", () => { assertEquals(slugs.length, uniqueSlugs.size, "Duplicate slugs detected"); }); - it("should have 121 registered errors", () => { + it("should have 122 registered errors", () => { const slugs = getAllSlugs(); - assertEquals(slugs.length, 121); + assertEquals(slugs.length, 122); }); it("registers every local integration boundary error", () => { From 5437181b414026ac894d03a4c5bc14fcdd6af38b Mon Sep 17 00:00:00 2001 From: Koji Wakayama Date: Sun, 23 Aug 2026 12:51:24 +0200 Subject: [PATCH 7/8] fix(mcp): let vf_create_project refuse exactly what veryfront init refuses (#4011) * fix(mcp): let vf_create_project refuse exactly what veryfront init refuses The tool kept its own pre-check, "Directory already exists: ", so an empty directory or a fresh clone holding only .git was refused here while `veryfront init` (since the current-directory and empty-directory fixes) scaffolds into both, and a real conflict was reported without naming the file. The pre-check is gone: `createProject` is the single authority, and its refusal - `Directory "x" already contains README.md. Use --force to overwrite.` - reaches the caller through the existing failure envelope. Tests: a directory holding a scaffold file is refused with the file named and left intact; an existing empty directory scaffolds. * fix(init): refuse a linked project root instead of scaffolding through it Dropping the `vf_create_project` pre-check handed the target decision to `createProject`, which never looked at the project root itself: `findUnwritablePaths` walks only the paths beneath it. A symlink at the root therefore passed, and the scaffold wrote its files, its `.gitignore` and its installed dependencies into the link target, which can sit outside the requested parent entirely. The tool reported success. The scaffold picks that path itself by joining the name onto the parent, so a link there sends every write somewhere the caller never named. `createProject` now refuses it, for the same reason a link at any other scaffold path is already refused. A parent directory the caller passed in is their own choice, so only the derived path is checked. Fixing it in `createProject` closes the same hole for `veryfront init`, not just the MCP tool. * Refuse linked gitignore before scaffold merge The project creation preflight already rejects symlinks on paths the scaffold writes outright. The generated .gitignore is merged instead of treated as a normal overwrite conflict, but the merge still writes to that path and would follow a symlink outside the project. This keeps regular .gitignore merge behavior while adding it to the write-through protection list, with shared and MCP regression coverage for the outside-target case. Constraint: Preserve existing .gitignore merge behavior for regular files Rejected: Add .gitignore to scaffoldWritePaths | that would turn normal .gitignore merges into overwrite conflicts Confidence: high Scope-risk: narrow Tested: VF_DISABLE_LRU_INTERVAL=1 deno test --no-check --allow-all cli/shared/project-creation.test.ts Tested: VF_DISABLE_LRU_INTERVAL=1 deno test --no-check --allow-all cli/mcp/tools/catalog-tools.test.ts Tested: deno lint cli/shared/project-creation.ts cli/shared/project-creation.test.ts cli/mcp/tools/catalog-tools.test.ts Tested: deno check cli/shared/project-creation.ts cli/shared/project-creation.test.ts cli/mcp/tools/catalog-tools.test.ts * Protect scaffold reuse lockfile leaves The project creation preflight now treats installer-generated lockfiles as possible writes when dependency installation is enabled, so fail-policy reuse rejects a user-owned package-lock before npm can replace it. The same protected-leaf check rejects a directory at merge-only leaves such as .gitignore before scaffold files are written, preventing partial project creation. Constraint: Preserve regular .gitignore merge behavior and force-overwrite behavior for ordinary lockfiles Rejected: Disable dependency installation for reused directories | too broad and would remove expected vf_create_project behavior Confidence: high Scope-risk: narrow Tested: VF_DISABLE_LRU_INTERVAL=1 deno test --no-check --allow-all cli/shared/project-creation.test.ts Tested: VF_DISABLE_LRU_INTERVAL=1 deno test --no-check --allow-all cli/mcp/tools/catalog-tools.test.ts Tested: deno lint cli/shared/project-creation.ts cli/shared/project-creation.test.ts cli/mcp/tools/catalog-tools.test.ts Tested: deno check cli/shared/project-creation.ts cli/shared/project-creation.test.ts cli/mcp/tools/catalog-tools.test.ts * Close remaining scaffold reuse write-through gaps The reuse preflight now covers npm's hidden lockfile and rejects non-file protected merge leaves before scaffold writes begin. The generated scaffold docs were refreshed so source anchors point at the current declarations. Constraint: Preserve regular .gitignore merge behavior and ordinary lockfile fail-policy semantics. Rejected: Reject any existing node_modules directory | too broad because only npm's hidden lockfile is a deterministic installer write target here. Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep merge-only leaves in protectedLeafPaths out of normal overwrite conflict detection, but preflight every non-regular leaf before writeGitignore runs. Tested: Deno 2.7.7; VF_DISABLE_LRU_INTERVAL=1 deno test --no-check --allow-all cli/shared/project-creation.test.ts Tested: Deno 2.7.7; VF_DISABLE_LRU_INTERVAL=1 deno test --no-check --allow-all cli/mcp/tools/catalog-tools.test.ts Tested: Deno 2.7.7; deno fmt --check cli/shared/project-creation.ts cli/shared/project-creation.test.ts cli/mcp/tools/catalog-tools.test.ts docs/api-reference/veryfront/scaffold.md Tested: Deno 2.7.7; deno lint cli/shared/project-creation.ts cli/shared/project-creation.test.ts cli/mcp/tools/catalog-tools.test.ts Tested: Deno 2.7.7; deno check cli/shared/project-creation.ts cli/shared/project-creation.test.ts cli/mcp/tools/catalog-tools.test.ts Tested: Deno 2.7.7; deno task docs:api-reference:check Not-tested: Full repository test suite. * Protect npm shrinkwrap during scaffold reuse npm treats npm-shrinkwrap.json as an installation-owned lockfile and can update it during install. Reused project directories now preflight that path with the rest of the installer write set so conflictPolicy fail refuses it before scaffold writes or dependency installation. Constraint: Keep dependency installation enabled for safe reused directories. Rejected: Disable npm install whenever a reused directory exists | too broad; only deterministic installer-owned write targets need preflight protection. Confidence: high Scope-risk: narrow Reversibility: clean Directive: Add future package-manager-owned write targets to installerWritePaths so conflict detection and write-through protection stay coupled. Tested: Deno 2.7.7; VF_DISABLE_LRU_INTERVAL=1 deno test --no-check --allow-all cli/shared/project-creation.test.ts Tested: Deno 2.7.7; VF_DISABLE_LRU_INTERVAL=1 deno test --no-check --allow-all cli/mcp/tools/catalog-tools.test.ts Tested: Deno 2.7.7; deno fmt --check cli/shared/project-creation.ts cli/shared/project-creation.test.ts cli/mcp/tools/catalog-tools.test.ts docs/api-reference/veryfront/scaffold.md Tested: Deno 2.7.7; deno lint cli/shared/project-creation.ts cli/shared/project-creation.test.ts cli/mcp/tools/catalog-tools.test.ts Tested: Deno 2.7.7; deno check cli/shared/project-creation.ts cli/shared/project-creation.test.ts cli/mcp/tools/catalog-tools.test.ts Tested: Deno 2.7.7; deno task docs:api-reference:check Not-tested: Full repository test suite. * Refuse npm node_modules reuse before install npm install can prune existing node_modules content before returning success. Reused project directories now treat node_modules as an npm installer conflict when dependency installation is enabled, so conflictPolicy fail refuses the directory before scaffold writes or install side effects. Constraint: Preserve safe reused-directory scaffolding when dependency installation has no existing npm-owned tree to mutate. Rejected: Treat node_modules as a protected write-through leaf | conflict detection gives the user-facing fail-policy error while existing symlink protection still comes from node_modules/.package-lock.json path traversal. Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep installer conflict paths separate from installer file write paths when the path is a directory-level npm side effect. Tested: Deno 2.7.7; VF_DISABLE_LRU_INTERVAL=1 deno test --no-check --allow-all cli/shared/project-creation.test.ts Tested: Deno 2.7.7; VF_DISABLE_LRU_INTERVAL=1 deno test --no-check --allow-all cli/mcp/tools/catalog-tools.test.ts Tested: Deno 2.7.7; deno fmt --check cli/shared/project-creation.ts cli/shared/project-creation.test.ts cli/mcp/tools/catalog-tools.test.ts docs/api-reference/veryfront/scaffold.md Tested: Deno 2.7.7; deno lint cli/shared/project-creation.ts cli/shared/project-creation.test.ts cli/mcp/tools/catalog-tools.test.ts Tested: Deno 2.7.7; deno check cli/shared/project-creation.ts cli/shared/project-creation.test.ts cli/mcp/tools/catalog-tools.test.ts Tested: Deno 2.7.7; deno task docs:api-reference:check Not-tested: Full repository test suite. * Restore project creation style invariants The scaffold creation module now keeps veryfront package imports with the other external imports and keeps the unwritable-paths documentation directly attached to the function it describes. Constraint: Address exact-head standards review without changing runtime behavior. Confidence: high Scope-risk: narrow Reversibility: clean Tested: Deno 2.7.7; VF_DISABLE_LRU_INTERVAL=1 deno test --no-check --allow-all cli/shared/project-creation.test.ts Tested: Deno 2.7.7; deno fmt --check cli/shared/project-creation.ts cli/shared/project-creation.test.ts cli/mcp/tools/catalog-tools.test.ts docs/api-reference/veryfront/scaffold.md Tested: Deno 2.7.7; deno lint cli/shared/project-creation.ts cli/shared/project-creation.test.ts cli/mcp/tools/catalog-tools.test.ts Tested: Deno 2.7.7; deno check cli/shared/project-creation.ts cli/shared/project-creation.test.ts cli/mcp/tools/catalog-tools.test.ts Tested: Deno 2.7.7; deno task docs:api-reference:check Not-tested: Full repository test suite. * Replace gitignore atomically before scaffolding Existing .gitignore files are merge-only, but direct writes can mutate hard-linked files and late write failures can leave a partial scaffold. The merge now writes a same-directory temporary file, renames it over .gitignore, and happens before the rest of the scaffold output. Constraint: Preserve regular .gitignore merge semantics while preventing writes through shared inodes or late permission failures. Rejected: Keep direct writeTextFile with more preflight checks | hard links are easier and safer to handle by replacing the path instead of mutating the inode. Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep .gitignore as a merge-only path; do not re-add it to overwrite conflict detection without preserving existing ignore entries. Tested: Deno 2.7.7; VF_DISABLE_LRU_INTERVAL=1 deno test --no-check --allow-all cli/shared/project-creation.test.ts Tested: Deno 2.7.7; VF_DISABLE_LRU_INTERVAL=1 deno test --no-check --allow-all cli/mcp/tools/catalog-tools.test.ts Tested: Deno 2.7.7; deno fmt --check cli/shared/project-creation.ts cli/shared/project-creation.test.ts cli/mcp/tools/catalog-tools.test.ts docs/api-reference/veryfront/scaffold.md Tested: Deno 2.7.7; deno lint cli/shared/project-creation.ts cli/shared/project-creation.test.ts cli/mcp/tools/catalog-tools.test.ts Tested: Deno 2.7.7; deno check cli/shared/project-creation.ts cli/shared/project-creation.test.ts cli/mcp/tools/catalog-tools.test.ts Tested: Deno 2.7.7; deno task docs:api-reference:check Not-tested: Full repository test suite. * Fail closed on unreadable gitignore merges Existing .gitignore content is merge input. Treating every read failure as absence could replace unreadable user content and then continue scaffolding. The merge now treats only missing .gitignore as absent; any other read failure happens before scaffold writes. Constraint: Preserve absent .gitignore behavior and atomic replacement semantics. Rejected: Swallow all read errors and rely on rename | replaces unreadable existing content under fail policy. Confidence: high Scope-risk: narrow Reversibility: clean Directive: Do not broaden read-error handling for merge-only files; only NotFound means absent. Tested: deno test --no-check --allow-all cli/shared/project-creation.test.ts Tested: deno test --no-check --allow-all cli/mcp/tools/catalog-tools.test.ts Tested: deno fmt --check cli/shared/project-creation.ts cli/shared/project-creation.test.ts cli/mcp/tools/catalog-tools.test.ts docs/api-reference/veryfront/scaffold.md Tested: deno lint cli/shared/project-creation.ts cli/shared/project-creation.test.ts cli/mcp/tools/catalog-tools.test.ts Tested: deno check cli/shared/project-creation.ts cli/shared/project-creation.test.ts cli/mcp/tools/catalog-tools.test.ts Tested: deno task docs:api-reference:check Not-tested: Full repository test suite. * Protect Bun scaffold installs from existing node_modules Bun installs dependencies into node_modules like npm-family package managers. Reused project targets must therefore reject an existing node_modules tree before installation can prune or replace user-owned files. The unsupported atomic-gitignore capability branch now also uses the file-local config error helper so the error stays on the registered VeryfrontError path. Constraint: Keep MCP create-project behavior unchanged; it currently exposes no runtime input and always calls shared creation with runtime node. Rejected: Add a Bun runtime option to vf_create_project | broadens the MCP tool contract beyond this conflict-safety fix. Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep installer conflict paths aligned with NPM_FAMILY_CLIENTS when a package manager writes node_modules. Tested: deno test --no-check --allow-all cli/shared/project-creation.test.ts Tested: deno test --no-check --allow-all cli/mcp/tools/catalog-tools.test.ts Tested: deno fmt --check cli/shared/project-creation.ts cli/shared/project-creation.test.ts cli/mcp/tools/catalog-tools.test.ts docs/api-reference/veryfront/scaffold.md Tested: deno lint cli/shared/project-creation.ts cli/shared/project-creation.test.ts cli/mcp/tools/catalog-tools.test.ts Tested: deno check cli/shared/project-creation.ts cli/shared/project-creation.test.ts cli/mcp/tools/catalog-tools.test.ts Tested: deno task docs:api-reference:check Not-tested: Full repository test suite. --------- Co-authored-by: Kentaro Wakayama --- cli/mcp/tools/catalog-tools.test.ts | 236 +++++++++++++++++- cli/mcp/tools/catalog-tools.ts | 12 +- cli/shared/project-creation.test.ts | 302 ++++++++++++++++++++++- cli/shared/project-creation.ts | 108 +++++++- docs/api-reference/veryfront/scaffold.md | 12 +- 5 files changed, 644 insertions(+), 26 deletions(-) diff --git a/cli/mcp/tools/catalog-tools.test.ts b/cli/mcp/tools/catalog-tools.test.ts index ed115fa998..10b5df7b28 100644 --- a/cli/mcp/tools/catalog-tools.test.ts +++ b/cli/mcp/tools/catalog-tools.test.ts @@ -5,7 +5,7 @@ import "#veryfront/schemas/_test-setup.ts"; import { assertEquals, assertExists } from "#veryfront/testing/assert.ts"; import { afterEach, describe, it } from "#veryfront/testing/bdd.ts"; -import { join } from "veryfront/platform/path"; +import { dirname, join } from "veryfront/platform/path"; import { EXPERIMENTAL_INTEGRATIONS_ENV } from "../../../src/integrations/feature-flags.ts"; import { resolveCreateProjectPaths, @@ -218,11 +218,12 @@ describe("mcp/tools/catalog-tools", () => { }); }); - it("keeps the existing-directory failure response", async () => { + it("refuses a directory holding files the scaffold would overwrite", async () => { const parentDir = await Deno.makeTempDir(); createdDirs.push(parentDir); const projectDir = join(parentDir, "example-app"); await Deno.mkdir(projectDir); + await Deno.writeTextFile(join(projectDir, "README.md"), "mine\n"); const result = await vfCreateProject.execute({ name: "Example App", @@ -230,8 +231,237 @@ describe("mcp/tools/catalog-tools", () => { directory: parentDir, }); + // Same rule and same words as `veryfront init`: the conflict is the file, + // and it is named. assertEquals(result.success, false); - assertEquals(result.message, `Directory already exists: ${projectDir}`); + assertEquals(result.projectDir, undefined); + assertEquals(result.message.includes("already contains README.md"), true); + assertEquals(await Deno.readTextFile(join(projectDir, "README.md")), "mine\n"); + }); + + it("scaffolds into an existing empty directory", async () => { + const parentDir = await Deno.makeTempDir(); + createdDirs.push(parentDir); + const projectDir = join(parentDir, "example-app"); + await Deno.mkdir(projectDir); + + await withFakeNpm(async () => { + const result = await vfCreateProject.execute({ + name: "Example App", + template: "minimal", + directory: parentDir, + }); + + assertEquals(result.success, true); + assertEquals(result.projectDir, projectDir); + }); + }); + + it("refuses a linked project directory instead of scaffolding outside the parent", async () => { + const parentDir = await Deno.makeTempDir(); + createdDirs.push(parentDir); + const outside = await Deno.makeTempDir(); + createdDirs.push(outside); + await Deno.symlink(outside, join(parentDir, "example-app")); + + const result = await vfCreateProject.execute({ + name: "Example App", + template: "minimal", + directory: parentDir, + }); + + assertEquals(result.success, false); + assertEquals(result.projectDir, undefined); + assertEquals(result.message.includes("is a link the scaffold cannot write through"), true); + // The link target is outside the requested parent; nothing was written there. + const written: string[] = []; + for await (const entry of Deno.readDir(outside)) written.push(entry.name); + assertEquals(written, []); + }); + + it("refuses a linked .gitignore instead of merging through it", async () => { + const parentDir = await Deno.makeTempDir(); + createdDirs.push(parentDir); + const projectDir = join(parentDir, "example-app"); + const outside = join(parentDir, "outside-gitignore"); + await Deno.mkdir(projectDir); + await Deno.writeTextFile(outside, "keep-me\n"); + await Deno.symlink(outside, join(projectDir, ".gitignore")); + + const result = await vfCreateProject.execute({ + name: "Example App", + template: "minimal", + directory: parentDir, + }); + + assertEquals(result.success, false); + assertEquals(result.projectDir, undefined); + assertEquals( + result.message.includes("already contains .gitignore as a file or a link"), + true, + ); + assertEquals(await Deno.readTextFile(outside), "keep-me\n"); + assertEquals(await Deno.readTextFile(join(projectDir, ".gitignore")), "keep-me\n"); + }); + + it("refuses a .gitignore directory before partially scaffolding", async () => { + const parentDir = await Deno.makeTempDir(); + createdDirs.push(parentDir); + const projectDir = join(parentDir, "example-app"); + await Deno.mkdir(join(projectDir, ".gitignore"), { recursive: true }); + + const result = await vfCreateProject.execute({ + name: "Example App", + template: "minimal", + directory: parentDir, + }); + + assertEquals(result.success, false); + assertEquals(result.projectDir, undefined); + assertEquals( + result.message.includes("already contains .gitignore as a file or a link"), + true, + ); + assertEquals( + await Deno.readTextFile(join(projectDir, ".gitignore", "README.md")).catch( + () => "absent", + ), + "absent", + ); + assertEquals( + await Deno.readTextFile(join(projectDir, "README.md")).catch(() => "absent"), + "absent", + ); + }); + + it("refuses a non-file .gitignore before partially scaffolding", async () => { + if (Deno.build.os === "windows") return; + + const parentDir = await Deno.makeTempDir(); + createdDirs.push(parentDir); + const projectDir = join(parentDir, "example-app"); + await Deno.mkdir(projectDir, { recursive: true }); + const command = new Deno.Command("mkfifo", { + args: [join(projectDir, ".gitignore")], + }); + const output = await command.output(); + if (!output.success) return; + + const result = await vfCreateProject.execute({ + name: "Example App", + template: "minimal", + directory: parentDir, + }); + + assertEquals(result.success, false); + assertEquals(result.projectDir, undefined); + assertEquals( + result.message.includes("already contains .gitignore as a file or a link"), + true, + ); + assertEquals( + await Deno.readTextFile(join(projectDir, "README.md")).catch(() => "absent"), + "absent", + ); + }); + + it("refuses a package lock before dependency installation can replace it", async () => { + const parentDir = await Deno.makeTempDir(); + createdDirs.push(parentDir); + const projectDir = join(parentDir, "example-app"); + const lockfile = join(projectDir, "package-lock.json"); + await Deno.mkdir(projectDir); + await Deno.writeTextFile(lockfile, "keep-me\n"); + + const result = await vfCreateProject.execute({ + name: "Example App", + template: "minimal", + directory: parentDir, + }); + + assertEquals(result.success, false); + assertEquals(result.projectDir, undefined); + assertEquals(result.message.includes("already contains package-lock.json"), true); + assertEquals(await Deno.readTextFile(lockfile), "keep-me\n"); + assertEquals( + await Deno.readTextFile(join(projectDir, "README.md")).catch(() => "absent"), + "absent", + ); + }); + + it("refuses npm's hidden lockfile before dependency installation can replace it", async () => { + const parentDir = await Deno.makeTempDir(); + createdDirs.push(parentDir); + const projectDir = join(parentDir, "example-app"); + const lockfile = join(projectDir, "node_modules", ".package-lock.json"); + await Deno.mkdir(join(projectDir, "node_modules"), { recursive: true }); + await Deno.writeTextFile(lockfile, "keep-me\n"); + + const result = await vfCreateProject.execute({ + name: "Example App", + template: "minimal", + directory: parentDir, + }); + + assertEquals(result.success, false); + assertEquals(result.projectDir, undefined); + assertEquals( + result.message.includes("already contains node_modules/.package-lock.json"), + true, + ); + assertEquals(await Deno.readTextFile(lockfile), "keep-me\n"); + assertEquals( + await Deno.readTextFile(join(projectDir, "README.md")).catch(() => "absent"), + "absent", + ); + }); + + it("refuses npm shrinkwrap before dependency installation can replace it", async () => { + const parentDir = await Deno.makeTempDir(); + createdDirs.push(parentDir); + const projectDir = join(parentDir, "example-app"); + const lockfile = join(projectDir, "npm-shrinkwrap.json"); + await Deno.mkdir(projectDir); + await Deno.writeTextFile(lockfile, "keep-me\n"); + + const result = await vfCreateProject.execute({ + name: "Example App", + template: "minimal", + directory: parentDir, + }); + + assertEquals(result.success, false); + assertEquals(result.projectDir, undefined); + assertEquals(result.message.includes("already contains npm-shrinkwrap.json"), true); + assertEquals(await Deno.readTextFile(lockfile), "keep-me\n"); + assertEquals( + await Deno.readTextFile(join(projectDir, "README.md")).catch(() => "absent"), + "absent", + ); + }); + + it("refuses existing node_modules before dependency installation can prune it", async () => { + const parentDir = await Deno.makeTempDir(); + createdDirs.push(parentDir); + const projectDir = join(parentDir, "example-app"); + const userFile = join(projectDir, "node_modules", "user-owned", "data.txt"); + await Deno.mkdir(dirname(userFile), { recursive: true }); + await Deno.writeTextFile(userFile, "keep-me\n"); + + const result = await vfCreateProject.execute({ + name: "Example App", + template: "minimal", + directory: parentDir, + }); + + assertEquals(result.success, false); + assertEquals(result.projectDir, undefined); + assertEquals(result.message.includes("already contains node_modules"), true); + assertEquals(await Deno.readTextFile(userFile), "keep-me\n"); + assertEquals( + await Deno.readTextFile(join(projectDir, "README.md")).catch(() => "absent"), + "absent", + ); }); it("reports project-name validation failures", async () => { diff --git a/cli/mcp/tools/catalog-tools.ts b/cli/mcp/tools/catalog-tools.ts index 6a6b09739c..4d99d08a7d 100644 --- a/cli/mcp/tools/catalog-tools.ts +++ b/cli/mcp/tools/catalog-tools.ts @@ -13,7 +13,7 @@ import { INTEGRATION_CATEGORIES } from "../../commands/init/catalog.ts"; import { createProject as createSharedProject } from "../../shared/project-creation.ts"; import { validateProjectName } from "../../shared/project-name.ts"; import type { MCPTool } from "../tools.ts"; -import { directoryExists, formatError, toSlug } from "./helpers.ts"; +import { formatError, toSlug } from "./helpers.ts"; import type { InitTemplate } from "../../commands/init/types.ts"; import type { IntegrationName } from "../../../templates/types.ts"; @@ -391,16 +391,16 @@ export const vfCreateProject: MCPTool = "cli.mcp.tool.vf_create_project", async () => { try { - const { name, parentDir, projectDir } = resolveCreateProjectPaths(input); + const { name, parentDir } = resolveCreateProjectPaths(input); const nameError = validateProjectName(name); if (nameError) { return { success: false, message: `Failed to create project: ${nameError}` }; } - if (await directoryExists(projectDir)) { - return { success: false, message: `Directory already exists: ${projectDir}` }; - } - + // Whether the target can be written to is `createProject`'s call, + // so this tool refuses exactly what `veryfront init` refuses: a file + // the scaffold would overwrite, named in the message - not a + // directory that merely exists. const creation = await createSharedProject({ name, parentDir, diff --git a/cli/shared/project-creation.test.ts b/cli/shared/project-creation.test.ts index 3c0645b57a..3a83305306 100644 --- a/cli/shared/project-creation.test.ts +++ b/cli/shared/project-creation.test.ts @@ -9,7 +9,7 @@ import { } from "#veryfront/testing/assert.ts"; import { describe, it } from "#veryfront/testing/bdd.ts"; import { exists, makeTempDir, remove } from "#veryfront/testing/deno-compat.ts"; -import { join } from "veryfront/platform/path"; +import { dirname, join } from "veryfront/platform/path"; import { formatCLIError, VeryfrontError } from "veryfront/errors"; import { STARTER_TEMPLATE_NAMES } from "../../templates/types.ts"; import { @@ -892,6 +892,284 @@ describe("createProject when a path cannot be written through", () => { } }); + it("refuses a linked .gitignore before merging it", async () => { + const parentDir = await makeTempDir({ prefix: "veryfront-create-gitignore-link-" }); + const projectDir = join(parentDir, "contract-project"); + const outside = join(parentDir, "outside-gitignore"); + + try { + await Deno.mkdir(projectDir); + await Deno.writeTextFile(outside, "keep-me\n"); + await Deno.symlink(outside, join(projectDir, ".gitignore")); + + await assertRejects( + () => createProject(baseRequest(parentDir)), + Error, + 'Directory "contract-project" already contains .gitignore as a file or a link', + ); + + assertEquals(await Deno.readTextFile(outside), "keep-me\n"); + assertEquals(await exists(join(projectDir, "README.md")), false); + } finally { + await remove(parentDir, { recursive: true }).catch(() => {}); + } + }); + + it("refuses a .gitignore directory before writing scaffold files", async () => { + const parentDir = await makeTempDir({ prefix: "veryfront-create-gitignore-dir-" }); + const projectDir = join(parentDir, "contract-project"); + + try { + await Deno.mkdir(join(projectDir, ".gitignore"), { recursive: true }); + + await assertRejects( + () => createProject(baseRequest(parentDir)), + Error, + 'Directory "contract-project" already contains .gitignore as a file or a link', + ); + + assertEquals(await exists(join(projectDir, "README.md")), false); + } finally { + await remove(parentDir, { recursive: true }).catch(() => {}); + } + }); + + it("refuses a non-file .gitignore before writing scaffold files", async () => { + if (Deno.build.os === "windows") return; + + const parentDir = await makeTempDir({ prefix: "veryfront-create-gitignore-fifo-" }); + const projectDir = join(parentDir, "contract-project"); + + try { + await Deno.mkdir(projectDir, { recursive: true }); + const command = new Deno.Command("mkfifo", { + args: [join(projectDir, ".gitignore")], + }); + const output = await command.output(); + if (!output.success) return; + + await assertRejects( + () => createProject(baseRequest(parentDir)), + Error, + 'Directory "contract-project" already contains .gitignore as a file or a link', + ); + + assertEquals(await exists(join(projectDir, "README.md")), false); + } finally { + await remove(parentDir, { recursive: true }).catch(() => {}); + } + }); + + it("replaces a hard-linked .gitignore without modifying the other link", async () => { + if (Deno.build.os === "windows") return; + + const parentDir = await makeTempDir({ prefix: "veryfront-create-gitignore-hardlink-" }); + const projectDir = join(parentDir, "contract-project"); + const outside = join(parentDir, "outside-gitignore"); + + try { + await Deno.mkdir(projectDir); + await Deno.writeTextFile(outside, "keep-me\n"); + try { + await Deno.link(outside, join(projectDir, ".gitignore")); + } catch { + return; + } + + await createProject(baseRequest(parentDir)); + + assertEquals(await Deno.readTextFile(outside), "keep-me\n"); + assertStringIncludes(await Deno.readTextFile(join(projectDir, ".gitignore")), "keep-me"); + assertEquals(await exists(join(projectDir, "README.md")), true); + } finally { + await remove(parentDir, { recursive: true }).catch(() => {}); + } + }); + + it("refuses an unreplacable .gitignore before writing scaffold files", async () => { + if (Deno.build.os === "windows") return; + + const parentDir = await makeTempDir({ prefix: "veryfront-create-gitignore-readonly-" }); + const projectDir = join(parentDir, "contract-project"); + + try { + await Deno.mkdir(projectDir); + await Deno.writeTextFile(join(projectDir, ".gitignore"), "keep-me\n"); + await Deno.chmod(projectDir, 0o500); + + await assertRejects( + () => createProject(baseRequest(parentDir)), + Error, + ); + + assertEquals(await Deno.readTextFile(join(projectDir, ".gitignore")), "keep-me\n"); + assertEquals(await exists(join(projectDir, "README.md")), false); + } finally { + await Deno.chmod(projectDir, 0o700).catch(() => {}); + await remove(parentDir, { recursive: true }).catch(() => {}); + } + }); + + it("refuses an unreadable .gitignore before writing scaffold files", async () => { + if (Deno.build.os === "windows") return; + + const parentDir = await makeTempDir({ prefix: "veryfront-create-gitignore-unreadable-" }); + const projectDir = join(parentDir, "contract-project"); + const gitignorePath = join(projectDir, ".gitignore"); + + try { + await Deno.mkdir(projectDir); + await Deno.writeTextFile(gitignorePath, "keep-me\n"); + const before = await Deno.lstat(gitignorePath); + await Deno.chmod(gitignorePath, 0o000); + + await assertRejects( + () => createProject(baseRequest(parentDir)), + Error, + ); + + const after = await Deno.lstat(gitignorePath); + assertEquals(after.ino, before.ino); + await Deno.chmod(gitignorePath, 0o600); + assertEquals(await Deno.readTextFile(gitignorePath), "keep-me\n"); + assertEquals(await exists(join(projectDir, "README.md")), false); + } finally { + await Deno.chmod(gitignorePath, 0o600).catch(() => {}); + await remove(parentDir, { recursive: true }).catch(() => {}); + } + }); + + it("refuses installer lockfiles before dependency installation can replace them", async () => { + const parentDir = await makeTempDir({ prefix: "veryfront-create-lockfile-conflict-" }); + const projectDir = join(parentDir, "contract-project"); + const lockfile = join(projectDir, "package-lock.json"); + + try { + await Deno.mkdir(projectDir); + await Deno.writeTextFile(lockfile, "keep-me\n"); + + await assertRejects( + () => + createProject({ + ...baseRequest(parentDir), + installDependencies: true, + }), + Error, + 'Directory "contract-project" already contains package-lock.json. Use --force to overwrite.', + ); + + assertEquals(await Deno.readTextFile(lockfile), "keep-me\n"); + assertEquals(await exists(join(projectDir, "README.md")), false); + } finally { + await remove(parentDir, { recursive: true }).catch(() => {}); + } + }); + + it("refuses npm's hidden lockfile before dependency installation can replace it", async () => { + const parentDir = await makeTempDir({ prefix: "veryfront-create-hidden-lockfile-" }); + const projectDir = join(parentDir, "contract-project"); + const lockfile = join(projectDir, "node_modules", ".package-lock.json"); + + try { + await Deno.mkdir(join(projectDir, "node_modules"), { recursive: true }); + await Deno.writeTextFile(lockfile, "keep-me\n"); + + await assertRejects( + () => + createProject({ + ...baseRequest(parentDir), + installDependencies: true, + }), + Error, + 'Directory "contract-project" already contains node_modules/.package-lock.json, node_modules. Use --force to overwrite.', + ); + + assertEquals(await Deno.readTextFile(lockfile), "keep-me\n"); + assertEquals(await exists(join(projectDir, "README.md")), false); + } finally { + await remove(parentDir, { recursive: true }).catch(() => {}); + } + }); + + it("refuses npm shrinkwrap before dependency installation can replace it", async () => { + const parentDir = await makeTempDir({ prefix: "veryfront-create-shrinkwrap-" }); + const projectDir = join(parentDir, "contract-project"); + const lockfile = join(projectDir, "npm-shrinkwrap.json"); + + try { + await Deno.mkdir(projectDir); + await Deno.writeTextFile(lockfile, "keep-me\n"); + + await assertRejects( + () => + createProject({ + ...baseRequest(parentDir), + installDependencies: true, + }), + Error, + 'Directory "contract-project" already contains npm-shrinkwrap.json. Use --force to overwrite.', + ); + + assertEquals(await Deno.readTextFile(lockfile), "keep-me\n"); + assertEquals(await exists(join(projectDir, "README.md")), false); + } finally { + await remove(parentDir, { recursive: true }).catch(() => {}); + } + }); + + it("refuses existing node_modules before dependency installation can prune it", async () => { + const parentDir = await makeTempDir({ prefix: "veryfront-create-node-modules-" }); + const projectDir = join(parentDir, "contract-project"); + const userFile = join(projectDir, "node_modules", "user-owned", "data.txt"); + + try { + await Deno.mkdir(dirname(userFile), { recursive: true }); + await Deno.writeTextFile(userFile, "keep-me\n"); + + await assertRejects( + () => + createProject({ + ...baseRequest(parentDir), + installDependencies: true, + }), + Error, + 'Directory "contract-project" already contains node_modules. Use --force to overwrite.', + ); + + assertEquals(await Deno.readTextFile(userFile), "keep-me\n"); + assertEquals(await exists(join(projectDir, "README.md")), false); + } finally { + await remove(parentDir, { recursive: true }).catch(() => {}); + } + }); + + it("refuses existing node_modules before Bun dependency installation can prune it", async () => { + const parentDir = await makeTempDir({ prefix: "veryfront-create-bun-node-modules-" }); + const projectDir = join(parentDir, "contract-project"); + const userFile = join(projectDir, "node_modules", "user-owned", "data.txt"); + + try { + await Deno.mkdir(dirname(userFile), { recursive: true }); + await Deno.writeTextFile(userFile, "keep-me\n"); + + await assertRejects( + () => + createProject({ + ...baseRequest(parentDir), + runtime: "bun", + installDependencies: true, + }), + Error, + 'Directory "contract-project" already contains node_modules. Use --force to overwrite.', + ); + + assertEquals(await Deno.readTextFile(userFile), "keep-me\n"); + assertEquals(await exists(join(projectDir, "README.md")), false); + } finally { + await remove(parentDir, { recursive: true }).catch(() => {}); + } + }); + it("still reports a real file at a scaffold path as an overwritable conflict", async () => { const parentDir = await makeTempDir({ prefix: "veryfront-create-leaf-file-" }); const projectDir = join(parentDir, "contract-project"); @@ -912,6 +1190,28 @@ describe("createProject when a path cannot be written through", () => { } }); + it("refuses a linked project root instead of scaffolding through it", async () => { + const parentDir = await makeTempDir({ prefix: "veryfront-create-linked-root-" }); + const outside = await makeTempDir({ prefix: "veryfront-create-outside-" }); + + try { + await Deno.symlink(outside, join(parentDir, "contract-project")); + + await assertRejects( + () => createProject({ ...baseRequest(parentDir), conflictPolicy: "overwrite" }), + Error, + 'Directory "contract-project" is a link the scaffold cannot write through', + ); + + // Nothing reached the link target, which is outside the parent entirely. + assertEquals(await exists(join(outside, "README.md")), false); + assertEquals(await exists(join(outside, "package.json")), false); + } finally { + await remove(parentDir, { recursive: true }).catch(() => {}); + await remove(outside, { recursive: true }).catch(() => {}); + } + }); + it("scaffolds normally when the directories it needs are absent or already directories", async () => { const parentDir = await makeTempDir({ prefix: "veryfront-create-blocked-clear-" }); const projectDir = join(parentDir, "contract-project"); diff --git a/cli/shared/project-creation.ts b/cli/shared/project-creation.ts index 7cf1dbe277..85ccf22a6e 100644 --- a/cli/shared/project-creation.ts +++ b/cli/shared/project-creation.ts @@ -5,9 +5,11 @@ import { TEMPLATE_NOT_FOUND, toError, } from "veryfront/errors"; +import { isNotFoundError } from "veryfront/fs"; import { cliLogger as logger } from "#cli/utils"; import { createFileSystem } from "veryfront/platform"; import { join } from "veryfront/platform/path"; +import { LOCKFILE_CLIENTS, NPM_FAMILY_CLIENTS } from "veryfront/utils/package-client"; import { ensureDir } from "#std/fs.ts"; import { buildDenoConfig, createDenoConfig } from "../commands/init/deno-config-generator.ts"; import { @@ -382,15 +384,26 @@ async function writeEnvFiles( async function writeGitignore(projectDir: string): Promise { const fs = createFileSystem(); + if (!fs.rename) { + throw createConfigError("Filesystem does not support atomic .gitignore replacement."); + } const gitignorePath = join(projectDir, ".gitignore"); + const temporaryPath = join(projectDir, `.gitignore.veryfront-${crypto.randomUUID()}.tmp`); let existingGitignore: string | undefined; try { existingGitignore = await fs.readTextFile(gitignorePath); - } catch { + } catch (error) { + if (!isNotFoundError(error)) throw error; existingGitignore = undefined; } - await fs.writeTextFile(gitignorePath, generateGitignoreContent(existingGitignore)); + await fs.writeTextFile(temporaryPath, generateGitignoreContent(existingGitignore)); + try { + await fs.rename(temporaryPath, gitignorePath); + } catch (error) { + await fs.remove(temporaryPath).catch(() => {}); + throw error; + } logger.debug("Updated file: .gitignore"); } @@ -504,6 +517,45 @@ function scaffoldWritePaths(assembly: ScaffoldAssembly, request: CreateProjectRe return paths; } +function installerWritePaths(request: CreateProjectRequest): string[] { + if (!request.installDependencies) return []; + + const packageManager = packageManagerPreference(request.runtime); + const lockfiles = LOCKFILE_CLIENTS + .filter(([, client]) => client === packageManager) + .map(([path]) => path); + if (packageManager === "npm") { + lockfiles.push("npm-shrinkwrap.json", "node_modules/.package-lock.json"); + } + return lockfiles; +} + +function installerConflictPaths(request: CreateProjectRequest): string[] { + const paths = installerWritePaths(request); + if ( + request.installDependencies && + NPM_FAMILY_CLIENTS.includes(packageManagerPreference(request.runtime)) + ) { + paths.push("node_modules"); + } + return paths; +} + +function conflictWritePaths( + assembly: ScaffoldAssembly, + request: CreateProjectRequest, +): string[] { + return [...scaffoldWritePaths(assembly, request), ...installerConflictPaths(request)]; +} + +function protectedMergePaths(): string[] { + return [".gitignore"]; +} + +function protectedLeafPaths(request: CreateProjectRequest): string[] { + return [...protectedMergePaths(), ...installerWritePaths(request)]; +} + async function findExistingPaths(dir: string, paths: string[]): Promise { const fs = createFileSystem(); const existing: string[] = []; @@ -513,6 +565,21 @@ async function findExistingPaths(dir: string, paths: string[]): Promise { + const fs = createFileSystem(); + if (!fs.lstat) return false; + try { + return (await fs.lstat(path)).isSymlink === true; + } catch { + return false; // Nothing there, so nothing to write through. + } +} + /** * Paths the scaffold cannot write through, checked before anything is written. * @@ -528,7 +595,11 @@ async function findExistingPaths(dir: string, paths: string[]): Promise { +async function findUnwritablePaths( + dir: string, + paths: string[], + protectedLeafPaths: string[] = [], +): Promise { const fs = createFileSystem(); // `lstat` is what makes a link visible: `stat` follows it and reports the // target. It is optional only for virtual filesystems that have no links of @@ -537,7 +608,7 @@ async function findUnwritablePaths(dir: string, paths: string[]): Promise(); - for (const path of paths) { + for (const path of [...paths, ...protectedLeafPaths]) { const segments = path.split("/"); for (let depth = 1; depth <= segments.length; depth++) { const prefix = segments.slice(0, depth).join("/"); @@ -552,6 +623,10 @@ async function findUnwritablePaths(dir: string, paths: string[]): Promise Date: Sun, 23 Aug 2026 13:11:11 +0200 Subject: [PATCH 8/8] test(suites): classify CLI filesystem coverage --- cli/commands/generate/generate.test.ts | 22 +- cli/mcp/tools/catalog-tools.test.ts | 248 +-------------- .../generate/generate-conflict.test.ts | 24 ++ .../catalog-tools-project-creation.test.ts | 297 ++++++++++++++++++ 4 files changed, 323 insertions(+), 268 deletions(-) create mode 100644 tests/integration/cli/commands/generate/generate-conflict.test.ts create mode 100644 tests/integration/cli/mcp/tools/catalog-tools-project-creation.test.ts diff --git a/cli/commands/generate/generate.test.ts b/cli/commands/generate/generate.test.ts index 9d0624475e..c7b38b52e7 100644 --- a/cli/commands/generate/generate.test.ts +++ b/cli/commands/generate/generate.test.ts @@ -3,8 +3,7 @@ import "#veryfront/schemas/_test-setup.ts"; * Tests for generate command */ -import { assertEquals, assertRejects } from "#veryfront/testing/assert.ts"; -import { VeryfrontError } from "veryfront/errors"; +import { assertEquals } from "#veryfront/testing/assert.ts"; import { describe, it } from "#veryfront/testing/bdd.ts"; import { generateCommand } from "./index.ts"; @@ -23,22 +22,3 @@ describe("generate command", () => { }); }); }); - -describe("generateCommand conflicts", () => { - it("refuses to overwrite an existing file with an already-exists error", async () => { - const projectDir = await Deno.makeTempDir({ prefix: "veryfront-generate-conflict-" }); - - try { - await generateCommand(projectDir, "tool", "calculator"); - - const error = await assertRejects(() => generateCommand(projectDir, "tool", "calculator")); - - assertEquals(error instanceof VeryfrontError, true); - assertEquals((error as VeryfrontError).slug, "already-exists"); - assertEquals((error as VeryfrontError).exitCode, 1); - assertEquals((error as VeryfrontError).detail?.includes("tools/calculator.ts"), true); - } finally { - await Deno.remove(projectDir, { recursive: true }).catch(() => {}); - } - }); -}); diff --git a/cli/mcp/tools/catalog-tools.test.ts b/cli/mcp/tools/catalog-tools.test.ts index 10b5df7b28..b650c2b987 100644 --- a/cli/mcp/tools/catalog-tools.test.ts +++ b/cli/mcp/tools/catalog-tools.test.ts @@ -5,7 +5,7 @@ import "#veryfront/schemas/_test-setup.ts"; import { assertEquals, assertExists } from "#veryfront/testing/assert.ts"; import { afterEach, describe, it } from "#veryfront/testing/bdd.ts"; -import { dirname, join } from "veryfront/platform/path"; +import { join } from "veryfront/platform/path"; import { EXPERIMENTAL_INTEGRATIONS_ENV } from "../../../src/integrations/feature-flags.ts"; import { resolveCreateProjectPaths, @@ -218,252 +218,6 @@ describe("mcp/tools/catalog-tools", () => { }); }); - it("refuses a directory holding files the scaffold would overwrite", async () => { - const parentDir = await Deno.makeTempDir(); - createdDirs.push(parentDir); - const projectDir = join(parentDir, "example-app"); - await Deno.mkdir(projectDir); - await Deno.writeTextFile(join(projectDir, "README.md"), "mine\n"); - - const result = await vfCreateProject.execute({ - name: "Example App", - template: "minimal", - directory: parentDir, - }); - - // Same rule and same words as `veryfront init`: the conflict is the file, - // and it is named. - assertEquals(result.success, false); - assertEquals(result.projectDir, undefined); - assertEquals(result.message.includes("already contains README.md"), true); - assertEquals(await Deno.readTextFile(join(projectDir, "README.md")), "mine\n"); - }); - - it("scaffolds into an existing empty directory", async () => { - const parentDir = await Deno.makeTempDir(); - createdDirs.push(parentDir); - const projectDir = join(parentDir, "example-app"); - await Deno.mkdir(projectDir); - - await withFakeNpm(async () => { - const result = await vfCreateProject.execute({ - name: "Example App", - template: "minimal", - directory: parentDir, - }); - - assertEquals(result.success, true); - assertEquals(result.projectDir, projectDir); - }); - }); - - it("refuses a linked project directory instead of scaffolding outside the parent", async () => { - const parentDir = await Deno.makeTempDir(); - createdDirs.push(parentDir); - const outside = await Deno.makeTempDir(); - createdDirs.push(outside); - await Deno.symlink(outside, join(parentDir, "example-app")); - - const result = await vfCreateProject.execute({ - name: "Example App", - template: "minimal", - directory: parentDir, - }); - - assertEquals(result.success, false); - assertEquals(result.projectDir, undefined); - assertEquals(result.message.includes("is a link the scaffold cannot write through"), true); - // The link target is outside the requested parent; nothing was written there. - const written: string[] = []; - for await (const entry of Deno.readDir(outside)) written.push(entry.name); - assertEquals(written, []); - }); - - it("refuses a linked .gitignore instead of merging through it", async () => { - const parentDir = await Deno.makeTempDir(); - createdDirs.push(parentDir); - const projectDir = join(parentDir, "example-app"); - const outside = join(parentDir, "outside-gitignore"); - await Deno.mkdir(projectDir); - await Deno.writeTextFile(outside, "keep-me\n"); - await Deno.symlink(outside, join(projectDir, ".gitignore")); - - const result = await vfCreateProject.execute({ - name: "Example App", - template: "minimal", - directory: parentDir, - }); - - assertEquals(result.success, false); - assertEquals(result.projectDir, undefined); - assertEquals( - result.message.includes("already contains .gitignore as a file or a link"), - true, - ); - assertEquals(await Deno.readTextFile(outside), "keep-me\n"); - assertEquals(await Deno.readTextFile(join(projectDir, ".gitignore")), "keep-me\n"); - }); - - it("refuses a .gitignore directory before partially scaffolding", async () => { - const parentDir = await Deno.makeTempDir(); - createdDirs.push(parentDir); - const projectDir = join(parentDir, "example-app"); - await Deno.mkdir(join(projectDir, ".gitignore"), { recursive: true }); - - const result = await vfCreateProject.execute({ - name: "Example App", - template: "minimal", - directory: parentDir, - }); - - assertEquals(result.success, false); - assertEquals(result.projectDir, undefined); - assertEquals( - result.message.includes("already contains .gitignore as a file or a link"), - true, - ); - assertEquals( - await Deno.readTextFile(join(projectDir, ".gitignore", "README.md")).catch( - () => "absent", - ), - "absent", - ); - assertEquals( - await Deno.readTextFile(join(projectDir, "README.md")).catch(() => "absent"), - "absent", - ); - }); - - it("refuses a non-file .gitignore before partially scaffolding", async () => { - if (Deno.build.os === "windows") return; - - const parentDir = await Deno.makeTempDir(); - createdDirs.push(parentDir); - const projectDir = join(parentDir, "example-app"); - await Deno.mkdir(projectDir, { recursive: true }); - const command = new Deno.Command("mkfifo", { - args: [join(projectDir, ".gitignore")], - }); - const output = await command.output(); - if (!output.success) return; - - const result = await vfCreateProject.execute({ - name: "Example App", - template: "minimal", - directory: parentDir, - }); - - assertEquals(result.success, false); - assertEquals(result.projectDir, undefined); - assertEquals( - result.message.includes("already contains .gitignore as a file or a link"), - true, - ); - assertEquals( - await Deno.readTextFile(join(projectDir, "README.md")).catch(() => "absent"), - "absent", - ); - }); - - it("refuses a package lock before dependency installation can replace it", async () => { - const parentDir = await Deno.makeTempDir(); - createdDirs.push(parentDir); - const projectDir = join(parentDir, "example-app"); - const lockfile = join(projectDir, "package-lock.json"); - await Deno.mkdir(projectDir); - await Deno.writeTextFile(lockfile, "keep-me\n"); - - const result = await vfCreateProject.execute({ - name: "Example App", - template: "minimal", - directory: parentDir, - }); - - assertEquals(result.success, false); - assertEquals(result.projectDir, undefined); - assertEquals(result.message.includes("already contains package-lock.json"), true); - assertEquals(await Deno.readTextFile(lockfile), "keep-me\n"); - assertEquals( - await Deno.readTextFile(join(projectDir, "README.md")).catch(() => "absent"), - "absent", - ); - }); - - it("refuses npm's hidden lockfile before dependency installation can replace it", async () => { - const parentDir = await Deno.makeTempDir(); - createdDirs.push(parentDir); - const projectDir = join(parentDir, "example-app"); - const lockfile = join(projectDir, "node_modules", ".package-lock.json"); - await Deno.mkdir(join(projectDir, "node_modules"), { recursive: true }); - await Deno.writeTextFile(lockfile, "keep-me\n"); - - const result = await vfCreateProject.execute({ - name: "Example App", - template: "minimal", - directory: parentDir, - }); - - assertEquals(result.success, false); - assertEquals(result.projectDir, undefined); - assertEquals( - result.message.includes("already contains node_modules/.package-lock.json"), - true, - ); - assertEquals(await Deno.readTextFile(lockfile), "keep-me\n"); - assertEquals( - await Deno.readTextFile(join(projectDir, "README.md")).catch(() => "absent"), - "absent", - ); - }); - - it("refuses npm shrinkwrap before dependency installation can replace it", async () => { - const parentDir = await Deno.makeTempDir(); - createdDirs.push(parentDir); - const projectDir = join(parentDir, "example-app"); - const lockfile = join(projectDir, "npm-shrinkwrap.json"); - await Deno.mkdir(projectDir); - await Deno.writeTextFile(lockfile, "keep-me\n"); - - const result = await vfCreateProject.execute({ - name: "Example App", - template: "minimal", - directory: parentDir, - }); - - assertEquals(result.success, false); - assertEquals(result.projectDir, undefined); - assertEquals(result.message.includes("already contains npm-shrinkwrap.json"), true); - assertEquals(await Deno.readTextFile(lockfile), "keep-me\n"); - assertEquals( - await Deno.readTextFile(join(projectDir, "README.md")).catch(() => "absent"), - "absent", - ); - }); - - it("refuses existing node_modules before dependency installation can prune it", async () => { - const parentDir = await Deno.makeTempDir(); - createdDirs.push(parentDir); - const projectDir = join(parentDir, "example-app"); - const userFile = join(projectDir, "node_modules", "user-owned", "data.txt"); - await Deno.mkdir(dirname(userFile), { recursive: true }); - await Deno.writeTextFile(userFile, "keep-me\n"); - - const result = await vfCreateProject.execute({ - name: "Example App", - template: "minimal", - directory: parentDir, - }); - - assertEquals(result.success, false); - assertEquals(result.projectDir, undefined); - assertEquals(result.message.includes("already contains node_modules"), true); - assertEquals(await Deno.readTextFile(userFile), "keep-me\n"); - assertEquals( - await Deno.readTextFile(join(projectDir, "README.md")).catch(() => "absent"), - "absent", - ); - }); - it("reports project-name validation failures", async () => { const result = await vfCreateProject.execute({ name: "invalid/name", diff --git a/tests/integration/cli/commands/generate/generate-conflict.test.ts b/tests/integration/cli/commands/generate/generate-conflict.test.ts new file mode 100644 index 0000000000..c224478fd2 --- /dev/null +++ b/tests/integration/cli/commands/generate/generate-conflict.test.ts @@ -0,0 +1,24 @@ +import "#veryfront/schemas/_test-setup.ts"; +import { assertEquals, assertRejects } from "#veryfront/testing/assert.ts"; +import { describe, it } from "#veryfront/testing/bdd.ts"; +import { VeryfrontError } from "veryfront/errors"; +import { generateCommand } from "../../../../../cli/commands/generate/index.ts"; + +describe("generateCommand conflicts", () => { + it("refuses to overwrite an existing file with an already-exists error", async () => { + const projectDir = await Deno.makeTempDir({ prefix: "veryfront-generate-conflict-" }); + + try { + await generateCommand(projectDir, "tool", "calculator"); + + const error = await assertRejects(() => generateCommand(projectDir, "tool", "calculator")); + + assertEquals(error instanceof VeryfrontError, true); + assertEquals((error as VeryfrontError).slug, "already-exists"); + assertEquals((error as VeryfrontError).exitCode, 1); + assertEquals((error as VeryfrontError).detail?.includes("tools/calculator.ts"), true); + } finally { + await Deno.remove(projectDir, { recursive: true }).catch(() => {}); + } + }); +}); diff --git a/tests/integration/cli/mcp/tools/catalog-tools-project-creation.test.ts b/tests/integration/cli/mcp/tools/catalog-tools-project-creation.test.ts new file mode 100644 index 0000000000..69cd641399 --- /dev/null +++ b/tests/integration/cli/mcp/tools/catalog-tools-project-creation.test.ts @@ -0,0 +1,297 @@ +import "#veryfront/schemas/_test-setup.ts"; +import { assertEquals } from "#veryfront/testing/assert.ts"; +import { afterEach, describe, it } from "#veryfront/testing/bdd.ts"; +import { dirname, join } from "veryfront/platform/path"; +import { vfCreateProject } from "../../../../../cli/mcp/tools/catalog-tools.ts"; + +async function createFakeNpm(): Promise { + const binDir = await Deno.makeTempDir(); + const logPath = join(binDir, "npm.log"); + const isWindows = Deno.build.os === "windows"; + const npmPath = join(binDir, isWindows ? "npm.cmd" : "npm"); + const script = isWindows + ? [ + "@echo off", + `>>"${logPath}" echo %CD% %*`, + `>package-lock.json echo {"lockfileVersion":3,"packages":{}}`, + "exit /b 0", + "", + ].join("\r\n") + : `#!/usr/bin/env sh +printf '%s\n' "$PWD $*" >> "${logPath}" +printf '%s\n' '{"lockfileVersion":3,"packages":{}}' > package-lock.json +exit 0 +`; + + await Deno.writeTextFile(npmPath, script); + if (!isWindows) await Deno.chmod(npmPath, 0o755); + return binDir; +} + +async function withFakeNpm(action: () => Promise): Promise { + const binDir = await createFakeNpm(); + const pathDelimiter = Deno.build.os === "windows" ? ";" : ":"; + const originalDenoPath = Deno.env.get("PATH"); + const nextPath = `${binDir}${pathDelimiter}${originalDenoPath ?? ""}`; + + try { + Deno.env.set("PATH", nextPath); + await action(); + } finally { + if (originalDenoPath === undefined) Deno.env.delete("PATH"); + else Deno.env.set("PATH", originalDenoPath); + await Deno.remove(binDir, { recursive: true }).catch(() => {}); + } +} + +describe("vfCreateProject filesystem conflicts", () => { + const createdDirs: string[] = []; + + afterEach(async () => { + for (const dir of createdDirs.splice(0)) { + await Deno.remove(dir, { recursive: true }).catch(() => {}); + } + }); + + it("refuses a directory holding files the scaffold would overwrite", async () => { + const parentDir = await Deno.makeTempDir(); + createdDirs.push(parentDir); + const projectDir = join(parentDir, "example-app"); + await Deno.mkdir(projectDir); + await Deno.writeTextFile(join(projectDir, "README.md"), "mine\n"); + + const result = await vfCreateProject.execute({ + name: "Example App", + template: "minimal", + directory: parentDir, + }); + + assertEquals(result.success, false); + assertEquals(result.projectDir, undefined); + assertEquals(result.message.includes("already contains README.md"), true); + assertEquals(await Deno.readTextFile(join(projectDir, "README.md")), "mine\n"); + }); + + it("scaffolds into an existing empty directory", async () => { + const parentDir = await Deno.makeTempDir(); + createdDirs.push(parentDir); + const projectDir = join(parentDir, "example-app"); + await Deno.mkdir(projectDir); + + await withFakeNpm(async () => { + const result = await vfCreateProject.execute({ + name: "Example App", + template: "minimal", + directory: parentDir, + }); + + assertEquals(result.success, true); + assertEquals(result.projectDir, projectDir); + }); + }); + + it("refuses a linked project directory instead of scaffolding outside the parent", async () => { + const parentDir = await Deno.makeTempDir(); + createdDirs.push(parentDir); + const outside = await Deno.makeTempDir(); + createdDirs.push(outside); + await Deno.symlink(outside, join(parentDir, "example-app")); + + const result = await vfCreateProject.execute({ + name: "Example App", + template: "minimal", + directory: parentDir, + }); + + assertEquals(result.success, false); + assertEquals(result.projectDir, undefined); + assertEquals(result.message.includes("is a link the scaffold cannot write through"), true); + const written: string[] = []; + for await (const entry of Deno.readDir(outside)) written.push(entry.name); + assertEquals(written, []); + }); + + it("refuses a linked .gitignore instead of merging through it", async () => { + const parentDir = await Deno.makeTempDir(); + createdDirs.push(parentDir); + const projectDir = join(parentDir, "example-app"); + const outside = join(parentDir, "outside-gitignore"); + await Deno.mkdir(projectDir); + await Deno.writeTextFile(outside, "keep-me\n"); + await Deno.symlink(outside, join(projectDir, ".gitignore")); + + const result = await vfCreateProject.execute({ + name: "Example App", + template: "minimal", + directory: parentDir, + }); + + assertEquals(result.success, false); + assertEquals(result.projectDir, undefined); + assertEquals( + result.message.includes("already contains .gitignore as a file or a link"), + true, + ); + assertEquals(await Deno.readTextFile(outside), "keep-me\n"); + assertEquals(await Deno.readTextFile(join(projectDir, ".gitignore")), "keep-me\n"); + }); + + it("refuses a .gitignore directory before partially scaffolding", async () => { + const parentDir = await Deno.makeTempDir(); + createdDirs.push(parentDir); + const projectDir = join(parentDir, "example-app"); + await Deno.mkdir(join(projectDir, ".gitignore"), { recursive: true }); + + const result = await vfCreateProject.execute({ + name: "Example App", + template: "minimal", + directory: parentDir, + }); + + assertEquals(result.success, false); + assertEquals(result.projectDir, undefined); + assertEquals( + result.message.includes("already contains .gitignore as a file or a link"), + true, + ); + assertEquals( + await Deno.readTextFile(join(projectDir, ".gitignore", "README.md")).catch( + () => "absent", + ), + "absent", + ); + assertEquals( + await Deno.readTextFile(join(projectDir, "README.md")).catch(() => "absent"), + "absent", + ); + }); + + it("refuses a non-file .gitignore before partially scaffolding", async () => { + if (Deno.build.os === "windows") return; + + const parentDir = await Deno.makeTempDir(); + createdDirs.push(parentDir); + const projectDir = join(parentDir, "example-app"); + await Deno.mkdir(projectDir, { recursive: true }); + const output = await new Deno.Command("mkfifo", { + args: [join(projectDir, ".gitignore")], + }).output(); + if (!output.success) return; + + const result = await vfCreateProject.execute({ + name: "Example App", + template: "minimal", + directory: parentDir, + }); + + assertEquals(result.success, false); + assertEquals(result.projectDir, undefined); + assertEquals( + result.message.includes("already contains .gitignore as a file or a link"), + true, + ); + assertEquals( + await Deno.readTextFile(join(projectDir, "README.md")).catch(() => "absent"), + "absent", + ); + }); + + it("refuses a package lock before dependency installation can replace it", async () => { + const parentDir = await Deno.makeTempDir(); + createdDirs.push(parentDir); + const projectDir = join(parentDir, "example-app"); + const lockfile = join(projectDir, "package-lock.json"); + await Deno.mkdir(projectDir); + await Deno.writeTextFile(lockfile, "keep-me\n"); + + const result = await vfCreateProject.execute({ + name: "Example App", + template: "minimal", + directory: parentDir, + }); + + assertEquals(result.success, false); + assertEquals(result.projectDir, undefined); + assertEquals(result.message.includes("already contains package-lock.json"), true); + assertEquals(await Deno.readTextFile(lockfile), "keep-me\n"); + assertEquals( + await Deno.readTextFile(join(projectDir, "README.md")).catch(() => "absent"), + "absent", + ); + }); + + it("refuses npm hidden lockfile before dependency installation can replace it", async () => { + const parentDir = await Deno.makeTempDir(); + createdDirs.push(parentDir); + const projectDir = join(parentDir, "example-app"); + const lockfile = join(projectDir, "node_modules", ".package-lock.json"); + await Deno.mkdir(join(projectDir, "node_modules"), { recursive: true }); + await Deno.writeTextFile(lockfile, "keep-me\n"); + + const result = await vfCreateProject.execute({ + name: "Example App", + template: "minimal", + directory: parentDir, + }); + + assertEquals(result.success, false); + assertEquals(result.projectDir, undefined); + assertEquals( + result.message.includes("already contains node_modules/.package-lock.json"), + true, + ); + assertEquals(await Deno.readTextFile(lockfile), "keep-me\n"); + assertEquals( + await Deno.readTextFile(join(projectDir, "README.md")).catch(() => "absent"), + "absent", + ); + }); + + it("refuses npm shrinkwrap before dependency installation can replace it", async () => { + const parentDir = await Deno.makeTempDir(); + createdDirs.push(parentDir); + const projectDir = join(parentDir, "example-app"); + const lockfile = join(projectDir, "npm-shrinkwrap.json"); + await Deno.mkdir(projectDir); + await Deno.writeTextFile(lockfile, "keep-me\n"); + + const result = await vfCreateProject.execute({ + name: "Example App", + template: "minimal", + directory: parentDir, + }); + + assertEquals(result.success, false); + assertEquals(result.projectDir, undefined); + assertEquals(result.message.includes("already contains npm-shrinkwrap.json"), true); + assertEquals(await Deno.readTextFile(lockfile), "keep-me\n"); + assertEquals( + await Deno.readTextFile(join(projectDir, "README.md")).catch(() => "absent"), + "absent", + ); + }); + + it("refuses existing node_modules before dependency installation can prune it", async () => { + const parentDir = await Deno.makeTempDir(); + createdDirs.push(parentDir); + const projectDir = join(parentDir, "example-app"); + const userFile = join(projectDir, "node_modules", "user-owned", "data.txt"); + await Deno.mkdir(dirname(userFile), { recursive: true }); + await Deno.writeTextFile(userFile, "keep-me\n"); + + const result = await vfCreateProject.execute({ + name: "Example App", + template: "minimal", + directory: parentDir, + }); + + assertEquals(result.success, false); + assertEquals(result.projectDir, undefined); + assertEquals(result.message.includes("already contains node_modules"), true); + assertEquals(await Deno.readTextFile(userFile), "keep-me\n"); + assertEquals( + await Deno.readTextFile(join(projectDir, "README.md")).catch(() => "absent"), + "absent", + ); + }); +});