diff --git a/cli/package.json b/cli/package.json index 74b7dd65f..5caeaaa86 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@openrouter/spawn", - "version": "0.2.73", + "version": "0.2.74", "type": "module", "bin": { "spawn": "cli.js" diff --git a/cli/src/__tests__/clear-history.test.ts b/cli/src/__tests__/clear-history.test.ts index ef418db76..34d37d350 100644 --- a/cli/src/__tests__/clear-history.test.ts +++ b/cli/src/__tests__/clear-history.test.ts @@ -268,33 +268,33 @@ describe("cmdListClear", () => { } }); - it("should call log.info when no history exists", () => { - cmdListClear(); + it("should call log.info when no history exists", async () => { + await cmdListClear(); expect(mockLogInfo).toHaveBeenCalledTimes(1); const msg = mockLogInfo.mock.calls[0][0] as string; expect(msg).toContain("No spawn history to clear"); }); - it("should call log.success with count when clearing records", () => { + it("should call log.success with count when clearing records", async () => { const records: SpawnRecord[] = [ { agent: "claude", cloud: "sprite", timestamp: "2026-01-01T00:00:00.000Z" }, { agent: "aider", cloud: "hetzner", timestamp: "2026-01-02T00:00:00.000Z" }, ]; writeFileSync(join(testDir, "history.json"), JSON.stringify(records)); - cmdListClear(); + await cmdListClear(); expect(mockLogSuccess).toHaveBeenCalledTimes(1); const msg = mockLogSuccess.mock.calls[0][0] as string; expect(msg).toContain("Cleared 2 spawn records from history"); }); - it("should use singular 'record' for a single entry", () => { + it("should use singular 'record' for a single entry", async () => { const records: SpawnRecord[] = [ { agent: "claude", cloud: "sprite", timestamp: "2026-01-01T00:00:00.000Z" }, ]; writeFileSync(join(testDir, "history.json"), JSON.stringify(records)); - cmdListClear(); + await cmdListClear(); expect(mockLogSuccess).toHaveBeenCalledTimes(1); const msg = mockLogSuccess.mock.calls[0][0] as string; expect(msg).toContain("Cleared 1 spawn record from history"); @@ -302,37 +302,37 @@ describe("cmdListClear", () => { expect(msg).not.toContain("Cleared 1 spawn records"); }); - it("should actually delete the history file", () => { + it("should actually delete the history file", async () => { const records: SpawnRecord[] = [ { agent: "claude", cloud: "sprite", timestamp: "2026-01-01T00:00:00.000Z" }, ]; writeFileSync(join(testDir, "history.json"), JSON.stringify(records)); - cmdListClear(); + await cmdListClear(); expect(existsSync(join(testDir, "history.json"))).toBe(false); }); - it("should handle empty array history file as no history", () => { + it("should handle empty array history file as no history", async () => { writeFileSync(join(testDir, "history.json"), "[]"); - cmdListClear(); + await cmdListClear(); expect(mockLogInfo).toHaveBeenCalledTimes(1); expect(mockLogSuccess).not.toHaveBeenCalled(); const msg = mockLogInfo.mock.calls[0][0] as string; expect(msg).toContain("No spawn history to clear"); }); - it("should handle corrupted history file as no history", () => { + it("should handle corrupted history file as no history", async () => { writeFileSync(join(testDir, "history.json"), "corrupt{{{"); - cmdListClear(); + await cmdListClear(); expect(mockLogInfo).toHaveBeenCalledTimes(1); expect(mockLogSuccess).not.toHaveBeenCalled(); const msg = mockLogInfo.mock.calls[0][0] as string; expect(msg).toContain("No spawn history to clear"); }); - it("should display correct count for large history", () => { + it("should display correct count for large history", async () => { const records: SpawnRecord[] = []; for (let i = 0; i < 50; i++) { records.push({ @@ -343,19 +343,19 @@ describe("cmdListClear", () => { } writeFileSync(join(testDir, "history.json"), JSON.stringify(records)); - cmdListClear(); + await cmdListClear(); expect(mockLogSuccess).toHaveBeenCalledTimes(1); const msg = mockLogSuccess.mock.calls[0][0] as string; expect(msg).toContain("Cleared 50 spawn records from history"); }); - it("should allow saving new records after clearing via cmdListClear", () => { + it("should allow saving new records after clearing via cmdListClear", async () => { const records: SpawnRecord[] = [ { agent: "claude", cloud: "sprite", timestamp: "2026-01-01T00:00:00.000Z" }, ]; writeFileSync(join(testDir, "history.json"), JSON.stringify(records)); - cmdListClear(); + await cmdListClear(); // Save new record after clearing saveSpawnRecord({ agent: "aider", cloud: "hetzner", timestamp: "2026-01-02T00:00:00.000Z" }); @@ -364,9 +364,9 @@ describe("cmdListClear", () => { expect(loaded[0].agent).toBe("aider"); }); - it("should use log.info for zero records and log.success for non-zero", () => { + it("should use log.info for zero records and log.success for non-zero", async () => { // Test with zero - cmdListClear(); + await cmdListClear(); expect(mockLogInfo).toHaveBeenCalledTimes(1); expect(mockLogSuccess).not.toHaveBeenCalled(); @@ -379,7 +379,7 @@ describe("cmdListClear", () => { { agent: "claude", cloud: "sprite", timestamp: "2026-01-01T00:00:00.000Z" }, ]; writeFileSync(join(testDir, "history.json"), JSON.stringify(records)); - cmdListClear(); + await cmdListClear(); expect(mockLogSuccess).toHaveBeenCalledTimes(1); expect(mockLogInfo).not.toHaveBeenCalled(); }); diff --git a/cli/src/commands.ts b/cli/src/commands.ts index dcdd3a5e5..3feb62c6e 100644 --- a/cli/src/commands.ts +++ b/cli/src/commands.ts @@ -329,7 +329,7 @@ export async function cmdInteractive(): Promise { const { sortedClouds, hintOverrides, credCount } = prioritizeCloudsByCredentials(clouds, manifest); if (credCount > 0) { - p.log.info(`${credCount} cloud${credCount > 1 ? "s" : ""} with credentials detected`); + p.log.info(`${credCount} cloud${credCount > 1 ? "s" : ""} with credentials detected (shown first)`); } const cloudChoice = await p.select({ @@ -469,7 +469,12 @@ function showDryRunPreview(manifest: Manifest, agent: string, cloud: string, pro } if (prompt) { - printDryRunSection("Prompt", [` ${prompt.length > 100 ? prompt.slice(0, 100) + "..." : prompt}`]); + const preview = prompt.length > 100 ? prompt.slice(0, 100) + "..." : prompt; + const lines = [` ${preview}`]; + if (prompt.length > 100) { + lines.push(pc.dim(` (${prompt.length} characters total)`)); + } + printDryRunSection("Prompt", lines); } p.log.success("Dry run complete -- no resources were provisioned"); @@ -1191,13 +1196,25 @@ async function interactiveListPicker(records: SpawnRecord[], manifest: Manifest await cmdRun(selected.agent, selected.cloud, selected.prompt); } -export function cmdListClear(): void { - const count = clearHistory(); - if (count === 0) { +export async function cmdListClear(): Promise { + const records = filterHistory(); + if (records.length === 0) { p.log.info("No spawn history to clear."); - } else { - p.log.success(`Cleared ${count} spawn record${count !== 1 ? "s" : ""} from history.`); + return; } + + if (isInteractiveTTY()) { + const shouldClear = await p.confirm({ + message: `Delete ${records.length} spawn record${records.length !== 1 ? "s" : ""} from history?`, + initialValue: false, + }); + if (p.isCancel(shouldClear) || !shouldClear) { + handleCancel(); + } + } + + const count = clearHistory(); + p.log.success(`Cleared ${count} spawn record${count !== 1 ? "s" : ""} from history.`); } export async function cmdList(agentFilter?: string, cloudFilter?: string): Promise { diff --git a/cli/src/index.ts b/cli/src/index.ts index 7c689e77f..1d5744a60 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -259,6 +259,7 @@ async function handleNoCommand(prompt: string | undefined, dryRun?: boolean): Pr console.error(pc.yellow("No interactive terminal detected.")); console.error(); console.error(` Launch directly: ${pc.cyan("spawn ")}`); + console.error(` Rerun previous: ${pc.cyan("spawn list")}`); console.error(` Browse agents: ${pc.cyan("spawn agents")}`); console.error(` Browse clouds: ${pc.cyan("spawn clouds")}`); console.error(` Full help: ${pc.cyan("spawn help")}`); @@ -284,6 +285,7 @@ function showVersion(): void { console.log(pc.dim(` ${process.versions.bun ? "bun" : "node"} ${process.versions.bun ?? process.versions.node} ${process.platform} ${process.arch}`)); const age = getCacheAge(); console.log(pc.dim(` manifest cache: ${formatCacheAge(age)}`)); + console.log(pc.dim(` https://github.com/OpenRouterTeam/spawn`)); console.log(pc.dim(` Run ${pc.cyan("spawn update")} to check for updates.`)); } @@ -364,7 +366,7 @@ function hasTrailingHelpFlag(args: string[]): boolean { async function dispatchListCommand(filteredArgs: string[]): Promise { if (hasTrailingHelpFlag(filteredArgs)) { cmdHelp(); return; } if (filteredArgs.slice(1).includes("--clear")) { - cmdListClear(); + await cmdListClear(); return; } const { agentFilter, cloudFilter } = parseListFilters(filteredArgs.slice(1));