From eea43adcad52dc46984ad20dc1bac18a4cb26c48 Mon Sep 17 00:00:00 2001 From: L <6723574+louisgv@users.noreply.github.com> Date: Fri, 20 Feb 2026 10:26:02 -0500 Subject: [PATCH] fix: re-exec with new binary after auto-update for all invocations (#1526) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs in reExecWithArgs(): 1. args.length === 0 early exit: Running bare `spawn` (interactive picker) after an auto-update would print "Run your spawn command again" and exit, requiring the user to manually re-invoke. Now always re-exec so the new flow triggers immediately. 2. process.argv[1] stale binary path: If the installer places the updated binary in a different directory than the currently running binary (e.g. old: ~/.local/bin, new: /usr/local/bin), re-exec would run the old stale binary. Fix: add findUpdatedBinary() which resolves via `which spawn` (PATH lookup) first, falling back to process.argv[1] only if which fails. Bump CLI version 0.5.17 → 0.5.18. Co-authored-by: Claude Sonnet 4.6 (1M context) --- cli/biome.json | 27 ++++++++++++ cli/bun.lock | 19 +++++++++ cli/package.json | 4 +- .../__tests__/agent-info-quickstart.test.ts | 8 ++-- .../__tests__/check-entity-messages.test.ts | 4 +- cli/src/update-check.ts | 42 ++++++++++++++----- 6 files changed, 86 insertions(+), 18 deletions(-) create mode 100644 cli/biome.json diff --git a/cli/biome.json b/cli/biome.json new file mode 100644 index 00000000..6de7a985 --- /dev/null +++ b/cli/biome.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.4.3/schema.json", + "vcs": { + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "files": { + "ignoreUnknown": false, + "includes": ["src/**/*.ts"] + }, + "formatter": { + "enabled": false + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "suspicious": { + "noExplicitAny": "error" + } + } + }, + "assist": { + "enabled": false + } +} diff --git a/cli/bun.lock b/cli/bun.lock index 72514b74..88476561 100644 --- a/cli/bun.lock +++ b/cli/bun.lock @@ -9,11 +9,30 @@ "picocolors": "^1.1.1", }, "devDependencies": { + "@biomejs/biome": "^2.4.3", "@types/bun": "^1.3.8", }, }, }, "packages": { + "@biomejs/biome": ["@biomejs/biome@2.4.3", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.4.3", "@biomejs/cli-darwin-x64": "2.4.3", "@biomejs/cli-linux-arm64": "2.4.3", "@biomejs/cli-linux-arm64-musl": "2.4.3", "@biomejs/cli-linux-x64": "2.4.3", "@biomejs/cli-linux-x64-musl": "2.4.3", "@biomejs/cli-win32-arm64": "2.4.3", "@biomejs/cli-win32-x64": "2.4.3" }, "bin": { "biome": "bin/biome" } }, "sha512-cBrjf6PNF6yfL8+kcNl85AjiK2YHNsbU0EvDOwiZjBPbMbQ5QcgVGFpjD0O52p8nec5O8NYw7PKw3xUR7fPAkQ=="], + + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.4.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eOafSFlI/CF4id2tlwq9CVHgeEqvTL5SrhWff6ZORp6S3NL65zdsR3ugybItkgF8Pf4D9GSgtbB6sE3UNgOM9w=="], + + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.4.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-V2+av4ilbWcBMNufTtMMXVW00nPwyIjI5qf7n9wSvUaZ+tt0EvMGk46g9sAFDJBEDOzSyoRXiSP6pCvKTOEbPA=="], + + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.4.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-0m+O0x9FgK99FAwDK+fiDtjs2wnqq7bvfj17KJVeCkTwT/liI+Q9njJG7lwXK0iSJVXeFNRIxukpVI3SifMYAA=="], + + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.4.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-QuFzvsGo8BA4Xm7jGX5idkw6BqFblcCPySMTvq0AhGYnhUej5VJIDJbmTKfHqwjHepZiC4fA+T5i6wmiZolZNw=="], + + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.4.3", "", { "os": "linux", "cpu": "x64" }, "sha512-NVqh0saIU0u5OfOp/0jFdlKRE59+XyMvWmtx0f6Nm/2OpdxBl04coRIftBbY9d1gfu+23JVv4CItAqPYrjYh5w=="], + + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.4.3", "", { "os": "linux", "cpu": "x64" }, "sha512-qEc0OCpj/uytruQ4wLM0yWNJLZy0Up8H1Er5MW3SrstqM6J2d4XqdNA86xzCy8MQCHpoVZ3lFye3GBlIL4/ljw=="], + + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.4.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-gRO96vrIARilv/Cp2ZnmNNL5LSZg3RO75GPp13hsLO3N4YVpE7saaMDp2bcyV48y2N2Pbit1brkGVGta0yd6VQ=="], + + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.4.3", "", { "os": "win32", "cpu": "x64" }, "sha512-vSm/vOJe06pf14aGHfHl3Ar91Nlx4YYmohElDJ+17UbRwe99n987S/MhAlQOkONqf1utJor04ChkCPmKb8SWdw=="], + "@clack/core": ["@clack/core@1.0.0", "", { "dependencies": { "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-Orf9Ltr5NeiEuVJS8Rk2XTw3IxNC2Bic3ash7GgYeA8LJ/zmSNpSQ/m5UAhe03lA6KFgklzZ5KTHs4OAMA/SAQ=="], "@clack/prompts": ["@clack/prompts@1.0.0", "", { "dependencies": { "@clack/core": "1.0.0", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" } }, "sha512-rWPXg9UaCFqErJVQ+MecOaWsozjaxol4yjnmYcGNipAWzdaWa2x+VJmKfGq7L0APwBohQOYdHC+9RO4qRXej+A=="], diff --git a/cli/package.json b/cli/package.json index 760f67f0..edefac8f 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@openrouter/spawn", - "version": "0.5.17", + "version": "0.5.18", "type": "module", "bin": { "spawn": "cli.js" @@ -9,6 +9,7 @@ "dev": "bun run src/index.ts", "build": "bun build src/index.ts --outfile cli.js --target bun --minify --packages bundle", "compile": "bun build src/index.ts --compile --outfile spawn", + "lint": "biome lint src/", "test": "bun test", "test:watch": "bun test --watch" }, @@ -17,6 +18,7 @@ "picocolors": "^1.1.1" }, "devDependencies": { + "@biomejs/biome": "^2.4.3", "@types/bun": "^1.3.8" } } diff --git a/cli/src/__tests__/agent-info-quickstart.test.ts b/cli/src/__tests__/agent-info-quickstart.test.ts index ed97d8a8..289628ab 100644 --- a/cli/src/__tests__/agent-info-quickstart.test.ts +++ b/cli/src/__tests__/agent-info-quickstart.test.ts @@ -269,7 +269,7 @@ function setupManifest(manifest: Manifest) { ok: true, json: async () => manifest, text: async () => JSON.stringify(manifest), - })) as any; + })) as unknown as typeof global.fetch; return loadManifest(true); } @@ -283,11 +283,11 @@ describe("cmdAgentInfo - printAgentQuickStart", () => { let savedEnv: Record; function getOutput(): string { - return consoleSpy.mock.calls.map((c: any[]) => c.join(" ")).join("\n"); + return consoleSpy.mock.calls.map((c: unknown[]) => c.join(" ")).join("\n"); } function getOutputLines(): string[] { - return consoleSpy.mock.calls.map((c: any[]) => c.join(" ")); + return consoleSpy.mock.calls.map((c: unknown[]) => c.join(" ")); } beforeEach(async () => { @@ -302,7 +302,7 @@ describe("cmdAgentInfo - printAgentQuickStart", () => { processExitSpy = spyOn(process, "exit").mockImplementation((() => { throw new Error("process.exit"); - }) as any); + }) as unknown as (code?: number) => never); originalFetch = global.fetch; diff --git a/cli/src/__tests__/check-entity-messages.test.ts b/cli/src/__tests__/check-entity-messages.test.ts index 920c239b..5e71ad03 100644 --- a/cli/src/__tests__/check-entity-messages.test.ts +++ b/cli/src/__tests__/check-entity-messages.test.ts @@ -98,11 +98,11 @@ function createManifest(): Manifest { } function infoCalls(): string[] { - return mockLogInfo.mock.calls.map((c: any[]) => c.join(" ")); + return mockLogInfo.mock.calls.map((c: unknown[]) => c.join(" ")); } function errorCalls(): string[] { - return mockLogError.mock.calls.map((c: any[]) => c.join(" ")); + return mockLogError.mock.calls.map((c: unknown[]) => c.join(" ")); } // ── Tests ─────────────────────────────────────────────────────────────────── diff --git a/cli/src/update-check.ts b/cli/src/update-check.ts index 593edf6e..504697c0 100644 --- a/cli/src/update-check.ts +++ b/cli/src/update-check.ts @@ -1,5 +1,5 @@ import "./unicode-detect.js"; // Ensure TERM is set before using symbols -import { execSync as nodeExecSync, execFileSync as nodeExecFileSync } from "child_process"; +import { execSync as nodeExecSync, execFileSync as nodeExecFileSync, type ExecSyncOptions, type ExecFileSyncOptions } from "child_process"; import pc from "picocolors"; import pkg from "../package.json" with { type: "json" }; import { RAW_BASE } from "./manifest.js"; @@ -8,8 +8,8 @@ const VERSION = pkg.version; // Internal executor for testability - can be replaced in tests export const executor = { - execSync: (cmd: string, options?: any) => nodeExecSync(cmd, options), - execFileSync: (file: string, args: string[], options?: any) => nodeExecFileSync(file, args, options), + execSync: (cmd: string, options?: ExecSyncOptions) => nodeExecSync(cmd, options), + execFileSync: (file: string, args: string[], options?: ExecFileSyncOptions) => nodeExecFileSync(file, args, options), }; // ── Constants ────────────────────────────────────────────────────────────────── @@ -79,19 +79,39 @@ function printUpdateBanner(latestVersion: string): void { console.error(); } +/** + * Find the spawn binary to re-exec after an update. + * + * Prefers `which spawn` (PATH resolution) over process.argv[1] because the + * installer may place the new binary in a different directory than where the + * currently running binary lives, causing re-exec to run the stale old binary. + */ +function findUpdatedBinary(): string { + try { + const result = executor.execSync("which spawn 2>/dev/null", { + encoding: "utf8", + shell: "/bin/bash", + }); + const found = result ? result.toString().trim() : ""; + if (found) return found; + } catch { + // fall through to argv fallback + } + return process.argv[1] || "spawn"; +} + /** Re-exec the updated binary with the original CLI arguments, forwarding the exit code */ function reExecWithArgs(): void { const args = process.argv.slice(2); - if (args.length === 0) { - console.error(pc.dim(" Run your spawn command again to use the new version.")); - console.error(); - process.exit(0); - return; // unreachable in production, but needed when process.exit is mocked in tests - } + const binPath = findUpdatedBinary(); - const binPath = process.argv[1] || "spawn"; - console.error(pc.dim(` Rerunning: spawn ${args.join(" ")}`)); + if (args.length === 0) { + console.error(pc.dim(" Restarting spawn with updated version...")); + } else { + console.error(pc.dim(` Rerunning: spawn ${args.join(" ")}`)); + } console.error(); + try { executor.execFileSync(binPath, args, { stdio: "inherit",