fix: resolve npm's global root on versioned (kegged) node layouts - #130
Open
adrianco wants to merge 1 commit into
Open
fix: resolve npm's global root on versioned (kegged) node layouts#130adrianco wants to merge 1 commit into
adrianco wants to merge 1 commit into
Conversation
`globalRoot()` falls back to an execPath-derived path whenever `npm` is not spawnable. That fallback only tried `<binDir>/../lib/node_modules` and `<binDir>/node_modules`, which assumes the executable sits directly under its prefix. Homebrew does not: `process.execPath` is symlink-resolved to `<prefix>/Cellar/node/<version>/bin/node`, so both candidates miss and `globalRoot()` throws "cannot determine npm global root (is npm installed?)" on a machine where npm is installed and working. mise, asdf, and nvm place their versioned trees at a similar depth. This is not a theoretical path: the test sandbox points PATH at a directory that does not exist by design (tests/kit/helpers/home-sandbox.mjs), so every sandboxed run takes the fallback. Six tests in tests/kit/provider-cli.test.mjs fail on any Homebrew-node macOS checkout, all surfacing as an opencode host-pick failure rather than as a path bug. At runtime the same throw reaches `ak host pick` whenever npm is off PATH. Walk the executable's ancestors (bounded to 5, never probing the filesystem root) so a linked prefix is recovered, and honour npm's own documented `npm_config_prefix` override ahead of any derivation. Nearest-first ordering is preserved, so a keg-local tree still wins where one exists, and the sibling layout stays the last resort. The walk is split into two exported, injectable functions so the layouts can be asserted as data without installing node five different ways, and without touching the process-wide cache.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
globalRoot()(src/lib/paths.mjs) falls back to an execPath-derived path whenevernpmisn't spawnable. That fallback tried only<binDir>/../lib/node_modulesand<binDir>/node_modules, which assumes the node executable sits directly under its prefix.Homebrew doesn't work that way.
process.execPathis symlink-resolved, so on a Homebrew install it is/opt/homebrew/Cellar/node/<version>/bin/node, not/opt/homebrew/bin/node. Both candidates miss, andglobalRoot()throwscannot determine npm global root (is npm installed?)on a machine where npm is installed and working. mise, asdf, and nvm place versioned trees at a similar depth.Why it matters
This isn't only a theoretical path.
tests/kit/helpers/home-sandbox.mjs:40pointsPATHat a directory that doesn't exist by design, so every sandboxed test takes the fallback. On a Homebrew-node macOS checkout, six tests intests/kit/provider-cli.test.mjsfail — and they surface as opencode host-pick failures rather than as a path bug:Reproducible in isolation:
At runtime the same throw reaches
ak host pickany time npm is offPATH.Change
Walk the executable's ancestors (bounded to 5, and never probing the filesystem root, since
/lib/node_modulesis not a prefix) so a linked prefix is recovered, and honour npm's own documentednpm_config_prefixoverride ahead of any derivation.Nearest-first ordering is preserved: a keg-local tree still wins where one exists, and the sibling layout stays the last resort.
npm root -gremains authoritative and is untouched.The walk is split into two exported, injectable functions (
globalRootCandidates,resolveGlobalRoot) so the layouts can be asserted as data — without installing node five different ways, and without touching the process-wide cache.Verification
tests/kit/paths-global-root.test.mjs— 8 tests covering Homebrew/linuxbrew kegged layouts, plain POSIX, nvm, thenpm_config_prefixoverride, last-resort sibling ordering, a filesystem fixture tree, and the null-rather-than-guess path.provider-clifailures now pass: 17/17.typecheck,lint,lint:md,build-check, andpnpm test(1461 tests, 0 failures).No runtime dependency added;
node:builtins only.