diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 2bc8dcd9f..c7ae064a3 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Fixed managed npm extension updates to avoid package managers installing or resolving pi host packages as peer dependencies ([#4907](https://github.com/earendil-works/pi/issues/4907)). + ## [0.75.5] - 2026-05-23 ### New Features diff --git a/packages/coding-agent/src/core/package-manager.ts b/packages/coding-agent/src/core/package-manager.ts index 06933bb27..1d7dc27d3 100644 --- a/packages/coding-agent/src/core/package-manager.ts +++ b/packages/coding-agent/src/core/package-manager.ts @@ -1706,13 +1706,25 @@ export class DefaultPackageManager implements PackageManager { private getNpmInstallArgs(specs: string[], installRoot: string): string[] { const packageManagerName = this.getPackageManagerName(); + // Extension packages run inside pi and resolve pi APIs through loader aliases/virtual modules. + // Disable peer dependency resolution for managed installs (npm's --legacy-peer-deps, and + // equivalent bun/pnpm settings) so package managers do not install or solve host-provided + // @earendil-works/pi-* peers. Stale auto-installed pi peers can otherwise block updates. if (packageManagerName === "bun") { - return ["install", ...specs, "--cwd", installRoot]; + return ["install", ...specs, "--cwd", installRoot, "--omit=peer"]; } if (packageManagerName === "pnpm") { - return ["install", ...specs, "--prefix", installRoot, "--config.strict-dep-builds=false"]; + return [ + "install", + ...specs, + "--prefix", + installRoot, + "--config.auto-install-peers=false", + "--config.strict-peer-dependencies=false", + "--config.strict-dep-builds=false", + ]; } - return ["install", ...specs, "--prefix", installRoot]; + return ["install", ...specs, "--prefix", installRoot, "--legacy-peer-deps"]; } private async installNpm(source: NpmSource, scope: SourceScope, temporary: boolean): Promise { diff --git a/packages/coding-agent/test/package-manager.test.ts b/packages/coding-agent/test/package-manager.test.ts index b464ca7d6..af3bc99d3 100644 --- a/packages/coding-agent/test/package-manager.test.ts +++ b/packages/coding-agent/test/package-manager.test.ts @@ -693,7 +693,17 @@ Content`, expect(runCommandSpy).toHaveBeenCalledWith( "mise", - ["exec", "node@20", "--", "npm", "install", "@scope/pkg", "--prefix", join(agentDir, "npm")], + [ + "exec", + "node@20", + "--", + "npm", + "install", + "@scope/pkg", + "--prefix", + join(agentDir, "npm"), + "--legacy-peer-deps", + ], undefined, ); }); @@ -714,7 +724,7 @@ Content`, expect(runCommandSpy).toHaveBeenCalledWith( "mise", - ["exec", "bun@1", "--", "bun", "install", "@scope/pkg", "--cwd", join(agentDir, "npm")], + ["exec", "bun@1", "--", "bun", "install", "@scope/pkg", "--cwd", join(agentDir, "npm"), "--omit=peer"], undefined, ); }); @@ -953,6 +963,8 @@ Content`, "pnpm-pkg", "--prefix", join(agentDir, "npm"), + "--config.auto-install-peers=false", + "--config.strict-peer-dependencies=false", "--config.strict-dep-builds=false", ]); mkdirSync(join(packagePath, "extensions"), { recursive: true }); @@ -2005,7 +2017,7 @@ export default function(api) { api.registerTool({ name: "test", description: "te ); expect(runCommandSpy).toHaveBeenCalledWith( "npm", - ["install", "example@latest", "--prefix", join(tempDir, ".pi", "npm")], + ["install", "example@latest", "--prefix", join(tempDir, ".pi", "npm"), "--legacy-peer-deps"], undefined, ); }); @@ -2044,7 +2056,13 @@ export default function(api) { api.registerTool({ name: "test", description: "te .mockImplementation(async (...callArgs: unknown[]) => { const [command, args] = callArgs as [string, string[]]; expect(command).toBe("npm"); - expect(args).toEqual(["install", "legacy-pkg@latest", "--prefix", join(agentDir, "npm")]); + expect(args).toEqual([ + "install", + "legacy-pkg@latest", + "--prefix", + join(agentDir, "npm"), + "--legacy-peer-deps", + ]); mkdirSync(managedPath, { recursive: true }); writeFileSync( join(managedPath, "package.json"), @@ -2154,13 +2172,27 @@ export default function(api) { api.registerTool({ name: "test", description: "te expect(runCommandSpy).toHaveBeenNthCalledWith( 1, "npm", - ["install", "user-old@latest", "user-unknown@latest", "--prefix", join(agentDir, "npm")], + [ + "install", + "user-old@latest", + "user-unknown@latest", + "--prefix", + join(agentDir, "npm"), + "--legacy-peer-deps", + ], undefined, ); expect(runCommandSpy).toHaveBeenNthCalledWith( 2, "npm", - ["install", "project-old@latest", "project-missing@latest", "--prefix", join(tempDir, ".pi", "npm")], + [ + "install", + "project-old@latest", + "project-missing@latest", + "--prefix", + join(tempDir, ".pi", "npm"), + "--legacy-peer-deps", + ], undefined, ); expect(updateGitSpy).toHaveBeenCalledTimes(4);