feat(node): auto-provision Node when there is no pin and no node on PATH#296
Conversation
On a fresh machine — nub installed before any Node — `nub <file>`, `nub watch`, and the hijack-`node` path no longer dead-end with "no Node binary found on PATH". The runner provisioning path now auto-provisions when there is no project pin and no node on PATH: store-first (reuse the highest supported version already cached, zero download), else download `latest` from nodejs.org. Sub-floor cached versions are skipped so an unpinned run downloads a working Node rather than failing the version floor. Unpinned `latest` is a deliberate moving target — no pin is written; `nub node pin <version>` opts into a reproducible version. The NoNodeOnPath message (still shown on non-provisioning status paths like `nub node which`) now suggests the working `nub node pin <version>` instead of the dead-ending bare `nub node install`. Closes #294 Claude-Session: https://claude.ai/code/session_01Xuh9u8b93eMNu53fTQTWX7
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — auto-provisioning Node on the zero-config runner path when there is no project pin and no node on PATH (the fresh-machine case, #294), plus the matching docs.
- Route no-pin/no-node to provisioning —
discover_or_provision_nodeno longer returnsNoNodeOnPathwhen the pin chain is empty; it calls the newprovision_unpinned_node(). Verified the no-pin branch ofdiscover_nodeyieldsNoNodeOnPathonly when PATH genuinely has no node, so the routing is sound. - Add
provision_unpinned_node()— store-first viahighest_store_node()(silent, no download), else downloadlatestwithpin_source: Noneso no pin is written and pin-disagreement warnings stay quiet. - Add
highest_store_node_in— highest cached version filtered tois_supported(), so a sub-floor-only store (e.g. a leftovernub node pin 16) falls through to downloadinglatestrather than dead-ending onUnsupported. - New
UnpinnedProvisionFailederror variant — distinct fromProvisionFailedbecause there's no pin to name; theNoNodeOnPathmessage now points atnub node pin <version>(the remedy that actually works from a fresh machine). - Docs —
node.mdxdocuments the fresh-machine flow and updates the precedence list's fallback entry.
The floor is respected on both unpinned branches (store-first filters is_supported; the download resolves latest), so the one caller that skips check_min_version (exec_under_project_node) is unaffected. The Using Node.js … (resolved from latest) / Installing from nodejs.org… strings in the docs console example match the real output in version_management/mod.rs. Tests cover the store-first/fall-through matrix and the reworded remedy. Clean, minimal, and correct.
Claude Opus | 𝕏
There was a problem hiding this comment.
Pull request overview
Fixes the fresh-machine “nub installed before Node” dead-end by teaching the file-run / watch / hijack-node path to auto-provision Node when there’s no project pin and no node on PATH (store-first, else download latest). This addresses #294.
Changes:
- Add an unpinned auto-provisioning path in Node discovery (prefer highest supported cached Node; otherwise resolve/download
latest). - Update discovery error surfaces (new
UnpinnedProvisionFailed, revisedNoNodeOnPathwording) and add a unit test for store-first selection behavior. - Document the fresh-machine behavior in the Node manager docs.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| site/content/docs/node.mdx | Documents the new no-pin/no-PATH-node behavior and updates version precedence wording. |
| crates/nub-core/src/node/discovery.rs | Implements unpinned auto-provisioning (store-first → else download latest), adds a new error variant, and extends tests. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| Drop a `.node-version` or `.nvmrc` in your project (or an `engines.node` range in `package.json`) and run `nub`. If that Node is already on your machine it's used as-is; if not, Nub downloads the matching stock build from nodejs.org — SHA-256 verified and cached under `~/.cache/nub/node` — then runs your code. Nub provisions the pinned version itself rather than handing off to `nvm`, `fnm`, or `mise`; with nothing pinned anywhere up the tree, it uses whatever `node` is already on your `PATH`. | ||
|
|
||
| ```console | ||
| $ echo {{NODE_VERSION}} > .node-version | ||
| $ nub index.ts | ||
| Using Node.js {{NODE_VERSION}} (resolved from .node-version) | ||
| Installing from nodejs.org... (24 MB) | ||
| Installed in 8.0s | ||
| # ...your script runs, on exactly the Node you pinned | ||
| ``` | ||
|
|
||
| On a fresh machine with no Node at all — you installed Nub before anything else — a plain `nub file.ts` still runs. With no pin and no `node` on `PATH`, Nub provisions the latest release and runs your code on it, reusing the newest version already in its cache before downloading. Pin a version whenever you want a reproducible one (`nub node pin <version>`); until then, each run picks up the latest. |
| #[error( | ||
| "no Node on PATH and no project pin — tried to provision the latest Node and failed: {reason}\n\ | ||
| \x20\x20Check your network / proxy, or install Node and put it on PATH." | ||
| )] | ||
| UnpinnedProvisionFailed { reason: String }, |
| /// Unpinned `latest` is a deliberate moving target: no pin is written, so each | ||
| /// fresh-machine run picks up the newest secure release. A project that wants a | ||
| /// reproducible version pins one (`nub node pin <version>`), which routes through | ||
| /// the pinned provisioning path instead. `pin_source` stays `None` — the run is | ||
| /// genuinely unpinned, so downstream pin-disagreement warnings don't fire. |
registry_config/npmrc_value reads project_root/.npmrc + $HOME/.npmrc, never the cache-store root — the test wrote its dead-registry override to cache/nub/.npmrc, which was never consulted. It passed on main only because an unrelated no-pin/no-node hard error also produced a nonzero exit code; this PR's auto-provision fix removes that error and unmasked the latent test bug. Write the override to the project's own .npmrc instead, so the test exercises the dead-registry rejection it was meant to.
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — one new commit since the prior pullfrog review (7ed4b6f): a fix to the PM-shim provisioning test that makes its dead-registry assertion actually load-bearing.
- Fix dead-registry location in
unpinned_path_miss_provisions_a_dynamic_default_within_the_lockfile_family— the test wrote its dead-registry.npmrcto the cache-store root (cache/nub/.npmrc), which the provisioning config path never reads, so the dead registry was inert and the failure was for the wrong reason. The commit relocates it to the project root (proj/.npmrc).
Verified the relocation is correct: pm::registry::registry_config resolves the base via workspace::scripts::npmrc_value(project_root, "registry"), which reads project_root/.npmrc then $HOME/.npmrc only — never the cache-store root. With HOME pointed at an empty dir and proj/.npmrc set to http://127.0.0.1:1/, the provision attempt now fails fast (connection refused) with zero real network, exactly as the assertions expect. The comment update accurately documents the config-resolution contract.
Claude Opus | 𝕏
|
Shipped in v0.3.0: https://github.com/nubjs/nub/releases/tag/v0.3.0 |


On a fresh machine (nub installed before any Node),
nub <file>/nub watch/ hijack-nodedead-ended withno Node binary found on PATH, and the error pointed at barenub node install, which itself errors with no pin.The runner path now auto-provisions when there's no pin and no node on PATH: store-first (highest supported cached version — silent, no download; sub-floor skipped), else download
latest. Unpinnedlateststays a moving target — no pin written;nub node pin <version>opts into a reproducible one. TheNoNodeOnPathmessage (still shown on status paths) now suggestsnub node pin <version>.Docs updated. Verified e2e; fmt/clippy/tests green locally.
Closes #294