mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-05-19 16:39:50 +00:00
fix: Detect swapped agent/cloud arguments and fix count pluralization (#178)
When users type "spawn sprite claude" instead of "spawn claude sprite", the CLI now detects the swap and suggests the correct order instead of showing a confusing "Unknown agent" error. Also fixes grammar in "spawn agents" and "spawn clouds" output (1 cloud vs 1 clouds). Agent: ux-engineer Co-authored-by: A <6723574+louisgv@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
836fd0db97
commit
86dfbacab0
5 changed files with 69 additions and 9 deletions
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@openrouter/spawn",
|
||||
"version": "0.2.13",
|
||||
"version": "0.2.14",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"spawn": "cli.js"
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ const mockManifest = createMockManifest();
|
|||
const mockLogError = mock(() => {});
|
||||
const mockLogInfo = mock(() => {});
|
||||
const mockLogStep = mock(() => {});
|
||||
const mockLogWarn = mock(() => {});
|
||||
const mockSpinnerStart = mock(() => {});
|
||||
const mockSpinnerStop = mock(() => {});
|
||||
|
||||
|
|
@ -40,6 +41,7 @@ mock.module("@clack/prompts", () => ({
|
|||
step: mockLogStep,
|
||||
info: mockLogInfo,
|
||||
error: mockLogError,
|
||||
warn: mockLogWarn,
|
||||
},
|
||||
intro: mock(() => {}),
|
||||
outro: mock(() => {}),
|
||||
|
|
@ -61,6 +63,7 @@ describe("Commands Error Paths", () => {
|
|||
mockLogError.mockClear();
|
||||
mockLogInfo.mockClear();
|
||||
mockLogStep.mockClear();
|
||||
mockLogWarn.mockClear();
|
||||
mockSpinnerStart.mockClear();
|
||||
mockSpinnerStop.mockClear();
|
||||
|
||||
|
|
@ -347,4 +350,49 @@ describe("Commands Error Paths", () => {
|
|||
expect(stepCalls.some((msg: string) => msg.includes("with prompt"))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ── cmdRun: swapped arguments detection ──────────────────────────────
|
||||
|
||||
describe("cmdRun - swapped arguments detection", () => {
|
||||
it("should detect when cloud and agent arguments are swapped", async () => {
|
||||
// "spawn sprite claude" should detect that sprite is a cloud and claude is an agent
|
||||
await expect(cmdRun("sprite", "claude")).rejects.toThrow("process.exit");
|
||||
expect(processExitSpy).toHaveBeenCalledWith(1);
|
||||
|
||||
const warnCalls = mockLogWarn.mock.calls.map((c: any[]) => c.join(" "));
|
||||
expect(warnCalls.some((msg: string) => msg.includes("swapped"))).toBe(true);
|
||||
});
|
||||
|
||||
it("should suggest the correct argument order when swapped", async () => {
|
||||
await expect(cmdRun("sprite", "claude")).rejects.toThrow("process.exit");
|
||||
|
||||
const infoCalls = mockLogInfo.mock.calls.map((c: any[]) => c.join(" "));
|
||||
expect(infoCalls.some((msg: string) => msg.includes("spawn claude sprite"))).toBe(true);
|
||||
});
|
||||
|
||||
it("should suggest correct order for hetzner/aider swap", async () => {
|
||||
await expect(cmdRun("hetzner", "aider")).rejects.toThrow("process.exit");
|
||||
|
||||
const warnCalls = mockLogWarn.mock.calls.map((c: any[]) => c.join(" "));
|
||||
expect(warnCalls.some((msg: string) => msg.includes("swapped"))).toBe(true);
|
||||
|
||||
const infoCalls = mockLogInfo.mock.calls.map((c: any[]) => c.join(" "));
|
||||
expect(infoCalls.some((msg: string) => msg.includes("spawn aider hetzner"))).toBe(true);
|
||||
});
|
||||
|
||||
it("should NOT trigger swap detection when both args are unknown", async () => {
|
||||
await expect(cmdRun("unknown1", "unknown2")).rejects.toThrow("process.exit");
|
||||
|
||||
const warnCalls = mockLogWarn.mock.calls.map((c: any[]) => c.join(" "));
|
||||
expect(warnCalls.some((msg: string) => msg.includes("swapped"))).toBe(false);
|
||||
});
|
||||
|
||||
it("should NOT trigger swap detection when agent is valid", async () => {
|
||||
// "spawn claude nonexistent" - agent is valid, cloud is not
|
||||
await expect(cmdRun("claude", "nonexistent")).rejects.toThrow("process.exit");
|
||||
|
||||
const warnCalls = mockLogWarn.mock.calls.map((c: any[]) => c.join(" "));
|
||||
expect(warnCalls.some((msg: string) => msg.includes("swapped"))).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ mock.module("@clack/prompts", () => ({
|
|||
log: {
|
||||
step: mock(() => {}),
|
||||
info: mock(() => {}),
|
||||
warn: mock(() => {}),
|
||||
error: mock(() => {}),
|
||||
},
|
||||
intro: mock(() => {}),
|
||||
|
|
@ -137,7 +138,8 @@ describe("Command Output Functions", () => {
|
|||
const output = consoleMocks.log.mock.calls.map((c: any[]) => c.join(" ")).join("\n");
|
||||
// claude has 2 clouds (sprite, hetzner), aider has 1 (sprite)
|
||||
expect(output).toContain("2 clouds");
|
||||
expect(output).toContain("1 clouds");
|
||||
expect(output).toContain("1 cloud");
|
||||
expect(output).not.toContain("1 clouds");
|
||||
});
|
||||
|
||||
it("should show agent descriptions", async () => {
|
||||
|
|
@ -187,7 +189,8 @@ describe("Command Output Functions", () => {
|
|||
const output = consoleMocks.log.mock.calls.map((c: any[]) => c.join(" ")).join("\n");
|
||||
// sprite has 2 agents (claude, aider), hetzner has 1 (claude)
|
||||
expect(output).toContain("2 agents");
|
||||
expect(output).toContain("1 agents");
|
||||
expect(output).toContain("1 agent");
|
||||
expect(output).not.toContain("1 agents");
|
||||
});
|
||||
|
||||
it("should show cloud descriptions", async () => {
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ mock.module("@clack/prompts", () => ({
|
|||
log: {
|
||||
step: mockLogStep,
|
||||
info: mockLogInfo,
|
||||
warn: mock(() => {}),
|
||||
error: mockLogError,
|
||||
},
|
||||
intro: mock(() => {}),
|
||||
|
|
|
|||
|
|
@ -244,14 +244,22 @@ export async function cmdRun(agent: string, cloud: string, prompt?: string): Pro
|
|||
process.exit(1);
|
||||
}
|
||||
|
||||
validateNonEmptyString(agent, "Agent name", "spawn agents");
|
||||
validateNonEmptyString(cloud, "Cloud name", "spawn clouds");
|
||||
|
||||
const [manifest, agentKey] = await validateAndGetAgent(agent);
|
||||
// Detect swapped arguments: user typed "spawn <cloud> <agent>" instead of "spawn <agent> <cloud>"
|
||||
const manifest = await loadManifestWithSpinner();
|
||||
if (!manifest.agents[agent] && manifest.clouds[agent] && manifest.agents[cloud]) {
|
||||
p.log.warn(`It looks like you swapped the agent and cloud arguments.`);
|
||||
p.log.info(`Try: ${pc.cyan(`spawn ${cloud} ${agent}`)}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
validateAgent(manifest, agent);
|
||||
validateCloud(manifest, cloud);
|
||||
validateImplementation(manifest, cloud, agentKey);
|
||||
validateImplementation(manifest, cloud, agent);
|
||||
|
||||
const agentName = manifest.agents[agentKey].name;
|
||||
const agentName = manifest.agents[agent].name;
|
||||
const cloudName = manifest.clouds[cloud].name;
|
||||
|
||||
if (prompt) {
|
||||
|
|
@ -260,7 +268,7 @@ export async function cmdRun(agent: string, cloud: string, prompt?: string): Pro
|
|||
p.log.step(`Launching ${pc.bold(agentName)} on ${pc.bold(cloudName)}...`);
|
||||
}
|
||||
|
||||
await execScript(cloud, agentKey, prompt);
|
||||
await execScript(cloud, agent, prompt);
|
||||
}
|
||||
|
||||
function getStatusDescription(status: number): string {
|
||||
|
|
@ -446,7 +454,7 @@ export async function cmdAgents(): Promise<void> {
|
|||
for (const key of agentKeys(manifest)) {
|
||||
const a = manifest.agents[key];
|
||||
const implCount = getImplementedClouds(manifest, key).length;
|
||||
console.log(` ${pc.green(key.padEnd(NAME_COLUMN_WIDTH))} ${a.name.padEnd(NAME_COLUMN_WIDTH)} ${pc.dim(`${implCount} clouds ${a.description}`)}`);
|
||||
console.log(` ${pc.green(key.padEnd(NAME_COLUMN_WIDTH))} ${a.name.padEnd(NAME_COLUMN_WIDTH)} ${pc.dim(`${implCount} cloud${implCount !== 1 ? "s" : ""} ${a.description}`)}`);
|
||||
}
|
||||
console.log();
|
||||
console.log(pc.dim(` Run ${pc.cyan("spawn <agent>")} for details, or ${pc.cyan("spawn <agent> <cloud>")} to launch.`));
|
||||
|
|
@ -464,7 +472,7 @@ export async function cmdClouds(): Promise<void> {
|
|||
for (const key of cloudKeys(manifest)) {
|
||||
const c = manifest.clouds[key];
|
||||
const implCount = getImplementedAgents(manifest, key).length;
|
||||
console.log(` ${pc.green(key.padEnd(NAME_COLUMN_WIDTH))} ${c.name.padEnd(NAME_COLUMN_WIDTH)} ${pc.dim(`${implCount} agents ${c.description}`)}`);
|
||||
console.log(` ${pc.green(key.padEnd(NAME_COLUMN_WIDTH))} ${c.name.padEnd(NAME_COLUMN_WIDTH)} ${pc.dim(`${implCount} agent${implCount !== 1 ? "s" : ""} ${c.description}`)}`);
|
||||
}
|
||||
console.log();
|
||||
console.log(pc.dim(` Run ${pc.cyan("spawn <agent> <cloud>")} to launch.`));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue