fix(coding-agent): name the package manager in pi update only when it owns the install (#11628)

Fixes #7756.

Problem: `detectInstallMethod()` classifies any path containing `/pnpm/`
as pnpm. An install that only lives under `PNPM_HOME`, for example a
wrapper under `$PNPM_HOME/global-nub/5/node_modules/...`, is detected as
pnpm. `pi update` then correctly refuses to self-update (the path is not
under `pnpm root -g`), but `getSelfUpdateUnavailableInstruction()`
printed:

```
This installation is not managed by a global pnpm install. Update it with the package manager, wrapper, or source checkout that provides it.
```

Fix: name the package manager only when it owns the install (managed but
not writable). All other cases fall through to the neutral message:

```
Update @earendil-works/pi-coding-agent using the package manager, wrapper, or source checkout that provides this installation.
```

`detectInstallMethod()` is unchanged: every user-facing path already
goes through the ownership check, and returning `unknown` would trip the
Windows "only supported for npm and pnpm" guard with a worse message.
The update banner already says `Run pi update` and no longer suggests
`pnpm install -g`.

The pi.dev installer is not affected: it verifies npm ownership with
`npm ls -g` and never infers pnpm from paths.

Tests: regression test in `test/config.test.ts` (fails without the fix).
`npm run check` passes.

This PR is AI-generated.
This commit is contained in:
Petr Houška 2026-10-30 21:23:10 +01:00 • committed by GitHub
parent a14cdf90f0
commit c2760d32cc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 32 additions and 5 deletions

View file

@ -39,6 +39,7 @@
- Fixed Windows paths being compared case-sensitively. When the working directory and home directory differed only in drive-letter casing (`c:\Users\me` vs `C:\Users\me`), skills in `~/.agents/skills` loaded twice, as project and user skills, and were reported as name collisions. The same mismatch also re-asked for saved project trust, hid sessions in a custom session directory from `--continue` and `/resume`, and stopped `pi remove` from matching configured local packages ([#10488](https://github.com/earendil-works/pi/issues/10488))
- Fixed paths next to the home directory being shown with a broken `~` prefix, for example `/home/me2/project` shown as `~2/project` when home is `/home/me`
- Fixed `pi mcp login -l` and `pi mcp logout -l` failing with `Unknown option --local.` although `pi mcp add` and `pi mcp remove` accept `-l`. With `-l`, they now require the server to be a project server. Unknown options are now reported as typed, and a mistyped server name gets a suggestion, for example `Did you mean "gurker"?`
- Fixed `pi update` naming pnpm for installs that only live under `PNPM_HOME`, for example a wrapper under `$PNPM_HOME/global-nub/`. pi said "not managed by a global pnpm install"; it now names the package manager only when that manager owns the install ([#7756](https://github.com/earendil-works/pi/issues/7756))
## [1.4.0] - 2026-10-21

View file

@ -345,11 +345,10 @@ export function getSelfUpdateUnavailableInstruction(
return `Download from: https://github.com/earendil-works/pi/releases/latest`;
}
const command = getSelfUpdateCommandForMethod(method, packageName, target, npmCommand);
if (command) {
if (isManagedByGlobalPackageManager(method, packageName, npmCommand) && !isSelfUpdatePathWritable()) {
return `This installation is managed by a global ${method} install, but the install path is not writable. Update it yourself with: ${command.display}`;
}
return `This installation is not managed by a global ${method} install. Update it with the package manager, wrapper, or source checkout that provides it.`;
// Path-based detection also matches installs that only live under a package manager's directory,
// such as a wrapper under PNPM_HOME, so name the package manager only when it owns the install.
if (command && isManagedByGlobalPackageManager(method, packageName, npmCommand) && !isSelfUpdatePathWritable()) {
return `This installation is managed by a global ${method} install, but the install path is not writable. Update it yourself with: ${command.display}`;
}
return `Update ${target.installSpec} using the package manager, wrapper, or source checkout that provides this installation.`;
}

View file

@ -182,6 +182,33 @@ describe("detectInstallMethod", () => {
);
});
// Issue #7756
test("does not name pnpm for unmanaged installs under PNPM_HOME", () => {
tempDir = mkdtempSync(join(tmpdir(), "pi-pnpm-home-"));
const packageDir = join(
tempDir,
".local",
"share",
"pnpm",
"global-nub",
"5",
"node_modules",
"@earendil-works",
"pi-coding-agent",
);
mkdirSync(packageDir, { recursive: true });
process.env.PI_PACKAGE_DIR = packageDir;
process.env.PATH = "";
process.argv[1] = join(packageDir, "dist", "cli.js");
setExecPath(join(packageDir, "dist", "cli.js"));
expect(detectInstallMethod()).toBe("pnpm");
expect(getSelfUpdateCommand("@earendil-works/pi-coding-agent")).toBeUndefined();
expect(getSelfUpdateUnavailableInstruction("@earendil-works/pi-coding-agent")).toBe(
"Update @earendil-works/pi-coding-agent using the package manager, wrapper, or source checkout that provides this installation.",
);
});
test("self-updates npm installs from custom prefixes", () => {
const { prefix } = createNpmPrefixInstall();