diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 0fd0c7313..e59f80e56 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -4,6 +4,7 @@ ### Fixed +- Fixed npm package update checks treating older registry versions as available updates, preventing `pi update` from downgrading already-newer installed packages ([#8226](https://github.com/earendil-works/pi/issues/8226)). - Fixed built-in llama.cpp models disappearing from `/model` when `/llama` refreshed a configured server under `PI_OFFLINE`, and included idle-slept `sleeping` router models in the selectable catalog ([#8167](https://github.com/earendil-works/pi/issues/8167)). - Fixed `pi.registerFlag()` accepting default values that do not match the declared flag type ([#8064](https://github.com/earendil-works/pi/issues/8064)). - Fixed Z.AI Coding Plan defaults referencing the removed GLM-5.1 model ([#8096](https://github.com/earendil-works/pi/issues/8096)). diff --git a/packages/coding-agent/src/core/package-manager.ts b/packages/coding-agent/src/core/package-manager.ts index e221d2e7e..c2e9e73d5 100644 --- a/packages/coding-agent/src/core/package-manager.ts +++ b/packages/coding-agent/src/core/package-manager.ts @@ -27,7 +27,7 @@ import type { Readable } from "node:stream"; import { globSync } from "glob"; import ignore from "ignore"; import { minimatch } from "minimatch"; -import { maxSatisfying, rcompare, satisfies, valid, validRange } from "semver"; +import { gt, maxSatisfying, rcompare, satisfies, valid, validRange } from "semver"; import { CONFIG_DIR_NAME } from "../config.ts"; import { spawnProcess, spawnProcessSync } from "../utils/child-process.ts"; import { type GitSource, parseGitUrl } from "../utils/git.ts"; @@ -1129,7 +1129,7 @@ export class DefaultPackageManager implements PackageManager { try { const targetVersion = await this.getLatestNpmVersion(source.version ? source.spec : source.name, source.range); - return targetVersion !== installedVersion; + return gt(targetVersion, installedVersion); } catch { // Preserve existing update behavior when version lookup fails. return true; @@ -1463,7 +1463,7 @@ export class DefaultPackageManager implements PackageManager { try { const targetVersion = await this.getLatestNpmVersion(source.version ? source.spec : source.name, source.range); - return targetVersion !== installedVersion; + return gt(targetVersion, installedVersion); } catch { return false; } diff --git a/packages/coding-agent/test/package-manager.test.ts b/packages/coding-agent/test/package-manager.test.ts index 31c89faff..86303aab9 100644 --- a/packages/coding-agent/test/package-manager.test.ts +++ b/packages/coding-agent/test/package-manager.test.ts @@ -2244,6 +2244,25 @@ export default function(api) { api.registerTool({ name: "test", description: "te expect(runCommandSpy).not.toHaveBeenCalled(); }); + it("should skip npm updates when the installed version is newer than the registry version", async () => { + const installedPath = join(tempDir, ".pi", "npm", "node_modules", "example"); + mkdirSync(installedPath, { recursive: true }); + writeFileSync(join(installedPath, "package.json"), JSON.stringify({ name: "example", version: "2.0.0" })); + settingsManager.setProjectPackages(["npm:example"]); + + const runCommandCaptureSpy = vi.spyOn(packageManager as any, "runCommandCapture").mockResolvedValue('"1.9.0"'); + const runCommandSpy = vi.spyOn(packageManager as any, "runCommand").mockResolvedValue(undefined); + + await packageManager.update("npm:example"); + + expect(runCommandCaptureSpy).toHaveBeenCalledWith( + "npm", + ["view", "example", "version", "--json"], + expect.objectContaining({ cwd: tempDir, timeoutMs: expect.any(Number) }), + ); + expect(runCommandSpy).not.toHaveBeenCalled(); + }); + it("should migrate legacy user npm installs into the managed npm root during update", async () => { const legacyRoot = join(tempDir, "legacy-global", "node_modules"); const legacyPath = join(legacyRoot, "legacy-pkg"); @@ -2504,6 +2523,18 @@ export default function(api) { api.registerTool({ name: "test", description: "te ]); }); + it("should not report npm updates when the installed version is newer than the registry version", async () => { + const installedPath = join(tempDir, ".pi", "npm", "node_modules", "example"); + mkdirSync(installedPath, { recursive: true }); + writeFileSync(join(installedPath, "package.json"), JSON.stringify({ name: "example", version: "2.0.0" })); + settingsManager.setProjectPackages(["npm:example"]); + + vi.spyOn(packageManager as any, "runCommandCapture").mockResolvedValue('"1.9.0"'); + + const updates = await packageManager.checkForAvailableUpdates(); + expect(updates).toEqual([]); + }); + it("should skip pinned packages when checking for updates", async () => { const installedNpmPath = join(tempDir, ".pi", "npm", "node_modules", "example"); mkdirSync(installedNpmPath, { recursive: true });