mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-05-10 04:09:40 +00:00
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:
parent
be48fe8576
commit
eea43adcad
6 changed files with 86 additions and 18 deletions
27
cli/biome.json
Normal file
27
cli/biome.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
19
cli/bun.lock
19
cli/bun.lock
|
|
@ -9,11 +9,30 @@
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@biomejs/biome": "^2.4.3",
|
||||||
"@types/bun": "^1.3.8",
|
"@types/bun": "^1.3.8",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages": {
|
"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/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=="],
|
"@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=="],
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@openrouter/spawn",
|
"name": "@openrouter/spawn",
|
||||||
"version": "0.5.17",
|
"version": "0.5.18",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"bin": {
|
"bin": {
|
||||||
"spawn": "cli.js"
|
"spawn": "cli.js"
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
"dev": "bun run src/index.ts",
|
"dev": "bun run src/index.ts",
|
||||||
"build": "bun build src/index.ts --outfile cli.js --target bun --minify --packages bundle",
|
"build": "bun build src/index.ts --outfile cli.js --target bun --minify --packages bundle",
|
||||||
"compile": "bun build src/index.ts --compile --outfile spawn",
|
"compile": "bun build src/index.ts --compile --outfile spawn",
|
||||||
|
"lint": "biome lint src/",
|
||||||
"test": "bun test",
|
"test": "bun test",
|
||||||
"test:watch": "bun test --watch"
|
"test:watch": "bun test --watch"
|
||||||
},
|
},
|
||||||
|
|
@ -17,6 +18,7 @@
|
||||||
"picocolors": "^1.1.1"
|
"picocolors": "^1.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@biomejs/biome": "^2.4.3",
|
||||||
"@types/bun": "^1.3.8"
|
"@types/bun": "^1.3.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -269,7 +269,7 @@ function setupManifest(manifest: Manifest) {
|
||||||
ok: true,
|
ok: true,
|
||||||
json: async () => manifest,
|
json: async () => manifest,
|
||||||
text: async () => JSON.stringify(manifest),
|
text: async () => JSON.stringify(manifest),
|
||||||
})) as any;
|
})) as unknown as typeof global.fetch;
|
||||||
return loadManifest(true);
|
return loadManifest(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -283,11 +283,11 @@ describe("cmdAgentInfo - printAgentQuickStart", () => {
|
||||||
let savedEnv: Record<string, string | undefined>;
|
let savedEnv: Record<string, string | undefined>;
|
||||||
|
|
||||||
function getOutput(): string {
|
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[] {
|
function getOutputLines(): string[] {
|
||||||
return consoleSpy.mock.calls.map((c: any[]) => c.join(" "));
|
return consoleSpy.mock.calls.map((c: unknown[]) => c.join(" "));
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
|
@ -302,7 +302,7 @@ describe("cmdAgentInfo - printAgentQuickStart", () => {
|
||||||
|
|
||||||
processExitSpy = spyOn(process, "exit").mockImplementation((() => {
|
processExitSpy = spyOn(process, "exit").mockImplementation((() => {
|
||||||
throw new Error("process.exit");
|
throw new Error("process.exit");
|
||||||
}) as any);
|
}) as unknown as (code?: number) => never);
|
||||||
|
|
||||||
originalFetch = global.fetch;
|
originalFetch = global.fetch;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -98,11 +98,11 @@ function createManifest(): Manifest {
|
||||||
}
|
}
|
||||||
|
|
||||||
function infoCalls(): string[] {
|
function infoCalls(): string[] {
|
||||||
return mockLogInfo.mock.calls.map((c: any[]) => c.join(" "));
|
return mockLogInfo.mock.calls.map((c: unknown[]) => c.join(" "));
|
||||||
}
|
}
|
||||||
|
|
||||||
function errorCalls(): string[] {
|
function errorCalls(): string[] {
|
||||||
return mockLogError.mock.calls.map((c: any[]) => c.join(" "));
|
return mockLogError.mock.calls.map((c: unknown[]) => c.join(" "));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Tests ───────────────────────────────────────────────────────────────────
|
// ── Tests ───────────────────────────────────────────────────────────────────
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import "./unicode-detect.js"; // Ensure TERM is set before using symbols
|
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 pc from "picocolors";
|
||||||
import pkg from "../package.json" with { type: "json" };
|
import pkg from "../package.json" with { type: "json" };
|
||||||
import { RAW_BASE } from "./manifest.js";
|
import { RAW_BASE } from "./manifest.js";
|
||||||
|
|
@ -8,8 +8,8 @@ const VERSION = pkg.version;
|
||||||
|
|
||||||
// Internal executor for testability - can be replaced in tests
|
// Internal executor for testability - can be replaced in tests
|
||||||
export const executor = {
|
export const executor = {
|
||||||
execSync: (cmd: string, options?: any) => nodeExecSync(cmd, options),
|
execSync: (cmd: string, options?: ExecSyncOptions) => nodeExecSync(cmd, options),
|
||||||
execFileSync: (file: string, args: string[], options?: any) => nodeExecFileSync(file, args, options),
|
execFileSync: (file: string, args: string[], options?: ExecFileSyncOptions) => nodeExecFileSync(file, args, options),
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Constants ──────────────────────────────────────────────────────────────────
|
// ── Constants ──────────────────────────────────────────────────────────────────
|
||||||
|
|
@ -79,19 +79,39 @@ function printUpdateBanner(latestVersion: string): void {
|
||||||
console.error();
|
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 */
|
/** Re-exec the updated binary with the original CLI arguments, forwarding the exit code */
|
||||||
function reExecWithArgs(): void {
|
function reExecWithArgs(): void {
|
||||||
const args = process.argv.slice(2);
|
const args = process.argv.slice(2);
|
||||||
if (args.length === 0) {
|
const binPath = findUpdatedBinary();
|
||||||
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 = process.argv[1] || "spawn";
|
if (args.length === 0) {
|
||||||
console.error(pc.dim(` Rerunning: spawn ${args.join(" ")}`));
|
console.error(pc.dim(" Restarting spawn with updated version..."));
|
||||||
|
} else {
|
||||||
|
console.error(pc.dim(` Rerunning: spawn ${args.join(" ")}`));
|
||||||
|
}
|
||||||
console.error();
|
console.error();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
executor.execFileSync(binPath, args, {
|
executor.execFileSync(binPath, args, {
|
||||||
stdio: "inherit",
|
stdio: "inherit",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue