mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-05-07 17:31:04 +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
|
|
@ -197,5 +197,120 @@ describe("update-check", () => {
|
|||
|
||||
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(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
|
||||
process.exit(0);
|
||||
// Re-exec the updated binary with the same arguments
|
||||
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) {
|
||||
console.error();
|
||||
console.error(pc.red(pc.bold(`${CROSS_MARK} Auto-update failed`)));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue