mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-05-01 21:30:21 +00:00
When a CLI auto-update triggers mid-command (e.g. `spawn claude sprite`), the updated binary now automatically re-runs with the original arguments instead of asking the user to manually re-run. Sets SPAWN_NO_UPDATE_CHECK=1 on re-exec to prevent infinite update loops. Falls back to the old "run again" message when no arguments were provided (bare `spawn`). Agent: ux-engineer Co-authored-by: A <6723574+louisgv@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c7bbe8bc3b
commit
716da5d43b
3 changed files with 143 additions and 5 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@openrouter/spawn",
|
"name": "@openrouter/spawn",
|
||||||
"version": "0.2.66",
|
"version": "0.2.67",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"bin": {
|
"bin": {
|
||||||
"spawn": "cli.js"
|
"spawn": "cli.js"
|
||||||
|
|
|
||||||
|
|
@ -197,5 +197,120 @@ describe("update-check", () => {
|
||||||
|
|
||||||
fetchSpy.mockRestore();
|
fetchSpy.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should re-exec with original args after successful update", async () => {
|
||||||
|
const originalArgv = process.argv;
|
||||||
|
process.argv = ["/usr/bin/bun", "/usr/local/bin/spawn", "claude", "sprite"];
|
||||||
|
|
||||||
|
const mockFetch = mock(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({ version: "0.3.0" }),
|
||||||
|
} as Response)
|
||||||
|
);
|
||||||
|
const fetchSpy = spyOn(global, "fetch").mockImplementation(mockFetch);
|
||||||
|
|
||||||
|
const { executor } = await import("../update-check.js");
|
||||||
|
const calls: string[] = [];
|
||||||
|
const execSyncSpy = spyOn(executor, "execSync").mockImplementation((cmd: string) => {
|
||||||
|
calls.push(cmd);
|
||||||
|
});
|
||||||
|
|
||||||
|
const { checkForUpdates } = await import("../update-check.js");
|
||||||
|
await checkForUpdates();
|
||||||
|
|
||||||
|
// First call: install script, second call: re-exec with original args
|
||||||
|
expect(calls.length).toBe(2);
|
||||||
|
expect(calls[0]).toContain("install.sh");
|
||||||
|
expect(calls[1]).toContain("spawn");
|
||||||
|
expect(calls[1]).toContain("claude");
|
||||||
|
expect(calls[1]).toContain("sprite");
|
||||||
|
|
||||||
|
// Should show rerunning message
|
||||||
|
const output = consoleErrorSpy.mock.calls.map((call) => call[0]).join("\n");
|
||||||
|
expect(output).toContain("Rerunning");
|
||||||
|
|
||||||
|
// Should set SPAWN_NO_UPDATE_CHECK=1 to prevent infinite loop
|
||||||
|
expect(execSyncSpy.mock.calls[1][1]).toHaveProperty("env");
|
||||||
|
expect(execSyncSpy.mock.calls[1][1].env.SPAWN_NO_UPDATE_CHECK).toBe("1");
|
||||||
|
|
||||||
|
expect(processExitSpy).toHaveBeenCalledWith(0);
|
||||||
|
|
||||||
|
fetchSpy.mockRestore();
|
||||||
|
execSyncSpy.mockRestore();
|
||||||
|
process.argv = originalArgv;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should forward exit code when re-exec fails", async () => {
|
||||||
|
const originalArgv = process.argv;
|
||||||
|
process.argv = ["/usr/bin/bun", "/usr/local/bin/spawn", "claude", "sprite"];
|
||||||
|
|
||||||
|
const mockFetch = mock(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({ version: "0.3.0" }),
|
||||||
|
} as Response)
|
||||||
|
);
|
||||||
|
const fetchSpy = spyOn(global, "fetch").mockImplementation(mockFetch);
|
||||||
|
|
||||||
|
const { executor } = await import("../update-check.js");
|
||||||
|
let callCount = 0;
|
||||||
|
const execSyncSpy = spyOn(executor, "execSync").mockImplementation(() => {
|
||||||
|
callCount++;
|
||||||
|
if (callCount === 2) {
|
||||||
|
// Re-exec fails with exit code 1
|
||||||
|
const err = new Error("Command failed") as Error & { status: number };
|
||||||
|
err.status = 42;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const { checkForUpdates } = await import("../update-check.js");
|
||||||
|
await checkForUpdates();
|
||||||
|
|
||||||
|
// Should forward the exit code from the re-exec
|
||||||
|
expect(processExitSpy).toHaveBeenCalledWith(42);
|
||||||
|
|
||||||
|
fetchSpy.mockRestore();
|
||||||
|
execSyncSpy.mockRestore();
|
||||||
|
process.argv = originalArgv;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not re-exec when run without arguments (bare spawn)", async () => {
|
||||||
|
const originalArgv = process.argv;
|
||||||
|
process.argv = ["/usr/bin/bun", "/usr/local/bin/spawn"];
|
||||||
|
|
||||||
|
const mockFetch = mock(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
ok: true,
|
||||||
|
json: () => Promise.resolve({ version: "0.3.0" }),
|
||||||
|
} as Response)
|
||||||
|
);
|
||||||
|
const fetchSpy = spyOn(global, "fetch").mockImplementation(mockFetch);
|
||||||
|
|
||||||
|
const { executor } = await import("../update-check.js");
|
||||||
|
const calls: string[] = [];
|
||||||
|
const execSyncSpy = spyOn(executor, "execSync").mockImplementation((cmd: string) => {
|
||||||
|
calls.push(cmd);
|
||||||
|
});
|
||||||
|
|
||||||
|
const { checkForUpdates } = await import("../update-check.js");
|
||||||
|
await checkForUpdates();
|
||||||
|
|
||||||
|
// Only one call: the install script (no re-exec)
|
||||||
|
expect(calls.length).toBe(1);
|
||||||
|
expect(calls[0]).toContain("install.sh");
|
||||||
|
|
||||||
|
// Should show "Run your spawn command again" instead
|
||||||
|
const output = consoleErrorSpy.mock.calls.map((call) => call[0]).join("\n");
|
||||||
|
expect(output).toContain("Run your spawn command again");
|
||||||
|
expect(output).not.toContain("Rerunning");
|
||||||
|
|
||||||
|
expect(processExitSpy).toHaveBeenCalledWith(0);
|
||||||
|
|
||||||
|
fetchSpy.mockRestore();
|
||||||
|
execSyncSpy.mockRestore();
|
||||||
|
process.argv = originalArgv;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -85,11 +85,34 @@ function performAutoUpdate(latestVersion: string): void {
|
||||||
|
|
||||||
console.error();
|
console.error();
|
||||||
console.error(pc.green(pc.bold(`${CHECK_MARK} Updated successfully!`)));
|
console.error(pc.green(pc.bold(`${CHECK_MARK} Updated successfully!`)));
|
||||||
console.error(pc.dim(" Run your spawn command again to use the new version."));
|
|
||||||
console.error();
|
|
||||||
|
|
||||||
// Exit cleanly after update
|
// Re-exec the updated binary with the same arguments
|
||||||
process.exit(0);
|
const args = process.argv.slice(2);
|
||||||
|
if (args.length > 0) {
|
||||||
|
const binPath = process.argv[1] || "spawn";
|
||||||
|
const quotedBin = `'${binPath.replace(/'/g, "'\\''")}'`;
|
||||||
|
const quotedArgs = args.map((a) => `'${a.replace(/'/g, "'\\''")}'`).join(" ");
|
||||||
|
console.error(pc.dim(` Rerunning: spawn ${args.join(" ")}`));
|
||||||
|
console.error();
|
||||||
|
try {
|
||||||
|
executor.execSync(`${quotedBin} ${quotedArgs}`, {
|
||||||
|
stdio: "inherit",
|
||||||
|
shell: "/bin/bash",
|
||||||
|
env: { ...process.env, SPAWN_NO_UPDATE_CHECK: "1" },
|
||||||
|
});
|
||||||
|
process.exit(0);
|
||||||
|
} catch (reexecErr) {
|
||||||
|
// Forward the exit code from the re-executed command
|
||||||
|
const code = reexecErr && typeof reexecErr === "object" && "status" in reexecErr
|
||||||
|
? (reexecErr as { status: number }).status
|
||||||
|
: 1;
|
||||||
|
process.exit(code);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error(pc.dim(" Run your spawn command again to use the new version."));
|
||||||
|
console.error();
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error();
|
console.error();
|
||||||
console.error(pc.red(pc.bold(`${CROSS_MARK} Auto-update failed`)));
|
console.error(pc.red(pc.bold(`${CROSS_MARK} Auto-update failed`)));
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue