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
11 changes: 11 additions & 0 deletions cli/commands/install/handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,17 @@ describe("commands/install/handler", () => {
it("reads the positional for uninstall too", () => {
assertEquals(parseCommandLine(["uninstall", "agents"]).target, "agents");
});

it("fails argument parsing for an unknown positional tool id", () => {
const result = parseInstallArgs(parseCliArgs(["install", "not-a-tool"]));
assertEquals(result.success, false);
assertEquals(result.error?.message.includes("Valid targets"), true);
});

it("fails argument parsing for an unknown --target too", () => {
const result = parseInstallArgs(parseCliArgs(["install", "--target", "not-a-tool"]));
assertEquals(result.success, false);
});
});

describe("uninstall argument extraction", () => {
Expand Down
9 changes: 7 additions & 2 deletions cli/commands/install/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,19 @@
*/

import { defineSchema, lazySchema } from "veryfront/schemas";
import { installCommand } from "./install.ts";
import { installCommand, isValidTargetSpec, VALID_TARGET_VALUES } from "./install.ts";
import { uninstallCommand } from "./uninstall.ts";
import { CommonArgs, createArgParser, parseArgsOrThrow } from "#cli/shared/args";
import type { ParsedArgs } from "#cli/shared/types";

const getInstallArgsSchema = defineSchema((v) =>
v.object({
target: v.string().optional(),
// Validated here rather than at install time so an unknown tool id is an
// argument error (exit 2), not a runtime failure (exit 1).
target: v.string().optional().refine(
(value) => value === undefined || isValidTargetSpec(value),
{ message: `unknown tool. Valid targets: ${VALID_TARGET_VALUES}` },
),
global: v.boolean().default(false),
force: v.boolean().default(false),
})
Expand Down
19 changes: 17 additions & 2 deletions cli/commands/install/install.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,23 @@ describe("install command integration", () => {
});

it("fails instead of installing something else for an unknown target", async () => {
const { code } = await runInstallArgs(["unknown-tool", "--force", "--no-input"]);
assertEquals(code, 1);
const { code, output } = await runInstallArgs(["unknown-tool", "--force", "--no-input"]);
// AGENTS.md reserves exit 2 for usage and argument errors.
assertEquals(code, 2);
assertEquals(output.includes("Valid targets"), true);

await assertFileNotExists(join(tempDir, "SKILL.md"));
await assertFileNotExists(join(tempDir, "AGENTS.md"));
});

it("fails an unknown --target with the same usage exit code", async () => {
const { code } = await runInstallArgs([
"--target",
"unknown-tool",
"--force",
"--no-input",
]);
assertEquals(code, 2);

await assertFileNotExists(join(tempDir, "SKILL.md"));
await assertFileNotExists(join(tempDir, "AGENTS.md"));
Expand Down
8 changes: 8 additions & 0 deletions cli/commands/install/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ export function parseTargetFlag(target: string): AIToolId[] {
return TargetFlagSchema.parse(target);
}

/** Valid target values, for messages and argument validation. */
export const VALID_TARGET_VALUES = [...AI_TOOLS.map((t) => t.id), "all"].join(", ");

/** True when the value names at least one known tool (or `all`). */
export function isValidTargetSpec(target: string): boolean {
return TargetFlagSchema.safeParse(target).success;
}

const getAIToolIdArraySchema = defineSchema((v) => v.array(AIToolIdSchema).min(1));

const AIToolIdArraySchema = lazySchema(getAIToolIdArraySchema);
Expand Down