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
6 changes: 4 additions & 2 deletions cli/commands/install/command-help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <tools>",
Expand All @@ -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",
Expand All @@ -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]",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
options: [
{
flag: "--target <tools>",
Expand All @@ -52,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",
Expand Down
37 changes: 36 additions & 1 deletion cli/commands/install/handler.test.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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({
Expand Down
2 changes: 1 addition & 1 deletion cli/commands/install/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
Expand Down
56 changes: 56 additions & 0 deletions cli/commands/install/install.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
assertEquals(await exists(path), true);
}
Expand Down Expand Up @@ -146,6 +162,46 @@ 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"));
});

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"));
});
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

describe("all targets", () => {
it("should install all tools with --target all", async () => {
const { code } = await runInstall("all");
Expand Down
8 changes: 4 additions & 4 deletions tests/docs/cli-install-commands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down