From d9ae8239cf01d2a109a00cec585eeede81def0ca Mon Sep 17 00:00:00 2001 From: Koji Wakayama Date: Tue, 11 Aug 2026 22:17:27 +0200 Subject: [PATCH 1/2] fix(cli): accept a bare tool name for veryfront install `veryfront install agents` silently dropped the positional and fell back to auto-detection, which in a fresh project writes SKILL.md and exits 0. The published installation and coding-agents pages still print that exact line while promising AGENTS.md, so a reader following the docs gets the wrong file with no warning. The arg parser already supports positionals; the install/uninstall spec just never declared one. Declaring `positional: 0` makes `veryfront install agents` resolve the same target as `--target agents`, with the explicit flag still winning when both are given. An unknown positional now fails validation instead of installing something else. --- cli/commands/install/command-help.ts | 5 +- cli/commands/install/handler.test.ts | 37 +++++++++++++- cli/commands/install/handler.ts | 2 +- .../install/install.integration.test.ts | 48 +++++++++++++++++++ tests/docs/cli-install-commands.test.ts | 8 ++-- 5 files changed, 92 insertions(+), 8 deletions(-) diff --git a/cli/commands/install/command-help.ts b/cli/commands/install/command-help.ts index 275551fd0e..e335a94c72 100644 --- a/cli/commands/install/command-help.ts +++ b/cli/commands/install/command-help.ts @@ -4,7 +4,7 @@ export const installHelp: CommandHelp = { name: "install", category: "project", description: "Install AI assistant integrations (Cursor, Claude Code, etc.)", - usage: "veryfront install [options]", + usage: "veryfront install [tools] [options]", options: [ { flag: "--target ", @@ -22,6 +22,7 @@ export const installHelp: CommandHelp = { ], examples: [ "veryfront install # Interactive multi-select", + "veryfront install agents # Same as --target agents", "veryfront install --target cursor", "veryfront install --target all", "veryfront install --target cursor,claude-code --force", @@ -38,7 +39,7 @@ export const uninstallHelp: CommandHelp = { name: "uninstall", category: "project", description: "Remove AI assistant integrations", - usage: "veryfront uninstall [options]", + usage: "veryfront uninstall [tools] [options]", options: [ { flag: "--target ", diff --git a/cli/commands/install/handler.test.ts b/cli/commands/install/handler.test.ts index 4f05e970e6..e2079bd99c 100644 --- a/cli/commands/install/handler.test.ts +++ b/cli/commands/install/handler.test.ts @@ -1,9 +1,16 @@ import "#veryfront/schemas/_test-setup.ts"; import { assertEquals } from "#veryfront/testing/assert.ts"; import { describe, it } from "#veryfront/testing/bdd.ts"; -import { handleInstallCommand, handleUninstallCommand } from "./handler.ts"; +import { handleInstallCommand, handleUninstallCommand, parseInstallArgs } from "./handler.ts"; +import { parseCliArgs } from "#cli/shared/args"; import type { ParsedArgs } from "#cli/shared/types"; +function parseCommandLine(argv: string[]) { + const result = parseInstallArgs(parseCliArgs(argv)); + if (!result.success) throw result.error; + return result.data; +} + function extractInstallArgs(args: ParsedArgs) { const target = typeof args.target === "string" ? args.target : undefined; return { @@ -118,6 +125,34 @@ describe("commands/install/handler", () => { }); }); + describe("bare positional target", () => { + it("reads the target from the first positional argument", () => { + assertEquals(parseCommandLine(["install", "agents"]).target, "agents"); + }); + + it("reads a comma-separated positional target", () => { + assertEquals( + parseCommandLine(["install", "cursor,claude-code"]).target, + "cursor,claude-code", + ); + }); + + it("lets --target win over the positional", () => { + assertEquals( + parseCommandLine(["install", "agents", "--target", "cursor"]).target, + "cursor", + ); + }); + + it("leaves the target unset when no positional is given", () => { + assertEquals(parseCommandLine(["install"]).target, undefined); + }); + + it("reads the positional for uninstall too", () => { + assertEquals(parseCommandLine(["uninstall", "agents"]).target, "agents"); + }); + }); + describe("uninstall argument extraction", () => { it("uses same parsing pattern as install", () => { const installResult = extractInstallArgs({ diff --git a/cli/commands/install/handler.ts b/cli/commands/install/handler.ts index cf177d74f8..e28c82051c 100644 --- a/cli/commands/install/handler.ts +++ b/cli/commands/install/handler.ts @@ -19,7 +19,7 @@ const getInstallArgsSchema = defineSchema((v) => const InstallArgsSchema = lazySchema(getInstallArgsSchema); export const parseInstallArgs = createArgParser(InstallArgsSchema, { - target: { keys: ["target", "t"], type: "string" }, + target: { keys: ["target", "t"], type: "string", positional: 0 }, global: { keys: ["global", "g"], type: "boolean" }, force: CommonArgs.force, }); diff --git a/cli/commands/install/install.integration.test.ts b/cli/commands/install/install.integration.test.ts index 34f59db946..7f4f9e9990 100644 --- a/cli/commands/install/install.integration.test.ts +++ b/cli/commands/install/install.integration.test.ts @@ -46,6 +46,22 @@ describe("install command integration", () => { }; } + async function runInstallArgs( + args: string[], + ): Promise<{ code: number; output: string }> { + const cliPath = new URL("../../main.ts", import.meta.url).pathname; + const result = await runCommand("deno", { + args: ["run", "--allow-all", cliPath, "install", ...args], + cwd: tempDir, + capture: true, + }); + + return { + code: result.code, + output: (result.stdout ?? "") + (result.stderr ?? ""), + }; + } + async function assertFileExists(path: string): Promise { assertEquals(await exists(path), true); } @@ -146,6 +162,38 @@ describe("install command integration", () => { }); }); + describe("bare positional target", () => { + it("installs AGENTS.md for `veryfront install agents`", async () => { + const { code } = await runInstallArgs(["agents", "--force", "--no-input"]); + assertEquals(code, 0); + + await assertFileExists(join(tempDir, "AGENTS.md")); + await assertFileNotExists(join(tempDir, "SKILL.md")); + }); + + it("installs the Claude Code integration for `veryfront install claude-code`", async () => { + const { code } = await runInstallArgs(["claude-code", "--force", "--no-input"]); + assertEquals(code, 0); + + await assertFileExists(join(tempDir, ".claude/CLAUDE.md")); + await assertFileNotExists(join(tempDir, "SKILL.md")); + }); + + it("prefers --target when both a flag and a positional are given", async () => { + const { code } = await runInstallArgs([ + "agents", + "--target", + "cursor", + "--force", + "--no-input", + ]); + assertEquals(code, 0); + + await assertFileExists(join(tempDir, ".cursorrules")); + await assertFileNotExists(join(tempDir, "AGENTS.md")); + }); + }); + describe("all targets", () => { it("should install all tools with --target all", async () => { const { code } = await runInstall("all"); diff --git a/tests/docs/cli-install-commands.test.ts b/tests/docs/cli-install-commands.test.ts index 7a7fea7476..a7e68bcfeb 100644 --- a/tests/docs/cli-install-commands.test.ts +++ b/tests/docs/cli-install-commands.test.ts @@ -7,10 +7,10 @@ * unpublished notes (`docs/internal`, `docs/rfcs`, `docs/evidence`) are out of * scope — they may quote a broken invocation deliberately. * - * The install command reads its target from `--target` only. A bare positional - * (`veryfront install agents`) is silently ignored and the command falls back to - * auto-detection, which in a fresh project writes `SKILL.md` instead of the - * `AGENTS.md` the docs describe. + * The install command takes its target from `--target` or from the first + * positional argument (`veryfront install agents`). A command line that selects + * neither falls back to auto-detection, which in a fresh project writes + * `SKILL.md` instead of the `AGENTS.md` the docs describe. */ import "#veryfront/schemas/_test-setup.ts"; From bdb264767ce213fdfc1428bc64532f9eb5838c07 Mon Sep 17 00:00:00 2001 From: Koji Wakayama Date: Tue, 11 Aug 2026 22:30:56 +0200 Subject: [PATCH 2/2] test(cli): cover the unknown positional install target Review follow-up: add the uninstall positional example to the command help and an end-to-end case asserting an unknown positional exits 1 without writing any integration file. --- cli/commands/install/command-help.ts | 1 + cli/commands/install/install.integration.test.ts | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/cli/commands/install/command-help.ts b/cli/commands/install/command-help.ts index e335a94c72..8a1d55e1ac 100644 --- a/cli/commands/install/command-help.ts +++ b/cli/commands/install/command-help.ts @@ -53,6 +53,7 @@ export const uninstallHelp: CommandHelp = { ], examples: [ "veryfront uninstall # Interactive multi-select", + "veryfront uninstall agents # Same as --target agents", "veryfront uninstall --target cursor", "veryfront uninstall --target all", "veryfront uninstall --global", diff --git a/cli/commands/install/install.integration.test.ts b/cli/commands/install/install.integration.test.ts index 7f4f9e9990..e336694f55 100644 --- a/cli/commands/install/install.integration.test.ts +++ b/cli/commands/install/install.integration.test.ts @@ -192,6 +192,14 @@ describe("install command integration", () => { await assertFileExists(join(tempDir, ".cursorrules")); await assertFileNotExists(join(tempDir, "AGENTS.md")); }); + + it("fails instead of installing something else for an unknown target", async () => { + const { code } = await runInstallArgs(["unknown-tool", "--force", "--no-input"]); + assertEquals(code, 1); + + await assertFileNotExists(join(tempDir, "SKILL.md")); + await assertFileNotExists(join(tempDir, "AGENTS.md")); + }); }); describe("all targets", () => {