Skip to content

Find npm installed by nvm when the app is launched from Finder - #5576

Merged
Gabriel Dufresne (GabrielDuf) merged 2 commits into
mainfrom
fix/issue-5553
Oct 22, 2026
Merged

Gabriel Dufresne (GabrielDuf) merged 2 commits into
mainfrom
fix/issue-5553

Conversation

@GabrielDuf

Copy link
Copy Markdown
Contributor
  • I have read the contributing guidelines, and I agree with the Code of Conduct.
  • Have you checked that there aren't other open pull requests for the same changes?
  • Have you tested that the committed code can be executed without errors? (build and unit tests only; the app was not launched from Finder with a real nvm install)
  • Have you confirmed that this issue is caused by UniGetUI itself, and not by the package manager or the package involved?

Root cause

On macOS a GUI-launched UniGetUI starts with launchd's minimal PATH. ProcessEnvironmentConfigurator.ExpandMacOSPath widens it with zsh -l -c "printenv PATH". That shell is a login shell but not an interactive one, so zsh reads .zprofile/.zlogin and skips .zshrc. The nvm installer writes its setup into .zshrc, so ~/.nvm/versions/node/<ver>/bin never reaches the process PATH.

Npm.FindCandidateExecutableFiles only called CoreTools.WhichMultiple("npm"), which on Unix searches only the process PATH. It returned zero candidates, so the log said "Command npm was not found on the system", and the custom-executable picker (which lists only those candidates) offered nothing to choose either. Npm had no install-location fallback like Cargo's GetCargoBinDirectories (~/.cargo/bin).

Finding the file alone would not be enough: nvm's npm is a symlink to npm-cli.js, which starts with #!/usr/bin/env node. env resolves node through PATH, so with the minimal PATH even the --version probe and every operation would fail once npm was detected.

Fix

All changes are in src/UniGetUI.PackageEngine.Managers.Npm/Npm.cs. Windows behaviour is unchanged.

  1. On non-Windows, FindCandidateExecutableFiles adds nvm installs after the PATH hits, with the same shape as Cargo's fallback (WhichMultiple first, then probed directories filtered for the executable bit, then de-duplicated). An npm already on PATH still wins, and nvm installs also show up in the custom-path picker.
  2. The new internal GetNvmBinDirectories(readEnv, userProfile) takes $NVM_DIR, or ~/.nvm if it is unset or blank, and lists versions/node/v*/bin newest first.
    • The version named by nvm's alias/default moves to the front, following alias chains such as lts/* -> lts/jod -> v22.11.0 (up to 8 hops). Partial versions such as 20 match by major/minor/patch prefix; node and stable keep newest-first.
    • IO and permission errors return an empty list.
  3. In _loadManagerExecutableFile on non-Windows, the directory of the chosen npm is appended to the process PATH when not already there (new internal AppendDirectoryToPath, which ignores a trailing separator), and the change is logged.
    • It runs before _loadManagerVersion and child processes inherit it, so the #!/usr/bin/env node shebang finds the node next to npm for both the version probe and operations.
    • Appending rather than prepending means npm-installed globals cannot shadow existing PATH entries for other managers.
    • It is idempotent across manager reloads, and also covers a user-picked custom npm outside PATH.

This follows the existing process-wide PATH handling in ProcessEnvironmentConfigurator rather than threading an environment through every operation launcher.

Verification

  • Build: dotnet build src/UniGetUI.PackageEngine.Tests/UniGetUI.PackageEngine.Tests.csproj -p:Platform=x64 gives 0 errors and no warnings in the changed files (two CA1416 warnings from a first draft were fixed with OperatingSystem guards).
  • Formatters: dotnet format whitespace and dotnet format style with --verify-no-changes, limited to the changed files, both exit 0. The unrestricted style gate on UniGetUI.Windows.slnx cannot load the Windows-only projects on macOS; the CI runner decides it.
  • Tests: the dev machine is Apple Silicon with no x64 dotnet host, so the new tests were run with -p:Platform=arm64: 7 total, 7 passed.
  • Revert proof: with Npm.cs reverted (and NpmNodePathTests.cs set aside, since it calls the new helper), NpmNvmDiscoveryTests gave 3 failed, 1 passed. DiscoveryFindsNpmInstalledByNvmOutsideThePath failed with "Assert.Contains() Failure: Item not found in collection", which is the reported symptom; both ordering tests failed; the executable-bit negative test passes either way, as expected. With the fix restored all 7 pass. The full suite is left to CI.

Regression tests

  • NpmNvmDiscoveryTests.DiscoveryFindsNpmInstalledByNvmOutsideThePath
  • NpmNvmDiscoveryTests.DiscoveryPrefersTheNvmDefaultAliasThenTheNewestVersion
  • NpmNvmDiscoveryTests.DiscoveryMatchesAPartialNvmDefaultVersion
  • NpmNvmDiscoveryTests.DiscoveryIgnoresNvmNpmWithoutExecutablePermissions
  • NpmNodePathTests.AppendDirectoryToPathAddsAMissingDirectoryAtTheEnd
  • NpmNodePathTests.AppendDirectoryToPathKeepsAPathThatAlreadyHasTheDirectory
  • NpmNodePathTests.AppendDirectoryToPathStartsAnEmptyPath

Not covered

  • Other Node version managers (fnm, volta, asdf and mise shims, n). Each has its own layout and would extend GetNvmBinDirectories the same way.
  • ExpandMacOSPath is unchanged. Also sourcing .zshrc (an interactive zsh -i -l) would affect every manager and risks the hangs [BUG] macOS startup can hang indefinitely while reading PATH from a zsh login shell #5236 guards against; that is a separate decision.
  • Windows is untouched (nvm-windows exposes npm through a PATH symlink, and the Windows path uses npm.cmd and PowerShell).
  • No end-to-end check on macOS: the GUI app was not launched from Finder with a real nvm install.

Review notes (non-blocking)

  • CI coverage gap: .github/workflows/dotnet-test.yml runs only on windows-latest, and all four NpmNvmDiscoveryTests return early on Windows, so on CI they pass without checking anything. Only the three platform-neutral NpmNodePathTests really run there; the regression proof exists only from local macOS runs.
  • The PATH append that makes #!/usr/bin/env node resolve is tested only through the pure helper AppendDirectoryToPath. Nothing exercises _loadManagerExecutableFile followed by the version probe against a real nvm npm, so that the probe and operations work is unverified end to end.
  • Appending rather than prepending means that if another node is already on PATH (for example Homebrew's /opt/homebrew/bin), nvm's npm runs under that node instead of its sibling. This only matters when the nvm npm is picked while a different node is on PATH; the trade-off is deliberate.
  • The process-wide PATH change is not undone if the user later switches the npm custom path, so a stale nvm bin directory stays on PATH for the session. Harmless, since it is appended.
  • Npm.IsExecutableFile duplicates Cargo's private helper rather than widening an API surface in this fix.

Closes #5553

🤖 Generated with Claude Code

A GUI-launched UniGetUI on macOS starts with launchd's minimal PATH.
ExpandMacOSPath widens it with a login, non-interactive zsh, which reads
.zprofile/.zlogin but skips .zshrc, where the nvm installer writes its
setup. ~/.nvm/versions/node/<ver>/bin therefore never reached the process
PATH, and Npm.FindCandidateExecutableFiles, which only searched PATH,
found no candidate, so npm was reported missing and could not be picked
by hand either.

On non-Windows, discovery now adds nvm installs after the PATH hits,
following Cargo's fallback shape: $NVM_DIR (or ~/.nvm) versions are
listed newest first, with nvm's alias/default (following alias chains
and partial versions) moved to the front, filtered for the executable
bit and de-duplicated. An npm already on PATH still wins.

nvm's npm is a script with a "#!/usr/bin/env node" shebang, so finding
it is not enough with the minimal PATH. When npm is loaded on
non-Windows, its directory is appended to the process PATH if missing,
before the version probe, so the probe and every operation inherit a
PATH where node resolves. Appending rather than prepending keeps that
directory from shadowing other managers' tools, and the append is
idempotent across reloads. Windows behaviour is unchanged.

Fixes #5553

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Custom alias resolution can select the wrong Node installation, and real Finder-launched npm execution remains unverified.

1 open issue
What changed in this PR

Addresses #5553 by discovering nvm-installed npm when Finder launches UniGetUI with a minimal PATH, without changing Windows discovery or shell startup behavior.

Changes:

  • Adds executable nvm candidates after PATH matches, ordered by the default alias and installed version.
  • Appends the selected npm directory to PATH so child processes can locate Node.
  • Adds regression tests for discovery, alias ordering, executable permissions, and PATH handling.
File Description
src/​UniGetUI.PackageEngine.Tests/​NpmNvmDiscoveryTests.cs Tests nvm discovery, ordering, and executable filtering.
src/​UniGetUI.PackageEngine.Tests/​NpmNodePathTests.cs Tests PATH appending and duplicate avoidance.
src/​UniGetUI.PackageEngine.Managers.Npm/​Npm.cs Adds nvm discovery, alias resolution, and PATH updates.

Review effort: Balanced

Comment thread src/UniGetUI.PackageEngine.Managers.Npm/Npm.cs Outdated
ResolveNvmAlias treated any alias value that started with 'v' or a digit
as a version, so a chain such as default -> verified -> v22.11.0 stopped
at "verified". MatchesNvmVersion could not parse that name, the
installations stayed sorted newest-first, and with v24 also installed the
newest Node was picked instead of the configured v22 default, exposing a
different set of global packages.

A value now counts as a version only when, after one optional leading
'v', it is one to three non-empty, all-digit, dot-separated components.
Anything else is followed as an alias file, still capped at 8 hops, and
node/stable keep resolving to no preference. The regression test
DiscoveryFollowsACustomNvmAliasWhoseNameStartsWithV covers the chain.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The process-wide PATH change needs end-to-end validation of npm initialization and operations with a real nvm installation launched from Finder.

No open issues 🎉

1 resolved since last review

Review effort: Balanced

@randy-but-a-ro randy-but-a-ro Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Pull request was approved automatically: the AI review is complete and all its review threads are resolved. 🎉

Integration Details
{
	"deliveryId": "7dc53650-ce2e-11f1-95ef-e821818989fa",
	"headSha": "0f6a3085c39d4742c1da248b5e306ed7f889e0c1",
	"reviewer": "copilot-pull-request-reviewer[bot]"
}

@GabrielDuf
Gabriel Dufresne (GabrielDuf) merged commit 25b063b into main Oct 22, 2026
6 checks passed
@GabrielDuf
Gabriel Dufresne (GabrielDuf) deleted the fix/issue-5553 branch October 22, 2026 18:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] npm not detected on mac

2 participants