Skip to main content

Add A Global pnpm Package

Global pnpm packages are managed via a plain list. The sync command installs missing packages, removes unmanaged packages, then runs pnpm update -g --latest for every unpinned package so installed packages are refreshed instead of staying within the semver range recorded at install time.

Preconditions

  • Node.js and pnpm are installed (both through mise in this setup; ~/.config/mise/config.toml pins the latest pnpm release for global use).
  • ~/.local/share/pnpm/bin is on PATH (the shell configs add it; the installer adds it for itself).
  • You verified the package name.

Steps

Example packages managed this way include @earendil-works/pi-coding-agent.

  1. Add the package name to:

    This file is installed as ~/.default-pnpm-pkgs. name@x.y.z pins an exact version; bare names track latest.

  2. Apply dotfiles (which triggers the hook):

    chezmoi apply

Or run the installer command directly:

Verification

pnpm ls -g --depth 0 | rg '<package-name>'

What It Does

The installer reads ~/.default-pnpm-pkgs, resolves pnpm root -g, and merges every exact desired package name into <pnpm root -g>/pnpm-workspace.yaml with allowBuilds[<name>] set to true before reading inventory or changing packages. Pins reduce to the bare name. Unrelated approval, denial, and placeholder entries remain unchanged, and this only approves listed top-level packages; it does not approve third-party transitive packages. This durable workspace setting means direct pnpm -g update commands inherit the same build decisions after a sync.

It then compares the desired list with pnpm ls -g --json, installs missing packages (pnpm add -g), re-pins packages whose installed version differs from an exact pin, uninstalls global packages not on the list (pnpm remove -g), then runs pnpm update -g --latest for each unpinned package.

pnpm 11+ installs every global package into its own hashed project directory under ~/.local/share/pnpm/global/v11/, and that directory moves on every add or update. Consumers that need a stable module path (Pi's packages setting loads pi-mcp-adapter and pi-subagents by path) read ~/.local/share/pnpm-global-links/node_modules/<package> instead; the installer rebuilds that symlink tree after every sync.

Package operations pass --yes and disconnect stdin so pnpm does not prompt during a chezmoi run, including when launched from a terminal. Unapproved dependency build scripts remain ignored.

For installs and updates of listed packages, the installer passes PNPM_CONFIG_BLOCK_EXOTIC_SUBDEPS=false to that pnpm subprocess. This permits URL/git dependencies throughout those packages' dependency trees, including the pkg.pr.new dependencies used by pi-mcp-adapter. The override is not written to pnpm configuration and does not affect removal, inventory, build approvals, release-age rules, or ordinary pnpm commands outside the installer.

If a package operation fails, the installer prints pnpm's error and stops with a nonzero exit status. It refreshes links from the installed state even after a partial sync. If that state cannot be read or parsed, it preserves the existing link tree and reports the failure.

Release-Age Quarantine

pnpm 11+ ships a built-in minimumReleaseAge of 1440 minutes: pnpm update -g --latest and pnpm outdated -g silently skip any version published less than a day ago, so ,update can report success while omp update (which reads the latest dist-tag directly) already sees a newer release.

home/dot_config/pnpm/readonly_config.yaml.tmpl renders ~/.config/pnpm/config.yaml with minimumReleaseAgeExclude covering every package in ~/.default-pnpm-pkgs (pins reduce to the bare name), so listed packages update as soon as they publish. Scoped packages are listed as @scope/*, not by exact name: pnpm's non-strict default auto-approves same-day transitive versions into the global project's own pnpm-workspace.yaml, and that project list replaces the global one, which would re-quarantine the top-level package on the next sync. Same-scope companions (for example the 17 @oh-my-pi/* packages behind omp) are the case that triggers this on every release, so the scope glob keeps the global list in effect. Unscoped packages keep exact names, so a same-day third-party transitive release can still trigger that write. The 05 hook hashes the config template, so changing exclusions re-runs the sync.

Check the effective list with:

pnpm config get --global minimum-release-age-exclude

Rollback / Undo

  1. Remove the package from home/readonly_dot_default-pnpm-pkgs.
  2. Re-apply:
chezmoi apply

(The package will be automatically uninstalled because it is no longer in the desired list).