fix: re-exec with new binary after auto-update for all invocations (#1526)

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) <noreply@anthropic.com>
This commit is contained in:
L 2026-02-20 10:26:02 -05:00 committed by GitHub
parent be48fe8576
commit eea43adcad
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 86 additions and 18 deletions

27
cli/biome.json Normal file
View file

@ -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
}
}

View file

@ -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=="],

View file

@ -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"
}
}

View file

@ -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<string, string | undefined>;
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;

View file

@ -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 ───────────────────────────────────────────────────────────────────

View file

@ -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",