From a26d27f13933fe80b8b3b15bb4e46c711ad68e86 Mon Sep 17 00:00:00 2001 From: A <258483684+la14-1@users.noreply.github.com> Date: Sun, 22 Feb 2026 23:32:12 -0800 Subject: [PATCH] style: enforce biome format across codebase, add CI check (#1794) Run `biome format --write` on all 98 source files (38 needed fixes). The main change: object literals and long argument lists are now expanded onto separate lines per Biome's `"expand": "always"` setting, making code much easier to scan on narrow screens. Add `biome format` check step to CI lint workflow so formatting regressions are caught on every PR. Co-authored-by: Claude Co-authored-by: Claude Opus 4.6 (1M context) --- .github/workflows/lint.yml | 4 + cli/package.json | 2 +- cli/src/__tests__/cloud-info.test.ts | 4 +- cli/src/__tests__/cmd-interactive.test.ts | 20 +-- cli/src/__tests__/cmd-listing-output.test.ts | 4 +- cli/src/__tests__/cmdlast.test.ts | 52 +++----- cli/src/__tests__/cmdlist-integration.test.ts | 68 +++------- cli/src/__tests__/cmdrun-happy-path.test.ts | 22 +++- cli/src/__tests__/commands-cloud-info.test.ts | 20 +-- cli/src/__tests__/commands-display.test.ts | 52 +++----- .../__tests__/commands-error-paths.test.ts | 4 +- .../__tests__/commands-swap-resolve.test.ts | 37 +++++- .../commands-update-download.test.ts | 92 ++++++++++--- .../__tests__/download-and-failure.test.ts | 68 +++++++--- .../manifest-cache-lifecycle.test.ts | 22 +++- cli/src/__tests__/manifest-validation.test.ts | 34 ++++- cli/src/__tests__/parse.test.ts | 44 +++++-- .../run-path-credential-display.test.ts | 7 +- cli/src/__tests__/ssh-keys.test.ts | 124 ++++++++++++++---- cli/src/__tests__/unknown-flags.test.ts | 94 ++++++++++--- cli/src/__tests__/update-check.test.ts | 66 ++++++++-- cli/src/__tests__/with-retry-result.test.ts | 21 ++- cli/src/aws/aws.ts | 37 ++++-- cli/src/commands.ts | 24 +++- cli/src/daytona/daytona.ts | 13 +- cli/src/digitalocean/digitalocean.ts | 26 +++- cli/src/flags.ts | 20 ++- cli/src/fly/fly.ts | 49 ++++--- cli/src/gcp/gcp.ts | 24 ++-- cli/src/hetzner/hetzner.ts | 22 +++- cli/src/local/local.ts | 16 ++- cli/src/manifest.ts | 12 +- cli/src/picker.ts | 119 ++++++++++++++--- cli/src/shared/agent-setup.ts | 7 +- cli/src/shared/oauth.ts | 4 +- cli/src/shared/result.ts | 20 ++- cli/src/shared/ssh-keys.ts | 59 +++++++-- cli/src/shared/ssh.ts | 45 +++++-- cli/src/shared/type-guards.ts | 8 +- cli/src/update-check.ts | 4 +- 40 files changed, 978 insertions(+), 392 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index c04fb4ce..666b1297 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -50,6 +50,10 @@ jobs: working-directory: cli run: bun install + - name: Run Biome format check + working-directory: cli + run: bunx @biomejs/biome format src/ + - name: Run Biome lint working-directory: cli run: bunx @biomejs/biome lint src/ diff --git a/cli/package.json b/cli/package.json index 802fef80..05d08a0f 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@openrouter/spawn", - "version": "0.7.8", + "version": "0.7.9", "type": "module", "bin": { "spawn": "cli.js" diff --git a/cli/src/__tests__/cloud-info.test.ts b/cli/src/__tests__/cloud-info.test.ts index 7f7e440c..2de89dfb 100644 --- a/cli/src/__tests__/cloud-info.test.ts +++ b/cli/src/__tests__/cloud-info.test.ts @@ -132,9 +132,7 @@ describe("cmdCloudInfo", () => { delete process.env.OPENROUTER_API_KEY; originalFetch = global.fetch; - global.fetch = mock(async () => - new Response(JSON.stringify(extendedManifest)), - ); + global.fetch = mock(async () => new Response(JSON.stringify(extendedManifest))); await loadManifest(true); }); diff --git a/cli/src/__tests__/cmd-interactive.test.ts b/cli/src/__tests__/cmd-interactive.test.ts index 69c171ee..ec8d7ebf 100644 --- a/cli/src/__tests__/cmd-interactive.test.ts +++ b/cli/src/__tests__/cmd-interactive.test.ts @@ -102,9 +102,7 @@ describe("cmdInteractive", () => { originalFetch = global.fetch; // Pre-load manifest - global.fetch = mock(async () => - new Response(JSON.stringify(mockManifest)), - ); + global.fetch = mock(async () => new Response(JSON.stringify(mockManifest))); await loadManifest(true); }); @@ -218,9 +216,7 @@ describe("cmdInteractive", () => { }, }; - global.fetch = mock(async () => - new Response(JSON.stringify(noCloudManifest)), - ); + global.fetch = mock(async () => new Response(JSON.stringify(noCloudManifest))); await loadManifest(true); selectReturnValues = [ @@ -243,9 +239,7 @@ describe("cmdInteractive", () => { }, }; - global.fetch = mock(async () => - new Response(JSON.stringify(noCloudManifest)), - ); + global.fetch = mock(async () => new Response(JSON.stringify(noCloudManifest))); await loadManifest(true); selectReturnValues = [ @@ -274,9 +268,7 @@ describe("cmdInteractive", () => { }, }; - global.fetch = mock(async () => - new Response(JSON.stringify(noCloudManifest)), - ); + global.fetch = mock(async () => new Response(JSON.stringify(noCloudManifest))); await loadManifest(true); selectReturnValues = [ @@ -450,7 +442,9 @@ describe("cmdInteractive", () => { return new Response(JSON.stringify(mockManifest)); } // Both primary and fallback fail - return new Response("Not Found", { status: 404 }); + return new Response("Not Found", { + status: 404, + }); }); await loadManifest(true); diff --git a/cli/src/__tests__/cmd-listing-output.test.ts b/cli/src/__tests__/cmd-listing-output.test.ts index 6ec2a1b6..5da91680 100644 --- a/cli/src/__tests__/cmd-listing-output.test.ts +++ b/cli/src/__tests__/cmd-listing-output.test.ts @@ -162,9 +162,7 @@ const { cmdMatrix, cmdAgents, cmdClouds, getTerminalWidth } = await import("../c // ── Helpers ────────────────────────────────────────────────────────────────── function setManifest(manifest: Manifest) { - global.fetch = mock(async () => - new Response(JSON.stringify(manifest)), - ); + global.fetch = mock(async () => new Response(JSON.stringify(manifest))); return loadManifest(true); } diff --git a/cli/src/__tests__/cmdlast.test.ts b/cli/src/__tests__/cmdlast.test.ts index ba121649..4dbe5548 100644 --- a/cli/src/__tests__/cmdlast.test.ts +++ b/cli/src/__tests__/cmdlast.test.ts @@ -104,9 +104,7 @@ describe("cmdLast", () => { cmdRunMock = mock(() => Promise.resolve()); // Prime the manifest cache with mock data - global.fetch = mock( - () => Promise.resolve(new Response(JSON.stringify(mockManifest))), - ); + global.fetch = mock(() => Promise.resolve(new Response(JSON.stringify(mockManifest)))); await loadManifest(true); global.fetch = originalFetch; @@ -202,9 +200,7 @@ describe("cmdLast", () => { it("should show 'Rerunning last spawn' when history exists", async () => { writeHistory(sampleRecords); - global.fetch = mock( - () => Promise.resolve(new Response(JSON.stringify(mockManifest))), - ); + global.fetch = mock(() => Promise.resolve(new Response(JSON.stringify(mockManifest)))); // We need to mock cmdRun to prevent actual execution // For now, just verify the message is shown @@ -221,9 +217,7 @@ describe("cmdLast", () => { it("should select the most recent record (newest first)", async () => { writeHistory(sampleRecords); - global.fetch = mock( - () => Promise.resolve(new Response(JSON.stringify(mockManifest))), - ); + global.fetch = mock(() => Promise.resolve(new Response(JSON.stringify(mockManifest)))); try { await cmdLast(); @@ -240,9 +234,7 @@ describe("cmdLast", () => { it("should display the record label with manifest display names", async () => { writeHistory(sampleRecords); - global.fetch = mock( - () => Promise.resolve(new Response(JSON.stringify(mockManifest))), - ); + global.fetch = mock(() => Promise.resolve(new Response(JSON.stringify(mockManifest)))); try { await cmdLast(); @@ -282,9 +274,7 @@ describe("cmdLast", () => { }, ]); - global.fetch = mock( - () => Promise.resolve(new Response(JSON.stringify(mockManifest))), - ); + global.fetch = mock(() => Promise.resolve(new Response(JSON.stringify(mockManifest)))); try { await cmdLast(); @@ -311,9 +301,7 @@ describe("cmdLast", () => { }, ]); - global.fetch = mock( - () => Promise.resolve(new Response(JSON.stringify(mockManifest))), - ); + global.fetch = mock(() => Promise.resolve(new Response(JSON.stringify(mockManifest)))); try { await cmdLast(); @@ -336,9 +324,7 @@ describe("cmdLast", () => { }, ]); - global.fetch = mock( - () => Promise.resolve(new Response(JSON.stringify(mockManifest))), - ); + global.fetch = mock(() => Promise.resolve(new Response(JSON.stringify(mockManifest)))); try { await cmdLast(); @@ -371,7 +357,11 @@ describe("cmdLast", () => { agent: "claude", cloud: "sprite", timestamp: "2026-01-01T00:00:00Z", - connection: { ip: "1.2.3.4", user: "root", server_name: "spawn-abc" }, + connection: { + ip: "1.2.3.4", + user: "root", + server_name: "spawn-abc", + }, }; const label = buildRecordLabel(record, mockManifest); expect(label).toBe("spawn-abc"); @@ -420,7 +410,11 @@ describe("cmdLast", () => { agent: "claude", cloud: "sprite", timestamp: "2026-01-01T00:00:00Z", - connection: { ip: "1.2.3.4", user: "root", deleted: true }, + connection: { + ip: "1.2.3.4", + user: "root", + deleted: true, + }, }; const subtitle = buildRecordSubtitle(record, mockManifest); @@ -440,9 +434,7 @@ describe("cmdLast", () => { }, ]); - global.fetch = mock( - () => Promise.resolve(new Response(JSON.stringify(mockManifest))), - ); + global.fetch = mock(() => Promise.resolve(new Response(JSON.stringify(mockManifest)))); try { await cmdLast(); @@ -465,9 +457,7 @@ describe("cmdLast", () => { }, ]); - global.fetch = mock( - () => Promise.resolve(new Response(JSON.stringify(mockManifest))), - ); + global.fetch = mock(() => Promise.resolve(new Response(JSON.stringify(mockManifest)))); try { await cmdLast(); @@ -499,9 +489,7 @@ describe("cmdLast", () => { }, ]); - global.fetch = mock( - () => Promise.resolve(new Response(JSON.stringify(mockManifest))), - ); + global.fetch = mock(() => Promise.resolve(new Response(JSON.stringify(mockManifest)))); try { await cmdLast(); diff --git a/cli/src/__tests__/cmdlist-integration.test.ts b/cli/src/__tests__/cmdlist-integration.test.ts index f2a6115b..814c5d49 100644 --- a/cli/src/__tests__/cmdlist-integration.test.ts +++ b/cli/src/__tests__/cmdlist-integration.test.ts @@ -118,9 +118,7 @@ describe("cmdList integration", () => { // Prime the manifest in-memory cache with mock data so tests don't // depend on network availability or stale values from other test files. - global.fetch = mock( - () => Promise.resolve(new Response(JSON.stringify(mockManifest))), - ); + global.fetch = mock(() => Promise.resolve(new Response(JSON.stringify(mockManifest)))); await loadManifest(true); global.fetch = originalFetch; @@ -232,9 +230,7 @@ describe("cmdList integration", () => { writeHistory(sampleRecords); // Mock fetch to return manifest (for display names) - global.fetch = mock( - () => Promise.resolve(new Response(JSON.stringify(mockManifest))), - ); + global.fetch = mock(() => Promise.resolve(new Response(JSON.stringify(mockManifest)))); await cmdList(); @@ -247,9 +243,7 @@ describe("cmdList integration", () => { it("should render records in reverse chronological order (newest first)", async () => { writeHistory(sampleRecords); - global.fetch = mock( - () => Promise.resolve(new Response(JSON.stringify(mockManifest))), - ); + global.fetch = mock(() => Promise.resolve(new Response(JSON.stringify(mockManifest)))); await cmdList(); @@ -268,9 +262,7 @@ describe("cmdList integration", () => { it("should show display names when manifest is available", async () => { writeHistory(sampleRecords); - global.fetch = mock( - () => Promise.resolve(new Response(JSON.stringify(mockManifest))), - ); + global.fetch = mock(() => Promise.resolve(new Response(JSON.stringify(mockManifest)))); await cmdList(); @@ -298,9 +290,7 @@ describe("cmdList integration", () => { it("should show rerun hint in footer", async () => { writeHistory(sampleRecords); - global.fetch = mock( - () => Promise.resolve(new Response(JSON.stringify(mockManifest))), - ); + global.fetch = mock(() => Promise.resolve(new Response(JSON.stringify(mockManifest)))); await cmdList(); @@ -313,9 +303,7 @@ describe("cmdList integration", () => { it("should show record count in footer", async () => { writeHistory(sampleRecords); - global.fetch = mock( - () => Promise.resolve(new Response(JSON.stringify(mockManifest))), - ); + global.fetch = mock(() => Promise.resolve(new Response(JSON.stringify(mockManifest)))); await cmdList(); @@ -332,9 +320,7 @@ describe("cmdList integration", () => { }, ]); - global.fetch = mock( - () => Promise.resolve(new Response(JSON.stringify(mockManifest))), - ); + global.fetch = mock(() => Promise.resolve(new Response(JSON.stringify(mockManifest)))); await cmdList(); @@ -357,9 +343,7 @@ describe("cmdList integration", () => { }, ]); - global.fetch = mock( - () => Promise.resolve(new Response(JSON.stringify(mockManifest))), - ); + global.fetch = mock(() => Promise.resolve(new Response(JSON.stringify(mockManifest)))); await cmdList(); @@ -380,9 +364,7 @@ describe("cmdList integration", () => { }, ]); - global.fetch = mock( - () => Promise.resolve(new Response(JSON.stringify(mockManifest))), - ); + global.fetch = mock(() => Promise.resolve(new Response(JSON.stringify(mockManifest)))); await cmdList(); @@ -421,9 +403,7 @@ describe("cmdList integration", () => { it("should filter by agent name", async () => { writeHistory(records); - global.fetch = mock( - () => Promise.resolve(new Response(JSON.stringify(mockManifest))), - ); + global.fetch = mock(() => Promise.resolve(new Response(JSON.stringify(mockManifest)))); await cmdList("claude"); @@ -435,9 +415,7 @@ describe("cmdList integration", () => { it("should filter by cloud name", async () => { writeHistory(records); - global.fetch = mock( - () => Promise.resolve(new Response(JSON.stringify(mockManifest))), - ); + global.fetch = mock(() => Promise.resolve(new Response(JSON.stringify(mockManifest)))); await cmdList(undefined, "hetzner"); @@ -448,9 +426,7 @@ describe("cmdList integration", () => { it("should filter by both agent and cloud", async () => { writeHistory(records); - global.fetch = mock( - () => Promise.resolve(new Response(JSON.stringify(mockManifest))), - ); + global.fetch = mock(() => Promise.resolve(new Response(JSON.stringify(mockManifest)))); await cmdList("claude", "sprite"); @@ -461,9 +437,7 @@ describe("cmdList integration", () => { it("should show 'Clear filter' hint when filters are active", async () => { writeHistory(records); - global.fetch = mock( - () => Promise.resolve(new Response(JSON.stringify(mockManifest))), - ); + global.fetch = mock(() => Promise.resolve(new Response(JSON.stringify(mockManifest)))); await cmdList("claude"); @@ -475,9 +449,7 @@ describe("cmdList integration", () => { it("should show filter suggestion hint when no filters active", async () => { writeHistory(records); - global.fetch = mock( - () => Promise.resolve(new Response(JSON.stringify(mockManifest))), - ); + global.fetch = mock(() => Promise.resolve(new Response(JSON.stringify(mockManifest)))); await cmdList(); @@ -490,9 +462,7 @@ describe("cmdList integration", () => { it("should show case-insensitive filter results", async () => { writeHistory(records); - global.fetch = mock( - () => Promise.resolve(new Response(JSON.stringify(mockManifest))), - ); + global.fetch = mock(() => Promise.resolve(new Response(JSON.stringify(mockManifest)))); await cmdList("CLAUDE"); @@ -540,9 +510,7 @@ describe("cmdList integration", () => { } writeHistory(manyRecords); - global.fetch = mock( - () => Promise.resolve(new Response(JSON.stringify(mockManifest))), - ); + global.fetch = mock(() => Promise.resolve(new Response(JSON.stringify(mockManifest)))); await cmdList(); @@ -559,9 +527,7 @@ describe("cmdList integration", () => { }, ]); - global.fetch = mock( - () => Promise.resolve(new Response(JSON.stringify(mockManifest))), - ); + global.fetch = mock(() => Promise.resolve(new Response(JSON.stringify(mockManifest)))); await cmdList(); diff --git a/cli/src/__tests__/cmdrun-happy-path.test.ts b/cli/src/__tests__/cmdrun-happy-path.test.ts index 67d07278..091b17b4 100644 --- a/cli/src/__tests__/cmdrun-happy-path.test.ts +++ b/cli/src/__tests__/cmdrun-happy-path.test.ts @@ -108,20 +108,32 @@ function mockFetchForDownload(opts: { // Primary script URL (openrouter.ai) if (urlStr.includes("openrouter.ai")) { if (primaryOk) { - return new Response(scriptContent, { status: primaryStatus }); + return new Response(scriptContent, { + status: primaryStatus, + }); } - return new Response("error", { status: primaryStatus, statusText: `HTTP ${primaryStatus}` }); + return new Response("error", { + status: primaryStatus, + statusText: `HTTP ${primaryStatus}`, + }); } // Fallback script URL (raw.githubusercontent.com) if (urlStr.includes("raw.githubusercontent.com")) { if (fallbackOk) { - return new Response(scriptContent, { status: fallbackStatus }); + return new Response(scriptContent, { + status: fallbackStatus, + }); } - return new Response("error", { status: fallbackStatus, statusText: `HTTP ${fallbackStatus}` }); + return new Response("error", { + status: fallbackStatus, + statusText: `HTTP ${fallbackStatus}`, + }); } - return new Response("not found", { status: 404 }); + return new Response("not found", { + status: 404, + }); }); } diff --git a/cli/src/__tests__/commands-cloud-info.test.ts b/cli/src/__tests__/commands-cloud-info.test.ts index 6ff7bf41..59b96f06 100644 --- a/cli/src/__tests__/commands-cloud-info.test.ts +++ b/cli/src/__tests__/commands-cloud-info.test.ts @@ -101,9 +101,7 @@ describe("cmdCloudInfo", () => { }); originalFetch = global.fetch; - global.fetch = mock(async () => - new Response(JSON.stringify(mockManifest)), - ); + global.fetch = mock(async () => new Response(JSON.stringify(mockManifest))); await loadManifest(true); }); @@ -175,9 +173,7 @@ describe("cmdCloudInfo", () => { describe("cloud with notes field", () => { it("should display notes when cloud has notes", async () => { - global.fetch = mock(async () => - new Response(JSON.stringify(manifestWithCloudNotes)), - ); + global.fetch = mock(async () => new Response(JSON.stringify(manifestWithCloudNotes))); await loadManifest(true); await cmdCloudInfo("sprite"); @@ -190,9 +186,7 @@ describe("cmdCloudInfo", () => { describe("cloud with no implemented agents", () => { it("should show no-agents message", async () => { - global.fetch = mock(async () => - new Response(JSON.stringify(manifestWithNotes)), - ); + global.fetch = mock(async () => new Response(JSON.stringify(manifestWithNotes))); await loadManifest(true); await cmdCloudInfo("emptycloud"); @@ -201,9 +195,7 @@ describe("cmdCloudInfo", () => { }); it("should still show cloud name for agent-less cloud", async () => { - global.fetch = mock(async () => - new Response(JSON.stringify(manifestWithNotes)), - ); + global.fetch = mock(async () => new Response(JSON.stringify(manifestWithNotes))); await loadManifest(true); await cmdCloudInfo("emptycloud"); @@ -213,9 +205,7 @@ describe("cmdCloudInfo", () => { }); it("should display notes for agent-less cloud", async () => { - global.fetch = mock(async () => - new Response(JSON.stringify(manifestWithNotes)), - ); + global.fetch = mock(async () => new Response(JSON.stringify(manifestWithNotes))); await loadManifest(true); await cmdCloudInfo("emptycloud"); diff --git a/cli/src/__tests__/commands-display.test.ts b/cli/src/__tests__/commands-display.test.ts index af8d32d6..79c88da3 100644 --- a/cli/src/__tests__/commands-display.test.ts +++ b/cli/src/__tests__/commands-display.test.ts @@ -146,9 +146,7 @@ describe("Commands Display Output", () => { }); originalFetch = global.fetch; - global.fetch = mock(async () => - new Response(JSON.stringify(mockManifest)), - ); + global.fetch = mock(async () => new Response(JSON.stringify(mockManifest))); await loadManifest(true); }); @@ -200,9 +198,7 @@ describe("Commands Display Output", () => { }); it("should show no-clouds message when agent has no implementations", async () => { - global.fetch = mock(async () => - new Response(JSON.stringify(noImplManifest)), - ); + global.fetch = mock(async () => new Response(JSON.stringify(noImplManifest))); await loadManifest(true); await cmdAgentInfo("claude"); @@ -256,9 +252,7 @@ describe("Commands Display Output", () => { }); it("should show 0 implemented when nothing is implemented", async () => { - global.fetch = mock(async () => - new Response(JSON.stringify(noImplManifest)), - ); + global.fetch = mock(async () => new Response(JSON.stringify(noImplManifest))); await loadManifest(true); await cmdMatrix(); @@ -443,8 +437,13 @@ describe("Commands Display Output", () => { describe("cmdUpdate", () => { it("should show already up to date when versions match", async () => { const pkg = await import("../../package.json"); - global.fetch = mock(async () => - new Response(JSON.stringify({ version: pkg.default.version })), + global.fetch = mock( + async () => + new Response( + JSON.stringify({ + version: pkg.default.version, + }), + ), ); await cmdUpdate(); @@ -472,8 +471,11 @@ describe("Commands Display Output", () => { }); it("should handle non-ok fetch response", async () => { - global.fetch = mock(async () => - new Response("error", { status: 500 }), + global.fetch = mock( + async () => + new Response("error", { + status: 500, + }), ); await cmdUpdate(); @@ -487,9 +489,7 @@ describe("Commands Display Output", () => { describe("cmdList - edge cases", () => { it("should handle single implementation correctly", async () => { - global.fetch = mock(async () => - new Response(JSON.stringify(singleImplManifest)), - ); + global.fetch = mock(async () => new Response(JSON.stringify(singleImplManifest))); await loadManifest(true); await cmdMatrix(); @@ -498,9 +498,7 @@ describe("Commands Display Output", () => { }); it("should handle manifest with many clouds", async () => { - global.fetch = mock(async () => - new Response(JSON.stringify(manyCloudManifest)), - ); + global.fetch = mock(async () => new Response(JSON.stringify(manyCloudManifest))); await loadManifest(true); await cmdMatrix(); @@ -537,9 +535,7 @@ describe("Commands Display Output", () => { }, }, }; - global.fetch = mock(async () => - new Response(JSON.stringify(manifestWithNotes)), - ); + global.fetch = mock(async () => new Response(JSON.stringify(manifestWithNotes))); await loadManifest(true); await cmdAgentInfo("codex"); @@ -559,9 +555,7 @@ describe("Commands Display Output", () => { describe("cmdAgentInfo - many clouds", () => { it("should list all implemented clouds for agent with many options", async () => { - global.fetch = mock(async () => - new Response(JSON.stringify(manyCloudManifest)), - ); + global.fetch = mock(async () => new Response(JSON.stringify(manyCloudManifest))); await loadManifest(true); await cmdAgentInfo("claude"); @@ -578,9 +572,7 @@ describe("Commands Display Output", () => { describe("cmdAgents - zero implementations", () => { it("should show 0 clouds for all agents", async () => { - global.fetch = mock(async () => - new Response(JSON.stringify(noImplManifest)), - ); + global.fetch = mock(async () => new Response(JSON.stringify(noImplManifest))); await loadManifest(true); await cmdAgents(); @@ -599,9 +591,7 @@ describe("Commands Display Output", () => { describe("cmdClouds - zero implementations", () => { it("should show 0 agents for all clouds", async () => { - global.fetch = mock(async () => - new Response(JSON.stringify(noImplManifest)), - ); + global.fetch = mock(async () => new Response(JSON.stringify(noImplManifest))); await loadManifest(true); await cmdClouds(); diff --git a/cli/src/__tests__/commands-error-paths.test.ts b/cli/src/__tests__/commands-error-paths.test.ts index 04104bbe..aba14043 100644 --- a/cli/src/__tests__/commands-error-paths.test.ts +++ b/cli/src/__tests__/commands-error-paths.test.ts @@ -76,9 +76,7 @@ describe("Commands Error Paths", () => { // Mock fetch to return our controlled manifest data originalFetch = global.fetch; - global.fetch = mock(async () => - new Response(JSON.stringify(mockManifest)), - ); + global.fetch = mock(async () => new Response(JSON.stringify(mockManifest))); // Force-refresh the manifest cache await loadManifest(true); diff --git a/cli/src/__tests__/commands-swap-resolve.test.ts b/cli/src/__tests__/commands-swap-resolve.test.ts index 546f6554..e3085896 100644 --- a/cli/src/__tests__/commands-swap-resolve.test.ts +++ b/cli/src/__tests__/commands-swap-resolve.test.ts @@ -367,7 +367,15 @@ describe("manifest validation (isValidManifest)", () => { }); it("should reject manifest missing 'agents' field", async () => { - global.fetch = mock(async () => new Response(JSON.stringify({ clouds: {}, matrix: {} }))); + global.fetch = mock( + async () => + new Response( + JSON.stringify({ + clouds: {}, + matrix: {}, + }), + ), + ); // Force refresh to avoid cache, should reject invalid manifest try { @@ -384,7 +392,15 @@ describe("manifest validation (isValidManifest)", () => { }); it("should reject manifest missing 'clouds' field", async () => { - global.fetch = mock(async () => new Response(JSON.stringify({ agents: {}, matrix: {} }))); + global.fetch = mock( + async () => + new Response( + JSON.stringify({ + agents: {}, + matrix: {}, + }), + ), + ); try { await loadManifest(true); @@ -399,7 +415,15 @@ describe("manifest validation (isValidManifest)", () => { }); it("should reject manifest missing 'matrix' field", async () => { - global.fetch = mock(async () => new Response(JSON.stringify({ agents: {}, clouds: {} }))); + global.fetch = mock( + async () => + new Response( + JSON.stringify({ + agents: {}, + clouds: {}, + }), + ), + ); try { await loadManifest(true); @@ -453,7 +477,12 @@ describe("manifest validation (isValidManifest)", () => { }); it("should handle HTTP error response gracefully", async () => { - global.fetch = mock(async () => new Response("Internal Server Error", { status: 500 })); + global.fetch = mock( + async () => + new Response("Internal Server Error", { + status: 500, + }), + ); try { await loadManifest(true); diff --git a/cli/src/__tests__/commands-update-download.test.ts b/cli/src/__tests__/commands-update-download.test.ts index 3074e137..ff289a6c 100644 --- a/cli/src/__tests__/commands-update-download.test.ts +++ b/cli/src/__tests__/commands-update-download.test.ts @@ -84,9 +84,15 @@ describe("cmdUpdate", () => { it("should report up-to-date when remote version matches current", async () => { global.fetch = mock(async (url: string) => { if (typeof url === "string" && url.includes("package.json")) { - return new Response(JSON.stringify({ version: VERSION })); + return new Response( + JSON.stringify({ + version: VERSION, + }), + ); } - return new Response("Not Found", { status: 404 }); + return new Response("Not Found", { + status: 404, + }); }); await cmdUpdate(); @@ -101,9 +107,15 @@ describe("cmdUpdate", () => { it("should report available update when remote version differs", async () => { global.fetch = mock(async (url: string) => { if (typeof url === "string" && url.includes("package.json")) { - return new Response(JSON.stringify({ version: "99.99.99" })); + return new Response( + JSON.stringify({ + version: "99.99.99", + }), + ); } - return new Response("Not Found", { status: 404 }); + return new Response("Not Found", { + status: 404, + }); }); await cmdUpdate(); @@ -115,7 +127,12 @@ describe("cmdUpdate", () => { }); it("should handle package.json fetch failure gracefully", async () => { - global.fetch = mock(async () => new Response("Internal Server Error", { status: 500 })); + global.fetch = mock( + async () => + new Response("Internal Server Error", { + status: 500, + }), + ); await cmdUpdate(); @@ -143,9 +160,15 @@ describe("cmdUpdate", () => { it("should handle update failure gracefully", async () => { global.fetch = mock(async (url: string) => { if (typeof url === "string" && url.includes("package.json")) { - return new Response(JSON.stringify({ version: "99.99.99" })); + return new Response( + JSON.stringify({ + version: "99.99.99", + }), + ); } - return new Response("Not Found", { status: 404 }); + return new Response("Not Found", { + status: 404, + }); }); // cmdUpdate now runs execSync which will fail in test env @@ -158,7 +181,14 @@ describe("cmdUpdate", () => { }); it("should start spinner with checking message", async () => { - global.fetch = mock(async () => new Response(JSON.stringify({ version: VERSION }))); + global.fetch = mock( + async () => + new Response( + JSON.stringify({ + version: VERSION, + }), + ), + ); await cmdUpdate(); @@ -169,9 +199,15 @@ describe("cmdUpdate", () => { it("should show version in spinner stop during update", async () => { global.fetch = mock(async (url: string) => { if (typeof url === "string" && url.includes("package.json")) { - return new Response(JSON.stringify({ version: "2.0.0" })); + return new Response( + JSON.stringify({ + version: "2.0.0", + }), + ); } - return new Response("Error", { status: 500 }); + return new Response("Error", { + status: 500, + }); }); await cmdUpdate(); @@ -219,7 +255,9 @@ describe("Script download and execution", () => { return new Response(JSON.stringify(mockManifest)); } // Both script URLs return 404 - return new Response("Not Found", { status: 404 }); + return new Response("Not Found", { + status: 404, + }); }); await loadManifest(true); @@ -237,7 +275,9 @@ describe("Script download and execution", () => { if (typeof url === "string" && url.includes("manifest.json")) { return new Response(JSON.stringify(mockManifest)); } - return new Response("Server Error", { status: 500 }); + return new Response("Server Error", { + status: 500, + }); }); await loadManifest(true); @@ -280,13 +320,17 @@ describe("Script download and execution", () => { } if (typeof url === "string" && url.includes("openrouter.ai")) { // Primary fails - return new Response("Service Unavailable", { status: 503 }); + return new Response("Service Unavailable", { + status: 503, + }); } if (typeof url === "string" && url.includes("raw.githubusercontent.com")) { // Fallback returns valid script return new Response("#!/bin/bash\nset -eo pipefail\necho 'hello'"); } - return new Response("Not found", { status: 404 }); + return new Response("Not found", { + status: 404, + }); }); await loadManifest(true); @@ -316,7 +360,9 @@ describe("Script download and execution", () => { if (typeof url === "string" && url.includes("manifest.json")) { return new Response(JSON.stringify(mockManifest)); } - return new Response("Not found", { status: 404 }); + return new Response("Not found", { + status: 404, + }); }); await loadManifest(true); @@ -389,7 +435,9 @@ describe("Script download and execution", () => { if (typeof url === "string" && url.includes("manifest.json")) { return new Response(JSON.stringify(mockManifest)); } - return new Response("Not Found", { status: 404 }); + return new Response("Not Found", { + status: 404, + }); }); await loadManifest(true); @@ -413,12 +461,18 @@ describe("Script download and execution", () => { return new Response(JSON.stringify(mockManifest)); } if (typeof url === "string" && url.includes("openrouter.ai")) { - return new Response("Error", { status: 500 }); + return new Response("Error", { + status: 500, + }); } if (typeof url === "string" && url.includes("raw.githubusercontent.com")) { - return new Response("Bad Gateway", { status: 502 }); + return new Response("Bad Gateway", { + status: 502, + }); } - return new Response("Not found", { status: 404 }); + return new Response("Not found", { + status: 404, + }); }); await loadManifest(true); diff --git a/cli/src/__tests__/download-and-failure.test.ts b/cli/src/__tests__/download-and-failure.test.ts index cfced9c9..fc19fc7e 100644 --- a/cli/src/__tests__/download-and-failure.test.ts +++ b/cli/src/__tests__/download-and-failure.test.ts @@ -71,9 +71,7 @@ describe("Download and Failure Pipeline", () => { let processExitSpy: ReturnType; /** Set up fetch to return manifest from manifest URLs and custom responses for script URLs */ - function setupFetch( - scriptHandler: (url: string) => Promise, - ) { + function setupFetch(scriptHandler: (url: string) => Promise) { global.fetch = mock(async (url: string | URL | Request, init?: RequestInit) => { const urlStr = typeof url === "string" ? url : url instanceof URL ? url.toString() : url.url; if (urlStr.includes("manifest.json")) { @@ -161,13 +159,17 @@ describe("Download and Failure Pipeline", () => { it("should fall back to GitHub raw URL when primary returns 404", async () => { await setupFetch(async (url) => { if (url.includes("openrouter.ai")) { - return new Response("Not Found", { status: 404 }); + return new Response("Not Found", { + status: 404, + }); } // GitHub raw fallback succeeds if (url.includes("raw.githubusercontent.com")) { return new Response("#!/bin/bash\nexit 0"); } - return new Response("Server Error", { status: 500 }); + return new Response("Server Error", { + status: 500, + }); }); try { @@ -188,12 +190,16 @@ describe("Download and Failure Pipeline", () => { it("should fall back to GitHub raw URL when primary returns 500", async () => { await setupFetch(async (url) => { if (url.includes("openrouter.ai")) { - return new Response("Server Error", { status: 500 }); + return new Response("Server Error", { + status: 500, + }); } if (url.includes("raw.githubusercontent.com")) { return new Response("#!/bin/bash\nexit 0"); } - return new Response("Server Error", { status: 500 }); + return new Response("Server Error", { + status: 500, + }); }); try { @@ -213,7 +219,9 @@ describe("Download and Failure Pipeline", () => { describe("download - both URLs fail", () => { it("should show 'script not found' when both return 404", async () => { await setupFetch(async () => { - return new Response("Not Found", { status: 404 }); + return new Response("Not Found", { + status: 404, + }); }); try { @@ -230,7 +238,12 @@ describe("Download and Failure Pipeline", () => { }); it("should suggest verifying the combination when both return 404", async () => { - await setupFetch(async () => new Response("Not Found", { status: 404 })); + await setupFetch( + async () => + new Response("Not Found", { + status: 404, + }), + ); try { await cmdRun("claude", "sprite"); @@ -243,7 +256,12 @@ describe("Download and Failure Pipeline", () => { }); it("should suggest reporting the issue when both return 404", async () => { - await setupFetch(async () => new Response("Not Found", { status: 404 })); + await setupFetch( + async () => + new Response("Not Found", { + status: 404, + }), + ); try { await cmdRun("claude", "sprite"); @@ -256,7 +274,12 @@ describe("Download and Failure Pipeline", () => { }); it("should show server error message when both return 500", async () => { - await setupFetch(async () => new Response("Server Error", { status: 500 })); + await setupFetch( + async () => + new Response("Server Error", { + status: 500, + }), + ); try { await cmdRun("claude", "sprite"); @@ -269,7 +292,12 @@ describe("Download and Failure Pipeline", () => { }); it("should mention temporary server issues on 500 errors", async () => { - await setupFetch(async () => new Response("Server Error", { status: 500 })); + await setupFetch( + async () => + new Response("Server Error", { + status: 500, + }), + ); try { await cmdRun("claude", "sprite"); @@ -286,9 +314,13 @@ describe("Download and Failure Pipeline", () => { await setupFetch(async (url) => { callCount++; if (url.includes("openrouter.ai")) { - return new Response("Not Found", { status: 404 }); + return new Response("Not Found", { + status: 404, + }); } - return new Response("Server Error", { status: 500 }); + return new Response("Server Error", { + status: 500, + }); }); try { @@ -380,7 +412,9 @@ describe("Download and Failure Pipeline", () => { if (url.includes("openrouter.ai")) { return new Response("no shebang here"); } - return new Response("Not Found", { status: 404 }); + return new Response("Not Found", { + status: 404, + }); }); try { @@ -397,7 +431,9 @@ describe("Download and Failure Pipeline", () => { if (url.includes("openrouter.ai")) { return new Response("\nError page"); } - return new Response("Not Found", { status: 404 }); + return new Response("Not Found", { + status: 404, + }); }); try { diff --git a/cli/src/__tests__/manifest-cache-lifecycle.test.ts b/cli/src/__tests__/manifest-cache-lifecycle.test.ts index 3e941a97..22aa3c3a 100644 --- a/cli/src/__tests__/manifest-cache-lifecycle.test.ts +++ b/cli/src/__tests__/manifest-cache-lifecycle.test.ts @@ -290,7 +290,12 @@ describe("Manifest Cache Lifecycle", () => { it("should fall back to stale cache on HTTP 500", async () => { global.fetch = mock(() => - Promise.resolve(new Response("Internal Server Error", { status: 500, statusText: "Internal Server Error" })), + Promise.resolve( + new Response("Internal Server Error", { + status: 500, + statusText: "Internal Server Error", + }), + ), ); mkdirSync(join(env.testDir, "spawn"), { @@ -307,7 +312,12 @@ describe("Manifest Cache Lifecycle", () => { it("should fall back to stale cache on HTTP 403 (rate limited)", async () => { global.fetch = mock(() => - Promise.resolve(new Response("Forbidden", { status: 403, statusText: "Forbidden" })), + Promise.resolve( + new Response("Forbidden", { + status: 403, + statusText: "Forbidden", + }), + ), ); mkdirSync(join(env.testDir, "spawn"), { @@ -322,7 +332,13 @@ describe("Manifest Cache Lifecycle", () => { }); it("should fall back to stale cache when fetch response json() throws", async () => { - global.fetch = mock(() => Promise.resolve(new Response("not valid json {{{", { status: 200 }))); + global.fetch = mock(() => + Promise.resolve( + new Response("not valid json {{{", { + status: 200, + }), + ), + ); mkdirSync(join(env.testDir, "spawn"), { recursive: true, diff --git a/cli/src/__tests__/manifest-validation.test.ts b/cli/src/__tests__/manifest-validation.test.ts index 0202f782..8ca3985b 100644 --- a/cli/src/__tests__/manifest-validation.test.ts +++ b/cli/src/__tests__/manifest-validation.test.ts @@ -30,7 +30,14 @@ describe("Manifest Validation Edge Cases", () => { it("should reject manifest missing agents field", async () => { global.fetch = mock(() => - Promise.resolve(new Response(JSON.stringify({ clouds: {}, matrix: {} }))), + Promise.resolve( + new Response( + JSON.stringify({ + clouds: {}, + matrix: {}, + }), + ), + ), ); try { @@ -43,7 +50,14 @@ describe("Manifest Validation Edge Cases", () => { it("should reject manifest missing clouds field", async () => { global.fetch = mock(() => - Promise.resolve(new Response(JSON.stringify({ agents: {}, matrix: {} }))), + Promise.resolve( + new Response( + JSON.stringify({ + agents: {}, + matrix: {}, + }), + ), + ), ); try { @@ -55,7 +69,14 @@ describe("Manifest Validation Edge Cases", () => { it("should reject manifest missing matrix field", async () => { global.fetch = mock(() => - Promise.resolve(new Response(JSON.stringify({ agents: {}, clouds: {} }))), + Promise.resolve( + new Response( + JSON.stringify({ + agents: {}, + clouds: {}, + }), + ), + ), ); try { @@ -103,7 +124,12 @@ describe("Manifest Validation Edge Cases", () => { // When GitHub returns a non-ok response, fetchManifestFromGitHub returns null // and loadManifest falls back to cache or throws global.fetch = mock(() => - Promise.resolve(new Response("Internal Server Error", { status: 500, statusText: "Internal Server Error" })), + Promise.resolve( + new Response("Internal Server Error", { + status: 500, + statusText: "Internal Server Error", + }), + ), ); try { diff --git a/cli/src/__tests__/parse.test.ts b/cli/src/__tests__/parse.test.ts index 243c7982..d1ba0cfd 100644 --- a/cli/src/__tests__/parse.test.ts +++ b/cli/src/__tests__/parse.test.ts @@ -3,11 +3,15 @@ import * as v from "valibot"; import { parseJsonWith, parseJsonRaw } from "../shared/parse"; describe("parseJsonWith", () => { - const NumberSchema = v.object({ count: v.number() }); + const NumberSchema = v.object({ + count: v.number(), + }); it("should return validated data for valid JSON matching the schema", () => { const result = parseJsonWith('{"count": 42}', NumberSchema); - expect(result).toEqual({ count: 42 }); + expect(result).toEqual({ + count: 42, + }); }); it("should return null for valid JSON that doesn't match the schema", () => { @@ -27,22 +31,38 @@ describe("parseJsonWith", () => { it("should handle nested schemas", () => { const NestedSchema = v.object({ - user: v.object({ name: v.string(), age: v.number() }), + user: v.object({ + name: v.string(), + age: v.number(), + }), }); const result = parseJsonWith('{"user": {"name": "Alice", "age": 30}}', NestedSchema); - expect(result).toEqual({ user: { name: "Alice", age: 30 } }); + expect(result).toEqual({ + user: { + name: "Alice", + age: 30, + }, + }); }); it("should handle optional fields", () => { - const OptSchema = v.object({ name: v.string(), email: v.optional(v.string()) }); + const OptSchema = v.object({ + name: v.string(), + email: v.optional(v.string()), + }); const result = parseJsonWith('{"name": "Bob"}', OptSchema); - expect(result).toEqual({ name: "Bob" }); + expect(result).toEqual({ + name: "Bob", + }); }); it("should handle record schemas", () => { const RecordSchema = v.record(v.string(), v.unknown()); const result = parseJsonWith('{"key": "value", "num": 1}', RecordSchema); - expect(result).toEqual({ key: "value", num: 1 }); + expect(result).toEqual({ + key: "value", + num: 1, + }); }); it("should reject array when object schema expected", () => { @@ -54,12 +74,18 @@ describe("parseJsonWith", () => { describe("parseJsonRaw", () => { it("should parse valid JSON to unknown", () => { const result = parseJsonRaw('{"key": "value"}'); - expect(result).toEqual({ key: "value" }); + expect(result).toEqual({ + key: "value", + }); }); it("should parse JSON arrays", () => { const result = parseJsonRaw("[1, 2, 3]"); - expect(result).toEqual([1, 2, 3]); + expect(result).toEqual([ + 1, + 2, + 3, + ]); }); it("should return null for invalid JSON", () => { diff --git a/cli/src/__tests__/run-path-credential-display.test.ts b/cli/src/__tests__/run-path-credential-display.test.ts index 273c15ef..99ed77db 100644 --- a/cli/src/__tests__/run-path-credential-display.test.ts +++ b/cli/src/__tests__/run-path-credential-display.test.ts @@ -111,7 +111,12 @@ function makeManifest(overrides?: Partial): Manifest { "localcloud/codex": "implemented", }, }; - return overrides ? { ...base, ...overrides } : base; + return overrides + ? { + ...base, + ...overrides, + } + : base; } // ── Mock @clack/prompts ───────────────────────────────────────────────── diff --git a/cli/src/__tests__/ssh-keys.test.ts b/cli/src/__tests__/ssh-keys.test.ts index 9ede90ef..8560e3e6 100644 --- a/cli/src/__tests__/ssh-keys.test.ts +++ b/cli/src/__tests__/ssh-keys.test.ts @@ -14,22 +14,26 @@ import { join } from "node:path"; mock.module("@clack/prompts", () => ({ multiselect: mock(() => Promise.resolve([])), isCancel: () => false, - log: { info: mock(() => {}), warn: mock(() => {}), error: mock(() => {}), step: mock(() => {}), message: mock(() => {}) }, - spinner: () => ({ start: mock(() => {}), stop: mock(() => {}) }), + log: { + info: mock(() => {}), + warn: mock(() => {}), + error: mock(() => {}), + step: mock(() => {}), + message: mock(() => {}), + }, + spinner: () => ({ + start: mock(() => {}), + stop: mock(() => {}), + }), select: mock(() => Promise.resolve("")), text: mock(() => Promise.resolve("")), })); // ── Import after @clack/prompts mock ──────────────────────────────────────── -const { - discoverSshKeys, - generateSshKey, - getSshFingerprint, - ensureSshKeys, - getSshKeyOpts, - _resetCache, -} = await import("../shared/ssh-keys"); +const { discoverSshKeys, generateSshKey, getSshFingerprint, ensureSshKeys, getSshKeyOpts, _resetCache } = await import( + "../shared/ssh-keys" +); // ─── Temp dir helpers ─────────────────────────────────────────────────────── @@ -38,7 +42,9 @@ let origHome: string | undefined; function setupTmpHome() { tmpDir = `/tmp/spawn-ssh-test-${Date.now()}-${Math.random().toString(36).slice(2)}`; - mkdirSync(tmpDir, { recursive: true }); + mkdirSync(tmpDir, { + recursive: true, + }); origHome = process.env.HOME; process.env.HOME = tmpDir; } @@ -46,7 +52,10 @@ function setupTmpHome() { function cleanupTmpHome() { process.env.HOME = origHome; try { - rmSync(tmpDir, { recursive: true, force: true }); + rmSync(tmpDir, { + recursive: true, + force: true, + }); } catch { // ignore } @@ -55,7 +64,10 @@ function cleanupTmpHome() { /** Create a fake SSH key pair in the temp ~/.ssh directory. */ function createFakeKeyPair(name: string, keyType: "ed25519" | "rsa" = "ed25519") { const sshDir = join(tmpDir, ".ssh"); - mkdirSync(sshDir, { recursive: true, mode: 0o700 }); + mkdirSync(sshDir, { + recursive: true, + mode: 0o700, + }); const privPath = join(sshDir, name); const pubPath = `${privPath}.pub`; @@ -63,26 +75,69 @@ function createFakeKeyPair(name: string, keyType: "ed25519" | "rsa" = "ed25519") if (keyType === "ed25519") { // Generate a real ed25519 key pair so ssh-keygen -lf works const result = Bun.spawnSync( - ["ssh-keygen", "-t", "ed25519", "-f", privPath, "-N", "", "-q", "-C", "test"], - { stdio: ["ignore", "ignore", "ignore"] }, + [ + "ssh-keygen", + "-t", + "ed25519", + "-f", + privPath, + "-N", + "", + "-q", + "-C", + "test", + ], + { + stdio: [ + "ignore", + "ignore", + "ignore", + ], + }, ); if (result.exitCode !== 0) { // Fallback: write placeholder files (ssh-keygen -lf may not work but existsSync will) - writeFileSync(privPath, "fake-private-key\n", { mode: 0o600 }); + writeFileSync(privPath, "fake-private-key\n", { + mode: 0o600, + }); writeFileSync(pubPath, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFake test\n"); } } else { const result = Bun.spawnSync( - ["ssh-keygen", "-t", "rsa", "-b", "2048", "-f", privPath, "-N", "", "-q", "-C", "test"], - { stdio: ["ignore", "ignore", "ignore"] }, + [ + "ssh-keygen", + "-t", + "rsa", + "-b", + "2048", + "-f", + privPath, + "-N", + "", + "-q", + "-C", + "test", + ], + { + stdio: [ + "ignore", + "ignore", + "ignore", + ], + }, ); if (result.exitCode !== 0) { - writeFileSync(privPath, "fake-private-key\n", { mode: 0o600 }); + writeFileSync(privPath, "fake-private-key\n", { + mode: 0o600, + }); writeFileSync(pubPath, "ssh-rsa AAAAFake test\n"); } } - return { privPath, pubPath }; + return { + privPath, + pubPath, + }; } // ─── Setup / Teardown ─────────────────────────────────────────────────────── @@ -107,7 +162,9 @@ describe("discoverSshKeys", () => { it("returns empty array when no .pub files exist", () => { const sshDir = join(tmpDir, ".ssh"); - mkdirSync(sshDir, { recursive: true }); + mkdirSync(sshDir, { + recursive: true, + }); writeFileSync(join(sshDir, "config"), "Host *\n"); const keys = discoverSshKeys(); expect(keys).toEqual([]); @@ -115,7 +172,9 @@ describe("discoverSshKeys", () => { it("skips .pub files without matching private key", () => { const sshDir = join(tmpDir, ".ssh"); - mkdirSync(sshDir, { recursive: true }); + mkdirSync(sshDir, { + recursive: true, + }); writeFileSync(join(sshDir, "orphan_key.pub"), "ssh-ed25519 AAAA...\n"); // No private key const keys = discoverSshKeys(); @@ -225,11 +284,26 @@ describe("ensureSshKeys", () => { describe("getSshKeyOpts", () => { it("builds -i flags for each key", () => { const keys = [ - { privPath: "/home/user/.ssh/id_ed25519", pubPath: "/home/user/.ssh/id_ed25519.pub", name: "id_ed25519", type: "ED25519" }, - { privPath: "/home/user/.ssh/id_rsa", pubPath: "/home/user/.ssh/id_rsa.pub", name: "id_rsa", type: "RSA" }, + { + privPath: "/home/user/.ssh/id_ed25519", + pubPath: "/home/user/.ssh/id_ed25519.pub", + name: "id_ed25519", + type: "ED25519", + }, + { + privPath: "/home/user/.ssh/id_rsa", + pubPath: "/home/user/.ssh/id_rsa.pub", + name: "id_rsa", + type: "RSA", + }, ]; const opts = getSshKeyOpts(keys); - expect(opts).toEqual(["-i", "/home/user/.ssh/id_ed25519", "-i", "/home/user/.ssh/id_rsa"]); + expect(opts).toEqual([ + "-i", + "/home/user/.ssh/id_ed25519", + "-i", + "/home/user/.ssh/id_rsa", + ]); }); it("returns empty array for empty keys", () => { diff --git a/cli/src/__tests__/unknown-flags.test.ts b/cli/src/__tests__/unknown-flags.test.ts index aac59205..fd32c089 100644 --- a/cli/src/__tests__/unknown-flags.test.ts +++ b/cli/src/__tests__/unknown-flags.test.ts @@ -213,7 +213,14 @@ describe("Unknown Flag Detection", () => { }); it("should allow --name", () => { - expect(findUnknownFlag(["claude", "sprite", "--name", "my-box"])).toBeNull(); + expect( + findUnknownFlag([ + "claude", + "sprite", + "--name", + "my-box", + ]), + ).toBeNull(); }); }); @@ -310,16 +317,26 @@ describe("KNOWN_FLAGS completeness", () => { it("should contain all expected flags", () => { const expected = [ - "--help", "-h", - "--version", "-v", "-V", - "--prompt", "-p", "--prompt-file", "-f", - "--dry-run", "-n", + "--help", + "-h", + "--version", + "-v", + "-V", + "--prompt", + "-p", + "--prompt-file", + "-f", + "--dry-run", + "-n", "--debug", "--headless", "--output", "--name", "--default", - "-a", "-c", "--agent", "--cloud", + "-a", + "-c", + "--agent", + "--cloud", "--clear", ]; for (const flag of expected) { @@ -330,23 +347,52 @@ describe("KNOWN_FLAGS completeness", () => { describe("expandEqualsFlags", () => { it("should expand --flag=value into two args", () => { - expect(expandEqualsFlags(["--prompt=hello"])).toEqual(["--prompt", "hello"]); + expect( + expandEqualsFlags([ + "--prompt=hello", + ]), + ).toEqual([ + "--prompt", + "hello", + ]); }); it("should expand multiple --flag=value pairs", () => { - expect(expandEqualsFlags(["--prompt=hello", "--name=box"])).toEqual([ - "--prompt", "hello", "--name", "box", + expect( + expandEqualsFlags([ + "--prompt=hello", + "--name=box", + ]), + ).toEqual([ + "--prompt", + "hello", + "--name", + "box", ]); }); it("should pass through args without equals", () => { - expect(expandEqualsFlags(["--help", "claude", "sprite"])).toEqual([ - "--help", "claude", "sprite", + expect( + expandEqualsFlags([ + "--help", + "claude", + "sprite", + ]), + ).toEqual([ + "--help", + "claude", + "sprite", ]); }); it("should not expand short flags", () => { - expect(expandEqualsFlags(["-p=value"])).toEqual(["-p=value"]); + expect( + expandEqualsFlags([ + "-p=value", + ]), + ).toEqual([ + "-p=value", + ]); }); it("should handle empty args", () => { @@ -354,12 +400,30 @@ describe("expandEqualsFlags", () => { }); it("should handle value containing equals sign", () => { - expect(expandEqualsFlags(["--prompt=a=b"])).toEqual(["--prompt", "a=b"]); + expect( + expandEqualsFlags([ + "--prompt=a=b", + ]), + ).toEqual([ + "--prompt", + "a=b", + ]); }); it("should handle mixed args", () => { - expect(expandEqualsFlags(["claude", "--prompt=hello", "sprite", "--dry-run"])).toEqual([ - "claude", "--prompt", "hello", "sprite", "--dry-run", + expect( + expandEqualsFlags([ + "claude", + "--prompt=hello", + "sprite", + "--dry-run", + ]), + ).toEqual([ + "claude", + "--prompt", + "hello", + "sprite", + "--dry-run", ]); }); }); diff --git a/cli/src/__tests__/update-check.test.ts b/cli/src/__tests__/update-check.test.ts index 4799aece..87fd0c4c 100644 --- a/cli/src/__tests__/update-check.test.ts +++ b/cli/src/__tests__/update-check.test.ts @@ -78,7 +78,13 @@ describe("update-check", () => { it("should check for updates on every run", async () => { const mockFetch = mock(() => - Promise.resolve(new Response(JSON.stringify({ version: "99.0.0" }))), + Promise.resolve( + new Response( + JSON.stringify({ + version: "99.0.0", + }), + ), + ), ); const fetchSpy = spyOn(global, "fetch").mockImplementation(mockFetch); @@ -98,7 +104,13 @@ describe("update-check", () => { it("should auto-update when newer version is available", async () => { const mockFetch = mock(() => - Promise.resolve(new Response(JSON.stringify({ version: "99.0.0" }))), + Promise.resolve( + new Response( + JSON.stringify({ + version: "99.0.0", + }), + ), + ), ); const fetchSpy = spyOn(global, "fetch").mockImplementation(mockFetch); @@ -130,7 +142,13 @@ describe("update-check", () => { it("should not update when up to date", async () => { const mockFetch = mock(() => - Promise.resolve(new Response(JSON.stringify({ version: "0.2.3" }))), + Promise.resolve( + new Response( + JSON.stringify({ + version: "0.2.3", + }), + ), + ), ); const fetchSpy = spyOn(global, "fetch").mockImplementation(mockFetch); @@ -167,7 +185,13 @@ describe("update-check", () => { it("should handle update failures gracefully", async () => { const mockFetch = mock(() => - Promise.resolve(new Response(JSON.stringify({ version: "99.0.0" }))), + Promise.resolve( + new Response( + JSON.stringify({ + version: "99.0.0", + }), + ), + ), ); const fetchSpy = spyOn(global, "fetch").mockImplementation(mockFetch); @@ -194,7 +218,11 @@ describe("update-check", () => { it("should handle bad response format", async () => { const mockFetch = mock(() => - Promise.resolve(new Response("Not Found", { status: 404 })), + Promise.resolve( + new Response("Not Found", { + status: 404, + }), + ), ); const fetchSpy = spyOn(global, "fetch").mockImplementation(mockFetch); @@ -217,7 +245,13 @@ describe("update-check", () => { ]; const mockFetch = mock(() => - Promise.resolve(new Response(JSON.stringify({ version: "99.0.0" }))), + Promise.resolve( + new Response( + JSON.stringify({ + version: "99.0.0", + }), + ), + ), ); const fetchSpy = spyOn(global, "fetch").mockImplementation(mockFetch); @@ -270,7 +304,13 @@ describe("update-check", () => { ]; const mockFetch = mock(() => - Promise.resolve(new Response(JSON.stringify({ version: "99.0.0" }))), + Promise.resolve( + new Response( + JSON.stringify({ + version: "99.0.0", + }), + ), + ), ); const fetchSpy = spyOn(global, "fetch").mockImplementation(mockFetch); @@ -279,7 +319,9 @@ describe("update-check", () => { const execFileSyncSpy = spyOn(executor, "execFileSync").mockImplementation(() => { // Re-exec fails with exit code 42 const err = new Error("Command failed"); - Object.assign(err, { status: 42 }); + Object.assign(err, { + status: 42, + }); throw err; }); @@ -303,7 +345,13 @@ describe("update-check", () => { ]; const mockFetch = mock(() => - Promise.resolve(new Response(JSON.stringify({ version: "99.0.0" }))), + Promise.resolve( + new Response( + JSON.stringify({ + version: "99.0.0", + }), + ), + ), ); const fetchSpy = spyOn(global, "fetch").mockImplementation(mockFetch); diff --git a/cli/src/__tests__/with-retry-result.test.ts b/cli/src/__tests__/with-retry-result.test.ts index f7fe24ca..28ee4d12 100644 --- a/cli/src/__tests__/with-retry-result.test.ts +++ b/cli/src/__tests__/with-retry-result.test.ts @@ -12,7 +12,10 @@ describe("Result constructors", () => { it("Ok creates a success result", () => { const r = Ok(42); expect(r.ok).toBe(true); - expect(r).toEqual({ ok: true, data: 42 }); + expect(r).toEqual({ + ok: true, + data: 42, + }); }); it("Ok works with void", () => { @@ -95,9 +98,16 @@ describe("withRetry", () => { }); it("returns correct typed value", async () => { - const fn = async () => Ok({ name: "test", count: 42 }); + const fn = async () => + Ok({ + name: "test", + count: 42, + }); const result = await withRetry("test", fn, 1, 0); - expect(result).toEqual({ name: "test", count: 42 }); + expect(result).toEqual({ + name: "test", + count: 42, + }); }); }); @@ -106,7 +116,10 @@ describe("withRetry", () => { describe("wrapSshCall", () => { it("returns Ok on success", async () => { const result = await wrapSshCall(Promise.resolve()); - expect(result).toEqual({ ok: true, data: undefined }); + expect(result).toEqual({ + ok: true, + data: undefined, + }); }); it("returns Err for transient SSH error (retryable)", async () => { diff --git a/cli/src/aws/aws.ts b/cli/src/aws/aws.ts index 74a541e0..80d6e9fe 100644 --- a/cli/src/aws/aws.ts +++ b/cli/src/aws/aws.ts @@ -135,7 +135,11 @@ const InstanceListSchema = v.object({ v.array( v.object({ name: v.optional(v.string()), - state: v.optional(v.object({ name: v.optional(v.string()) })), + state: v.optional( + v.object({ + name: v.optional(v.string()), + }), + ), publicIpAddress: v.optional(v.string()), bundleId: v.optional(v.string()), }), @@ -235,11 +239,16 @@ async function lightsailRest(target: string, body = "{}"): Promise { ], ...(awsSessionToken ? (() => { - const tokenHeader: [string, string] = [ + const tokenHeader: [ + string, + string, + ] = [ "x-amz-security-token", awsSessionToken, ]; - return [tokenHeader]; + return [ + tokenHeader, + ]; })() : []), [ @@ -977,15 +986,19 @@ export async function interactiveSession(cmd: string): Promise { const escapedCmd = fullCmd.replace(/'/g, "'\\''"); const keyOpts = getSshKeyOpts(await ensureSshKeys()); const exitCode = await new Promise((resolve, reject) => { - const child = spawn("ssh", [ - ...SSH_BASE_OPTS, - ...keyOpts, - "-t", - `${SSH_USER}@${instanceIp}`, - `bash -c '${escapedCmd}'`, - ], { - stdio: "inherit", - }); + const child = spawn( + "ssh", + [ + ...SSH_BASE_OPTS, + ...keyOpts, + "-t", + `${SSH_USER}@${instanceIp}`, + `bash -c '${escapedCmd}'`, + ], + { + stdio: "inherit", + }, + ); child.on("close", (code) => resolve(code ?? 0)); child.on("error", reject); }); diff --git a/cli/src/commands.ts b/cli/src/commands.ts index 75ddf9a9..aa3a6590 100644 --- a/cli/src/commands.ts +++ b/cli/src/commands.ts @@ -55,7 +55,9 @@ import { destroyServer as spriteDestroyServer, ensureSpriteCli, ensureSpriteAuth // ── Schemas ────────────────────────────────────────────────────────────────── -const PkgVersionSchema = v.object({ version: v.string() }); +const PkgVersionSchema = v.object({ + version: v.string(), +}); // ── Helpers ──────────────────────────────────────────────────────────────────── @@ -2158,7 +2160,11 @@ export function buildRecordSubtitle(r: SpawnRecord, manifest: Manifest | null): const agentDisplay = resolveDisplayName(manifest, r.agent, "agent"); const cloudDisplay = resolveDisplayName(manifest, r.cloud, "cloud"); const relative = formatRelativeTime(r.timestamp); - const parts = [agentDisplay, cloudDisplay, relative]; + const parts = [ + agentDisplay, + cloudDisplay, + relative, + ]; if (r.connection?.deleted) { parts.push("[deleted]"); } @@ -2451,7 +2457,9 @@ async function handleRecordAction(selected: SpawnRecord, manifest: Manifest | nu if (selected.name) { process.env.SPAWN_NAME = selected.name; } - p.log.step(`Spawning ${pc.bold(buildRecordLabel(selected, manifest))} ${pc.dim(`(${buildRecordSubtitle(selected, manifest)})`)}`); + p.log.step( + `Spawning ${pc.bold(buildRecordLabel(selected, manifest))} ${pc.dim(`(${buildRecordSubtitle(selected, manifest)})`)}`, + ); await cmdRun(selected.agent, selected.cloud, selected.prompt); } @@ -2460,7 +2468,9 @@ async function handleRecordAction(selected: SpawnRecord, manifest: Manifest | nu async function activeServerPicker(records: SpawnRecord[], manifest: Manifest | null): Promise { const { pickToTTYWithActions } = await import("./picker.js"); - const remaining = [...records]; + const remaining = [ + ...records, + ]; while (remaining.length > 0) { const options = remaining.map((r) => ({ @@ -2485,7 +2495,11 @@ async function activeServerPicker(records: SpawnRecord[], manifest: Manifest | n const conn = picked.connection; const canDestroy = conn?.cloud && conn.cloud !== "local" && !conn.deleted && (conn.server_id || conn.server_name); - const deleteOptions: { value: string; label: string; hint?: string }[] = []; + const deleteOptions: { + value: string; + label: string; + hint?: string; + }[] = []; if (canDestroy) { deleteOptions.push({ value: "destroy", diff --git a/cli/src/daytona/daytona.ts b/cli/src/daytona/daytona.ts index fe072eb7..6caac91b 100644 --- a/cli/src/daytona/daytona.ts +++ b/cli/src/daytona/daytona.ts @@ -62,10 +62,11 @@ function toRecord(val: unknown): Record | null { /** Filter an array to only Record entries. */ function toObjectArray(val: unknown): Record[] { - if (!Array.isArray(val)) { return []; } + if (!Array.isArray(val)) { + return []; + } return val.filter( - (item): item is Record => - item !== null && typeof item === "object" && !Array.isArray(item), + (item): item is Record => item !== null && typeof item === "object" && !Array.isArray(item), ); } @@ -631,10 +632,6 @@ export async function listServers(): Promise { const name = typeof s.name === "string" ? s.name : "N/A"; const id = typeof s.id === "string" ? s.id : "N/A"; const state = typeof s.state === "string" ? s.state : "N/A"; - console.log( - pad(name.slice(0, 24), 25) + - pad(id.slice(0, 39), 40) + - pad(state.slice(0, 11), 12), - ); + console.log(pad(name.slice(0, 24), 25) + pad(id.slice(0, 39), 40) + pad(state.slice(0, 11), 12)); } } diff --git a/cli/src/digitalocean/digitalocean.ts b/cli/src/digitalocean/digitalocean.ts index ef1848df..75ca8ec8 100644 --- a/cli/src/digitalocean/digitalocean.ts +++ b/cli/src/digitalocean/digitalocean.ts @@ -130,7 +130,9 @@ function toObjectArray(val: unknown): Record[] { if (!Array.isArray(val)) { return []; } - return val.filter((item): item is Record => item !== null && typeof item === "object" && !Array.isArray(item)); + return val.filter( + (item): item is Record => item !== null && typeof item === "object" && !Array.isArray(item), + ); } // ─── Token Persistence ─────────────────────────────────────────────────────── @@ -661,7 +663,9 @@ export async function createServer(name: string, tier?: CloudInitTier): Promise< // Get all SSH key IDs const { text: keysText } = await doApi("GET", "/account/keys"); const keysData = parseJson(keysText); - const sshKeyIds: number[] = toObjectArray(keysData?.ssh_keys).map((k) => typeof k.id === "number" ? k.id : 0).filter((n) => n > 0); + const sshKeyIds: number[] = toObjectArray(keysData?.ssh_keys) + .map((k) => (typeof k.id === "number" ? k.id : 0)) + .filter((n) => n > 0); const userdata = getCloudInitUserdata(tier); const body = JSON.stringify({ @@ -934,9 +938,19 @@ export async function interactiveSession(cmd: string, ip?: string): Promise((resolve, reject) => { - const child = spawn("ssh", [...SSH_BASE_OPTS, ...keyOpts, "-t", `root@${serverIp}`, fullCmd], { - stdio: "inherit", - }); + const child = spawn( + "ssh", + [ + ...SSH_BASE_OPTS, + ...keyOpts, + "-t", + `root@${serverIp}`, + fullCmd, + ], + { + stdio: "inherit", + }, + ); child.on("close", (code) => resolve(code ?? 0)); child.on("error", reject); }); @@ -1062,7 +1076,7 @@ export async function listServers(): Promise { console.log("-".repeat(80)); for (const d of droplets) { const networks = d.networks; - const v4 = (networks && typeof networks === "object" && "v4" in networks) ? networks.v4 : undefined; + const v4 = networks && typeof networks === "object" && "v4" in networks ? networks.v4 : undefined; const v4Arr = toObjectArray(v4); const publicNet = v4Arr.find((n) => n.type === "public"); const ip = String(publicNet?.ip_address ?? "N/A"); diff --git a/cli/src/flags.ts b/cli/src/flags.ts index 6c8aa254..c272f74d 100644 --- a/cli/src/flags.ts +++ b/cli/src/flags.ts @@ -1,16 +1,26 @@ /** CLI flag definitions and utilities — single source of truth for flag validation */ export const KNOWN_FLAGS = new Set([ - "--help", "-h", - "--version", "-v", "-V", - "--prompt", "-p", "--prompt-file", "-f", - "--dry-run", "-n", + "--help", + "-h", + "--version", + "-v", + "-V", + "--prompt", + "-p", + "--prompt-file", + "-f", + "--dry-run", + "-n", "--debug", "--headless", "--output", "--name", "--default", - "-a", "-c", "--agent", "--cloud", + "-a", + "-c", + "--agent", + "--cloud", "--clear", ]); diff --git a/cli/src/fly/fly.ts b/cli/src/fly/fly.ts index 486d1d71..0fc5501a 100644 --- a/cli/src/fly/fly.ts +++ b/cli/src/fly/fly.ts @@ -169,11 +169,12 @@ function parseJson(text: string): Record | null { } function toObjectArray(val: unknown): Record[] { - if (!Array.isArray(val)) { return []; } - return val - .filter((item): item is Record => - item !== null && typeof item === "object" && !Array.isArray(item), - ); + if (!Array.isArray(val)) { + return []; + } + return val.filter( + (item): item is Record => item !== null && typeof item === "object" && !Array.isArray(item), + ); } function hasError(text: string): boolean { @@ -552,7 +553,9 @@ interface OrgEntry { function parseOrgsJson(json: string): OrgEntry[] { const raw = parseJsonRaw(json); - if (!raw || typeof raw !== "object") { return []; } + if (!raw || typeof raw !== "object") { + return []; + } let orgs: Record[] = []; if (Array.isArray(raw)) { @@ -560,7 +563,9 @@ function parseOrgsJson(json: string): OrgEntry[] { } else { // Re-parse as Record via valibot schema const data = parseJson(json); - if (!data) { return []; } + if (!data) { + return []; + } if (data.nodes) { orgs = toObjectArray(data.nodes); @@ -1020,18 +1025,22 @@ export async function interactiveSession(cmd: string): Promise { const flyCmd = getCmd()!; const exitCode = await new Promise((resolve, reject) => { - const child = spawn(flyCmd, [ - "ssh", - "console", - "-a", - flyAppName, - "--pty", - "-C", - `bash -c '${escapedCmd}'`, - ], { - stdio: "inherit", - env: process.env, - }); + const child = spawn( + flyCmd, + [ + "ssh", + "console", + "-a", + flyAppName, + "--pty", + "-C", + `bash -c '${escapedCmd}'`, + ], + { + stdio: "inherit", + env: process.env, + }, + ); child.on("close", (code) => resolve(code ?? 0)); child.on("error", reject); }); @@ -1182,7 +1191,7 @@ export async function destroyServer(appName?: string): Promise { const resp = await flyApi("GET", `/apps/${name}/machines`); const machines = parseJsonRaw(resp); const machineList = toObjectArray(Array.isArray(machines) ? machines : []); - const ids: string[] = machineList.map((m) => typeof m.id === "string" ? m.id : "").filter(Boolean); + const ids: string[] = machineList.map((m) => (typeof m.id === "string" ? m.id : "")).filter(Boolean); for (const mid of ids) { logStep(`Stopping machine ${mid}...`); diff --git a/cli/src/gcp/gcp.ts b/cli/src/gcp/gcp.ts index 5ba2e2c8..31ba940f 100644 --- a/cli/src/gcp/gcp.ts +++ b/cli/src/gcp/gcp.ts @@ -895,16 +895,20 @@ export async function interactiveSession(cmd: string): Promise { const keyOpts = getSshKeyOpts(await ensureSshKeys()); const exitCode = await new Promise((resolve, reject) => { - const child = spawn("ssh", [ - ...SSH_BASE_OPTS, - ...keyOpts, - "-t", - `${username}@${gcpServerIp}`, - `bash -c ${shellQuote(fullCmd)}`, - ], { - stdio: "inherit", - env: process.env, - }); + const child = spawn( + "ssh", + [ + ...SSH_BASE_OPTS, + ...keyOpts, + "-t", + `${username}@${gcpServerIp}`, + `bash -c ${shellQuote(fullCmd)}`, + ], + { + stdio: "inherit", + env: process.env, + }, + ); child.on("close", (code) => resolve(code ?? 0)); child.on("error", reject); }); diff --git a/cli/src/hetzner/hetzner.ts b/cli/src/hetzner/hetzner.ts index 17180c63..27484f5e 100644 --- a/cli/src/hetzner/hetzner.ts +++ b/cli/src/hetzner/hetzner.ts @@ -230,9 +230,7 @@ export async function ensureSshKey(): Promise { const data = parseJson(resp); const sshKeys = toObjectArray(data?.ssh_keys); - const alreadyRegistered = sshKeys.some( - (k) => fingerprint && k.fingerprint === fingerprint, - ); + const alreadyRegistered = sshKeys.some((k) => fingerprint && k.fingerprint === fingerprint); if (alreadyRegistered) { logInfo(`SSH key '${key.name}' already registered with Hetzner`); @@ -537,9 +535,19 @@ export async function interactiveSession(cmd: string, ip?: string): Promise((resolve, reject) => { - const child = spawn("ssh", [...SSH_BASE_OPTS, ...keyOpts, "-t", `root@${serverIp}`, fullCmd], { - stdio: "inherit", - }); + const child = spawn( + "ssh", + [ + ...SSH_BASE_OPTS, + ...keyOpts, + "-t", + `root@${serverIp}`, + fullCmd, + ], + { + stdio: "inherit", + }, + ); child.on("close", (code) => resolve(code ?? 0)); child.on("error", reject); }); @@ -657,7 +665,7 @@ export async function listServers(): Promise { const pad = (str: string, n: number) => (str + " ".repeat(n)).slice(0, n); const str = (val: unknown, fallback = "N/A"): string => - typeof val === "string" ? val : (val != null ? String(val) : fallback); + typeof val === "string" ? val : val != null ? String(val) : fallback; console.log(pad("NAME", 25) + pad("ID", 12) + pad("STATUS", 12) + pad("IP", 16) + pad("TYPE", 10)); console.log("-".repeat(75)); for (const s of servers) { diff --git a/cli/src/local/local.ts b/cli/src/local/local.ts index 04910035..871ca4aa 100644 --- a/cli/src/local/local.ts +++ b/cli/src/local/local.ts @@ -71,10 +71,17 @@ export function uploadFile(localPath: string, remotePath: string): void { /** Launch an interactive shell session locally. */ export async function interactiveSession(cmd: string): Promise { return new Promise((resolve, reject) => { - const child = spawn("bash", ["-c", cmd], { - stdio: "inherit", - env: process.env, - }); + const child = spawn( + "bash", + [ + "-c", + cmd, + ], + { + stdio: "inherit", + env: process.env, + }, + ); child.on("close", (code) => resolve(code ?? 0)); child.on("error", reject); }); @@ -109,4 +116,3 @@ export function saveLocalConnection(): void { }); Bun.write(`${dir}/last-connection.json`, json + "\n"); } - diff --git a/cli/src/manifest.ts b/cli/src/manifest.ts index 4d5b177c..a01d4ae1 100644 --- a/cli/src/manifest.ts +++ b/cli/src/manifest.ts @@ -150,7 +150,17 @@ function stripDangerousKeys(obj: unknown): unknown { } function isValidManifest(data: unknown): data is Manifest { - return data !== null && typeof data === "object" && !Array.isArray(data) && "agents" in data && "clouds" in data && "matrix" in data && !!data.agents && !!data.clouds && !!data.matrix; + return ( + data !== null && + typeof data === "object" && + !Array.isArray(data) && + "agents" in data && + "clouds" in data && + "matrix" in data && + !!data.agents && + !!data.clouds && + !!data.matrix + ); } async function fetchManifestFromGitHub(): Promise { diff --git a/cli/src/picker.ts b/cli/src/picker.ts index 20a9baab..3573eb00 100644 --- a/cli/src/picker.ts +++ b/cli/src/picker.ts @@ -83,13 +83,24 @@ const A = { }; /** Truncate a string to `max` visible characters, adding \u2026 if needed. */ -const trunc = (s: string, max: number): string => - s.length <= max ? s : s.slice(0, Math.max(max - 1, 0)) + "\u2026"; +const trunc = (s: string, max: number): string => (s.length <= max ? s : s.slice(0, Math.max(max - 1, 0)) + "\u2026"); /** Get terminal column width from a tty file descriptor. */ function getTTYCols(ttyFd: number): number { try { - const res = spawnSync("stty", ["size"], { stdio: [ttyFd, "pipe", "pipe"] }); + const res = spawnSync( + "stty", + [ + "size", + ], + { + stdio: [ + ttyFd, + "pipe", + "pipe", + ], + }, + ); if (res.status === 0 && res.stdout) { const parts = res.stdout.toString().trim().split(/\s+/); if (parts.length >= 2) { @@ -270,7 +281,10 @@ export function pickToTTY(config: PickConfig): string | null { // Replace picker with a one-line confirmation w(A.up(pickerHeight) + A.col1 + A.clearBelow); const opt = config.options[selected]; - w(`${A.green}${A.bold}> ${config.message}:${A.reset} ` + `${A.cyan}${trunc(opt.label, maxW - config.message.length - 4)}${A.reset}\r\n`); + w( + `${A.green}${A.bold}> ${config.message}:${A.reset} ` + + `${A.cyan}${trunc(opt.label, maxW - config.message.length - 4)}${A.reset}\r\n`, + ); break outer; } @@ -305,10 +319,20 @@ export function pickToTTY(config: PickConfig): string | null { * When deleteKey is enabled, pressing 'd' returns { action: "delete" }. */ export function pickToTTYWithActions(config: PickConfig): PickResult { - const cancel: PickResult = { action: "cancel", value: null, index: -1 }; + const cancel: PickResult = { + action: "cancel", + value: null, + index: -1, + }; if (config.options.length === 0) { - return config.defaultValue ? { action: "select", value: config.defaultValue, index: 0 } : cancel; + return config.defaultValue + ? { + action: "select", + value: config.defaultValue, + index: 0, + } + : cancel; } // ── open /dev/tty ────────────────────────────────────────────────────────── @@ -318,24 +342,67 @@ export function pickToTTYWithActions(config: PickConfig): PickResult { } catch { // Fall back to basic pickFallback which returns a value const val = pickFallback(config); - return val ? { action: "select", value: val, index: config.options.findIndex((o) => o.value === val) } : cancel; + return val + ? { + action: "select", + value: val, + index: config.options.findIndex((o) => o.value === val), + } + : cancel; } // ── save terminal settings ──────────────────────────────────────────────── - const savedRes = spawnSync("stty", ["-g"], { stdio: [ttyFd, "pipe", "pipe"] }); + const savedRes = spawnSync( + "stty", + [ + "-g", + ], + { + stdio: [ + ttyFd, + "pipe", + "pipe", + ], + }, + ); if (savedRes.status !== 0 || !savedRes.stdout) { fs.closeSync(ttyFd); const val = pickFallback(config); - return val ? { action: "select", value: val, index: config.options.findIndex((o) => o.value === val) } : cancel; + return val + ? { + action: "select", + value: val, + index: config.options.findIndex((o) => o.value === val), + } + : cancel; } const savedSettings = savedRes.stdout.toString().trim(); // ── enable raw / no-echo mode ───────────────────────────────────────────── - const rawRes = spawnSync("stty", ["raw", "-echo"], { stdio: [ttyFd, "pipe", "pipe"] }); + const rawRes = spawnSync( + "stty", + [ + "raw", + "-echo", + ], + { + stdio: [ + ttyFd, + "pipe", + "pipe", + ], + }, + ); if (rawRes.status !== 0) { fs.closeSync(ttyFd); const val = pickFallback(config); - return val ? { action: "select", value: val, index: config.options.findIndex((o) => o.value === val) } : cancel; + return val + ? { + action: "select", + value: val, + index: config.options.findIndex((o) => o.value === val), + } + : cancel; } // ── helpers ─────────────────────────────────────────────────────────────── @@ -347,7 +414,19 @@ export function pickToTTYWithActions(config: PickConfig): PickResult { const restore = () => { try { - spawnSync("stty", [savedSettings], { stdio: [ttyFd, "pipe", "pipe"] }); + spawnSync( + "stty", + [ + savedSettings, + ], + { + stdio: [ + ttyFd, + "pipe", + "pipe", + ], + }, + ); } catch {} w(A.showC); try { @@ -435,18 +514,28 @@ export function pickToTTYWithActions(config: PickConfig): PickResult { // ── confirm ──────────────────────────────────────────────────────── case "\r": case "\n": { - result = { action: "select", value: config.options[selected].value, index: selected }; + result = { + action: "select", + value: config.options[selected].value, + index: selected, + }; // Replace picker with a one-line confirmation w(A.up(pickerHeight) + A.col1 + A.clearBelow); const opt = config.options[selected]; - w(`${A.green}${A.bold}> ${config.message}:${A.reset} ${A.cyan}${trunc(opt.label, maxW - config.message.length - 4)}${A.reset}\r\n`); + w( + `${A.green}${A.bold}> ${config.message}:${A.reset} ${A.cyan}${trunc(opt.label, maxW - config.message.length - 4)}${A.reset}\r\n`, + ); break outer; } // ── delete ─────────────────────────────────────────────────────── case "d": if (config.deleteKey) { - result = { action: "delete", value: config.options[selected].value, index: selected }; + result = { + action: "delete", + value: config.options[selected].value, + index: selected, + }; w(A.up(pickerHeight) + A.col1 + A.clearBelow); break outer; } diff --git a/cli/src/shared/agent-setup.ts b/cli/src/shared/agent-setup.ts index 8de929c6..a46d3701 100644 --- a/cli/src/shared/agent-setup.ts +++ b/cli/src/shared/agent-setup.ts @@ -51,12 +51,7 @@ export async function installAgent( ): Promise { logStep(`Installing ${agentName}...`); try { - await withRetry( - `${agentName} install`, - () => wrapSshCall(runner.runServer(installCmd, timeoutSecs)), - 2, - 10, - ); + await withRetry(`${agentName} install`, () => wrapSshCall(runner.runServer(installCmd, timeoutSecs)), 2, 10); } catch { logError(`${agentName} installation failed`); throw new Error(`${agentName} install failed`); diff --git a/cli/src/shared/oauth.ts b/cli/src/shared/oauth.ts index 860806a5..ea35ad8f 100644 --- a/cli/src/shared/oauth.ts +++ b/cli/src/shared/oauth.ts @@ -6,7 +6,9 @@ import { logInfo, logWarn, logError, logStep, prompt, openBrowser, validateModel // ─── Schemas ───────────────────────────────────────────────────────────────── -const OAuthKeySchema = v.object({ key: v.string() }); +const OAuthKeySchema = v.object({ + key: v.string(), +}); // ─── Key Validation ────────────────────────────────────────────────────────── diff --git a/cli/src/shared/result.ts b/cli/src/shared/result.ts index f8dbab2d..0d954c08 100644 --- a/cli/src/shared/result.ts +++ b/cli/src/shared/result.ts @@ -5,6 +5,20 @@ // is retryable (return Err) or fatal (throw), instead of relying on brittle // error-message pattern matching after the fact. -export type Result = { ok: true; data: T } | { ok: false; error: Error }; -export const Ok = (data: T): Result => ({ ok: true, data }); -export const Err = (error: Error): Result => ({ ok: false, error }); +export type Result = + | { + ok: true; + data: T; + } + | { + ok: false; + error: Error; + }; +export const Ok = (data: T): Result => ({ + ok: true, + data, +}); +export const Err = (error: Error): Result => ({ + ok: false, + error, +}); diff --git a/cli/src/shared/ssh-keys.ts b/cli/src/shared/ssh-keys.ts index 2d015836..aaf596f2 100644 --- a/cli/src/shared/ssh-keys.ts +++ b/cli/src/shared/ssh-keys.ts @@ -87,8 +87,18 @@ export function discoverSshKeys(): SshKeyPair[] { function getKeyType(pubPath: string): string { try { const result = Bun.spawnSync( - ["ssh-keygen", "-lf", pubPath], - { stdio: ["ignore", "pipe", "pipe"] }, + [ + "ssh-keygen", + "-lf", + pubPath, + ], + { + stdio: [ + "ignore", + "pipe", + "pipe", + ], + }, ); const output = new TextDecoder().decode(result.stdout).trim(); // Format: "256 SHA256:xxx user@host (ED25519)" @@ -107,12 +117,31 @@ export function generateSshKey(): SshKeyPair { const privPath = `${sshDir}/id_ed25519`; const pubPath = `${privPath}.pub`; - mkdirSync(sshDir, { recursive: true, mode: 0o700 }); + mkdirSync(sshDir, { + recursive: true, + mode: 0o700, + }); logStep("Generating SSH key..."); const result = Bun.spawnSync( - ["ssh-keygen", "-t", "ed25519", "-f", privPath, "-N", "", "-C", "spawn"], - { stdio: ["ignore", "pipe", "pipe"] }, + [ + "ssh-keygen", + "-t", + "ed25519", + "-f", + privPath, + "-N", + "", + "-C", + "spawn", + ], + { + stdio: [ + "ignore", + "pipe", + "pipe", + ], + }, ); if (result.exitCode !== 0) { throw new Error("SSH key generation failed"); @@ -132,8 +161,20 @@ export function generateSshKey(): SshKeyPair { /** Get the MD5 fingerprint of a public key (for cloud provider matching). */ export function getSshFingerprint(pubPath: string): string { const result = Bun.spawnSync( - ["ssh-keygen", "-lf", pubPath, "-E", "md5"], - { stdio: ["ignore", "pipe", "pipe"] }, + [ + "ssh-keygen", + "-lf", + pubPath, + "-E", + "md5", + ], + { + stdio: [ + "ignore", + "pipe", + "pipe", + ], + }, ); const output = new TextDecoder().decode(result.stdout).trim(); // Format: "2048 MD5:xx:xx:xx... user@host (ED25519)" @@ -162,7 +203,9 @@ export async function ensureSshKeys(): Promise { if (discovered.length === 0) { const generated = generateSshKey(); - cachedKeys = [generated]; + cachedKeys = [ + generated, + ]; return cachedKeys; } diff --git a/cli/src/shared/ssh.ts b/cli/src/shared/ssh.ts index 49f1baf4..0bf05a7a 100644 --- a/cli/src/shared/ssh.ts +++ b/cli/src/shared/ssh.ts @@ -7,13 +7,20 @@ import { connect } from "node:net"; /** Base SSH options shared across all clouds (array form for Bun.spawn). */ export const SSH_BASE_OPTS: string[] = [ - "-o", "StrictHostKeyChecking=no", - "-o", "UserKnownHostsFile=/dev/null", - "-o", "LogLevel=ERROR", - "-o", "ConnectTimeout=10", - "-o", "ServerAliveInterval=15", - "-o", "ServerAliveCountMax=3", - "-o", "BatchMode=yes", + "-o", + "StrictHostKeyChecking=no", + "-o", + "UserKnownHostsFile=/dev/null", + "-o", + "LogLevel=ERROR", + "-o", + "ConnectTimeout=10", + "-o", + "ServerAliveInterval=15", + "-o", + "ServerAliveCountMax=3", + "-o", + "BatchMode=yes", ]; // ─── Helpers ───────────────────────────────────────────────────────────────── @@ -32,7 +39,10 @@ export function sleep(ms: number): Promise { */ export function tcpCheck(host: string, port: number, timeoutMs = 2000): Promise { return new Promise((resolve) => { - const socket = connect({ host, port }); + const socket = connect({ + host, + port, + }); const timer = setTimeout(() => { socket.destroy(); resolve(false); @@ -81,7 +91,9 @@ export async function waitForSsh(opts: WaitForSshOpts): Promise { const maxAttempts = opts.maxAttempts ?? 36; // Build SSH args - const sshArgs: string[] = [...SSH_BASE_OPTS]; + const sshArgs: string[] = [ + ...SSH_BASE_OPTS, + ]; if (sshKeyPath) { sshArgs.push("-i", sshKeyPath); } @@ -117,8 +129,19 @@ export async function waitForSsh(opts: WaitForSshOpts): Promise { for (let i = 1; i <= handshakeAttempts; i++) { try { const proc = Bun.spawn( - ["ssh", ...sshArgs, `${user}@${host}`, "echo ok"], - { stdio: ["ignore", "pipe", "pipe"] }, + [ + "ssh", + ...sshArgs, + `${user}@${host}`, + "echo ok", + ], + { + stdio: [ + "ignore", + "pipe", + "pipe", + ], + }, ); const [stdout, stderr] = await Promise.all([ new Response(proc.stdout).text(), diff --git a/cli/src/shared/type-guards.ts b/cli/src/shared/type-guards.ts index a6bdd575..4f2bdfc8 100644 --- a/cli/src/shared/type-guards.ts +++ b/cli/src/shared/type-guards.ts @@ -8,10 +8,14 @@ export function isNumber(val: unknown): val is number { return typeof val === "number"; } -export function hasStatus(err: unknown): err is { status: number } { +export function hasStatus(err: unknown): err is { + status: number; +} { return err !== null && typeof err === "object" && "status" in err && typeof err.status === "number"; } -export function hasMessage(err: unknown): err is { message: string } { +export function hasMessage(err: unknown): err is { + message: string; +} { return err !== null && typeof err === "object" && "message" in err && typeof err.message === "string"; } diff --git a/cli/src/update-check.ts b/cli/src/update-check.ts index 913be8c7..9f937ce8 100644 --- a/cli/src/update-check.ts +++ b/cli/src/update-check.ts @@ -25,7 +25,9 @@ export const executor = { // ── Schemas ────────────────────────────────────────────────────────────────── -const PkgVersionSchema = v.object({ version: v.string() }); +const PkgVersionSchema = v.object({ + version: v.string(), +}); const FETCH_TIMEOUT = 10000; // 10 seconds const UPDATE_BACKOFF_MS = 60 * 60 * 1000; // 1 hour