mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-04-28 03:49:31 +00:00
feat: v1.0.0 golden release — auto-update now opt-in (#3254)
Two changes to update behavior: 1. Auto-update is now opt-in via SPAWN_AUTO_UPDATE=1 (default: notify only) 2. Even with auto-update on, only patch versions install automatically (e.g. 1.0.0 → 1.0.5 yes, 1.0.0 → 1.1.0 no) This pins users to a stable major.minor — bug fixes flow automatically but new features require an explicit `spawn update`. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1cf0e0b9c6
commit
317227bd41
3 changed files with 56 additions and 17 deletions
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@openrouter/spawn",
|
||||
"version": "0.32.4",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"bin": {
|
||||
"spawn": "cli.js"
|
||||
|
|
|
|||
|
|
@ -34,6 +34,8 @@ function mockEnv() {
|
|||
process.env.NODE_ENV = undefined;
|
||||
process.env.BUN_ENV = undefined;
|
||||
process.env.SPAWN_NO_UPDATE_CHECK = undefined;
|
||||
// Enable auto-update for tests that verify update behavior
|
||||
process.env.SPAWN_AUTO_UPDATE = "1";
|
||||
return originalEnv;
|
||||
}
|
||||
|
||||
|
|
@ -92,7 +94,7 @@ describe("update-check", () => {
|
|||
});
|
||||
|
||||
it("should check for updates on every run", async () => {
|
||||
const mockFetch = mock(() => Promise.resolve(new Response("99.0.0\n")));
|
||||
const mockFetch = mock(() => Promise.resolve(new Response("1.0.99\n")));
|
||||
const fetchSpy = spyOn(global, "fetch").mockImplementation(mockFetch);
|
||||
|
||||
// Mock execFileSync to prevent actual update + re-exec
|
||||
|
|
@ -108,7 +110,7 @@ describe("update-check", () => {
|
|||
});
|
||||
|
||||
it("should auto-update when newer version is available", async () => {
|
||||
const mockFetch = mock(() => Promise.resolve(new Response("99.0.0\n")));
|
||||
const mockFetch = mock(() => Promise.resolve(new Response("1.0.99\n")));
|
||||
const fetchSpy = spyOn(global, "fetch").mockImplementation(mockFetch);
|
||||
|
||||
// Mock execFileSync to prevent actual update + re-exec
|
||||
|
|
@ -121,7 +123,7 @@ describe("update-check", () => {
|
|||
// Should have printed update message to stderr
|
||||
const output = consoleErrorSpy.mock.calls.map((call) => call[0]).join("\n");
|
||||
expect(output).toContain("Update available");
|
||||
expect(output).toContain("99.0.0");
|
||||
expect(output).toContain("1.0.99");
|
||||
expect(output).toContain("Updating automatically");
|
||||
|
||||
// Should have called execFileSync for curl, bash, which, and re-exec
|
||||
|
|
@ -167,7 +169,7 @@ describe("update-check", () => {
|
|||
});
|
||||
|
||||
it("should handle update failures gracefully", async () => {
|
||||
const mockFetch = mock(() => Promise.resolve(new Response("99.0.0\n")));
|
||||
const mockFetch = mock(() => Promise.resolve(new Response("1.0.99\n")));
|
||||
const fetchSpy = spyOn(global, "fetch").mockImplementation(mockFetch);
|
||||
|
||||
// Mock execFileSync to throw an error (curl fetch fails)
|
||||
|
|
@ -210,7 +212,7 @@ describe("update-check", () => {
|
|||
});
|
||||
|
||||
it("should redirect install script stdout to stderr when jsonOutput=true", async () => {
|
||||
const mockFetch = mock(() => Promise.resolve(new Response("99.0.0\n")));
|
||||
const mockFetch = mock(() => Promise.resolve(new Response("1.0.99\n")));
|
||||
const fetchSpy = spyOn(global, "fetch").mockImplementation(mockFetch);
|
||||
|
||||
const { executor } = await import("../update-check.js");
|
||||
|
|
@ -247,7 +249,7 @@ describe("update-check", () => {
|
|||
});
|
||||
|
||||
it("should use inherit stdio for install script when jsonOutput=false", async () => {
|
||||
const mockFetch = mock(() => Promise.resolve(new Response("99.0.0\n")));
|
||||
const mockFetch = mock(() => Promise.resolve(new Response("1.0.99\n")));
|
||||
const fetchSpy = spyOn(global, "fetch").mockImplementation(mockFetch);
|
||||
|
||||
const { executor } = await import("../update-check.js");
|
||||
|
|
@ -287,7 +289,7 @@ describe("update-check", () => {
|
|||
"sprite",
|
||||
];
|
||||
|
||||
const mockFetch = mock(() => Promise.resolve(new Response("99.0.0\n")));
|
||||
const mockFetch = mock(() => Promise.resolve(new Response("1.0.99\n")));
|
||||
const fetchSpy = spyOn(global, "fetch").mockImplementation(mockFetch);
|
||||
|
||||
const { executor } = await import("../update-check.js");
|
||||
|
|
@ -350,7 +352,7 @@ describe("update-check", () => {
|
|||
"sprite",
|
||||
];
|
||||
|
||||
const mockFetch = mock(() => Promise.resolve(new Response("99.0.0\n")));
|
||||
const mockFetch = mock(() => Promise.resolve(new Response("1.0.99\n")));
|
||||
const fetchSpy = spyOn(global, "fetch").mockImplementation(mockFetch);
|
||||
|
||||
const { executor } = await import("../update-check.js");
|
||||
|
|
@ -427,7 +429,7 @@ describe("update-check", () => {
|
|||
"/usr/local/bin/spawn",
|
||||
];
|
||||
|
||||
const mockFetch = mock(() => Promise.resolve(new Response("99.0.0\n")));
|
||||
const mockFetch = mock(() => Promise.resolve(new Response("1.0.99\n")));
|
||||
const fetchSpy = spyOn(global, "fetch").mockImplementation(mockFetch);
|
||||
|
||||
const { executor } = await import("../update-check.js");
|
||||
|
|
|
|||
|
|
@ -67,10 +67,12 @@ async function fetchLatestVersion(): Promise<string | null> {
|
|||
return fallback.ok ? fallback.data : null;
|
||||
}
|
||||
|
||||
function parseSemver(v: string): number[] {
|
||||
return v.split(".").map((n) => Number.parseInt(n, 10) || 0);
|
||||
}
|
||||
|
||||
function compareVersions(current: string, latest: string): boolean {
|
||||
// Simple semantic version comparison (assumes format: major.minor.patch)
|
||||
const parseSemver = (v: string): number[] => v.split(".").map((n) => Number.parseInt(n, 10) || 0);
|
||||
|
||||
const currentParts = parseSemver(current);
|
||||
const latestParts = parseSemver(latest);
|
||||
|
||||
|
|
@ -86,6 +88,13 @@ function compareVersions(current: string, latest: string): boolean {
|
|||
return false;
|
||||
}
|
||||
|
||||
/** Check if two versions share the same major.minor (e.g. 1.0.x). */
|
||||
function isSameMinor(current: string, latest: string): boolean {
|
||||
const c = parseSemver(current);
|
||||
const l = parseSemver(latest);
|
||||
return c[0] === l[0] && c[1] === l[1];
|
||||
}
|
||||
|
||||
// ── Failure Backoff ──────────────────────────────────────────────────────────
|
||||
|
||||
function isUpdateBackedOff(): boolean {
|
||||
|
|
@ -167,6 +176,24 @@ function printUpdateBanner(latestVersion: string): void {
|
|||
console.error();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a non-blocking update notice without auto-installing.
|
||||
* Users can update manually with `spawn update` or set SPAWN_AUTO_UPDATE=1.
|
||||
*/
|
||||
function printUpdateNotice(latestVersion: string): void {
|
||||
console.error();
|
||||
console.error(
|
||||
pc.yellow(" Update available: ") +
|
||||
pc.dim(`v${VERSION}`) +
|
||||
pc.yellow(" → ") +
|
||||
pc.green(pc.bold(`v${latestVersion}`)),
|
||||
);
|
||||
console.error(
|
||||
pc.dim(` Run ${pc.cyan("spawn update")} to install, or set SPAWN_AUTO_UPDATE=1 for automatic updates`),
|
||||
);
|
||||
console.error();
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the spawn binary to re-exec after an update.
|
||||
*
|
||||
|
|
@ -362,12 +389,22 @@ export async function checkForUpdates(jsonOutput = false): Promise<void> {
|
|||
// Record successful check so we don't hit the network again for an hour
|
||||
markUpdateChecked();
|
||||
|
||||
// Auto-update if newer version is available
|
||||
// Notify if newer version is available
|
||||
if (compareVersions(VERSION, latestVersion)) {
|
||||
const r = tryCatch(() => performAutoUpdate(latestVersion, jsonOutput));
|
||||
if (!r.ok) {
|
||||
logWarn("Auto-update encountered an error");
|
||||
logDebug(getErrorMessage(r.error));
|
||||
// Only auto-update within the same major.minor (patch updates only).
|
||||
// e.g. 1.0.0 → 1.0.5 is allowed, 1.0.0 → 1.1.0 is not.
|
||||
const patchOnly = isSameMinor(VERSION, latestVersion);
|
||||
|
||||
if (patchOnly && process.env.SPAWN_AUTO_UPDATE === "1") {
|
||||
// Opt-in auto-update for patch versions
|
||||
const r = tryCatch(() => performAutoUpdate(latestVersion, jsonOutput));
|
||||
if (!r.ok) {
|
||||
logWarn("Auto-update encountered an error");
|
||||
logDebug(getErrorMessage(r.error));
|
||||
}
|
||||
} else {
|
||||
// Show notice: either auto-update is off, or it's a minor/major bump
|
||||
printUpdateNotice(latestVersion);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue