diff --git a/packages/cli/package.json b/packages/cli/package.json index 55fe52c2..3a2c15cc 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@openrouter/spawn", - "version": "1.1.1", + "version": "1.0.20", "type": "module", "bin": { "spawn": "cli.js" diff --git a/packages/cli/src/__tests__/update-check.test.ts b/packages/cli/src/__tests__/update-check.test.ts index 926a81b4..5dee0536 100644 --- a/packages/cli/src/__tests__/update-check.test.ts +++ b/packages/cli/src/__tests__/update-check.test.ts @@ -486,9 +486,9 @@ describe("update-check", () => { // - SPAWN_NO_AUTO_UPDATE=1 suppresses auto-install entirely describe("update policy", () => { it("auto-installs patch bumps even without SPAWN_AUTO_UPDATE=1", async () => { - // 1.1.0 -> 1.1.99 is a patch bump (same major.minor) + // 1.0.20 -> 1.0.99 is a patch bump (same major.minor) process.env.SPAWN_AUTO_UPDATE = undefined; - const fetchSpy = spyOn(global, "fetch").mockImplementation(() => Promise.resolve(new Response("1.1.99\n"))); + const fetchSpy = spyOn(global, "fetch").mockImplementation(() => Promise.resolve(new Response("1.0.99\n"))); const { executor } = await import("../update-check.js"); const execFileSyncSpy = spyOn(executor, "execFileSync").mockImplementation((file: string) => Buffer.from(file === "curl" ? FAKE_INSTALL_SCRIPT : ""), @@ -507,8 +507,8 @@ describe("update-check", () => { execFileSyncSpy.mockRestore(); }); - it("shows notice only for minor bumps without SPAWN_AUTO_UPDATE=1", async () => { - // 1.1.0 -> 1.2.0 is a minor bump + it("auto-installs minor bumps (same major)", async () => { + // 1.0.20 -> 1.2.0 is a minor bump — should auto-install process.env.SPAWN_AUTO_UPDATE = undefined; const fetchSpy = spyOn(global, "fetch").mockImplementation(() => Promise.resolve(new Response("1.2.0\n"))); const { executor } = await import("../update-check.js"); @@ -519,20 +519,15 @@ describe("update-check", () => { const { checkForUpdates } = await import("../update-check.js"); await checkForUpdates(); - const output = consoleErrorSpy.mock.calls.map((call: unknown[]) => call[0]).join("\n"); - // Notice should mention the version jump - expect(output).toContain("Update available"); - expect(output).toContain("1.2.0"); - // Must NOT auto-install — no curl, no bash, no re-exec - expect(execFileSyncSpy).not.toHaveBeenCalled(); - expect(processExitSpy).not.toHaveBeenCalled(); + // Should auto-install: curl to fetch script, bash to run it, which + re-exec + expect(execFileSyncSpy).toHaveBeenCalled(); fetchSpy.mockRestore(); execFileSyncSpy.mockRestore(); }); it("shows notice only for major bumps without SPAWN_AUTO_UPDATE=1", async () => { - // 1.1.0 -> 2.0.0 is a major bump + // 1.0.20 -> 2.0.0 is a major bump — should NOT auto-install process.env.SPAWN_AUTO_UPDATE = undefined; const fetchSpy = spyOn(global, "fetch").mockImplementation(() => Promise.resolve(new Response("2.0.0\n"))); const { executor } = await import("../update-check.js"); @@ -550,8 +545,8 @@ describe("update-check", () => { execFileSyncSpy.mockRestore(); }); - it("auto-installs minor bumps WITH SPAWN_AUTO_UPDATE=1", async () => { - // 1.1.0 -> 1.2.0 with opt-in env var + it("auto-installs major bumps WITH SPAWN_AUTO_UPDATE=1", async () => { + // 1.0.20 -> 1.2.0 with opt-in env var process.env.SPAWN_AUTO_UPDATE = "1"; const fetchSpy = spyOn(global, "fetch").mockImplementation(() => Promise.resolve(new Response("1.2.0\n"))); const { executor } = await import("../update-check.js"); diff --git a/packages/cli/src/update-check.ts b/packages/cli/src/update-check.ts index 13a6ec04..1345a784 100644 --- a/packages/cli/src/update-check.ts +++ b/packages/cli/src/update-check.ts @@ -89,13 +89,6 @@ 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 { @@ -439,25 +432,22 @@ export async function checkForUpdates(jsonOutput = false): Promise { // Notify (or auto-install) if a newer version is available. if (compareVersions(VERSION, latestVersion)) { - // Update policy, semver-aligned: + // Update policy: // - // PATCH bumps (same major.minor, e.g. 1.0.5 → 1.0.7) are always - // auto-installed. Patches are reserved for bug fixes and security - // hardening — users benefit from getting them without opting in, and - // the blast radius is bounded by semver: no behavior changes, no - // breaking changes, no new features. + // PATCH and MINOR bumps (e.g. 1.0.5 → 1.0.7, 1.0.x → 1.1.0) are + // auto-installed. These contain bug fixes, security hardening, and + // new features that users benefit from getting promptly. // - // MINOR / MAJOR bumps (e.g. 1.0.x → 1.1.0, 1.x.x → 2.0.0) respect - // SPAWN_AUTO_UPDATE=1 as opt-in. These can contain behavior changes - // and users should decide when to move to them. + // MAJOR bumps (e.g. 1.x.x → 2.0.0) respect SPAWN_AUTO_UPDATE=1 + // as opt-in, since these can contain breaking changes. // - // SPAWN_NO_AUTO_UPDATE=1 lets users opt OUT of patch-level auto-update - // entirely if they need a fully pinned CLI (CI environments, etc.). - const patchOnly = isSameMinor(VERSION, latestVersion); + // SPAWN_NO_AUTO_UPDATE=1 lets users opt OUT of auto-update entirely + // if they need a fully pinned CLI (CI environments, etc.). + const sameMajor = parseSemver(VERSION)[0] === parseSemver(latestVersion)[0]; const explicitOptOut = process.env.SPAWN_NO_AUTO_UPDATE === "1"; const explicitOptIn = process.env.SPAWN_AUTO_UPDATE === "1"; - const shouldAutoInstall = !explicitOptOut && (patchOnly || explicitOptIn); + const shouldAutoInstall = !explicitOptOut && (sameMajor || explicitOptIn); if (shouldAutoInstall) { const r = tryCatch(() => performAutoUpdate(latestVersion, jsonOutput)); @@ -466,7 +456,7 @@ export async function checkForUpdates(jsonOutput = false): Promise { logDebug(getErrorMessage(r.error)); } } else { - // Minor/major bump without opt-in, or explicit opt-out — show notice. + // Major bump without opt-in, or explicit opt-out — show notice. printUpdateNotice(latestVersion); } }