fix(coding-agent): skip redundant npm package updates

closes #3000
This commit is contained in:
Mario Zechner
2026-04-20 19:12:52 +02:00
parent 3054fd7a3b
commit b73212616d
3 changed files with 44 additions and 1 deletions

View File

@@ -9,6 +9,7 @@
### Fixed
- Fixed `pi update` reinstalling npm packages that are already at the latest published version by checking the installed package version before running `npm install <pkg>@latest` ([#3000](https://github.com/badlogic/pi-mono/issues/3000))
- Fixed built-in tool wrapping to use the same extension-runner context path as extension tools, so built-in tools receive execution context and `read` can warn when the current model does not support images ([#3429](https://github.com/badlogic/pi-mono/issues/3429))
- Fixed threaded `/resume` session relationships and current-session detection to canonicalize symlinked session paths during selector comparisons, so shared session directories no longer break parent-child matching or active-session delete protection ([#3364](https://github.com/badlogic/pi-mono/issues/3364))
- Fixed `/session`, Sessions docs, and CLI help to consistently document that session reuse supports both file paths and session IDs, and that `/session` shows the current session ID ([#3390](https://github.com/badlogic/pi-mono/issues/3390))

View File

@@ -1001,6 +1001,20 @@ export class DefaultPackageManager implements PackageManager {
const parsed = this.parseSource(source);
if (parsed.type === "npm") {
if (parsed.pinned) return;
const installedPath = this.getNpmInstallPath(parsed, scope);
const installedVersion = existsSync(installedPath) ? this.getInstalledNpmVersion(installedPath) : undefined;
if (installedVersion) {
try {
const latestVersion = await this.getLatestNpmVersion(parsed.name);
if (latestVersion === installedVersion) {
return;
}
} catch {
// Preserve existing update behavior when version lookup fails.
}
}
await this.withProgress("update", source, `Updating ${source}...`, async () => {
await this.installNpm(
{

View File

@@ -1455,13 +1455,22 @@ export default function(api) { api.registerTool({ name: "test", description: "te
});
describe("offline mode and network timeouts", () => {
it("should update project npm packages using @latest", async () => {
it("should update project npm packages using @latest when newer version is available", async () => {
const installedPath = join(tempDir, ".pi", "npm", "node_modules", "example");
mkdirSync(installedPath, { recursive: true });
writeFileSync(join(installedPath, "package.json"), JSON.stringify({ name: "example", version: "1.0.0" }));
settingsManager.setProjectPackages(["npm:example"]);
const runCommandCaptureSpy = vi.spyOn(packageManager as any, "runCommandCapture").mockResolvedValue('"1.2.3"');
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).toHaveBeenCalledWith(
"npm",
["install", "example@latest", "--prefix", join(tempDir, ".pi", "npm")],
@@ -1469,6 +1478,25 @@ export default function(api) { api.registerTool({ name: "test", description: "te
);
});
it("should skip project npm update when installed version matches latest", async () => {
const installedPath = join(tempDir, ".pi", "npm", "node_modules", "example");
mkdirSync(installedPath, { recursive: true });
writeFileSync(join(installedPath, "package.json"), JSON.stringify({ name: "example", version: "1.2.3" }));
settingsManager.setProjectPackages(["npm:example"]);
const runCommandCaptureSpy = vi.spyOn(packageManager as any, "runCommandCapture").mockResolvedValue('"1.2.3"');
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 suggest npm source prefixes for update lookups", async () => {
settingsManager.setProjectPackages(["npm:example"]);