Skip to content

Pi repeatedly runs pnpm install -g for npm packages in settings.json on every startup with pnpm 11 #4501

@ianbmacdonald

Description

@ianbmacdonald

What happened?

After migrating to pnpm 11, launching pi repeatedly runs global pnpm install operations for every package declared in ~/.pi/agent/settings.json under packages.

The packages are already installed according to pnpm list -g --depth 0, but Pi re-runs the package bootstrap on every startup.

settings.json

{
  "lastChangelogVersion": "0.74.0",
  "packages": [
    "npm:pi-web-access",
    "npm:@plannotator/pi-extension",
    "npm:pi-subagents",
    "npm:pi-lens",
    "npm:pi-tool-display",
    "npm:pi-powerline-footer"
  ],
  "npmCommand": ["pnpm"]
}

Startup output

On each pi launch, Pi runs global installs sequentially:

Packages: +21
Progress: resolved 21, reused 21, downloaded 0, added 0, done

global:
+ pi-web-access 0.10.7

Done in 990ms using pnpm v11.1.1

Packages: +297
Progress: resolved 306, reused 297, downloaded 0, added 0, done

global:
+ @plannotator/pi-extension 0.19.14

Done in 2s using pnpm v11.1.1

Packages: +3
Progress: resolved 3, reused 3, downloaded 0, added 0, done

global:
+ pi-subagents 0.24.2

Done in 468ms using pnpm v11.1.1

Packages: +247
Progress: resolved 264, reused 247, downloaded 0, added 0, done

global:
+ pi-lens 3.8.43

Done in 1.7s using pnpm v11.1.1

Packages: +244
Progress: resolved 253, reused 244, downloaded 0, added 0, done

global:
+ pi-tool-display 0.3.6

Done in 1.2s using pnpm v11.1.1

Packages: +242
Progress: resolved 251, reused 242, downloaded 0, added 0, done

global:
+ pi-powerline-footer 0.5.4

Done in 1.1s using pnpm v11.1.1

Total: ~7.5s of no-op package resolution on every startup.

Already installed globally

$ pnpm list -g --depth 0

/home/imac/.local/share/pnpm/global/v11 (PRIVATE)
dependencies:
@earendil-works/pi-coding-agent@0.74.0
@plannotator/pi-extension@0.19.14
pi-lens@3.8.43
pi-powerline-footer@0.5.4
pi-subagents@0.24.2
pi-tool-display@0.3.6
pi-web-access@0.10.7

Root cause

Pi's package-manager.tsgetNpmInstallPath() joins getGlobalNpmRoot() (which runs pnpm root -g) with source.name, then resolvePackageSources() checks existsSync(installedPath).

With pnpm 11, pnpm root -g returns:

/home/imac/.local/share/pnpm/global/v11

Pi constructs and checks:

/home/imac/.local/share/pnpm/global/v11/pi-web-access  ← does not exist

But pnpm 11 stores packages in content-hashed directories:

/home/imac/.local/share/pnpm/global/v11/10c947-19e23d3dc9b/node_modules/pi-web-access

So existsSync() always returns falseneedsInstall = truepnpm install -g fires for every package on every startup.

The global/v11/ directory contains hashed dirs and symlinks, not a flat node_modules/<name> tree:

/home/imac/.local/share/pnpm/global/v11/
├── 0108ced76e89ba0de26e5dc78c03480eecf6f536153b5e5951240be7397a7b68 -> 10c95a-19e23d3e0ac
├── 10c947-19e23d3dc9b/
├── 10c95a-19e23d3e0ac/
├── 10c96e-19e23d3e91c/
├── 10c980-19e23d3eb34/
├── 10c9a6-19e23d3f262/
├── 10c9c2-19e23d3f77a/
├── pnpm-workspace.yaml
└── ... (symlinks to hashed dirs)

Each hashed dir contains its own node_modules/ with the package inside — there is no top-level node_modules/ flat tree that the npm-style path construction expects.

Environment

pi: 0.74.0
node: v24.15.0
pnpm: 11.1.1
pnpm bin -g: /home/imac/.local/share/pnpm/bin
pnpm root -g: /home/imac/.local/share/pnpm/global/v11
shell: bash
OS: Ubuntu/Linux

Expected behavior

If the npm packages listed in settings.json.packages are already installed globally and resolvable, Pi should not run pnpm install -g for each package on every startup.

Possible fix

getGlobalNpmRoot() / getNpmInstallPath() could use pnpm list -g --json or require.resolve() to locate an already-installed package instead of constructing an npm-style path that does not exist in pnpm 11's layout. This is similar to how the bun path was fixed in #3809 — the global root resolution logic needs a pnpm-specific branch that accounts for pnpm 11's content-addressable global layout.

Related issues

Metadata

Metadata

Assignees

Labels

closed-because-refactorClosed while the project refactor is in progressinprogressIssue is being worked on

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions