feat: override node bundled npm by specified version of npm#7559
feat: override node bundled npm by specified version of npm#7559
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds a new "overrides" feature to the mise registry that allows tools to control PATH ordering by specifying which tools they should override. Specifically, this enables npm to override node's bundled npm version by placing npm's binaries earlier in the PATH.
Key Changes:
- Adds
overridesfield to registry tools to specify PATH ordering relationships - Implements custom sorting in
list_paths()to respect override relationships - Adds npm → node override configuration to ensure installed npm takes precedence
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| src/toolset/toolset_paths.rs | Implements sorting logic to order tools based on override relationships before building PATH |
| src/registry.rs | Adds overrides field to RegistryTool struct |
| schema/mise-registry.json | Adds JSON schema definition for the new overrides field |
| registry.toml | Configures npm to override node in PATH ordering |
| build.rs | Adds codegen to parse overrides from registry.toml and include in generated code |
| e2e/registry/test_overrides | Adds end-to-end test verifying npm version takes precedence over node's bundled npm |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| installed.sort_by(|(a, _), (b, _)| { | ||
| let id_a = a.id(); | ||
| let id_b = b.id(); | ||
|
|
||
| if let Some(tool_a) = REGISTRY.get(id_a) | ||
| && tool_a.overrides.contains(&id_b) | ||
| { | ||
| return std::cmp::Ordering::Less; | ||
| } | ||
| if let Some(tool_b) = REGISTRY.get(id_b) | ||
| && tool_b.overrides.contains(&id_a) | ||
| { | ||
| return std::cmp::Ordering::Greater; | ||
| } | ||
| std::cmp::Ordering::Equal | ||
| }); | ||
|
|
There was a problem hiding this comment.
The comparison function violates the transitivity and totality requirements for a proper sort comparator. While it works for simple cases like "npm overrides node", it will fail if the registry includes chains of overrides (A overrides B, B overrides C).
For example, if npm overrides node and node overrides another tool X:
- npm vs node returns Less (npm < node)
- node vs X returns Less (node < X)
- npm vs X returns Equal (should return Less to maintain transitivity)
This violates the total order requirement and can lead to undefined behavior in Rust's sort implementation, potentially causing panics or incorrect ordering.
To fix this, implement a proper topological sort that builds a dependency graph and orders tools accordingly. Alternatively, compute the full transitive closure of override relationships and use that for comparisons. Also consider adding validation in build.rs to detect and reject circular override relationships.
| installed.sort_by(|(a, _), (b, _)| { | |
| let id_a = a.id(); | |
| let id_b = b.id(); | |
| if let Some(tool_a) = REGISTRY.get(id_a) | |
| && tool_a.overrides.contains(&id_b) | |
| { | |
| return std::cmp::Ordering::Less; | |
| } | |
| if let Some(tool_b) = REGISTRY.get(id_b) | |
| && tool_b.overrides.contains(&id_a) | |
| { | |
| return std::cmp::Ordering::Greater; | |
| } | |
| std::cmp::Ordering::Equal | |
| }); | |
| // Order installed tools using a topological sort based on override relationships. | |
| // Tools that override others come earlier; unrelated tools are ordered by their IDs. | |
| let n = installed.len(); | |
| if n > 1 { | |
| // Precompute tool IDs for deterministic tie-breaking. | |
| let ids: Vec<String> = installed | |
| .iter() | |
| .map(|(p, _)| p.id().to_string()) | |
| .collect(); | |
| // Build adjacency list and in-degree counts for Kahn's algorithm. | |
| let mut adj: Vec<Vec<usize>> = vec![Vec::new(); n]; | |
| let mut indegree: Vec<usize> = vec![0; n]; | |
| for i in 0..n { | |
| let id_i = installed[i].0.id(); | |
| if let Some(tool_i) = REGISTRY.get(id_i) { | |
| for j in 0..n { | |
| if i == j { | |
| continue; | |
| } | |
| let id_j = installed[j].0.id(); | |
| if tool_i.overrides.contains(&id_j) { | |
| adj[i].push(j); | |
| indegree[j] += 1; | |
| } | |
| } | |
| } | |
| } | |
| // Kahn's topological sort with ID-based tie-breaking for a total, deterministic order. | |
| let mut zero_indegree: Vec<usize> = (0..n).filter(|&i| indegree[i] == 0).collect(); | |
| zero_indegree.sort_by(|&i, &j| ids[i].cmp(&ids[j])); | |
| let mut order: Vec<usize> = Vec::with_capacity(n); | |
| let mut seen: Vec<bool> = vec![false; n]; | |
| while !zero_indegree.is_empty() { | |
| // Always take the smallest ID among zero in-degree nodes for determinism. | |
| let u = zero_indegree.remove(0); | |
| if seen[u] { | |
| continue; | |
| } | |
| seen[u] = true; | |
| order.push(u); | |
| for &v in &adj[u] { | |
| if indegree[v] > 0 { | |
| indegree[v] -= 1; | |
| if indegree[v] == 0 { | |
| zero_indegree.push(v); | |
| } | |
| } | |
| } | |
| // Re-sort candidates after updates to maintain ID-based ordering. | |
| zero_indegree.sort_by(|&i, &j| ids[i].cmp(&ids[j])); | |
| } | |
| if order.len() < n { | |
| // Fallback for cycles or unreachable nodes: append remaining nodes, | |
| // ordered by ID, so we still produce a total order. | |
| let mut remaining: Vec<usize> = (0..n).filter(|&i| !seen[i]).collect(); | |
| remaining.sort_by(|&i, &j| ids[i].cmp(&ids[j])); | |
| order.extend(remaining); | |
| } | |
| // Rebuild `installed` according to the computed order. | |
| let mut reordered = Vec::with_capacity(n); | |
| for idx in order { | |
| reordered.push(installed[idx].clone()); | |
| } | |
| installed = reordered; | |
| } |
There was a problem hiding this comment.
I think we don't need to support chain/circular overrides for simplicity for now.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
|
Bugbot run |
### 🚀 Features - **(hooks)** add tool context env vars to postinstall hooks by @jdx in [#7521](#7521) - **(sops)** support standard SOPS environment variables by @yordis in [#7461](#7461) - **(tasks)** Add disable_spec_from_run_scripts setting by @iamkroot in [#7471](#7471) - **(tasks)** Add task_show_full_cmd setting by @iamkroot in [#7344](#7344) - **(tasks)** enable naked task completions and ::: separator by @jdx in [#7524](#7524) - add Forgejo backend by @roele in [#7469](#7469) - override node bundled npm by specified version of npm by @risu729 in [#7559](#7559) ### 🐛 Bug Fixes - **(aqua)** fix tree-sitter bin path regression by @risu729 in [#7535](#7535) - **(ci)** exclude subcrate tags from release workflow by @jdx in [#7517](#7517) - **(e2e)** remove hardcoded year from version check by @jdx in [#7584](#7584) - **(github)** asset matcher does not handle mixed archive/binary assets properly by @roele in [#7566](#7566) - **(github)** prioritize .zip on windows by @risu729 in [#7568](#7568) - **(github)** prefer .zip over non-archive extensions on linux by @risu729 in [#7587](#7587) - **(npm)** always use hoisted installs of bun by @sushichan044 in [#7542](#7542) - **(npm)** suppress NPM_CONFIG_UPDATE_NOTIFIER by @risu729 in [#7556](#7556) - **(registry)** fix biome test to handle version prefix by @jdx in [#7585](#7585) - **(tasks)** load monorepo task dirs without config by @matixlol in [#7478](#7478) - force reshim when windows_shim_mode is hardlink by @roele in [#7537](#7537) - simple .tar files are not extracted properly by @roele in [#7567](#7567) - quiet kerl update output by @iloveitaly in [#7467](#7467) ### 📚 Documentation - **(registry)** remove ubi backend from preferred backends list by @risu729 in [#7555](#7555) - **(tasks)** remove advanced usage specs sections from toml-tasks.md by @risu729 in [#7538](#7538) - fix invalid config section `[aliases]` by @muzimuzhi in [#7518](#7518) - Fix path to GitLab backend source by @henrebotha in [#7529](#7529) - Fix path to GitLab backend source by @henrebotha in [#7531](#7531) - update `mise --version` output by @muzimuzhi in [#7530](#7530) ### 🧪 Testing - **(win)** use pester in backend tests by @risu729 in [#7536](#7536) - update e2e tests to use `[tool_alias]` instead of `[alias]` by @muzimuzhi in [#7520](#7520) ### 📦️ Dependency Updates - update alpine:edge docker digest to ea71a03 by @renovate[bot] in [#7545](#7545) - update docker/setup-buildx-action digest to 8d2750c by @renovate[bot] in [#7546](#7546) - update ghcr.io/jdx/mise:copr docker digest to 23f4277 by @renovate[bot] in [#7548](#7548) - update ghcr.io/jdx/mise:alpine docker digest to 0adc211 by @renovate[bot] in [#7547](#7547) - lock file maintenance by @renovate[bot] in [#7211](#7211) - lock file maintenance by @renovate[bot] in [#7572](#7572) - replace dependency @tsconfig/node18 with @tsconfig/node20 by @renovate[bot] in [#7543](#7543) - replace dependency @tsconfig/node20 with @tsconfig/node22 by @renovate[bot] in [#7544](#7544) ### 📦 Registry - add zarf by @joonas in [#7525](#7525) - update aws-vault to maintained fork by @h3y6e in [#7527](#7527) - fix claude backend http for windows-x64 by @granstrand in [#7540](#7540) - add sqlc by @phm07 in [#7570](#7570) - use spm backend for swift-package-list by @risu729 in [#7569](#7569) - add npm (npm:npm) by @risu729 in [#7557](#7557) - add github backend for tmux by @ll-nick in [#7472](#7472) ### Chore - **(release)** update Changelog for v2025.12.13 by @muzimuzhi in [#7522](#7522) ### New Contributors - @ll-nick made their first contribution in [#7472](#7472) - @sushichan044 made their first contribution in [#7542](#7542) - @phm07 made their first contribution in [#7570](#7570) - @granstrand made their first contribution in [#7540](#7540) - @h3y6e made their first contribution in [#7527](#7527) - @matixlol made their first contribution in [#7478](#7478) ## 📦 Aqua Registry Updates #### New Packages (9) - [`anomalyco/opencode`](https://github.com/anomalyco/opencode) - [`astral-sh/ty`](https://github.com/astral-sh/ty) - [`github/copilot-cli`](https://github.com/github/copilot-cli) - [`github/gh-ost`](https://github.com/github/gh-ost) - [`golangci/golines`](https://github.com/golangci/golines) - [`jamf/Notifier`](https://github.com/jamf/Notifier) - [`microsoft/vscode/code`](https://github.com/microsoft/vscode/code) - [`pranshuparmar/witr`](https://github.com/pranshuparmar/witr) - [`spinel-coop/rv`](https://github.com/spinel-coop/rv) #### Updated Packages (37) - [`FiloSottile/age`](https://github.com/FiloSottile/age) - [`alvinunreal/tmuxai`](https://github.com/alvinunreal/tmuxai) - [`aquasecurity/starboard`](https://github.com/aquasecurity/starboard) - [`aristocratos/btop`](https://github.com/aristocratos/btop) - [`biomejs/biome`](https://github.com/biomejs/biome) - [`bootandy/dust`](https://github.com/bootandy/dust) - [`borgbackup/borg`](https://github.com/borgbackup/borg) - [`bvaisvil/zenith`](https://github.com/bvaisvil/zenith) - [`cri-o/cri-o`](https://github.com/cri-o/cri-o) - [`cubefs/cubefs`](https://github.com/cubefs/cubefs) - [`domoritz/arrow-tools/csv2arrow`](https://github.com/domoritz/arrow-tools/csv2arrow) - [`domoritz/arrow-tools/csv2parquet`](https://github.com/domoritz/arrow-tools/csv2parquet) - [`domoritz/arrow-tools/json2arrow`](https://github.com/domoritz/arrow-tools/json2arrow) - [`domoritz/arrow-tools/json2parquet`](https://github.com/domoritz/arrow-tools/json2parquet) - [`fission/fission`](https://github.com/fission/fission) - [`folbricht/desync`](https://github.com/folbricht/desync) - [`go-acme/lego`](https://github.com/go-acme/lego) - [`gohugoio/hugo`](https://github.com/gohugoio/hugo) - [`gohugoio/hugo/hugo-extended`](https://github.com/gohugoio/hugo/hugo-extended) - [`golang.org/x/perf/cmd/benchstat`](https://github.com/golang.org/x/perf/cmd/benchstat) - [`gsamokovarov/jump`](https://github.com/gsamokovarov/jump) - [`haskell/cabal/cabal-install`](https://github.com/haskell/cabal/cabal-install) - [`kptdev/kpt`](https://github.com/kptdev/kpt) - [`kubescape/kubescape`](https://github.com/kubescape/kubescape) - [`mas-cli/mas`](https://github.com/mas-cli/mas) - [`maxpert/marmot`](https://github.com/maxpert/marmot) - [`mistakenelf/fm`](https://github.com/mistakenelf/fm) - [`psf/black`](https://github.com/psf/black) - [`redpanda-data/connect`](https://github.com/redpanda-data/connect) - [`rest-sh/restish`](https://github.com/rest-sh/restish) - [`saucelabs/forwarder`](https://github.com/saucelabs/forwarder) - [`sethvargo/ratchet`](https://github.com/sethvargo/ratchet) - [`stackrox/kube-linter`](https://github.com/stackrox/kube-linter) - [`steveyegge/beads`](https://github.com/steveyegge/beads) - [`suzuki-shunsuke/rgo`](https://github.com/suzuki-shunsuke/rgo) - [`txn2/kubefwd`](https://github.com/txn2/kubefwd) - [`zyedidia/micro`](https://github.com/zyedidia/micro)
- Fix timeout issue when using npm backend tools after v2026.1.0 - The issue was caused by circular dependency: `npm:npm` was added to registry in #7557, and npm was added to `NPMBackend::get_dependencies()`, causing infinite loop when resolving dependencies - Skip adding `npm` to dependencies when the tool itself is `npm` to break the cycle Related PRs: - #7557 (registry: add npm) - #7559 (feat: override node bundled npm) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Prevents circular dependency/timeouts when resolving `npm:npm`. > > - Update `NPMBackend::get_dependencies()` to omit `"npm"` when the tool itself is `"npm"`; otherwise keep `node/npm/bun/pnpm` > - Add unit tests validating dependency sets for `npm:npm` and other packages (e.g., `npm:prettier`) > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 5a6888e. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Based on #7557.
Fixes #7083. I think this is safe, but I'm not too sure if this doesn't have any breaking changes.
I believe the order of paths is not guaranteed, but some users might be relying on the current behaviour.
Note
Introduces tool overriding to control PATH precedence and applies it to
npmvsnode.overridesarray to registry schema (schema/mise-registry.json), codegen (build.rs), and runtime model (src/registry.rs)Toolset::list_pathsso tools listed inoverridescome earlier in PATHregistry.tomlto settools.npm.overrides = ["node"]e2e/registry/test_overridesverifying specifiednpmversion takes precedence regardless of tool declaration orderlist_pathscache-key refactor for stable sortingWritten by Cursor Bugbot for commit 0eacd68. This will update automatically on new commits. Configure here.