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
15 changes: 2 additions & 13 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { registerIdentityCommand, tryRunAtIdentityShortcut } from "./identityMan
import { profileExists, registerProfileCommand } from "./configProfiles";
import { registerRulesCommand } from "./directoryRules";
import { resolveClaudeHome, resolveLayoutPaths, type LayoutPaths } from "./paths";
import { registerRunCommand } from "./runCommand";
import { runLauncher, type FarmRuntime } from "./launcher";
import { parseLauncherArgv } from "./launcher/argv";
import { decideConfigProfile, decideIdentity, loadIdentity } from "./launcher/identity";
Expand Down Expand Up @@ -57,19 +58,7 @@ function buildClaudeUseProgram(): Command {
registerConfigureCommand(program, paths);
registerDoctorCommand(program, paths);
registerShimCommand(program, paths);

program
.command("run")
.description(
"Run the launcher pipeline directly, without needing a `claude`-named binary on PATH. " +
"Every argument is forwarded exactly as `claude` would receive it.",
)
.allowUnknownOption()
.helpOption(false)
.argument("[args...]", "Arguments to forward, e.g. @<name>, --config-profile <name>, or any Claude Code flag.")
.action(async (args: readonly string[]) => {
await runClaude(args);
});
registerRunCommand(program, runClaude);

return program;
}
Expand Down
43 changes: 43 additions & 0 deletions src/runCommand.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Command } from "commander";
import { describe, expect, it, vi } from "vitest";

import { registerRunCommand } from "./runCommand";

/** Mirrors how `buildClaudeUseProgram` sets up its parent program before registering `run` -- `passThroughOptions` on the `run` subcommand requires `enablePositionalOptions` on the parent chain, so a test program needs the same setup to exercise the real behaviour rather than commander's unrelated "broken pass-through" guard error. */
function buildTestProgram(): Command {
return new Command().exitOverride().enablePositionalOptions();
}

describe("registerRunCommand", () => {
it("preserves a literal -- inside the forwarded args", async () => {
const handler = vi.fn<(args: readonly string[]) => Promise<void>>().mockResolvedValue(undefined);
const program = buildTestProgram();
registerRunCommand(program, handler);

await program.parseAsync(["run", "mcp", "add", "agent-comms", "--", "npx", "-y", "agent-comms", "bridge", "mcp"], {
from: "user",
});

expect(handler).toHaveBeenCalledWith(["mcp", "add", "agent-comms", "--", "npx", "-y", "agent-comms", "bridge", "mcp"]);
});

it("forwards a flag with no -- unchanged", async () => {
const handler = vi.fn<(args: readonly string[]) => Promise<void>>().mockResolvedValue(undefined);
const program = buildTestProgram();
registerRunCommand(program, handler);

await program.parseAsync(["run", "--config-profile", "work", "@myid"], { from: "user" });

expect(handler).toHaveBeenCalledWith(["--config-profile", "work", "@myid"]);
});

it("forwards an empty args list when nothing follows run", async () => {
const handler = vi.fn<(args: readonly string[]) => Promise<void>>().mockResolvedValue(undefined);
const program = buildTestProgram();
registerRunCommand(program, handler);

await program.parseAsync(["run"], { from: "user" });

expect(handler).toHaveBeenCalledWith([]);
});
});
21 changes: 21 additions & 0 deletions src/runCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { Command } from "commander";

/**
* Registers `claude-use run [args...]`, forwarding every argument to `handler` exactly as typed. Kept in its own module, separate from `cli.ts`'s `buildClaudeUseProgram`, so its Commander parsing behaviour -- in particular, preserving a literal `--` inside the forwarded args rather than losing it -- is testable without importing `cli.ts` itself, which runs the real CLI as a top-level side effect on import.
*/
export function registerRunCommand(program: Command, handler: (args: readonly string[]) => Promise<void>): void {
program
.command("run")
.description(
"Run the launcher pipeline directly, without needing a `claude`-named binary on PATH. " +
"Every argument is forwarded exactly as `claude` would receive it.",
)
.allowUnknownOption()
// Without this, Commander treats a literal `--` inside the forwarded args as its own end-of-options marker and strips it before it reaches `args`, so a downstream flag meant to be shielded from parsing (e.g. `claude mcp add name -- npx -y pkg`) arrives at the real `claude` binary with no `--` at all, and its own Commander parser then rejects the bare `-y` as an unknown option of its own. `passThroughOptions` makes Commander forward every token after the first one verbatim, `--` included. It requires the parent program to have already called `enablePositionalOptions()`, which `buildClaudeUseProgram` does.
.passThroughOptions()
.helpOption(false)
.argument("[args...]", "Arguments to forward, e.g. @<name>, --config-profile <name>, or any Claude Code flag.")
.action(async (args: readonly string[]) => {
await handler(args);
});
}
Loading