Skip to content

Redirect pnpm, bun, deno, and gem global installs to ~/.local per the inbox#584 decision - #659

Merged
mitodrummer merged 13 commits into
mainfrom
pnpm-stack-global-bin
Oct 2, 2026
Merged

mitodrummer merged 13 commits into
mainfrom
pnpm-stack-global-bin

Conversation

@agent-137

@agent-137 agent-137 commented Aug 28, 2026 •

Copy link
Copy Markdown
Member

Implements the decision recorded in gominimal/inbox#584 for four ecosystems — follow-up to gominimal/inbox#559, whose npm half is #657: global installs redirect to ~/.local via each tool's narrowest honored knob, delivered at the package level. The failure modes below were observed in a live session.

What breaks today

Tool Failure in a session
pnpm refuses: global bin directory ".../pnpm/bin" is not in PATH (PATH is fixed; pnpm setup has nothing to edit)
bun bun add -g succeeds into a cache-derived dir whose bin is off PATH — binaries never resolve
deno deno install -g targets ~/.deno/bin, off PATH; deno's own hint ("Add ... to PATH") is impossible to follow
gem auto-falls back to user install (good), but binstubs land in ~/.local/share/gem/ruby/<v>/bin, off PATH

The fixes

  • pnpm — /usr/bin wrappers default PNPM_CONFIG_GLOBAL_BIN_DIR to $HOME/.local/bin. pnpm 12.5.1 reads it from the env (crates/config/src/env_overlay.rs) and uses it in place of <pnpm home>/bin (derive_layout.rs), so only the bin dir moves; the store and global package dir stay at pnpm's defaults. A caller's own PNPM_CONFIG_GLOBAL_BIN_DIR or PNPM_HOME wins.
  • bun — wrappers default BUN_INSTALL_BIN=$HOME/.local/bin and BUN_INSTALL_GLOBAL_DIR=$HOME/.local/share/bun/global (bun 1.3.14 PackageManagerOptions.zig). Both are needed: without the second, the global dir falls back under $XDG_CACHE_HOME and clearing the cache leaves the ~/.local/bin links dangling. A caller's value of either, or a caller's BUN_INSTALL, wins.
  • deno — wrapper defaults DENO_INSTALL_ROOT=$HOME/.local; deno installs into <root>/bin.
  • All three wrappers leave the variables alone when HOME is unset, and exec the real entry point from /usr/libexec/<pkg>/.
  • gem — no wrapper needed: RubyGems' system config path is /usr/etc/gemrc, inside the package, so the ruby package ships one with install:, update: and uninstall: keys each set to --bindir ~/.local/bin. Per-command keys rather than a gem: line because RubyGems merges config files key by key, so a user's own gem: --no-document would have dropped the bindir. GEMRC/~/.gemrc/CLI still override. Bundler ignores these keys, so bundle install executables stay off PATH (out of scope).
  • Env overrides outrank config files for both pnpm and bun, so a globalBinDir in pnpm's config or globalBinDir/globalDir in bunfig is overridden by the wrapper default; setting the env var (or PNPM_HOME/BUN_INSTALL) is the way to choose another location.
  • Every package gains a regression test that performs a real global install offline (see Verification); bun/deno libexec outputs carry allow_executable = true (a Data output matching a binary fails min check without it), and their standalone smoketests target the raw ELFs since standaloneTest runs without a shell.
  • bun's bunx stays an argv0 symlink beside the real binary, preserving name-based multiplexing (verified live through an exec'd wrapper).

Not included, deliberately: go (finds GOROOT relative to its own executable, and half the catalog builds with it — needs its own PR and verification) and cargo (blocked on whether rust sessions should link at all — cc/ld are absent today). Both tracked in gominimal/inbox#584.

min task run sandboxes have no ~/.local/bin on PATH, so global installs made inside a task remain out of scope; gominimal/inbox#584 tracks that.

An earlier revision did the pnpm half as a stack-level build_env_vars entry; reverted in favor of package delivery (covers stack-less consumers, $HOME not a literal /home). History carries both steps; squash-merge collapses them.

Verification

The CI test blocks are the verification for the current revision. Each one installs something globally, offline, and asserts that PATH=/usr/bin:/bin:$HOME/.local/bin command -v <bin> resolves to ~/.local/bin/<bin> and runs:

  • pnpm (global_install): pnpm add -g of a local file: package with a bin; also asserts pnpm store path is not under ~/.local/store, and that PNPM_CONFIG_GLOBAL_BIN_DIR and PNPM_HOME set by the caller still decide pnpm bin -g.
  • bun (global_install): bun add -g of the same kind of package; also asserts the global dir is ~/.local/share/bun/global, and that a caller's BUN_INSTALL suppresses the defaults.
  • deno (global_install): deno install -g -n gbin of a local script.
  • ruby (gem_global_install): gem build + gem install --local of a minimal gem with an executable, with a user ~/.gemrc carrying gem: --no-document; then gem uninstall -x removes the stub.

Standalone tests run with the build-sandbox env (HOME=/state/home, no ~/.local/bin on PATH), so each test spells out a session-like PATH.

Outside CI: the pnpm, bun and deno test bodies were run locally against the upstream 12.5.1 / 1.3.14 / 2.9.5 release binaries behind the generated wrappers (pnpm and deno ship those exact tarballs; the bun package builds from source, so its binary differs), with a fresh HOME, and pass; the pnpm test fails against the previous PNPM_HOME wrapper. The ruby test has not been run outside CI.

Live Minimal session (2026-10-02, this revision): all four stacks pass on macOS 26.6.2 / Apple Silicon with min 0.6.0-dev.78 (minvmd backend, guest kernel 6.12.105 aarch64), in a session pinned to locked_commit = 7fb6d21d on this branch with packages base, pnpm, bun, deno, ruby, ruby-lsp, activated with --no-loadouts, a fresh HOME, and ~/.local/bin removed before each stack. The wrappers at /usr/bin/{pnpm,bun,deno} were byte-identical to this PR's heredocs and /usr/etc/gemrc carried the three --bindir lines.

  • pnpm 12.5.1: pnpm add -g file:… → ~/.local/bin/gbin-pnpm, runs; pnpm bin -g → ~/.local/bin; store at ~/.local/share/pnpm/store/v11, nothing under ~/.local/store; PNPM_CONFIG_GLOBAL_BIN_DIR and PNPM_HOME from the caller each decide pnpm bin -g.
  • bun 1.3.14: bun add -g → ~/.local/bin/gbin-bun (symlink into ~/.local/share/bun/global), runs; $XDG_CACHE_HOME/.bun/install/global not created; BUN_INSTALL moves both the global dir and the bin dir; bunx --version and an exec'd /usr/bin/bunx both answer.
  • deno 2.9.5: deno install -g -n gbin → ~/.local/bin/gbin, runs; ~/.deno not created; DENO_INSTALL_ROOT from the caller wins.
  • ruby 4.0.6 / RubyGems 4.0.16, ruby-lsp 0.26.10: gem env merges the system --bindir lines with a user ~/.gemrc of gem: --no-document; gem build + gem install --local → ~/.local/bin/gbin-gem, runs, no docs generated; gem uninstall -x leaves ~/.local/bin empty; a GEMRC override redirects the stub. ruby-lsp --version answers and a JSON-RPC initialize round-trip returns capabilities; its vendored bins stay under /usr/lib/ruby-lsp/gems/bin.

Observed and unrelated to this PR: bun's add banner reports 1.3.14-canary.1 while bun --version reports 1.3.14 (source-build version string); the ruby-lsp launcher composes a bundle on first start and self-updates to 0.26.11 from the network; pnpm prints its update nag. shellcheck: lines added to the build scripts and the generated wrappers are clean; pre-existing findings are untouched.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JGTXbdK7hgDupktNZEkALY

Summary by CodeRabbit

  • Features
    • Bun and Deno now use ~/.local for global installation locations when the relevant settings are unset.
    • pnpm now defaults global command-line executables to ~/.local/bin when PNPM_HOME is unset.
    • RubyGems now places installed command-line executables in ~/.local/bin by default.
    • Bun, Deno, and pnpm continue to forward command-line arguments.

@coderabbitai

coderabbitai Bot commented Aug 28, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: gominimal/pkgs/.coderabbit.yaml

Review profile: CHILL

Plan: Essentials

Run ID: 149e0684-68e2-49e2-b76c-a3a8c7e64594

📥 Commits

Reviewing files that changed from the base of the PR and between 6c0a814 and 7fb6d21.

📒 Files selected for processing (8)
  • packages/bun/build.ncl
  • packages/bun/build.sh
  • packages/deno/build.ncl
  • packages/deno/build.sh
  • packages/pnpm/build.ncl
  • packages/pnpm/build.sh
  • packages/ruby/build.ncl
  • packages/ruby/build.sh

Included review availability: This review used your included allowance. 0 included reviews remain after this review. Your included PR review attempts over the past 7 days set your current allowance at 3 reviews per hour.


📝 Walkthrough

Walkthrough

Bun, Deno, and pnpm now install executables under /usr/libexec and use /usr/bin wrappers to configure user-local paths and forward arguments. RubyGems configures executable installation under ~/.local/bin. Ruby LSP sets an explicit executable destination.

Changes

User-local Installation Defaults

Layer / File(s) Summary
Bun and Deno wrappers
packages/bun/build.sh, packages/bun/build.ncl, packages/deno/build.sh, packages/deno/build.ncl
The binaries are installed under /usr/libexec. The /usr/bin wrappers set the documented user-local environment defaults and forward arguments. Build outputs and standalone tests cover the binary paths and global installs.
pnpm wrappers
packages/pnpm/build.sh, packages/pnpm/build.ncl
The /usr/bin symlinks are replaced with wrappers that execute the corresponding /usr/libexec commands and conditionally set PNPM_CONFIG_GLOBAL_BIN_DIR. Tests check the native binary path, global command location, store path, and configured bin directories.
RubyGems executable paths
packages/ruby/build.sh, packages/ruby/build.ncl, packages/ruby-lsp/build.sh
Ruby adds gem configuration for ~/.local/bin and a test for executable installation and removal. Ruby LSP passes an explicit --bindir under the vendored gems directory.

Priority: ⬇️ Low

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Bug fix

Suggested reviewers: bryan-minimal

Merge Risk: ⚪ Minimal · up to 7fb6d

The user-local installation defaults have no identified merge-blocking issue. Merge after normal build and package checks pass.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 5…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: redirecting global installs for pnpm, Bun, Deno, and RubyGems to ~/.local.
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Comment @coderabbitai help to get the list of available commands.

@agent-137 agent-137 changed the title Make pnpm add -g work in sessions via stack-level PNPM_HOME Make pnpm add -g work in sessions by defaulting PNPM_HOME in the pnpm package wrappers Aug 28, 2026
@agent-137 agent-137 changed the title Make pnpm add -g work in sessions by defaulting PNPM_HOME in the pnpm package wrappers Make pnpm add -g and bun add -g work in sessions via PNPM_HOME/BUN_INSTALL-defaulting wrappers Aug 28, 2026
@agent-137 agent-137 changed the title Make pnpm add -g and bun add -g work in sessions via PNPM_HOME/BUN_INSTALL-defaulting wrappers Redirect pnpm, bun, deno, and gem global installs to ~/.local per the inbox#584 decision Aug 28, 2026
@agent-137

Copy link
Copy Markdown
Member Author

CI failure post-mortem (both failures were mine, both fixed in d5142f9):

  1. minimal-check / missing runtime_deps (pnpm, bun, deno) — the new #!/bin/sh wrappers need a runtime provider for bin/sh; the checker caught that none was declared. bash (which ships the sh symlink) is now in each package's runtime_deps.
  2. minimal-check / standalone tests (ruby-lsp) — the ruby package's new system gemrc leaked into ruby-lsp's vendored build: its gem install picked up the global --bindir ~/.local/bin default, wrote the binstub into the build sandbox home, and /usr/bin/ruby-lsp then exec'd a missing path. Fixed with an explicit --bindir "$GEMS/bin" (CLI beats gemrc). ruby-lsp is the only build-time gem install in the catalog, so no other package needs the same treatment.

@bryan thanks for the re-trigger — my fix is rebased on top of your no-op, so the new run includes both. The 2h bun-build hang looks unrelated to the diff content (the bun build.sh changes only touch the install step after bun run build:release); if it hangs again on this run it's worth a look independent of this PR.

@bryan-minimal

Copy link
Copy Markdown
Member

Heads-up: this now conflicts with main (CONFLICTING / DIRTY) on all four files it touches, after three PRs landed today. The conflicts are mechanical, but one of them has a wrong resolution that would re-open a two-day outage, so it seemed worth writing down rather than letting it get resolved by reflex.

Also worth saying: the last CI run here completed 2026-08-28, so its red is stale and predates everything below.

The one that matters: don't restore node-lts in pnpm

packages/pnpm/build.ncl on main is now:

runtime_deps = [
  subsetOf node ["node"],   # NOT node-lts, and NOT the whole package
  coreutils,
],

This branch still has node-lts. Merging that back would undo #671.

Why it matters (pkgs#665/#670/#671): next and agent-browser both declare node themselves, so when pnpm also carried node-lts, two npm installs landed in one sandbox at the same paths. The sandbox hardlinks closure members first-writer-wins per file, so /usr/lib/node_modules/npm/ came out as a blend of both versions — minipass-flush/index.js from npm 11.17.0 (which imports minipass by its 7.x named export) beside a nested minipass@3.3.6 from npm 11.11.1 (default export only). class Flush extends undefined throws, and every npm install dies. Both packages were individually fine; only the merge was broken. It blocked next for two days and took four PRs down with it.

The subsetOf form means pnpm contributes only the node binary and can never contribute an npm tree at all, so the failure is structurally impossible rather than just currently-absent.

Suggested resolution: keep main's subsetOf node ["node"], then re-apply your bash addition and the global_bin_dir test on top. Your changes and #671 are orthogonal — nothing here depends on it being node-lts specifically.

The other three are additive

  • packages/pnpm/build.sh — your PNPM_HOME wrapper; main is unchanged here apart from fix(pnpm): depend on node, not node-lts — two npms in one sandbox break each other #671's comment block.
  • packages/bun/build.ncl — main added three OutputData globs for completions (bash_completion, fish_completion, zsh_completion) and OutputData to the minimal.ncl destructure. Your libexec glob and OutputData import sit alongside them; keep both sets.
  • packages/bun/build.sh — main appends completion installs from the committed completions/bun.{bash,zsh,fish}. Those read from the extracted source, so they are independent of where the binary ends up.

Two things I noticed, offered as questions not objections

Your standaloneTest fix is a real find — "standaloneTest runs with no shell in the sandbox, so a #!/bin/sh wrapper would ENOENT there." That is the kind of thing that would otherwise show up as a baffling test failure much later.

Adding bash to runtime_deps of both pnpm and bun widens what they impose on every consumer. That is the same axis #671 just narrowed pnpm along, so it is worth a deliberate look rather than an accident. Concretely: bash and bash-bootstrap both ship /usr/bin/bash and /usr/bin/sh, and the two binaries genuinely differ (1,222,160 vs 1,133,600 bytes — --with-installed-readline vs --disable-readline). A fleet audit found only e2fsprogs and mtools currently hold both, so this is not firing today; the question is whether pushing bash into every pnpm/bun consumer's closure creates new pairs. If the wrapper only needs /bin/sh, a narrower dep would keep the surface small.

No action needed from me — flagging because the pnpm resolution is easy to get wrong and expensive when it is.

mitodrummer and others added 7 commits September 17, 2026 19:37
pnpm refuses global installs when its derived global bin dir
(~/.local/share/pnpm/bin) is not on PATH, and the session PATH is fixed,
so `pnpm setup` has nothing to edit. pnpm 11 honors no npm_config_*/
pnpm_config_* env spellings for this; the one lever is PNPM_HOME, whose
bin/ subdir becomes the global bin dir. Pointing it at ~/.local lands
global bins in /home/.local/bin, which sessions already have on PATH.

Stack build_env_vars fold into session composition with session-declared
vars taking precedence, so users can still override.

Refs gominimal/inbox#584 (follow-up to gominimal/inbox#559)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JGTXbdK7hgDupktNZEkALY
The stack leverages the pnpm package, so defaulting PNPM_HOME in the
package's /usr/bin wrappers covers every consumer — stack-composed
projects and bare [session].packages entries alike — where the stack
env var only reached projects with a [stack] block. The wrappers use
`: "${PNPM_HOME:=$HOME/.local}"`, so a caller's own PNPM_HOME still
wins, and $HOME replaces the stack variant's hardcoded /home.

Refs gominimal/inbox#584

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JGTXbdK7hgDupktNZEkALY
The previous commit's message covered the whole stack-to-package move
but only captured the stack revert; this adds the other half: replace
the /usr/bin/pnpm and /usr/bin/pnpx symlinks with wrappers defaulting
PNPM_HOME to ~/.local (caller's PNPM_HOME wins), plus a global_bin_dir
regression test.

Refs gominimal/inbox#584

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JGTXbdK7hgDupktNZEkALY
Same shape as the pnpm fix: bun's derived global location
(cache-derived, e.g. /state/cache/.bun) keeps `bun add -g` bins off
PATH. Move the real binary to /usr/libexec/bun and make /usr/bin/bun
and /usr/bin/bunx wrappers that default BUN_INSTALL to ~/.local, so
global bins land in ~/.local/bin (on the session PATH); a caller's
BUN_INSTALL wins. bunx stays an argv0 symlink beside the real binary,
so bun's name-based multiplexing is preserved (verified live). The
standalone smoketest now targets the raw ELF, since a #!/bin/sh
wrapper cannot run in the shell-less standaloneTest sandbox.

Refs gominimal/inbox#584

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JGTXbdK7hgDupktNZEkALY
deno: same wrapper shape as pnpm/bun — the real binary moves to
/usr/libexec/deno and /usr/bin/deno defaults DENO_INSTALL_ROOT to
~/.local, so `deno install -g` bins land in ~/.local/bin (on the
session PATH) instead of ~/.deno/bin; a caller's own value wins.
The standalone smoketest targets the raw ELF (no shell in that
sandbox for a #!/bin/sh wrapper).

gem: RubyGems' system config path is /usr/etc/gemrc — inside the
package — so ship one with `gem: --bindir ~/.local/bin`. gem already
falls back to user installs on the read-only store; this moves only
the binstubs onto the session PATH. Verified: ~ expands, the flag is
harmless for non-install commands, and uninstall removes the stub
symmetrically. User config (GEMRC, ~/.gemrc, CLI) still wins.

Also mark the bun and deno libexec outputs allow_executable — a Data
output matching a binary fails `min check` without it.

Refs gominimal/inbox#584

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JGTXbdK7hgDupktNZEkALY
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The #!/bin/sh wrappers made pnpm, bun, and deno need a runtime
provider for bin/sh — declare bash (which ships the sh symlink) in
each package's runtime_deps, per the missing-runtime-deps checker.

The ruby package's new system gemrc (binstubs default to ~/.local/bin)
leaked into ruby-lsp's vendored build: its `gem install` picked up the
default and wrote the binstub into the build sandbox home instead of
$GEMS/bin, so /usr/bin/ruby-lsp exec'd a missing path. Pass --bindir
explicitly there — CLI flags beat gemrc — keeping the vendored install
self-contained. ruby-lsp is the only build-time `gem install` in the
catalog.

Refs gominimal/inbox#584

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JGTXbdK7hgDupktNZEkALY
@mitodrummer

Copy link
Copy Markdown
Member

Rebased onto main (9abf7d0c), now MERGEABLE. Thanks for writing the pnpm one down — it is exactly the resolution reflex would have gotten wrong.

Conflicts, as resolved

  • packages/pnpm/build.ncl — kept main's subsetOf node ["node"] and the comment block explaining why, verbatim. node-lts is gone from this branch; fix(pnpm): depend on node, not node-lts — two npms in one sandbox break each other #671 stands. My two additions sit on top: bash in runtime_deps and the global_bin_dir test. Nothing here depended on it being node-lts — you were right that they are orthogonal.
  • packages/bun/build.ncl — both sets of outputs kept: your three completion OutputData globs plus my libexec glob (with allow_executable = true). OutputData was already in the destructure from your side, so the import line needed no change.
  • packages/bun/build.sh — both blocks kept. The libexec install + wrapper emission runs first, then the completions install from completions/bun.{bash,zsh,fish}. They are independent: the completion files are read from the extracted source, not from wherever the binary landed.
  • packages/pnpm/build.sh — additive, no interaction with fix(pnpm): depend on node, not node-lts — two npms in one sandbox break each other #671's comment block.

min check passes on all nine touched files. The output-dependent checkers (enumerate bins, output types valid, missing runtime_deps, standalone tests) report Skip locally because nothing is built in this session — CI runs them for real.

On bash in runtime_deps

I went looking for the narrower dep you suggested, and it does not exist:

  • subsetOf bash ["sh"] would ship a dangling symlink. packages/bash/build.sh:25 is ln -sf bash $OUTPUT_DIR/usr/bin/sh — sh is a relative symlink to bash, so a subset carrying the sh output without the bash output resolves to nothing.
  • subsetOf bash ["sh", "bash"] would not narrow the axis you care about. It drops mans, includes and locales — real bytes, but not the ones in play. The bash/bash-bootstrap pairing you found is a collision on exactly /usr/bin/bash and /usr/bin/sh, and any subset that gives a working /bin/sh ships both of those paths by construction.
  • Nothing else in the catalog provides /bin/sh. No dash, busybox, toybox or ash package exists; bash and bash-bootstrap are the only two providers, and bash-bootstrap is a toolchain-bootstrap package, not something to push into consumer closures.

So the choice is bash or no wrapper. For bun and deno there is no no-wrapper option — the thing that has to set the env var runs before an ELF starts, which means a shell. bash in runtime_deps is also the established idiom rather than a novel imposition: 28 packages already do it, including python, and subsetOf bash appears zero times in the repo.

Where I think your concern actually lands is the bash/bash-bootstrap pairing itself — it is a property of those two packages both owning /usr/bin/{bash,sh}, and it will keep being reachable through any of those 28 packages regardless of what this PR does. Happy to open a separate issue to track making that pair structurally impossible (an sh-only provider, or having bash-bootstrap stop claiming those paths once the real bash exists) if you think it is worth one — but it seems wrong to make this PR the place that gets solved.

CI

The red on this PR predates the rebase (last run 2026-08-28, as you noted). A fresh run should pick up the current tip.

pnpm moved to the 12.5.1 native binary on main. Keep that layout and
carry the PNPM_HOME wrapper over it: every /usr/bin entry (pnpm, pn,
pnpx, pnx) is now a wrapper that execs its /usr/libexec/pnpm counterpart
by real path, the ELF test points at the binary in libexec, and the
global_bin_dir test is kept.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@mitodrummer
mitodrummer marked this pull request as ready for review September 28, 2026 19:37
@mitodrummer
mitodrummer requested a review from a team as a code owner September 28, 2026 19:37
mitodrummer and others added 5 commits September 30, 2026 04:42
pnpm 12 reads PNPM_CONFIG_GLOBAL_BIN_DIR and uses it in place of the
<pnpm home>/bin it would otherwise derive. Defaulting that alone lands
`pnpm add -g` bins in ~/.local/bin while the store and global package
dir stay at pnpm's own defaults. A caller's PNPM_CONFIG_GLOBAL_BIN_DIR
or PNPM_HOME still wins. See gominimal/inbox#584.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
bun 1.3.14 reads BUN_INSTALL_BIN for the global bin symlinks and
BUN_INSTALL_GLOBAL_DIR for the package dir they point into. The wrapper
defaults both: without the second, the global dir falls back under
$XDG_CACHE_HOME and clearing the cache leaves the ~/.local/bin links
dangling. A caller's value of either, or a caller's BUN_INSTALL, wins.
See gominimal/inbox#584.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
RubyGems merges config files key by key, so a user's own
`gem: --no-document` replaced the shipped `gem:` line and dropped the
bindir. Set --bindir on install, update and uninstall instead. Bundler
ignores these keys; its executables stay off PATH (out of scope here).
See gominimal/inbox#584.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
With HOME unset the pnpm, bun and deno wrappers would have exported
paths under /.local. Leave the variables alone then, so each tool
falls back to its own resolution.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Each package's test now installs a local package or script globally
(pnpm add -g, bun add -g, deno install -g, gem install --local) and
asserts the executable resolves from ~/.local/bin on a session-like
PATH and runs, instead of probing the env var. pnpm and bun also check
that a caller's override still wins, pnpm that its store stays out of
~/.local, and gem that a user `gem:` line keeps the bindir and that
uninstall removes the stub.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@mitodrummer

Copy link
Copy Markdown
Member

/review

cvince pushed a commit to capysc/pkgs that referenced this pull request Oct 2, 2026
…ak each other (gominimal#671)

* fix(pnpm): depend on node, not node-lts — two npms in one sandbox breaks both

pnpm only needs *a* node for its `#!/usr/bin/env node` shebang, but whichever
one it names enters the closure of every package built with pnpm. `next` and
`agent-browser` both declare `node` themselves, so each sandbox held TWO npm
installs at the same paths.

The sandbox hardlinks closure members first-writer-wins PER FILE, so
/usr/lib/node_modules/npm came out a blend: minipass-flush/index.js from npm
11.17.0 (named `{ Minipass }` import) beside a nested minipass@3.3.6 from npm
11.11.1 (default export only). That tree never existed upstream and cannot
load — `class Flush extends undefined` throws and every `npm install` dies.
It broke `next` for two days and blocked gominimal#665, gominimal#667, gominimal#669 and gominimal#659.

Verified: closure of next 75->74 and agent-browser 114->113 with node-lts gone,
capy (the only other node-lts consumer) untouched; next, pnpm and agent-browser
each build --rebuild clean and pass min check 15/15.

Supersedes gominimal#670, whose fix was a no-op against node-lts's own output.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refine: take only the node BINARY, not the whole package

Review point from Bryan: pnpm should not be dictating a node version to its
consumers at all. The spec language cannot express that today — RuntimeDep is
BuildSpec | Subset | UpstreamPkg, all concrete, with no "consumer provides it"
form — but `subsetOf` gets most of the way there.

pnpm needs a node to interpret `#!/usr/bin/env node` and nothing else. Taking
`subsetOf node ["node"]` instead of the whole package means pnpm can never
contribute an npm tree to any closure, so the two-npms-merged-per-file failure
is structurally impossible rather than merely absent from today's fleet.

Residual, and worth stating: pnpm still names a version, so a future node-lts
consumer adding pnpm would collide on the `node` binary. That is a much smaller
failure than an unloadable module graph, and the real fix is a virtual/provided
dependency in the spec language.

Verified with the subset: next, pnpm and agent-browser all build --rebuild
clean and pass min check, including pnpm's `missing runtime_deps` — which
independently confirms the bare binary satisfies the shebang.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@mitodrummer

Copy link
Copy Markdown
Member

/review

@mitodrummer
mitodrummer added this pull request to the merge queue Oct 2, 2026
Merged via the queue into main with commit 2cb8bce Oct 2, 2026
11 checks passed
@mitodrummer
mitodrummer deleted the pnpm-stack-global-bin branch October 2, 2026 18:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants