Repository navigation
Find npm installed by nvm when the app is launched from Finder - #5576
Merged
Merged
Conversation
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>
Contributor
There was a problem hiding this comment.
🟡 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
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>
There was a problem hiding this comment.
🤖 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]"
}
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.

Root cause
On macOS a GUI-launched UniGetUI starts with launchd's minimal PATH.
ProcessEnvironmentConfigurator.ExpandMacOSPathwidens it withzsh -l -c "printenv PATH". That shell is a login shell but not an interactive one, so zsh reads.zprofile/.zloginand skips.zshrc. The nvm installer writes its setup into.zshrc, so~/.nvm/versions/node/<ver>/binnever reaches the process PATH.Npm.FindCandidateExecutableFilesonly calledCoreTools.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'sGetCargoBinDirectories(~/.cargo/bin).Finding the file alone would not be enough: nvm's
npmis a symlink tonpm-cli.js, which starts with#!/usr/bin/env node.envresolvesnodethrough PATH, so with the minimal PATH even the--versionprobe 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.FindCandidateExecutableFilesadds nvm installs after the PATH hits, with the same shape as Cargo's fallback (WhichMultiplefirst, 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.GetNvmBinDirectories(readEnv, userProfile)takes$NVM_DIR, or~/.nvmif it is unset or blank, and listsversions/node/v*/binnewest first.alias/defaultmoves to the front, following alias chains such aslts/*->lts/jod->v22.11.0(up to 8 hops). Partial versions such as20match by major/minor/patch prefix;nodeandstablekeep newest-first._loadManagerExecutableFileon non-Windows, the directory of the chosen npm is appended to the process PATH when not already there (new internalAppendDirectoryToPath, which ignores a trailing separator), and the change is logged._loadManagerVersionand child processes inherit it, so the#!/usr/bin/env nodeshebang finds thenodenext to npm for both the version probe and operations.This follows the existing process-wide PATH handling in
ProcessEnvironmentConfiguratorrather than threading an environment through every operation launcher.Verification
dotnet build src/UniGetUI.PackageEngine.Tests/UniGetUI.PackageEngine.Tests.csproj -p:Platform=x64gives 0 errors and no warnings in the changed files (two CA1416 warnings from a first draft were fixed withOperatingSystemguards).dotnet format whitespaceanddotnet format stylewith--verify-no-changes, limited to the changed files, both exit 0. The unrestricted style gate onUniGetUI.Windows.slnxcannot load the Windows-only projects on macOS; the CI runner decides it.-p:Platform=arm64: 7 total, 7 passed.Npm.csreverted (andNpmNodePathTests.csset aside, since it calls the new helper),NpmNvmDiscoveryTestsgave 3 failed, 1 passed.DiscoveryFindsNpmInstalledByNvmOutsideThePathfailed 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.DiscoveryFindsNpmInstalledByNvmOutsideThePathNpmNvmDiscoveryTests.DiscoveryPrefersTheNvmDefaultAliasThenTheNewestVersionNpmNvmDiscoveryTests.DiscoveryMatchesAPartialNvmDefaultVersionNpmNvmDiscoveryTests.DiscoveryIgnoresNvmNpmWithoutExecutablePermissionsNpmNodePathTests.AppendDirectoryToPathAddsAMissingDirectoryAtTheEndNpmNodePathTests.AppendDirectoryToPathKeepsAPathThatAlreadyHasTheDirectoryNpmNodePathTests.AppendDirectoryToPathStartsAnEmptyPathNot covered
GetNvmBinDirectoriesthe same way.ExpandMacOSPathis unchanged. Also sourcing.zshrc(an interactivezsh -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.npm.cmdand PowerShell).Review notes (non-blocking)
.github/workflows/dotnet-test.ymlruns only onwindows-latest, and all fourNpmNvmDiscoveryTestsreturn early on Windows, so on CI they pass without checking anything. Only the three platform-neutralNpmNodePathTestsreally run there; the regression proof exists only from local macOS runs.#!/usr/bin/env noderesolve is tested only through the pure helperAppendDirectoryToPath. Nothing exercises_loadManagerExecutableFilefollowed by the version probe against a real nvm npm, so that the probe and operations work is unverified end to end.nodeis 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.Npm.IsExecutableFileduplicates Cargo's private helper rather than widening an API surface in this fix.Closes #5553
🤖 Generated with Claude Code