Skip to content

feat(runtime): node version switching and aube self-version management#861

Merged
jdx merged 7 commits into
mainfrom
feat/node-runtime-switching
Jun 11, 2026
Merged

feat(runtime): node version switching and aube self-version management#861
jdx merged 7 commits into
mainfrom
feat/node-runtime-switching

Conversation

@jdx

@jdx jdx commented Jun 11, 2026

Copy link
Copy Markdown
Owner

Projects can now pin Node (devEngines.runtime / .node-version / .nvmrc) and aube itself (packageManager / devEngines.packageManager); aube applies both with no shims or shell activation — running through aube is the switch. mise users keep a single store on disk: installed versions under ~/.local/share/mise/installs/ are reused read-only, and missing installs are delegated to mise install by default.

Node runtime switching

  • Version sources, highest precedence first: devEngines.runtime (OpenJS spec, pnpm 10.14+/11 compatible) > .node-version > .nvmrc (searched upward, stopping at $HOME). Requests: exact versions, ranges, lts, latest, codenames (lts/jod).
  • Resolution order (zero network on the hot path): PATH node if it satisfies → installed versions (mise installs — skipping incomplete markers and alias symlinks — plus aube's own ~/.local/share/aube/nodejs/) → download per the new runtimeInstaller setting (auto|mise|aube, default auto = delegate to mise install node@<exact> when mise is on PATH, falling back to a SHASUMS256-verified nodejs.org download; nodeDownloadMirrors.release is now honored).
  • Injection at every spawn site: aube run/aubr, aube exec (incl. #!/usr/bin/env node shebangs), aubx/dlx (resolved from the original cwd, not the scratch dir), root + dep lifecycle scripts, and the build jail. npm_node_execpath/NODE exported npm-style. Projects with no pin behave exactly as before — not even a node --version probe.
  • Lockfile pins: devEngines resolutions are recorded using pnpm 10.14+'s exact node@runtime: shape (importer dep + variations resolution with per-platform URLs and sha256- integrity), verified against pnpm's own types. Parsing these entries doubles as a compat fix — aube previously misread pnpm-11 lockfiles carrying them as registry deps. npm/yarn/bun formats skip the pin (warned once per install).
  • Engines interplay: engines.node/engineStrict now validate against the switched node; the nodeVersion setting stays validation-only (pnpm semantics). runtimeOnFail (download|error|warn|ignore) is the air-gapped-CI override; version files default to download, bare devEngines to the spec's error.
  • CLI: aube runtime set node <version> (pnpm 11 parity — writes devEngines, installs, records the pin; -g delegates to mise use -g) and aube runtime list; aube doctor gains node-source/node-requested/node-provenance/node-bin rows.

aube self-version switching (corepack semantics)

  • managePackageManagerVersions — previously a documented no-op stub — now does what it says (default on, pnpm 10 parity): when the running aube doesn't satisfy devEngines.packageManager (ranges) or packageManager: "aube@<exact>", aube locates the pinned version (mise installs reused; GitHub release archives downloaded into ~/.local/share/aube/self/ otherwise) and re-execs it with the same argv, preserving the aube/aubr/aubx multicall name. Runs before dispatch, so aube install and chained auto-installs all execute under the pinned binary. A guard env degrades a broken install to a warning — never an exec loop.
  • The startup guard's packageManagerStrictVersion hard-error is superseded while switching is on; managePackageManagerVersions=false restores validation-only behavior.
  • Self-downloads are verified against GitHub's server-computed release asset digests (assets[].digest, tamper-evident under immutable releases) — covering every release that already exists, with no checksum publishing needed in the release workflow. Release metadata (and the full version list for range pins) comes from mise-versions.jdx.dev — CDN-cached, no rate limits, the same service mise consults — with the GitHub API as the fallback (honoring GITHUB_TOKEN/GH_TOKEN, attached only to the real api.github.com host), then .sha256 siblings for custom mirrors, then TLS-only. Returned metadata must echo the requested tag. Everything degrades gracefully on misses rather than failing an install.
  • Intel macOS has no published release asset, so self-download errors there with a pointer to mise.

Testing

  • 1,853 unit tests; cargo clippy --all-targets -- -D warnings and cargo fmt --check clean.
  • 37 new bats tests across runtime.bats (hermetic: source precedence, mise discovery/incomplete/symlink rules, onFail matrix, pnpm-11 lockfile compat), runtime_download.bats (local static mirror: download, checksum-mismatch rejection, mise-stub delegation + fallback, pin recording, offline pin reuse), and self_version.bats (re-exec, multicall preservation, range→best-installed, loop guard, setting off).
  • One existing test updated intentionally: guardrails.bats's packageManagerStrictVersion rejects mismatched aube version now pins manage-package-manager-versions=false, since rejection-on-mismatch is no longer the default — switching is.
  • Review findings from cursor/greptile addressed in follow-up commits: version-less devEngines.runtime entries no longer read as drift, branch-lockfile merges union runtime variants across platforms, and alias specs (lts/latest) consult the index under every onFail policy before concluding mismatch — each with a regression test.
  • Manually verified on a real machine: .nvmrc resolving to a mise-installed node with zero downloads, and a packageManager: "aube@1.16.1" pin re-exec'ing mise's installed 1.16.1.

🤖 Generated with Claude Code


Note

High Risk
Touches every script/binary spawn path, download integrity, and optional self-re-exec; lockfile/runtime drift behavior affects frozen CI installs.

Overview
Adds built-in Node.js runtime management so projects no longer depend on mise shims for version switching: pins from devEngines.runtime, .node-version, or .nvmrc are resolved (PATH → mise/aube installs → verified download), injected when running scripts and binaries, and optionally recorded in lockfiles using pnpm 11’s node@runtime: / variations encoding.

The new aube-runtime crate owns discovery, nodejs.org index/SHASUMS fetching, extraction, mise delegation (runtimeInstaller), and related settings (nodeDownloadMirrors, runtimeOnFail). aube manifest gains tolerant devEngines parsing; aube-lockfile lifts runtime pins out of ordinary deps (fixing mis-read of pnpm 10.14+ lockfiles), round-trips them, merges cross-platform variants, and treats manifest/lock runtime drift on existing pins only.

CLI surface: aube runtime list / runtime set node <version> (writes devEngines, installs, updates pins). Docs and usage KDL updated; stable runtime error/warning codes added. managePackageManagerVersions is wired to re-exec a pinned aube binary (mise reuse + GitHub/mise-versions downloads) per the broader PR scope beyond the lockfile/manifest hunks shown.

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

Summary by CodeRabbit

  • New Features

    • Added aube runtime (list/set) to inspect, pin, and install project Node versions; per-project Node switching (devEngines.runtime, .node-version, .nvmrc) with ranges/LTS/latest support.
    • Runtime installer delegation (mise vs aube), on-fail policies, and optional self-version switching for pinned aube.
  • Behavior Changes

    • Resolved Node is injected into script PATH/env and used for engine checks; lockfiles now record resolved Node pins for offline reuse.
  • Documentation

    • Full docs for runtime switching, CLI, lockfile pins, settings, and mirrors.
  • Tests

    • End-to-end and unit tests added/expanded for runtime and self-version flows.

Projects can now pin Node (devEngines.runtime / .node-version /
.nvmrc) and aube itself (packageManager / devEngines.packageManager);
aube applies both with no shims or shell activation — running through
aube is the switch.

Node runtime switching:
- resolution order: PATH node, installed versions (mise installs are
  reused read-only; aube's own dir), then download per the new
  runtimeInstaller setting (auto = delegate to mise when present)
- resolved bin dir injected at every spawn site: run/aubr, exec
  shebangs, aubx/dlx, lifecycle scripts, and the build jail
- devEngines pins are recorded in the lockfile using pnpm 10.14+'s
  node@runtime: shape; parsing those entries also fixes reading
  pnpm-11 lockfiles that carry them
- engines checks validate against the switched node; nodeVersion
  stays validation-only (pnpm semantics)
- new `aube runtime set|list` (pnpm 11 parity), doctor rows, and
  runtimeOnFail as the air-gapped opt-out

aube self-version switching (corepack semantics):
- managePackageManagerVersions (previously a documented no-op,
  default true) now locates or installs the pinned aube — mise
  installs reused, GitHub release downloads otherwise — and re-execs
  it with the same argv, preserving the aube/aubr/aubx multicall name
- a guard env degrades broken installs to a warning, never a loop
- release workflow now publishes a .sha256 per asset; self-downloads
  verify against it

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f7294b75-04c2-4b21-937b-d1f7216b785a

📥 Commits

Reviewing files that changed from the base of the PR and between d35c4dd and 0075d96.

📒 Files selected for processing (19)
  • README.md
  • crates/aube-codes/src/errors.rs
  • crates/aube-lockfile/src/merge.rs
  • crates/aube-runtime/src/discover.rs
  • crates/aube-runtime/src/error.rs
  • crates/aube-runtime/src/index.rs
  • crates/aube-runtime/src/paths.rs
  • crates/aube-runtime/src/self_install.rs
  • crates/aube-runtime/src/shasums.rs
  • crates/aube-settings/settings.toml
  • crates/aube/src/commands/install/mod.rs
  • crates/aube/src/commands/runtime.rs
  • crates/aube/src/runtime.rs
  • crates/aube/src/self_version.rs
  • docs/error-codes.data.json
  • docs/package-manager/node-runtime.md
  • docs/pnpm-users.md
  • docs/settings/index.md
  • test/runtime.bats
✅ Files skipped from review due to trivial changes (4)
  • README.md
  • docs/package-manager/node-runtime.md
  • docs/pnpm-users.md
  • docs/settings/index.md
🚧 Files skipped from review as they are similar to previous changes (12)
  • crates/aube-runtime/src/error.rs
  • crates/aube-lockfile/src/merge.rs
  • crates/aube-runtime/src/shasums.rs
  • crates/aube-runtime/src/paths.rs
  • crates/aube/src/self_version.rs
  • crates/aube-runtime/src/discover.rs
  • crates/aube-settings/settings.toml
  • crates/aube/src/commands/runtime.rs
  • crates/aube-runtime/src/index.rs
  • test/runtime.bats
  • crates/aube/src/commands/install/mod.rs
  • crates/aube-runtime/src/self_install.rs

📝 Walkthrough

Walkthrough

Adds manifest/devEngines parsing, a new aube-runtime crate for Node resolution/installation, lockfile runtime-pin support, CLI runtime commands, wiring to switch/propagate Node runtimes, and comprehensive tests and docs.

Changes

Node runtime switching

Layer / File(s) Summary
Contracts, settings, and CLI surface
README.md, aube.usage.kdl, crates/aube-codes/*, crates/aube-manifest/src/*, crates/aube-settings/settings.toml, docs/*
Adds devEngines.runtime parsing, OnFail policies, workspace settings, CLI spec for aube runtime, error/warning codes, and documentation.
Runtime crate core
crates/aube-runtime/Cargo.toml, crates/aube-runtime/src/*
New crate implements discovery, spec parsing, platform detection, HTTP/index fetching, SHASUM handling, extraction, installers, variant mapping, resolver, and self-install logic.
Lockfile runtime pins and pnpm IO
crates/aube-lockfile/src/*, crates/aube-resolver/src/*
Adds typed runtimes to LockfileGraph, pnpm read/write support for type: variations runtime pins, merge union logic, and drift detection.
CLI wiring and propagation
crates/aube/src/*, crates/aube/Cargo.toml, crates/aube-scripts/src/*, test/*
Wires runtime ensure into commands, script settings, child env/path propagation, self-version re-exec, aube runtime command, and end-to-end Bats tests.

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant Manifest
  participant Runtime
  participant Index
  participant Installer
  CLI->>Manifest: read devEngines.runtime / packageManager
  CLI->>Runtime: ensure_for_cwd / ensure
  Runtime->>Index: load_index / load_shasums
  Runtime->>Installer: install (mise or aube)
  Installer->>Runtime: installed node path + version
  Runtime->>CLI: provide path_entries, node_program, and apply_child_env
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~90+ minutes

Poem

🐇 I sniff the index, hop and browse,
Pins and SHASUMS in my house.
I fetch, I stash, I stitch with care,
Aube finds Node — I clap my hare.
Carrots, builds, and tests — joy shared.

Comment thread crates/aube-lockfile/src/drift.rs
Comment thread crates/aube-lockfile/src/merge.rs
GitHub computes a server-side SHA-256 for every release asset and
exposes it as `assets[].digest` (tamper-evident under immutable
releases), so self-version downloads verify against that instead of
aube publishing checksum files — covering every release that already
exists, with no release-workflow change. A `.sha256` sibling remains
the fallback for custom mirrors, then TLS-only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a new aube-runtime crate and wires it through the full command surface to give projects zero-shim Node.js version switching (from devEngines.runtime, .node-version, .nvmrc) and corepack-style aube self-version re-exec (packageManager / devEngines.packageManager). It also extends the pnpm lockfile format to record and verify typed node@runtime: pins (pnpm 10.14+/11 shape) and adds aube runtime list/set CLI commands.

  • Runtime resolution & install: PATH probe → installed scan (mise + aube's own store) → index-backed download with SHA-256 verification; onFail policies and runtimeOnFail override gate downloads vs. metadata fetches independently; alias specs (lts, latest, codenames) correctly consult the network index before applying warn/error policy.
  • Lockfile integration: pnpm-shaped lockfiles gain RuntimePin / RuntimeVariant typed structs; importer node@runtime: deps are lifted out of the package graph on read and re-emitted on write; drift detection flags changed or dropped specifiers; lockfile_node_pin gates a full YAML parse behind a cheap substring probe.
  • Self-version switching: maybe_switch runs before command dispatch, re-execs into the pinned aube binary (unix execvp, Windows spawn+exit), with a version-scoped loop guard and graceful onFail degradation.

Confidence Score: 4/5

Safe to merge with one fix recommended: lockfile_node_pin in runtime.rs should probe only the file parse_lockfile will actually read, to avoid silently dropping a pin when both aube-lock.yaml and pnpm-lock.yaml coexist.

The implementation is thorough and well-tested. The core download, verification, and re-exec logic is correct. One reproducibility defect exists: lockfile_node_pin probes both lockfiles for the runtime-pin substring but parse_lockfile only reads the highest-precedence file — if an older aube-lock.yaml exists without a pin while pnpm-lock.yaml has one, frozen installs silently resolve from the range instead of the exact pinned version.

crates/aube/src/runtime.rs — the lockfile_node_pin probe/parse inconsistency.

Important Files Changed

Filename Overview
crates/aube/src/runtime.rs Process-global Node runtime context and lockfile pin integration. Contains a probe/parse mismatch in lockfile_node_pin: the any() check spans both aube-lock.yaml and pnpm-lock.yaml, but parse_lockfile only reads the highest-precedence existing file, so the pin can be silently missed during upgrade scenarios.
crates/aube-runtime/src/resolver.rs New Node resolution state machine: zero-network fast paths for exact/range specs, index-backed resolution for alias specs (lts/latest/codename), and correct onFail policy gating. The alias-spec fix (locally_decidable guard) addresses the previously reported false-positive warn path.
crates/aube-runtime/src/self_install.rs aube self-download: GitHub asset digest (tamper-evident) as primary checksum, .sha256 sidecar fallback for custom mirrors, TLS-only last resort. Double-checked locking with FSLock, atomic rename, and loop guard. Logic is sound.
crates/aube/src/self_version.rs corepack-style aube self-version switch: extract_pin, version satisfaction check, install/re-exec flow, and version-scoped loop guard. Logic is correct; onFail policies for all four paths behave as documented.
crates/aube-runtime/src/installer.rs Node runtime download/verify/extract with FSLock TOCTOU guard, SHA-256 checksum enforcement, atomic rename publish, and 24h GC. All invariant-annotated expect() calls are sound.
crates/aube-runtime/src/http.rs Thin retry HTTP layer: validate_url allows http:// in addition to https://, which undermines checksum protection when a user-configured mirror is an HTTP URL.
crates/aube-lockfile/src/pnpm/read.rs Adds pnpm 10.14+ runtime: dep parsing: strips node@runtime: importer entries into graph.runtimes, converts variations blocks into typed RuntimeVariant structs. The compat fix for pnpm-11 lockfiles is well-integrated.
crates/aube-lockfile/src/pnpm/write.rs Emits runtime pin packages entries in pnpm 10.14+ shape (node@runtime: with variations block). The annotated expect() at the single-entry bin map is safe.
crates/aube-lockfile/src/drift.rs Adds runtime_drift_reason: correctly flags a changed specifier or removed devEngines entry as drift, deliberately does NOT flag missing pin to avoid false positives from non-pnpm lockfile formats.
crates/aube-manifest/src/dev_engines.rs Tolerant devEngines parser: single-object or array forms, unknown slots pass through via extra BTreeMap, OnFail enum covers all spec values.
crates/aube/src/startup.rs packageManagerStrictVersion guard correctly superseded when managePackageManagerVersions=true. Logic is consistent with the PR design intent.

Reviews (4): Last reviewed commit: "fix(runtime): address second-round revie..." | Re-trigger Greptile

Comment thread crates/aube-runtime/src/resolver.rs Outdated
Comment thread crates/aube-lockfile/src/pnpm/write.rs Outdated
Comment thread crates/aube-runtime/src/resolver.rs Outdated
The digest lookup is the only GitHub API call in runtime switching
and already degrades gracefully on 403/429, but unauthenticated calls
share a 60/hr per-IP limit — exactly where cold self-installs cluster
(ephemeral CI runners, NATed offices). Attach GITHUB_TOKEN/GH_TOKEN
when present, and only for the real api.github.com host so an
AUBE_SELF_API_BASE override can never siphon the token.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread crates/aube/src/runtime.rs
Jeff Dickey and others added 2 commits June 11, 2026 15:57
mise-versions.jdx.dev serves a CDN-cached, rate-limit-free mirror of
the release version list and GitHub release metadata (including asset
digests) — the same service mise itself consults. Use it as the
primary source for self-version pins:

- range pins now resolve against the full published version list
  (max-satisfying) instead of only the latest announcement
- digest lookups hit the versions-host release proxy first; the
  GitHub API (with GITHUB_TOKEN) is the fallback, then .sha256
  siblings for mirrors, then TLS-only
- release metadata must echo the requested tag, guarding against
  stale or mis-keyed proxy cache entries

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- drift: a devEngines.runtime entry that names node but declares no
  version carries no concrete range — resolution ignores it, so it no
  longer reads as a removed pin (frozen installs stopped failing over
  a field that changes nothing) [cursor]
- merge: same-version runtime pins now union per-platform variants
  (dst wins on target collisions) and report specifier disagreements
  instead of silently dropping the incoming side [cursor]
- resolver: alias specs (lts/latest/codenames) consult the dist index
  under every onFail policy before concluding mismatch — an installed
  node that IS the latest LTS no longer draws a false warning; under
  warn/ignore an unreachable index degrades instead of blocking
  [greptile]
- style: annotate the bin-form unwrap, drop the dummy 0.0.0 version
  by making DownloadProgress::on_phase take Option<&Version>
  [greptile]

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@cursor cursor 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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 2a13e87. Configure here.

Comment thread crates/aube/src/commands/install/mod.rs Outdated

@coderabbitai coderabbitai 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.

Actionable comments posted: 15

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
.rules (1)

26-38: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add aube-util to the crate inventory.

Cargo.toml still lists aube-util as a workspace crate, so this inventory no longer describes the full workspace. If the cardinality in crates[12] is meant to stay accurate, it needs to become crates[13] once the missing entry is added.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.rules around lines 26 - 38, The crate inventory block labeled
"crates[12]{name,role}" is missing the aube-util entry (Cargo.toml still
references aube-util), so either add an entry for "aube-util" with its short
role description into that list or update the cardinality to "crates[13]" and
append the new line; locate the inventory block (the header
"crates[12]{name,role}" and the following crate lines) and insert a concise
"aube-util,..." line describing its role or increment the index and include the
aube-util entry to keep the workspace inventory consistent with Cargo.toml.
crates/aube-scripts/src/lib.rs (1)

887-920: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Jailed scripts still lose npm_node_execpath and NODE.

These vars are set before apply_jail_env(), but apply_jail_env() calls env_clear(). That means root scripts keep the switched-node env, while jailed dependency scripts silently drop it, which breaks the new “export node execpath for every script” behavior.

Suggested fix
 fn apply_script_settings_env(cmd: &mut tokio::process::Command, settings: &ScriptSettings) {
     // Strip credentials that aube itself owns before we spawn any
     // lifecycle script. AUBE_AUTH_TOKEN is aube's own registry login
     // token. No transitive postinstall has any business reading it.
@@
     if settings.shell_emulator {
         cmd.env("npm_config_shell_emulator", "true");
     }
+    if let Some(node_exe) = settings.node_exe.as_deref() {
+        cmd.env("npm_node_execpath", node_exe)
+            .env("NODE", node_exe);
+    }
 }
@@
-    // Set after the jail's env_clear (builder env calls compose in
-    // order), so jailed builds see the pinned node too.
-    if let Some(node_exe) = &settings.node_exe {
-        cmd.env("npm_node_execpath", node_exe).env("NODE", node_exe);
-    }
-
     // Pass INIT_CWD the way npm/pnpm do — the directory the user
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aube-scripts/src/lib.rs` around lines 887 - 920, The node execpath env
vars (npm_node_execpath and NODE) are set on cmd before apply_jail_env(), but
apply_jail_env() calls env_clear() which removes them for jailed scripts; update
the code so the node execpath is preserved for jailed scripts by moving the
logic that sets npm_node_execpath/NODE to after apply_jail_env() is invoked (or
alternatively, modify apply_jail_env() to re-add these vars when
settings.node_exe is present). Specifically adjust the block that references
settings.node_exe and node_exe so it runs after apply_jail_env(&mut cmd, ...),
or ensure apply_jail_env preserves or re-inserts those env vars when jail is
Some.
crates/aube/src/commands/install/fetch.rs (1)

91-103: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update the spawn error text to match runtime switching behavior.

After Line 91 switched to crate::runtime::node_program(), Line 101’s "from PATH" wording can be incorrect when Node comes from a resolved/pinned runtime. Please make the message runtime-source agnostic (or include the resolved program path).

Suggested patch
-                    miette!(
-                        "execute {} with Node.js from PATH: {e}{chain}",
-                        local.specifier()
-                    )
+                    miette!(
+                        "execute {} with resolved Node.js runtime: {e}{chain}",
+                        local.specifier()
+                    )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aube/src/commands/install/fetch.rs` around lines 91 - 103, The spawn
error message for the Command created via
tokio::process::Command::new(crate::runtime::node_program()) is misleading
because it hardcodes "from PATH"; update the miette! error construction in the
.map_err closure to be runtime-source agnostic by either removing "from PATH" or
including the resolved program path (crate::runtime::node_program() or a local
variable holding it) in the message so it accurately reflects whether Node was
taken from PATH or a pinned runtime; adjust the miette! invocation that
references local.specifier() accordingly.
crates/aube/src/commands/exec.rs (1)

296-315: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Mirror the runtime injection in the non-status exec path.

This helper still launches the single-package path without the runtime PATH prepend or apply_child_env, so direct aube exec <bin> can miss the switched Node while the parallel/status path works. That breaks #!/usr/bin/env node shims and also leaks into callers like aube run <bin>.

Suggested fix
     } else {
         let exec_path = resolve_exec_shim(bin_path);
         let mut cmd = tokio::process::Command::new(exec_path);
         cmd.args(args);
+        let runtime_dirs = crate::runtime::path_entries();
+        if !runtime_dirs.is_empty() {
+            cmd.env("PATH", aube_scripts::prepend_paths(&runtime_dirs));
+        }
         cmd
     };
+    crate::runtime::apply_child_env(&mut command);
     let status = command
         .current_dir(cwd)
         .stderr(aube_scripts::child_stderr())
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aube/src/commands/exec.rs` around lines 296 - 315, The non-status exec
branch that builds the tokio::process::Command (created via resolve_exec_shim)
currently skips the runtime PATH injection and environment adjustments; update
that branch so it mirrors the status/parallel path by computing bin_dir =
super::project_modules_dir(cwd).join(".bin"), combining it with
crate::runtime::path_entries(), building new_path via
aube_scripts::prepend_paths(&path_dirs), setting cmd.env("PATH", &new_path), and
then calling the same apply_child_env(command) (or the appropriate function used
elsewhere to inject runtime env) on the built Command before returning; keep the
existing cmd.args(args) and ensure you reference resolve_exec_shim,
project_modules_dir, crate::runtime::path_entries, aube_scripts::prepend_paths,
and apply_child_env to locate where to apply these changes.
crates/aube/src/commands/dlx.rs (1)

149-167: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Resolve the runtime before the local-bin fast path returns.

aube dlx <cmd> can return through the local node_modules/.bin shortcut before Line 167 runs, so that execution path never honors the project's runtime pin. The new resolution needs to happen before any executable return, not just before the scratch-dir flow.

Suggested fix
     // Bin name is only used in the non-shell path. Under shell-mode the
     // user assembles their own line and we run it through `sh -c`, so any
     // bin lookup is the shell's job.
     let bin_name = bin_name_for(&command);
+    crate::runtime::ensure_for_cwd(&crate::dirs::cwd()?).await?;
     if !explicit_package && !shell_mode && can_use_local_bin(&command) {
         let initial_cwd = crate::dirs::cwd()?;
         if let Some(project_dir) = crate::dirs::find_project_root(&initial_cwd) {
             let bin_path = super::project_modules_dir(&project_dir)
                 .join(".bin")
@@
-    crate::runtime::ensure_for_cwd(&crate::dirs::cwd()?).await?;
-
     let tmp = tempfile::Builder::new()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aube/src/commands/dlx.rs` around lines 149 - 167, The local-bin fast
path can return before runtime resolution runs, so move the call to
crate::runtime::ensure_for_cwd(&crate::dirs::cwd()?).await? to occur before the
early return branch: call ensure_for_cwd right after computing initial_cwd (or
at the top of the block guarded by !explicit_package && !shell_mode &&
can_use_local_bin(&command)) and before checking project root / bin_path and
before invoking super::exec::exec_bin, so that the runtime is resolved for the
user's project (respecting .nvmrc/devEngines) regardless of whether exec_bin
returns early.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/aube-lockfile/src/merge.rs`:
- Around line 267-284: The runtime merge loop in merge.rs (the for (name,
incoming) in src.runtimes and Entry::Occupied branch) only checks
slot.get().version against incoming.version and therefore misses conflicts in
other fields; update the Occupied branch to compare all relevant Runtime fields
(specifier, dev, has_bin, variants, and version) between slot.get() and
incoming, and if any of those differ push a clear conflict message (e.g.,
"runtime `{}`: kept <existing> over <incoming> (diffs: ...)" or list which
fields differ) while still retaining the existing entry; ensure equality across
all fields avoids reporting a conflict.

In `@crates/aube-runtime/src/discover.rs`:
- Around line 117-123: The discovery currently accepts any regular file at
node_bin (node_paths_in) which lets non-executable/corrupt extracts pass
validate_install and enter the installed-runtime set; update the check that
currently uses node_bin.is_file() to also verify the file is executable
(platform-appropriate permission check) so validate_install() rejects files
without execute bits before selecting the runtime. Locate the node_paths_in /
discovery branch in discover.rs and replace the simple is_file() test with a
combined is_file() && is_executable check (using
std::os::unix::fs::PermissionsExt or equivalent on Windows) so non-executable
node binaries are filtered out.

In `@crates/aube-runtime/src/error.rs`:
- Around line 57-74: The Error::Io variant is currently mapped to
errors::ERR_AUBE_RUNTIME_DOWNLOAD_FAILED in impl Error::code(), which
misclassifies lock/acquire, mkdir, archive-open, and publish/rename I/O
failures; change this by either (A) adding a new stable code constant (e.g.
errors::ERR_AUBE_RUNTIME_IO) and return it from Error::Io in Error::code(), or
(B) split Error::Io into narrower variants (e.g. IoLock, IoCreateDir,
IoOpenArchive, IoPublishRename) and map each new variant to its own ERR_AUBE_*
constant; update the errors module to declare any new ERR_AUBE_* identifiers and
adjust any tests or places that pattern-match on Error::Io or expect the old
code.

In `@crates/aube-runtime/src/index.rs`:
- Around line 86-173: The code in load_index (and similarly install /
download_verify_extract) performs blocking std::fs I/O on async paths (e.g.,
std::fs::read, std::fs::create_dir_all, aube_util::fs_atomic::atomic_write)
which can block Tokio workers; change those to async-aware operations by either
using tokio::fs equivalents or moving the blocking sections into
tokio::task::spawn_blocking closures that return the same results, e.g., wrap
the initial cache read/deserialization, the cache write/parent-dir creation, and
any other std::fs usages in spawn_blocking (or replace with
tokio::fs::read/to_vec/create_dir_all and an async atomic-write helper) so
load_index, install, and download_verify_extract no longer perform blocking
filesystem calls on the async runtime.
- Around line 45-46: The is_lts method currently only treats
LtsField::Codename(_) as LTS, so LtsField::Flag(true) values from mirrors are
ignored; update is_lts (in the implementation for LtsField/where NodeSpec::Lts
flows through) to also return true for LtsField::Flag(true) (or remove the Flag
variant if you intend not to support boolean flags) — locate the is_lts function
and the LtsField enum and add a match arm for LtsField::Flag(true) (or remove
references to Flag and adjust callers like NodeSpec::Lts accordingly) so boolean
LTS indicators are handled consistently.

In `@crates/aube-runtime/src/self_install.rs`:
- Around line 50-53: The self_dir function currently treats an empty
AUBE_SELF_DIR value as a valid path (resolving to the working directory); update
self_dir to ignore empty environment values by checking the OsString returned
from std::env::var_os("AUBE_SELF_DIR") and returning None if it is empty (i.e.,
treat empty AUBE_SELF_DIR as unset) before converting to a PathBuf; adjust the
early-return logic in self_dir accordingly so only non-empty values are turned
into PathBufs.
- Around line 135-137: The current find call that assigns exe (the array of
dir.join(exe_name) and dir.join("bin").join(exe_name)) uses only p.is_file() so
it can pick non-executable files; change the predicate to p.is_file() &&
is_executable(p). Implement a small helper is_executable(path: &Path) -> bool
and use it in the closure passed to find: on Unix (cfg(unix)) check
metadata().permissions().mode() & 0o111 != 0 via
std::os::unix::fs::PermissionsExt, and on Windows (cfg(windows)) treat files
with executable extensions (e.g. .exe, .bat, .cmd) as executable (or call
appropriate WinAPI/MetadataExt if preferred). Ensure the new predicate is used
where exe is selected so only usable binaries are returned.

In `@crates/aube-runtime/src/shasums.rs`:
- Around line 70-76: The cached SHASUMS read path uses Shasums::parse and
returns immediately even when the parsed result contains no valid entries;
change the logic after calling Shasums::parse to validate the parsed object has
at least one entry (e.g. check a entries/len/is_empty accessor) and only return
Ok(parsed) if non-empty; otherwise treat it as a cache miss (optionally delete
the corrupted cache_path) so the code falls through to the network fetch logic
that already rejects empty responses. Ensure the same validation is applied to
the cached-path branch and mirrors the non-empty check used around the network
handling (the code near the existing network-response rejection).

In `@crates/aube-settings/settings.toml`:
- Around line 2000-2010: Update the docs for managePackageManagerVersions to
clarify that self-version switching of the aube binary is performed by aube
itself (mise/installer logic) and that runtimeInstaller refers only to the Node
runtime installer policy; adjust the paragraph mentioning runtimeInstaller so it
does not imply ownership of downloading pinned aube binaries, and explicitly
state how packageManager (pins like `aube@…`), packageManagerStrict and
packageManagerStrictVersion behave with respect to validation vs switching and
who performs the download/re-exec.

In `@crates/aube/src/commands/install/mod.rs`:
- Around line 397-415: Root-level preinstall hooks are run before the project's
Node runtime is resolved; move the runtime resolution so lifecycle scripts run
with the correct switched runtime. Relocate the crate::runtime::ensure(...) call
(passing &cwd, Some(&manifest), RuntimeSettings::from_ctx(&settings_ctx), and
lockfile_pre_parse...runtimes.get("node")) and the subsequent
super::configure_script_settings(&settings_ctx) so they execute before the
run_root_lifecycle(...) invocation, ensuring the runtime is resolved and script
settings are updated prior to running root lifecycle hooks.

In `@crates/aube/src/commands/runtime.rs`:
- Around line 175-196: The current branch uses
NodeRuntime::new(cfg).resolve(&request, None, ...) which still applies
PATH-first and reuse logic; when installer is aube (cfg.installer ==
aube_runtime::InstallerMode::Aube) you must force aube-managed install instead
of letting resolve pick an ambient/node reuse. Change the call so it bypasses
PATH/reuse — e.g., set the RuntimeConfig or request flags that disable reuse (a
"force" or allow_reuse = false / prefer_download = true option) or call the API
variant that mandates download/install rather than standard resolve; update the
construction around aube_runtime::RuntimeConfig, aube_runtime::NodeRequest, and
the NodeRuntime::resolve invocation to use that force-install path when
InstallerMode::Aube is selected.

In `@crates/aube/src/runtime.rs`:
- Around line 92-93: The process-global RUNTIME can be initialized without a
lockfile pin by runtime::ensure_for_cwd(...) which wins over a later
install::run that wants to call runtime::ensure(..., lock_pin=Some(...)); fix by
changing call order in callers (crate::commands::run.rs and
crate::commands::exec.rs) to invoke ensure_installed(no_install).await? (which
may run install::run and thus call runtime::ensure with a lockfile pin) before
calling crate::runtime::ensure_for_cwd(&crate::dirs::cwd()?) so the
lockfile-pinned ensure runs first and initializes static RUNTIME with the pinned
version.

In `@crates/aube/src/self_version.rs`:
- Around line 64-75: The current check uses
std::env::var_os(REEXEC_GUARD_ENV).is_some() which treats any re-exec marker as
"already switched"; instead read the guard value
(std::env::var_os(REEXEC_GUARD_ENV)), compare it against the target version/pin
(use pin.raw or the exact version string produced by reexec()), and only skip
switching when they are equal; if the values differ, clear/unset the guard or
proceed to reexec; apply the same fix to the other occurrence referenced (around
lines 222-224) so REEXEC_GUARD_ENV/AUBE_SELF_SWITCHED is scoped to the requested
version rather than presence alone.
- Around line 77-109: When resolving range pins in this block (match on
pin.spec) avoid calling aube_runtime::latest_aube_version() before honoring the
pin.on_fail policy: if no installed version matches (result of
aube_runtime::list_installed_aube() filtering), check pin.on_fail and perform
the fallback (warn/ignore/error) immediately for non-download modes instead of
fetching from the network; only call aube_runtime::latest_aube_version(2).await
when pin.on_fail indicates a download/remote resolution path. Update the logic
around pin, pin.spec, best_installed, aube_runtime::latest_aube_version, and the
self_pin_unsatisfied returns so that on_fail is consulted first and triggers the
correct self_pin_unsatisfied behavior or fallback before any remote network
call.

In `@README.md`:
- Line 261: The README has inconsistent pnpm compatibility claims: update either
the lockfile table (the `pnpm-lock.yaml v9` entry) to indicate support for pnpm
11 lockfiles or narrow the sentence that claims "aube also matches pnpm 11's
runtime surface" so it only refers to the runtime surface (e.g., "matches pnpm
11's runtime surface but uses pnpm-lock.yaml v9") to avoid conflict; locate the
sentence that begins "aube also matches pnpm 11's runtime surface" and the
lockfile table entry `pnpm-lock.yaml v9` and make them consistent.

---

Outside diff comments:
In @.rules:
- Around line 26-38: The crate inventory block labeled "crates[12]{name,role}"
is missing the aube-util entry (Cargo.toml still references aube-util), so
either add an entry for "aube-util" with its short role description into that
list or update the cardinality to "crates[13]" and append the new line; locate
the inventory block (the header "crates[12]{name,role}" and the following crate
lines) and insert a concise "aube-util,..." line describing its role or
increment the index and include the aube-util entry to keep the workspace
inventory consistent with Cargo.toml.

In `@crates/aube-scripts/src/lib.rs`:
- Around line 887-920: The node execpath env vars (npm_node_execpath and NODE)
are set on cmd before apply_jail_env(), but apply_jail_env() calls env_clear()
which removes them for jailed scripts; update the code so the node execpath is
preserved for jailed scripts by moving the logic that sets
npm_node_execpath/NODE to after apply_jail_env() is invoked (or alternatively,
modify apply_jail_env() to re-add these vars when settings.node_exe is present).
Specifically adjust the block that references settings.node_exe and node_exe so
it runs after apply_jail_env(&mut cmd, ...), or ensure apply_jail_env preserves
or re-inserts those env vars when jail is Some.

In `@crates/aube/src/commands/dlx.rs`:
- Around line 149-167: The local-bin fast path can return before runtime
resolution runs, so move the call to
crate::runtime::ensure_for_cwd(&crate::dirs::cwd()?).await? to occur before the
early return branch: call ensure_for_cwd right after computing initial_cwd (or
at the top of the block guarded by !explicit_package && !shell_mode &&
can_use_local_bin(&command)) and before checking project root / bin_path and
before invoking super::exec::exec_bin, so that the runtime is resolved for the
user's project (respecting .nvmrc/devEngines) regardless of whether exec_bin
returns early.

In `@crates/aube/src/commands/exec.rs`:
- Around line 296-315: The non-status exec branch that builds the
tokio::process::Command (created via resolve_exec_shim) currently skips the
runtime PATH injection and environment adjustments; update that branch so it
mirrors the status/parallel path by computing bin_dir =
super::project_modules_dir(cwd).join(".bin"), combining it with
crate::runtime::path_entries(), building new_path via
aube_scripts::prepend_paths(&path_dirs), setting cmd.env("PATH", &new_path), and
then calling the same apply_child_env(command) (or the appropriate function used
elsewhere to inject runtime env) on the built Command before returning; keep the
existing cmd.args(args) and ensure you reference resolve_exec_shim,
project_modules_dir, crate::runtime::path_entries, aube_scripts::prepend_paths,
and apply_child_env to locate where to apply these changes.

In `@crates/aube/src/commands/install/fetch.rs`:
- Around line 91-103: The spawn error message for the Command created via
tokio::process::Command::new(crate::runtime::node_program()) is misleading
because it hardcodes "from PATH"; update the miette! error construction in the
.map_err closure to be runtime-source agnostic by either removing "from PATH" or
including the resolved program path (crate::runtime::node_program() or a local
variable holding it) in the message so it accurately reflects whether Node was
taken from PATH or a pinned runtime; adjust the miette! invocation that
references local.specifier() accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f4f34e8a-c95c-4802-956d-69d1f6f2d8d5

📥 Commits

Reviewing files that changed from the base of the PR and between 8695089 and 7a8f9c8.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (73)
  • .rules
  • Cargo.toml
  • README.md
  • aube.usage.kdl
  • crates/aube-codes/src/errors.rs
  • crates/aube-codes/src/warnings.rs
  • crates/aube-lockfile/src/drift.rs
  • crates/aube-lockfile/src/lib.rs
  • crates/aube-lockfile/src/merge.rs
  • crates/aube-lockfile/src/pnpm/format.rs
  • crates/aube-lockfile/src/pnpm/raw.rs
  • crates/aube-lockfile/src/pnpm/read.rs
  • crates/aube-lockfile/src/pnpm/tests.rs
  • crates/aube-lockfile/src/pnpm/write.rs
  • crates/aube-lockfile/src/yarn/tests.rs
  • crates/aube-manifest/src/dev_engines.rs
  • crates/aube-manifest/src/lib.rs
  • crates/aube-manifest/src/workspace/config.rs
  • crates/aube-resolver/src/peer_context.rs
  • crates/aube-resolver/src/resolve.rs
  • crates/aube-resolver/src/tests.rs
  • crates/aube-runtime/Cargo.toml
  • crates/aube-runtime/src/discover.rs
  • crates/aube-runtime/src/error.rs
  • crates/aube-runtime/src/extract.rs
  • crates/aube-runtime/src/http.rs
  • crates/aube-runtime/src/index.rs
  • crates/aube-runtime/src/installer.rs
  • crates/aube-runtime/src/lib.rs
  • crates/aube-runtime/src/mise.rs
  • crates/aube-runtime/src/paths.rs
  • crates/aube-runtime/src/platform.rs
  • crates/aube-runtime/src/progress.rs
  • crates/aube-runtime/src/resolver.rs
  • crates/aube-runtime/src/self_install.rs
  • crates/aube-runtime/src/shasums.rs
  • crates/aube-runtime/src/sources.rs
  • crates/aube-runtime/src/spec.rs
  • crates/aube-scripts/src/lib.rs
  • crates/aube-settings/settings.toml
  • crates/aube/Cargo.toml
  • crates/aube/src/commands/dlx.rs
  • crates/aube/src/commands/doctor.rs
  • crates/aube/src/commands/exec.rs
  • crates/aube/src/commands/install/fetch.rs
  • crates/aube/src/commands/install/mod.rs
  • crates/aube/src/commands/install/resolve.rs
  • crates/aube/src/commands/mod.rs
  • crates/aube/src/commands/run.rs
  • crates/aube/src/commands/runtime.rs
  • crates/aube/src/commands/script_settings.rs
  • crates/aube/src/commands/security_scanner.rs
  • crates/aube/src/engines.rs
  • crates/aube/src/main.rs
  • crates/aube/src/pnpmfile.rs
  • crates/aube/src/runtime.rs
  • crates/aube/src/self_version.rs
  • crates/aube/src/startup.rs
  • docs/.vitepress/config.mts
  • docs/cli/commands.json
  • docs/cli/index.md
  • docs/cli/runtime.md
  • docs/cli/runtime/list.md
  • docs/cli/runtime/set.md
  • docs/error-codes.data.json
  • docs/package-manager/lockfiles.md
  • docs/package-manager/node-runtime.md
  • docs/pnpm-users.md
  • docs/settings/index.md
  • test/guardrails.bats
  • test/runtime.bats
  • test/runtime_download.bats
  • test/self_version.bats

Comment on lines +267 to +284
for (name, incoming) in src.runtimes {
use std::collections::btree_map::Entry;
match dst.runtimes.entry(name) {
Entry::Vacant(slot) => {
slot.insert(incoming);
}
Entry::Occupied(slot) => {
if slot.get().version != incoming.version {
report.conflicts.push(format!(
"runtime `{}`: kept {} over {}",
slot.key(),
slot.get().version,
incoming.version
));
}
}
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Runtime merge conflict detection is too narrow.

Line 274 only compares version. If two branch lockfiles keep the same version but differ in specifier, dev, has_bin, or variants, one side is silently ignored with no conflict report.

Suggested fix
-            Entry::Occupied(slot) => {
-                if slot.get().version != incoming.version {
+            Entry::Occupied(slot) => {
+                if slot.get().specifier != incoming.specifier
+                    || slot.get().version != incoming.version
+                    || slot.get().dev != incoming.dev
+                    || slot.get().has_bin != incoming.has_bin
+                    || slot.get().variants != incoming.variants
+                {
                     report.conflicts.push(format!(
                         "runtime `{}`: kept {} over {}",
                         slot.key(),
                         slot.get().version,
                         incoming.version
                     ));
                 }
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (name, incoming) in src.runtimes {
use std::collections::btree_map::Entry;
match dst.runtimes.entry(name) {
Entry::Vacant(slot) => {
slot.insert(incoming);
}
Entry::Occupied(slot) => {
if slot.get().version != incoming.version {
report.conflicts.push(format!(
"runtime `{}`: kept {} over {}",
slot.key(),
slot.get().version,
incoming.version
));
}
}
}
}
for (name, incoming) in src.runtimes {
use std::collections::btree_map::Entry;
match dst.runtimes.entry(name) {
Entry::Vacant(slot) => {
slot.insert(incoming);
}
Entry::Occupied(slot) => {
if slot.get().specifier != incoming.specifier
|| slot.get().version != incoming.version
|| slot.get().dev != incoming.dev
|| slot.get().has_bin != incoming.has_bin
|| slot.get().variants != incoming.variants
{
report.conflicts.push(format!(
"runtime `{}`: kept {} over {}",
slot.key(),
slot.get().version,
incoming.version
));
}
}
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aube-lockfile/src/merge.rs` around lines 267 - 284, The runtime merge
loop in merge.rs (the for (name, incoming) in src.runtimes and Entry::Occupied
branch) only checks slot.get().version against incoming.version and therefore
misses conflicts in other fields; update the Occupied branch to compare all
relevant Runtime fields (specifier, dev, has_bin, variants, and version) between
slot.get() and incoming, and if any of those differ push a clear conflict
message (e.g., "runtime `{}`: kept <existing> over <incoming> (diffs: ...)" or
list which fields differ) while still retaining the existing entry; ensure
equality across all fields avoids reporting a conflict.

Comment thread crates/aube-runtime/src/discover.rs
Comment thread crates/aube-runtime/src/error.rs
Comment thread crates/aube-runtime/src/index.rs Outdated
Comment on lines +86 to +173
pub(crate) async fn load_index(http: &Http, cfg: &RuntimeConfig) -> Result<Vec<IndexEntry>, Error> {
let base = cfg.mirror_base();
let cache_path = paths::index_cache_path(&base);
let cached: Option<CachedIndex> = cache_path
.as_deref()
.and_then(|p| std::fs::read(p).ok())
.and_then(|bytes| serde_json::from_slice(&bytes).ok());

if let Some(ref c) = cached {
let age = now_epoch().saturating_sub(c.fetched_at);
if age < INDEX_TTL_SECS || cfg.network == NetworkMode::Offline {
return Ok(c.entries.clone());
}
} else if cfg.network == NetworkMode::Offline {
return Err(Error::Offline {
what: "the Node.js release index".to_string(),
});
}

let url = format!("{base}/index.json");
let resp = match http
.get(
&url,
cached.as_ref().and_then(|c| c.etag.as_deref()),
cached.as_ref().and_then(|c| c.last_modified.as_deref()),
false,
)
.await
{
Ok(resp) => resp,
Err(e) => {
// Network trouble with a (stale) cache on disk: serve the
// cache. A 30-minute-old index is far better than failing
// `aubr test` outright on a flaky connection.
if let Some(c) = cached {
tracing::debug!(error = %e, "node index refresh failed; serving stale cache");
return Ok(c.entries);
}
return Err(e);
}
};

let (entries, etag, last_modified) = match resp.body {
None => {
// 304 — refresh the cache timestamp so the TTL restarts.
let c = cached.expect("304 implies a conditional request from cache");
(
c.entries,
resp.etag.or(c.etag),
resp.last_modified.or(c.last_modified),
)
}
Some(body) => {
let bytes = body.bytes().await.map_err(|e| Error::DownloadFailed {
url: url.clone(),
reason: e.to_string(),
})?;
let entries: Vec<IndexEntry> =
serde_json::from_slice(&bytes).map_err(|e| Error::DownloadFailed {
url: url.clone(),
reason: format!("invalid index.json: {e}"),
})?;
(entries, resp.etag, resp.last_modified)
}
};

if let Some(path) = cache_path {
let wrapper = CachedIndex {
etag,
last_modified,
fetched_at: now_epoch(),
entries: entries.clone(),
};
if let Ok(bytes) = serde_json::to_vec(&wrapper)
&& let Some(parent) = path.parent()
{
let _ = std::fs::create_dir_all(parent);
if let Err(e) = aube_util::fs_atomic::atomic_write(&path, &bytes) {
tracing::warn!(
code = aube_codes::warnings::WARN_AUBE_CACHE_WRITE_FAILED,
path = %path.display(),
error = %e,
"failed to write node index cache"
);
}
}
}
Ok(entries)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Runtime cache/install paths share the same blocking-I/O-on-async root cause.

load_index() in crates/aube-runtime/src/index.rs and install() / download_verify_extract() in crates/aube-runtime/src/installer.rs all use std::fs on async paths. The common fix is to move the runtime cache/setup/publish filesystem work behind tokio::fs or a few dedicated spawn_blocking helpers so runtime switching does not pin Tokio workers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aube-runtime/src/index.rs` around lines 86 - 173, The code in
load_index (and similarly install / download_verify_extract) performs blocking
std::fs I/O on async paths (e.g., std::fs::read, std::fs::create_dir_all,
aube_util::fs_atomic::atomic_write) which can block Tokio workers; change those
to async-aware operations by either using tokio::fs equivalents or moving the
blocking sections into tokio::task::spawn_blocking closures that return the same
results, e.g., wrap the initial cache read/deserialization, the cache
write/parent-dir creation, and any other std::fs usages in spawn_blocking (or
replace with tokio::fs::read/to_vec/create_dir_all and an async atomic-write
helper) so load_index, install, and download_verify_extract no longer perform
blocking filesystem calls on the async runtime.

Source: Coding guidelines

Comment on lines +175 to +196
// aube-managed global install: fetch into the runtime dir and tell
// the user how to reach it — aube deliberately ships no shims or
// shell activation.
let version: node_semver::Version = resolved.parse().into_diagnostic()?;
let cfg = aube_runtime::RuntimeConfig {
installer: aube_runtime::InstallerMode::Aube,
mirror: settings.mirror.clone(),
network: aube_runtime::NetworkMode::Online,
retries: 2,
};
let request = aube_runtime::NodeRequest {
spec: aube_runtime::NodeSpec::Exact(version),
raw: resolved.to_string(),
on_fail: aube_manifest::OnFail::Download,
source: aube_runtime::RequestSource::DevEngines,
origin: std::path::PathBuf::from("aube runtime set -g"),
};
let resolution = aube_runtime::NodeRuntime::new(cfg)
.resolve(&request, None, &aube_runtime::NoopProgress)
.await
.map_err(|e| miette!(code = e.code(), "{e}"))?
.ok_or_else(|| miette!("runtime resolution returned no install"))?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Don't use the normal PATH-first resolver for the aube-managed global install path.

This branch promises an aube-managed global install, but NodeRuntime::resolve(...) follows the normal precedence from the PR: ambient PATH and reused installs win before download. So aube runtime set -g can "succeed" against /usr/bin/node or an existing mise install and never materialize anything under aube's runtime directory.

Use a force-install path here, or bypass PATH/mise reuse when runtimeInstaller=aube is selected.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aube/src/commands/runtime.rs` around lines 175 - 196, The current
branch uses NodeRuntime::new(cfg).resolve(&request, None, ...) which still
applies PATH-first and reuse logic; when installer is aube (cfg.installer ==
aube_runtime::InstallerMode::Aube) you must force aube-managed install instead
of letting resolve pick an ambient/node reuse. Change the call so it bypasses
PATH/reuse — e.g., set the RuntimeConfig or request flags that disable reuse (a
"force" or allow_reuse = false / prefer_download = true option) or call the API
variant that mandates download/install rather than standard resolve; update the
construction around aube_runtime::RuntimeConfig, aube_runtime::NodeRequest, and
the NodeRuntime::resolve invocation to use that force-install path when
InstallerMode::Aube is selected.

Comment thread crates/aube/src/runtime.rs
Comment thread crates/aube/src/self_version.rs Outdated
Comment on lines +77 to +109
// Resolve the pin to an exact version: exact pins directly, ranges
// against installed versions first, then the latest published
// release.
let target = match &pin.spec {
aube_runtime::NodeSpec::Exact(v) => v.clone(),
spec => {
let best_installed = aube_runtime::list_installed_aube()
.into_iter()
.filter(|i| spec.satisfied_by(&i.version) == Some(true))
.map(|i| i.version)
.max();
match best_installed {
Some(v) => v,
None => match aube_runtime::latest_aube_version(2).await {
Ok(latest) if spec.satisfied_by(&latest) == Some(true) => latest,
Ok(latest) => {
return self_pin_unsatisfied(
&pin,
format!(
"no installed aube satisfies {} and the newest release is {latest}",
pin.raw
),
);
}
Err(e) => {
return self_pin_unsatisfied(
&pin,
format!("could not resolve {}: {e}", pin.raw),
);
}
},
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Honor onFail before resolving range pins over the network.

For devEngines.packageManager ranges, this branch calls latest_aube_version() whenever no installed version matches, and only consults pin.on_fail afterward. In warn / ignore / error modes that means an offline machine, or a range that no longer includes the newest release, turns into a hard failure before the configured fallback behavior is reached. Only the download path should need remote target resolution.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aube/src/self_version.rs` around lines 77 - 109, When resolving range
pins in this block (match on pin.spec) avoid calling
aube_runtime::latest_aube_version() before honoring the pin.on_fail policy: if
no installed version matches (result of aube_runtime::list_installed_aube()
filtering), check pin.on_fail and perform the fallback (warn/ignore/error)
immediately for non-download modes instead of fetching from the network; only
call aube_runtime::latest_aube_version(2).await when pin.on_fail indicates a
download/remote resolution path. Update the logic around pin, pin.spec,
best_installed, aube_runtime::latest_aube_version, and the self_pin_unsatisfied
returns so that on_fail is consulted first and triggers the correct
self_pin_unsatisfied behavior or fallback before any remote network call.

Comment thread README.md

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

♻️ Duplicate comments (5)
crates/aube-runtime/src/self_install.rs (2)

58-61: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Empty AUBE_SELF_DIR value still accepted.

The past review comment regarding empty AUBE_SELF_DIR values remains unaddressed. Line 59 accepts an empty OsString, which resolves to the current working directory and could misdirect self-install artifacts.

Proposed fix
 pub fn self_dir() -> Option<PathBuf> {
     if let Some(dir) = std::env::var_os("AUBE_SELF_DIR") {
-        return Some(PathBuf::from(dir));
+        if !dir.is_empty() {
+            return Some(PathBuf::from(dir));
+        }
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aube-runtime/src/self_install.rs` around lines 58 - 61, The current
self_dir() accepts an empty AUBE_SELF_DIR value which std::env::var_os returns
as Some("") and then PathBuf::from("") resolves to the current working
directory; modify self_dir() to treat empty or all-whitespace AUBE_SELF_DIR as
unset by checking the OsString value (e.g., convert to string lossily or check
is_empty on OsStr) before returning Some(PathBuf), and only return
Some(PathBuf::from(dir)) when the variable is non-empty; otherwise continue to
fall through to the non-env resolution path.

143-145: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Executable usability check still missing.

The past review comment about validating executable permissions remains unaddressed. A non-executable file named aube passes the is_file() check and will fail at re-exec time.

Proposed fix
     let exe = [dir.join(exe_name), dir.join("bin").join(exe_name)]
         .into_iter()
-        .find(|p| p.is_file())?;
+        .find(|p| is_usable_executable(p))?;
     Some(InstalledAube {
         // ...
     })
 }
+
+fn is_usable_executable(path: &Path) -> bool {
+    if !path.is_file() {
+        return false;
+    }
+    #[cfg(unix)]
+    {
+        use std::os::unix::fs::PermissionsExt;
+        if let Ok(meta) = std::fs::metadata(path) {
+            return (meta.permissions().mode() & 0o111) != 0;
+        }
+        return false;
+    }
+    #[cfg(not(unix))]
+    {
+        true
+    }
+}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aube-runtime/src/self_install.rs` around lines 143 - 145, The current
finder lets non-executable files pass because it only checks is_file(); create
and use an is_executable(path: &Path) helper and change the find closure used
when computing exe to require both is_file() and is_executable(). Implement
is_executable to, on Unix (use std::os::unix::fs::PermissionsExt), check
metadata()?.permissions().mode() & 0o111 != 0, and on Windows check common
executable extensions (e.g., "exe","bat","cmd") or use Windows-specific metadata
if preferred; update the code that computes exe (the let exe = ... .find(|p| ...
)?) to call this helper so only truly executable candidates are selected.
crates/aube/src/self_version.rs (2)

67-75: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Re-exec guard still checks presence only, not version match.

The past review concern remains: the guard at line 67 checks is_some() rather than comparing the guard value against the target version. A nested aube invocation from a different project (pinning a different version) will skip switching because AUBE_SELF_SWITCHED is set, even though it contains the wrong version.

Proposed fix
-    if std::env::var_os(REEXEC_GUARD_ENV).is_some() {
+    let already_switched = std::env::var(REEXEC_GUARD_ENV).ok();
+    // Only skip if we already switched TO THIS EXACT version
+    if already_switched.as_deref() == Some(&target.to_string()) {
+        return Ok(());
+    }
+    // If we switched to a DIFFERENT version and still don't satisfy, warn
+    if already_switched.is_some() {
         tracing::warn!(
             code = aube_codes::warnings::WARN_AUBE_RUNTIME_VERSION_MISMATCH,
             requested = pin.raw,
             running = running_version(),
             "switched aube binary still does not satisfy the project's pin; continuing"
         );
         return Ok(());
     }

Note: This requires moving the guard check after target resolution, or storing the requested pin rather than the resolved version in the guard.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aube/src/self_version.rs` around lines 67 - 75, The re-exec guard
currently only checks presence via std::env::var_os(REEXEC_GUARD_ENV).is_some()
and should instead compare the guard's value to the intended target version;
change the logic so after you've resolved the target pin (use pin.raw or the
resolved identifier you choose) you read std::env::var(REEXEC_GUARD_ENV) and
only early-return if the env value equals that target string, otherwise proceed
with switching; alternatively, set REEXEC_GUARD_ENV to the requested pin
(pin.raw) when spawning the nested aube so the existing presence check remains
valid, and update references around REEXEC_GUARD_ENV, pin.raw, and
running_version() accordingly.

88-118: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Network call precedes onFail check for range pins.

The past review concern persists: when no installed version satisfies a range pin, available_aube_versions(2).await is called before onFail is consulted. If the network call fails (lines 112-117), self_pin_unsatisfied returns an error even when onFail is warn or ignore.

Proposed fix
             match best_installed {
                 Some(v) => v,
-                None => match aube_runtime::available_aube_versions(2).await {
+                None => {
+                    // Check onFail before network call for non-download modes
+                    match pin.on_fail {
+                        aube_manifest::OnFail::Ignore => return Ok(()),
+                        aube_manifest::OnFail::Warn => {
+                            tracing::warn!(
+                                code = aube_codes::warnings::WARN_AUBE_RUNTIME_VERSION_MISMATCH,
+                                requested = pin.raw,
+                                running = running_version(),
+                                source = pin.source,
+                                "no installed aube satisfies {} (onFail: warn); continuing",
+                                pin.raw
+                            );
+                            return Ok(());
+                        }
+                        aube_manifest::OnFail::Error => {
+                            return self_pin_unsatisfied(
+                                &pin,
+                                format!("no installed aube satisfies {} and onFail is \"error\"", pin.raw),
+                            );
+                        }
+                        aube_manifest::OnFail::Download => {}
+                    }
+                    match aube_runtime::available_aube_versions(2).await {
                     // ... rest of the match
+                    }
+                }
             }
crates/aube/src/runtime.rs (1)

92-93: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Lockfile pin bypass via OnceCell initialization order persists.

The past review concern about the process-global RUNTIME initialization order remains. When run/exec commands call ensure_for_cwd(lock_pin=None) before auto-install triggers install::run(..., lock_pin=Some(...)), the OnceCell first-write semantics cause the lockfile pin to be silently ignored.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aube/src/runtime.rs` around lines 92 - 93, The process-global static
RUNTIME (tokio::sync::OnceCell<RuntimeContext>) allows a first-write to silently
fix the lock pin, so when run/exec call ensure_for_cwd(lock_pin=None) before
auto-install calls install::run(..., lock_pin=Some(...)) the real pin is
ignored; change initialization so the RuntimeContext creation path honors a
later-provided lock_pin instead of being permanently set by the first writer:
update the code paths that set RUNTIME (the OnceCell initializer and the
function that constructs RuntimeContext) to accept and propagate an optional
lock_pin, and replace the current blind OnceCell::set usage with a
compare-and-update or lazy initializer that merges or prefers non-None lock_pin
(e.g., try_insert/update the existing RuntimeContext to inject the lock_pin when
a prior empty/None pin exists) so ensure_for_cwd, install::run, and
RuntimeContext all consistently observe the correct lock_pin.
🧹 Nitpick comments (2)
crates/aube-lockfile/src/pnpm/tests.rs (1)

3317-3389: ⚖️ Poor tradeoff

Extend merge test coverage to multi-target variants and overlapping targets.

The test validates the basic union path (non-overlapping, single-target variants), but doesn't cover:

  1. Multi-target variants: A variant with targets: [darwin/arm64, darwin/x64] to verify that partial target overlap is handled correctly.
  2. Overlapping targets: Both branches having a variant for the same target (e.g., darwin/arm64) to confirm "dst wins" behavior and conflict reporting.
  3. Differing dev/has_bin flags: Variants with the same version but different metadata to verify conflict detection.

Adding these cases would catch the .any() bug identified in merge.rs line 298 and validate the conflict-reporting logic.

💡 Suggested test outline
#[test]
fn runtime_pin_merge_handles_multi_target_variants() {
    // dst: variant with [darwin/arm64]
    // incoming: variant with [darwin/arm64, darwin/x64]
    // Expected: both variants present (or merged intelligently),
    // no data loss of darwin/x64
}

#[test]
fn runtime_pin_merge_reports_conflict_on_target_collision() {
    // dst: variant for darwin/arm64 with URL=U1
    // incoming: variant for darwin/arm64 with URL=U2
    // Expected: keep dst, report conflict
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aube-lockfile/src/pnpm/tests.rs` around lines 3317 - 3389, Add two
focused tests next to runtime_pin_merge_unions_variants to cover multi-target
and overlapping-target cases: (1) a test named
runtime_pin_merge_handles_multi_target_variants that creates a dst RuntimePin
with a RuntimeVariant containing targets [darwin/arm64] and an incoming
RuntimePin whose RuntimeVariant contains targets [darwin/arm64, darwin/x64],
then runs merge_branch_lockfiles and asserts the merged LockfileGraph retains
darwin/x64 (no silent drop); (2) a test named
runtime_pin_merge_reports_conflict_on_target_collision that creates dst and
incoming RuntimePins both targeting darwin/arm64 but with differing
archive/url/integrity or differing dev/has_bin flags, then runs
merge_branch_lockfiles and asserts the merged graph keeps dst’s variant while
the returned report contains a conflict entry for the runtime pin; reference the
RuntimePin, RuntimeVariant, RuntimeTarget types and use
parse/write/merge_branch_lockfiles as in the existing test to exercise the code
path around the .any() logic in merge.rs so overlapping-target logic and
dev/has_bin flag disagreements are asserted.
crates/aube-lockfile/src/merge.rs (1)

273-282: ⚡ Quick win

Consider reporting conflicts when dev or has_bin differ.

When versions match but dev or has_bin flags differ between branches, the merge silently keeps dst's values. For consistency with the specifier conflict reporting (lines 288–295), consider logging a conflict when these fields differ, even though dst wins.

📋 Suggested addition

After line 282, before the specifier check:

                     continue;
                 }
+                if slot.get().dev != incoming.dev {
+                    report.conflicts.push(format!(
+                        "runtime `{}` dev flag: kept {} over {}",
+                        slot.key(),
+                        slot.get().dev,
+                        incoming.dev
+                    ));
+                }
+                if slot.get().has_bin != incoming.has_bin {
+                    report.conflicts.push(format!(
+                        "runtime `{}` has_bin: kept {} over {}",
+                        slot.key(),
+                        slot.get().has_bin,
+                        incoming.has_bin
+                    ));
+                }
                 // Same resolved version: a silently-kept dst would
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/aube-lockfile/src/merge.rs` around lines 273 - 282, When merging
runtime entries in Entry::Occupied, detect and report conflicts if
slot.get().dev != incoming.dev or slot.get().has_bin != incoming.has_bin even
when versions match; update the block handling the occupied slot (where
slot.get() and incoming are compared and report.conflicts is pushed for version
mismatches) to also push descriptive conflict messages for dev and has_bin
differences (similar style to the existing specifier conflict reporting) before
continuing to the specifier check so the conflict log records these differing
flags while still keeping dst's values.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/aube-lockfile/src/merge.rs`:
- Around line 298-306: The variant-union check misuses .any(), causing an
incoming Variant (variable name variant) to be skipped if any one of its targets
is present in dst_pin.variants; change the predicate so we only skip the
incoming variant when all of its targets are already present in dst_pin (i.e.,
replace the .any() check with an .all() style check over variant.targets against
dst_pin.variants/dv.targets), so dst_pin.variants.push(variant) only happens
when at least one target in variant is uncovered.

---

Duplicate comments:
In `@crates/aube-runtime/src/self_install.rs`:
- Around line 58-61: The current self_dir() accepts an empty AUBE_SELF_DIR value
which std::env::var_os returns as Some("") and then PathBuf::from("") resolves
to the current working directory; modify self_dir() to treat empty or
all-whitespace AUBE_SELF_DIR as unset by checking the OsString value (e.g.,
convert to string lossily or check is_empty on OsStr) before returning
Some(PathBuf), and only return Some(PathBuf::from(dir)) when the variable is
non-empty; otherwise continue to fall through to the non-env resolution path.
- Around line 143-145: The current finder lets non-executable files pass because
it only checks is_file(); create and use an is_executable(path: &Path) helper
and change the find closure used when computing exe to require both is_file()
and is_executable(). Implement is_executable to, on Unix (use
std::os::unix::fs::PermissionsExt), check metadata()?.permissions().mode() &
0o111 != 0, and on Windows check common executable extensions (e.g.,
"exe","bat","cmd") or use Windows-specific metadata if preferred; update the
code that computes exe (the let exe = ... .find(|p| ... )?) to call this helper
so only truly executable candidates are selected.

In `@crates/aube/src/runtime.rs`:
- Around line 92-93: The process-global static RUNTIME
(tokio::sync::OnceCell<RuntimeContext>) allows a first-write to silently fix the
lock pin, so when run/exec call ensure_for_cwd(lock_pin=None) before
auto-install calls install::run(..., lock_pin=Some(...)) the real pin is
ignored; change initialization so the RuntimeContext creation path honors a
later-provided lock_pin instead of being permanently set by the first writer:
update the code paths that set RUNTIME (the OnceCell initializer and the
function that constructs RuntimeContext) to accept and propagate an optional
lock_pin, and replace the current blind OnceCell::set usage with a
compare-and-update or lazy initializer that merges or prefers non-None lock_pin
(e.g., try_insert/update the existing RuntimeContext to inject the lock_pin when
a prior empty/None pin exists) so ensure_for_cwd, install::run, and
RuntimeContext all consistently observe the correct lock_pin.

In `@crates/aube/src/self_version.rs`:
- Around line 67-75: The re-exec guard currently only checks presence via
std::env::var_os(REEXEC_GUARD_ENV).is_some() and should instead compare the
guard's value to the intended target version; change the logic so after you've
resolved the target pin (use pin.raw or the resolved identifier you choose) you
read std::env::var(REEXEC_GUARD_ENV) and only early-return if the env value
equals that target string, otherwise proceed with switching; alternatively, set
REEXEC_GUARD_ENV to the requested pin (pin.raw) when spawning the nested aube so
the existing presence check remains valid, and update references around
REEXEC_GUARD_ENV, pin.raw, and running_version() accordingly.

---

Nitpick comments:
In `@crates/aube-lockfile/src/merge.rs`:
- Around line 273-282: When merging runtime entries in Entry::Occupied, detect
and report conflicts if slot.get().dev != incoming.dev or slot.get().has_bin !=
incoming.has_bin even when versions match; update the block handling the
occupied slot (where slot.get() and incoming are compared and report.conflicts
is pushed for version mismatches) to also push descriptive conflict messages for
dev and has_bin differences (similar style to the existing specifier conflict
reporting) before continuing to the specifier check so the conflict log records
these differing flags while still keeping dst's values.

In `@crates/aube-lockfile/src/pnpm/tests.rs`:
- Around line 3317-3389: Add two focused tests next to
runtime_pin_merge_unions_variants to cover multi-target and overlapping-target
cases: (1) a test named runtime_pin_merge_handles_multi_target_variants that
creates a dst RuntimePin with a RuntimeVariant containing targets [darwin/arm64]
and an incoming RuntimePin whose RuntimeVariant contains targets [darwin/arm64,
darwin/x64], then runs merge_branch_lockfiles and asserts the merged
LockfileGraph retains darwin/x64 (no silent drop); (2) a test named
runtime_pin_merge_reports_conflict_on_target_collision that creates dst and
incoming RuntimePins both targeting darwin/arm64 but with differing
archive/url/integrity or differing dev/has_bin flags, then runs
merge_branch_lockfiles and asserts the merged graph keeps dst’s variant while
the returned report contains a conflict entry for the runtime pin; reference the
RuntimePin, RuntimeVariant, RuntimeTarget types and use
parse/write/merge_branch_lockfiles as in the existing test to exercise the code
path around the .any() logic in merge.rs so overlapping-target logic and
dev/has_bin flag disagreements are asserted.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: dce194bf-7aed-4fe4-af57-cf0dabb485d9

📥 Commits

Reviewing files that changed from the base of the PR and between 7a8f9c8 and 2a13e87.

📒 Files selected for processing (13)
  • crates/aube-lockfile/src/drift.rs
  • crates/aube-lockfile/src/merge.rs
  • crates/aube-lockfile/src/pnpm/tests.rs
  • crates/aube-lockfile/src/pnpm/write.rs
  • crates/aube-runtime/src/http.rs
  • crates/aube-runtime/src/installer.rs
  • crates/aube-runtime/src/lib.rs
  • crates/aube-runtime/src/progress.rs
  • crates/aube-runtime/src/resolver.rs
  • crates/aube-runtime/src/self_install.rs
  • crates/aube/src/runtime.rs
  • crates/aube/src/self_version.rs
  • test/runtime_download.bats
🚧 Files skipped from review as they are similar to previous changes (8)
  • crates/aube-runtime/src/progress.rs
  • crates/aube-runtime/src/http.rs
  • crates/aube-runtime/src/lib.rs
  • crates/aube-runtime/src/installer.rs
  • crates/aube-lockfile/src/drift.rs
  • test/runtime_download.bats
  • crates/aube-lockfile/src/pnpm/write.rs
  • crates/aube-runtime/src/resolver.rs

Comment thread crates/aube-lockfile/src/merge.rs Outdated
Jeff Dickey and others added 2 commits June 11, 2026 16:09
mise draws its own download UI: delegation now inherits stderr (any
live aube progress renderer is paused around the child via new
on_external_tool hooks) while stdout stays piped into tracing so
commands whose stdout is a contract (aubx tool | jq) never see mise
output. Self-downloads get a real clx progress bar — spinner, byte
counts, verifying/extracting phase labels — degrading to plain lines
in text mode or off-terminal, and aube self-version downloads share
the same reporter.

Also fixes the CI lint failures: needless_match in the linux-only
detect_musl (never compiled by local macOS clippy) and shellcheck
SC2034 on the mirror-wait loop variable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Structural (cursor medium / coderabbit major):
- the lockfile runtime pin is now read on every resolution path:
  ensure_for_cwd probes the lockfile itself (substring-gated parse,
  cheap on unpinned hot paths), so `aubr` resolves the same exact
  version `aube install` pinned instead of best-in-range — and the
  install pipeline resolves the runtime *before* root preinstall
  hooks, which previously ran on the ambient node
- `--offline` now blocks runtime downloads like registry fetches
  (caches still serve); the install's network mode threads into
  runtime resolution
- the self-version re-exec guard is scoped to the resolved target
  version, so a nested aube invocation in another project can still
  switch; range pins honor onFail before failing on an unreachable
  version list
- `aube runtime set -g` reports what actually happened (already on
  PATH / already installed via mise / freshly installed) instead of
  claiming an install

Smaller (coderabbit):
- branch-lockfile variant union keeps multi-target variants whose
  targets are only partially covered (.any → .all)
- runtime store I/O errors get their own ERR_AUBE_RUNTIME_IO (generic
  exit) instead of masquerading as download failures
- `lts: true` from a mirror counts as LTS; corrupted SHASUMS cache
  entries refetch instead of failing until manually deleted
- discovery requires the exec bit on unix; empty AUBE_RUNTIME_DIR /
  AUBE_SELF_DIR env values no longer resolve to the working dir
- docs: /settings links fixed (vitepress dead-link CI failure),
  runtimeInstaller documents that it also governs aube self-installs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jdx jdx merged commit b578b76 into main Jun 11, 2026
18 checks passed
@jdx jdx deleted the feat/node-runtime-switching branch June 11, 2026 22:19
@cursor cursor Bot mentioned this pull request Jun 11, 2026
colinhacks added a commit to nubjs/aube that referenced this pull request Jun 12, 2026
…val jdx#858/jdx#860

Merge (not rebase) of jdx/aube main @ ab844b5 into nub-integration.
Resolved 9 conflicts preserving nub's embedder behavior:

- errors.rs / error-codes.data.json: kept nub's lockfile codes
  (OUTDATED/DECLARATION_MISMATCH/AMBIGUOUS, exit 13/14/15) AND took
  upstream's ERR_AUBE_RESOLUTION_SHAPE_MISMATCH, reassigning its
  exit code 13->16 to avoid colliding with nub's OUTDATED_LOCKFILE.
- aube-scripts/lib.rs: ScriptSettings carries BOTH nub's
  env_overlay/path_prepends embedder overlay AND upstream's
  node_bin_dir/node_exe; run_script composes both PATH layers.
- script_settings.rs: carry-forward embedder overlay + upstream runtime.
- lifecycle.rs / default_trust.rs: adopt jdx#860 decide_package(source_key)
  AND keep nub's defaultTrust floor (Unspecified => floor.trusts) arm;
  decide_with_floor now threads the source key internally.
- install/mod.rs + resolve.rs: kept nub's default_lockfile_kind seam,
  added upstream's refresh_lockfile_pin (inert under nub).
- startup.rs: kept nub's configurable PackageManagerNames policy,
  folded in upstream's managePackageManagerVersions self-switch guard.
- main.rs stays nub's thin lib-wrapper; ported jdx#861 CLI surface
  (Runtime subcommand, self_version::maybe_switch) into lib.rs;
  added mod runtime / mod self_version to lib.rs.

jdx#861 dormancy gate (the one aube behavior change, default==upstream):
runtime::set_runtime_switching_enabled(bool) OnceLock, default TRUE.
ensure / ensure_for_cwd / refresh_lockfile_pin short-circuit to
path_fallback when false, before any .nvmrc/.node-version/devEngines
read. Re-exported from lib.rs; nub flips it false so aube's runtime
resolver stays compiled-but-inert and nub keeps owning Node.

Tests: aube cargo test 1965 passed / 0 failed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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