Skip to content

fix(npm): resolve unlocked latest to the npm version of a graph-locked install - #14307

Merged
jdx merged 1 commit into
mainfrom
fix/npm-latest-installed-aube-identity
Oct 11, 2026
Merged

jdx merged 1 commit into
mainfrom
fix/npm-latest-installed-aube-identity

Conversation

@jdx

@jdx jdx commented Oct 11, 2026 •

Copy link
Copy Markdown
Owner

https://entire.io/gh/jdx/mise/trails/649

Discussion #14276 reports this bug. Suppose a project with a lockfile installs an npm tool. mise builds that install from the locked dependency graph and stores it as <version>~aube~<digest>. Another config that asks for the same tool as latest without a lockfile (typically the global config) resolves latest to the newest installed version. It used that private directory name as the version:

PS (global)> mise ls
Tool            Version                              Source                      Requested
prettier        3.9.9 (missing) [prettier-djym7d3g]  ~\.config\mise\config.toml  latest
prettier        3.9.9 [prettier-zk2ialuk]
mise WARN  missing: prettier@3.9.9~aube~b9c537a2e2590f84
PS (global)> prettier -h
mise ERROR Tool not installed for shim: prettier

With install_layout = "identity", the suffixed name was hashed into an installation identity that nothing has. The tool showed as missing and its shims failed even though it was installed. With the legacy layout the tool still ran, but the private name leaked into mise current, env and warnings as if it were the version.

Now, when latest resolves to an installed version, mise uses the npm version (3.9.9) and keeps pointing at the installation it found. This is the same normalization mise lock got in #13542. In the identity layout the found name is a compatibility link, so the installation is looked up through that link rather than by re-hashing the name. Pinning prettier@3.9.9 in the global config was the workaround and is no longer needed.

Validation

  • New e2e backend/test_npm_aube_unlocked_latest: installs npm:inflight@latest from a lockfile in one directory, then resolves latest without a lockfile in another. It checks mise current, ls --missing, ls and env show no ~aube~ name or missing tool, and where points at the graph install. It runs under both layouts. Before the fix it failed in both (identity layout: 1.0.6 (missing) [inflight-jfo7ej4u]). With the compatibility-link lookup removed, the identity-layout where check fails.
  • test_npm_aube*, cli/test_install_layout*, and the tool_version/lockfile/install_layout/npm unit tests pass. One tool_version unit test, has_linked_version_ignores_mise_managed_absolute_targets, fails in this sandbox with and without this change. cargo clippy --workspace --all-features --all-targets -- -D warnings passes.

🤖 Generated with Claude Code

AI-assisted — Tool: Claude Code; model: anthropic/claude-opus-5-5; version: 2.1.295.


Note

Medium Risk
Touches core version resolution and install-path lookup for npm latest and aube-suffixed installs; behavior change is targeted but affects tool selection and shims globally when those installs exist.

Overview
Fixes unlocked latest for npm tools when the newest on-disk install is a lockfile graph directory named \<version\>~aube~\<digest\>. mise now strips that private suffix to the real npm version and keeps the resolved install path, instead of treating the directory name as the version (missing tools, broken shims under identity layout, ~aube~ leaking into current / ls / warnings elsewhere).

strip_install_path_identity resolves the install directory via physical_dir in the identity layout so a compatibility link name is not re-hashed into a nonexistent identity. resolve_version applies the same normalization when latest picks latest_installed_version.

Adds e2e test_npm_aube_unlocked_latest (locked vs unlocked configs, both install layouts).

Reviewed by Cursor Bugbot for commit aa615ea. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • Bug Fixes
    • Fixed version reporting for tools installed with identity-based layouts when an unlocked project uses latest. The detected version and resolved install path now match the installed tool, and commands no longer incorrectly report it as missing.

…d install

When the newest installed copy of an npm tool was one a lockfile built
(`<version>~aube~<digest>`), another config asking for `latest` without a
lockfile took that directory name as its version. In the identity install
layout it then hashed to an installation that does not exist, so the tool
was reported missing and its shims failed.

Strip the private suffix when `latest` resolves to an installed version, and
keep pointing at the installation the name links to.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Entire-Checkpoint: 01M4NPKK6MGCCQHTNN4CG27AHT
@coderabbitai

coderabbitai Bot commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository YAML (base), Central YAML (inherited), Organization UI (inherited)
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: bcd8e8ef-1f40-4ebf-887d-0dd6bedc7f7d

📥 Commits

Reviewing files that changed from the base of the PR and between 4251fe8 and aa615ea.


📒 Files selected for processing (2)
  • e2e/backend/test_npm_aube_unlocked_latest
  • src/toolset/tool_version.rs

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 1 remain after this review.



📝 Walkthrough

Walkthrough

The latest installed-version path now removes the private install identity suffix and restores the physical install directory. An end-to-end test covers npm:inflight in identity and legacy install layouts.

Changes

npm latest install paths

Layer / File(s) Summary
Normalize latest versions and verify install paths
src/toolset/tool_version.rs, e2e/backend/test_npm_aube_unlocked_latest
The latest installed-version shortcut strips install-path identity before returning the version. The regression test checks the reported version, missing status, displayed name, resolved path, and environment output in both layouts.

Priority: ⬇️ Low

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix

Suggested reviewers: risu729


Merge Risk: ⚪ Minimal · up to aa615

The change makes an unlocked latest request for an npm tool installed from a lockfile report the real npm version instead of the private install directory suffix. No actionable merge-blocking risk remains.

Pre-merge checks | Passed 4 | Failed 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 2 functions across 1 files. (1 skipped: 1… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the main change: resolving an unlocked npm latest request to the version from a graph-locked installation.
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.

Full details: Docstring Coverage

Explanation

Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 2 functions across 1 files. (1 skipped: 1 unsupported.)


  • Fix all pre-merge checks with AI
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@greptile-apps

greptile-apps Bot commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; no actionable defects were found.

Summary

Fixes unlocked latest requests that select an installed npm tool with a graph-specific directory name.

  • Unlocked npm latest uses the package version and its graph install.

Reviews (1) · Last reviewed commit: "fix(npm): resolve unlocked latest to the..." · Reviewed by Greptile

@jdx
jdx merged commit f0f41dc into main Oct 11, 2026
53 of 55 checks passed
@jdx
jdx deleted the fix/npm-latest-installed-aube-identity branch October 11, 2026 15:17
jdx added a commit that referenced this pull request Oct 11, 2026
#14307 fixed unlocked `latest` on main by stripping the `~aube~` suffix and
keeping the graph-locked install it found. Take that design, drop this
branch's competing one, and apply the same normalization where a prefix,
an installed match, or a package.json range picks from installed versions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Entire-Checkpoint: 01M4NRY8PFG55JYFZ092FK7QSN
@github-actions

Copy link
Copy Markdown
Contributor

Instruction counts

Nothing was compared, and so nothing was gated. No series appears on both sides: either the base has no measurements recorded, or the two were measured on different runner classes, which are deliberately not comparable — counts shift between machine types by more than a real regression does.

New, nothing to compare against: env on jdx-perf-v1-ubuntu24.04-x64-mise-rust1.97.1-img5263c143, hook-env on jdx-perf-v1-ubuntu24.04-x64-mise-rust1.97.1-img5263c143, ls on jdx-perf-v1-ubuntu24.04-x64-mise-rust1.97.1-img5263c143, registry on jdx-perf-v1-ubuntu24.04-x64-mise-rust1.97.1-img5263c143, startup on jdx-perf-v1-ubuntu24.04-x64-mise-rust1.97.1-img5263c143

Only instruction counts gate. Wall clock is shown for context — on identical hardware it moves 4-20% run to run.

Measured by tak — instruction-counted CLI benchmarks, stored in this repository's git notes.

aa615ea7cc10 vs 4251fe8c04c1 · measured on the runner, not pushed to the history.

jdx added a commit that referenced this pull request Oct 12, 2026
… layout (#14309)

<!-- entire-trail-link-start -->
https://entire.io/gh/jdx/mise/trails/651
<!-- entire-trail-link-end -->

This follows up on #14307, which fixed the npm tool reported missing in
discussion #14276. A project with
a `mise.lock` installs the tool from its locked dependency graph, which
is stored as `<version>~aube~<digest>`. Another config with no lockfile
then resolves `latest` against installed versions. #14307 made that
config report the npm version, but with `install_layout = "identity"` it
also made the config run the project's lockfile-built installation:

```
$ mise -C global current npm:inflight
1.0.6
$ mise -C global where npm:inflight
~/.local/share/mise/installs/inflight-p54rj3gd   # the project's graph install
```

The identity layout keeps a graph install to the lockfile that made it.
It already did that for a pinned version: `npm:inflight@1.0.6` in the
global config never used the project's install, but
`npm:inflight@latest` did. Now an unlocked request stands for the npm
version and gets its own plain installation. If that plain install is
missing, mise reports it as missing and `mise install` adds it, separate
from the project's. The project keeps running its graph install, and
later lockfile changes in the project no longer change what the global
config runs. The legacy layout is unchanged from #14307: it reports the
npm version and keeps using the directory that was found.

Requests by version prefix (`"npm:inflight" = "1.0"`), an installed
fuzzy match, and package.json semver ranges also resolve against
installed versions. Before this change they still took the private name
as the version:

```
$ mise -C global current npm:inflight   # "npm:inflight" = "1.0"
1.0.6~aube~883b3ea0f0f63119
```

They now get the same handling as `latest`. pipx tools installed from a
uv graph (`~uv~`) are treated the same way.

Note that `mise where npm:inflight` with no version re-resolves the
configured request without reading `mise.lock`. Inside a locked project
under the identity layout, it can therefore print the plain install
while `mise ls --current` and the shims use the graph install. This was
already true for pinned versions and is not changed here.

### Validation
- `backend/test_npm_aube_unlocked_latest` now runs `latest` and a `1.0`
prefix in an unlocked config under both layouts, next to a project
locked with `mise lock`. Identity layout: the version is reported
without `~aube~`, the unlocked config is missing until `mise install`
adds a separate plain install, the prefix request uses that plain
install, and the project still runs its graph install. Legacy layout:
both requests use the found directory. The test fails on `main`. With
only the fuzzy-match path left unconverted, the prefix case fails with
`1.0.6~aube~883b3ea0f0f63119`.
- `test_npm_aube*` and `cli/test_install_layout*` e2e tests, the
`tool_version`/`lockfile`/`install_layout`/`npm` unit tests, and `cargo
clippy --workspace --all-features --all-targets -- -D warnings` pass.
One `tool_version` unit test,
`has_linked_version_ignores_mise_managed_absolute_targets`, fails in
this sandbox with and without this change.
- The package.json range path is converted the same way but has no e2e
coverage.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

*AI-assisted — Tool: Claude Code; model: anthropic/claude-opus-5-5;
version: 2.1.295.*

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Changes core version resolution for npm aube and pipx uv graph
installs across multiple code paths; wrong behavior could mis-route
installs or PATH, though legacy layout is preserved and e2e coverage was
expanded.
> 
> **Overview**
> Fixes unlocked npm/pipx resolution when the only on-disk match is a
lockfile graph directory (`<version>~aube~<digest>` or `~uv~`).
**Identity layout** now treats `latest`, version prefixes, fuzzy
matches, and package.json semver ranges as the **logical npm version**
and resolves them to a **separate plain install**, not the locked
project’s graph tree—so they show as missing until `mise install` and
the lockfile project keeps its graph install. **Legacy layout** behavior
is unchanged: report the logical version but keep using the directory
that was found.
> 
> Implementation refactors install-path stripping into
`install_path_identity_version()` and routes all “pick from installed
versions” paths through new **`from_installed()`**, which either
re-resolves under identity layout or calls
`strip_install_path_identity()` as before.
> 
> The **`test_npm_aube_unlocked_latest`** e2e test now locks via `mise
lock`, adds a **`1.0` prefix** config, and asserts identity vs legacy
`where` paths and that the locked project still runs its graph install.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
0655288. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Unlocked, `latest`, and version-prefix requests now resolve
consistently with identity-based and legacy installation layouts.
* Locked projects continue using their graph-specific installations,
while identity-based layouts keep unlocked installs separate from locked
installs.
* Tool listings no longer expose internal graph identifiers for unlocked
requests.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
jdx pushed a commit that referenced this pull request Oct 13, 2026
<!-- entire-trail-link-start -->
https://entire.io/gh/jdx/mise/trails/640
<!-- entire-trail-link-end -->

### 🚀 Features

- **(bootstrap)** add apt repositories and PPAs with
[bootstrap.apt.sources] by @jdx in
[#14306](#14306)
- **(bootstrap)** embed apt source signing keys inline in .sources files
by @jdx in [#14348](#14348)
- **(bootstrap)** render templates in [bootstrap.repos] checkout paths
by @jdx in [#14359](#14359)
- **(bootstrap)** declare macOS login items by @jdx in
[#14354](#14354)
- **(bootstrap)** layer dnf packages with rpm-ostree on Fedora Atomic
hosts by @jdx in [#14357](#14357)
- **(bootstrap)** support run0 and doas for privilege elevation by @jdx
in [#14351](#14351)
- **(dotfiles)** show tracked entries that no variant selects in status
by @jdx in [#14266](#14266)
- **(dotfiles)** group mise dot status into sections by @jdx in
[#14361](#14361)
- **(github)** add oauth_copy_code to copy the device code to the
clipboard by @jdx in [#14269](#14269)
- **(github)** keep the native OAuth refresh token in a password manager
or keychain by @jdx in [#14360](#14360)
- **(http)** add libc() and avx2() template functions by @jdx in
[#14298](#14298)
- **(pypi)** read upload times from PEP 691 simple indexes by @jdx in
[#14352](#14352)
- **(rust)** download rustup-init from RUSTUP_UPDATE_ROOT by @jdx in
[#14356](#14356)

### 🐛 Bug Fixes

- **(aqua)** warn once for packages with no repo to list versions from
by @jdx in [#14336](#14336)
- **(backend)** keep decoded download file names inside the download
directory by @JamBalaya56562 in
[#14274](#14274)
- **(bootstrap)** support dnf package groups by @jdx in
[#14347](#14347)
- **(bootstrap)** don't print a stdin-only helper as a manual command by
@jdx in [#14350](#14350)
- **(bootstrap)** explain why dnf packages cannot install on ostree
hosts by @jdx in [#14353](#14353)
- **(brew)** install bottle etc files and run post_install_steps when
pouring by @jdx in [#14272](#14272)
- **(brew)** refuse to chown a prefix owned by another user by @jdx in
[#14345](#14345)
- **(brew)** preserve protected post-install copy destinations by @jdx
in [#14311](#14311)
- **(brew-cask)** support generate completions in evaluated casks by
@pmmmwh in [#14255](#14255)
- **(docker)** install libatomic1 in the Debian image so Node.js 26 runs
by @jdx in [#14303](#14303)
- **(dotfiles)** keep existing ciphertext when an encrypted file is
unchanged by @jdx in [#14300](#14300)
- **(dotfiles)** stop a newly selected profile from deleting its shared
file by @jdx in [#14275](#14275)
- **(dotfiles)** restore config entries when adopting under a symlinked
home by @jdx in [#14315](#14315)
- **(dotfiles)** show per-file diffs for directory copy entries in dot
diff by @jdx in [#14333](#14333)
- **(dotfiles)** apply mise's own config before planning the other
entries by @jdx in [#14339](#14339)
- **(go)** read an exact go line in go.mod as the toolchain version by
@jdx in [#14308](#14308)
- **(http)** retry artifact downloads started by mise exec and shims by
@jdx in [#14342](#14342)
- **(npm)** resolve unlocked latest to the npm version of a graph-locked
install by @jdx in [#14307](#14307)
- **(npm)** give unlocked requests their own install under the identity
layout by @jdx in [#14309](#14309)
- **(oci)** ignore host apt hooks when installing apt bootstrap packages
by @jdx in [#14296](#14296)
- **(packslip)** let a workflow list carry the signer pin across a
workflow move by @jdx in
[#14338](#14338)
- **(packslip)** keep a renamed project's lockfile signer under packslip
1.7 by @jdx in [#14362](#14362)
- **(prune)** keep a uv-locked pipx tool's installs for every Python by
@jdx in [#14335](#14335)
- **(prune)** keep versions of task-scoped tools by @jdx in
[#14334](#14334)
- **(schema)** accept [bootstrap.secrets] by @JamBalaya56562 in
[#14322](#14322)
- **(schema)** accept [bootstrap.groups] and [bootstrap.users] by
@JamBalaya56562 in [#14340](#14340)
- **(schema)** accept table forms for vars._.file and vars._.source by
@JamBalaya56562 in [#14383](#14383)
- **(shim)** keep other projects' command wrappers on reshim by @jdx in
[#14355](#14355)
- **(task)** bump gix so remote tasks work in git hooks by @any-victor
in [#14316](#14316)
- hide the Windows console for the tool-update service by @jdx in
[#14344](#14344)

### 🚜 Refactor

- replace serde_yaml with serde-saphyr by @jdx in
[#14328](#14328)

### 📚 Documentation

- **(bootstrap)** explain when to use --from versus --adopt by @jdx in
[#14301](#14301)
- **(bootstrap)** recommend fish = "auto" for fish shell activation by
@jdx in [#14304](#14304)
- **(dotfiles)** explain when to use sync, pull, and apply by @jdx in
[#14343](#14343)
- **(shims)** explain how to check whether a tool is installed by @jdx
in [#14337](#14337)
- switch body text to DM Sans and tighten the releases page by @jdx in
[#14302](#14302)
- self-host DM Sans and JetBrains Mono so they load on mise.jdx.dev by
@jdx in [#14310](#14310)

### 🧪 Testing

- **(bootstrap)** don't depend on bc being absent in the apt sources
test by @jdx in [#14371](#14371)

### 📦️ Dependency Updates

- install Homebrew uses_from_macos dependencies on Linux by @itskyf in
[#14288](#14288)
- bump packslip to 1.7.0 by @jdx in
[#14363](#14363)
- lock file maintenance by @renovate[bot] in
[#14320](#14320)
- update github actions by @renovate[bot] in
[#14380](#14380)
- update dependency python to v3.14.8 by @renovate[bot] in
[#14381](#14381)
- update rust crates by @renovate[bot] in
[#14382](#14382)
- update docker images by @renovate[bot] in
[#14379](#14379)

### 📦 Registry

- install opencode 2 from npm by @jdx in
[#14295](#14295)

### Chore

- **(ci)** switch to jactionlint v2 and drop zizmor by @jdx in
[#14256](#14256)
- fix CI for the aqua registry update and the unsafe-libyaml advisory by
@jdx in [#14326](#14326)

### Ci

- double the unit-macos job timeout by @jdx in
[#14346](#14346)

### Security

- **(config)** require trust for path: tool versions by @jdx in
[#14358](#14358)

### New Contributors

- @pmmmwh made their first contribution in
[#14255](#14255)
- @itskyf made their first contribution in
[#14288](#14288)

## 📦 Aqua Registry Updates

### New Packages (3)

- [`Alice39s/xmtr`](https://github.com/Alice39s/xmtr)
-
[`bufbuild/protoschema-plugins/protoc-gen-jsonschema`](https://github.com/bufbuild/protoschema-plugins)
- [`takumin/qemu-bin`](https://github.com/takumin/qemu-bin)

### Updated Packages (3)

- [`bootandy/dust`](https://github.com/bootandy/dust)
-
[`connectrpc/connect-go/protoc-gen-connect-go`](https://github.com/connectrpc/connect-go)
- [`sharkdp/hyperfine`](https://github.com/sharkdp/hyperfine)
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.

1 participant