Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 5 additions & 7 deletions cli/commands/generate/command.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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}`);
Expand Down
13 changes: 12 additions & 1 deletion cli/commands/generate/handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);
});
});
13 changes: 7 additions & 6 deletions cli/commands/generate/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -30,11 +31,11 @@ export async function handleGenerateCommand(args: ParsedArgs): Promise<void> {
showHeader();
const result = parseGenerateArgs(args);
if (!result.success) {
throw new Error(
`Invalid arguments. Usage: veryfront generate <type> <name>\n\nValid types: ${
throw INVALID_ARGUMENT.create({
detail: `Invalid arguments. Usage: veryfront generate <type> <name>\n\nValid types: ${
VALID_TYPES.join(", ")
}`,
);
});
}
const { type, name } = result.data;

Expand All @@ -45,11 +46,11 @@ export async function handleGenerateCommand(args: ParsedArgs): Promise<void> {
}

if (!type || !name) {
throw new Error(
`Invalid arguments. Usage: veryfront generate <type> <name>\n\nValid types: ${
throw INVALID_ARGUMENT.create({
detail: `Invalid arguments. Usage: veryfront generate <type> <name>\n\nValid types: ${
VALID_TYPES.join(", ")
}`,
);
});
}

await generateCommand(cwd(), type, name);
Expand Down
4 changes: 2 additions & 2 deletions cli/commands/init/init-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 });
}
}

Expand Down
20 changes: 10 additions & 10 deletions cli/commands/init/init.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});

Expand All @@ -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 {
Expand Down
11 changes: 11 additions & 0 deletions cli/commands/init/runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down Expand Up @@ -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);
});
});
8 changes: 5 additions & 3 deletions cli/commands/init/runtime.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { INVALID_ARGUMENT } from "veryfront/errors";
import type { InitRuntime } from "./types.ts";

const VALID_RUNTIMES: readonly InitRuntime[] = ["node", "bun", "deno"];
Expand All @@ -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 },
});
}
16 changes: 0 additions & 16 deletions cli/mcp/tools/catalog-tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,22 +218,6 @@ describe("mcp/tools/catalog-tools", () => {
});
});

it("keeps the existing-directory failure response", async () => {
const parentDir = await Deno.makeTempDir();
createdDirs.push(parentDir);
const projectDir = join(parentDir, "example-app");
await Deno.mkdir(projectDir);

const result = await vfCreateProject.execute({
name: "Example App",
template: "minimal",
directory: parentDir,
});

assertEquals(result.success, false);
assertEquals(result.message, `Directory already exists: ${projectDir}`);
});

it("reports project-name validation failures", async () => {
const result = await vfCreateProject.execute({
name: "invalid/name",
Expand Down
12 changes: 6 additions & 6 deletions cli/mcp/tools/catalog-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -391,16 +391,16 @@ export const vfCreateProject: MCPTool<CreateProjectInput, CreateProjectResult> =
"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,
Expand Down
2 changes: 1 addition & 1 deletion cli/router.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
21 changes: 20 additions & 1 deletion cli/shared/args.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -13,6 +14,7 @@ import {
extractArg,
extractArgs,
GLOBAL_BOOLEAN_FLAGS,
parseArgsOrThrow,
parseCliArgs,
} from "./args.ts";
import { COMMANDS } from "../help/command-definitions.ts";
Expand Down Expand Up @@ -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");
});
});
8 changes: 5 additions & 3 deletions cli/shared/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -203,9 +204,10 @@ export function parseArgsOrThrow<T>(
): 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;
}
Expand Down
Loading
Loading