Node manager
Manage the Node versions Nub provisions — pin a version and it's fetched automatically, or drive the cache explicitly with the install, list, uninstall, and pin subcommands.
Nub runs your code on stock Node and provisions the right version for you. Pin a version, run a file, and the matching Node is downloaded and cached without a second command. The nub node subcommands are for the steps you want to take deliberately: warming the cache before you run, listing what's installed, reclaiming disk, and recording a pin.
Automatic provisioning
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.
$ echo 26.7.0 > .node-version
$ nub index.ts
Using Node.js 26.7.0 (resolved from .node-version)
Installing from nodejs.org... (24 MB)
Installed in 8.0s
# ...your script runs, on exactly the Node you pinnedOn 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.
$ nub app.ts # fresh machine: no pin, no Node on PATH
Using Node.js 26.7.0 (resolved from latest)
Installing from nodejs.org... (30 MB)
Installed in 5.1s
# ...your script runsFor the full provisioning story — the ~/.cache/nub layout, the per-invocation shim — see Running files → The Node version.
Version precedence
Two questions decide which Node runs: which version is pinned, then where that binary comes from. Both are resolved in a fixed order.
Which version is pinned
Nub walks up from your working directory to the nearest pin, highest precedence first:
NODE_EXECUTABLE— an absolute path to a Node binary (a hard override)package.json#/devEngines/runtime— thenodeentry.node-version.nvmrc.tool-versions— the asdf/mise file; itsnodejsornodelinepackage.json#/engines/node— a range or exact version- Nothing pinned anywhere up the tree — whatever
nodeis on yourPATH, or the latest release when there's nonodeat all.
This resolved Node version is auto-installed and cached for future runs.
$ echo 26 > .node-version
$ nub hello.ts
Using Node.js 26.7.0 (resolved from .node-version)
Installed in 9.8s
Hello world!Within a single directory, .node-version wins over .nvmrc (it's the tool-agnostic standard). Discovery walks up the directory tree to the nearest pin and skips pin files that live inside an installed dependency (under node_modules) — a dependency's own CI pin never drives your project. The package.json fields (devEngines.runtime, engines.node) are read from the workspace root manifest when one exists above you, else the nearest package.json — a monorepo pins its Node once at the root.
Both fields take the engines.node grammar, so either can express a range or an exact version:
{
"devEngines": { "runtime": { "name": "node", "version": "22.15.0" } },
"engines": { "node": ">=20 <23" }
}The first pins exactly 22.15.0, patch included. The second resolves to the newest published release satisfying the range.
Nub reads only the node entry's version under devEngines.runtime, and provisions that version regardless of onFail. That field governs entries declaring a non-Node runtime instead: Nub refuses by default, onFail: "warn" prints a notice and falls through, onFail: "ignore" falls through silently. Setting NODE_EXECUTABLE skips resolution altogether — it bypasses the pin files, the cache, the nvm scan, and any download, though the binary's version is still detected, so the floor check and tier dispatch still apply.
Dependency build scripts follow the same root anchor during an install, so every member of a monorepo builds native dependencies under one Node rather than switching per member. Running a script with nub run inside a member still uses that member's own pin.
Read the full docs for the devEngines and engines fields on docs.npmjs.com.
Where the binary comes from
Once a version is pinned, Nub finds a binary for it in order:
nodeonPATH, if its version satisfies the pin — sofnm/Volta/miseauto-switching just works.- Nub's own download store (
~/.cache/nub/node/<version>/). - An
nvm-installed version (nvm scan). - Download the matching stock build from nodejs.org — SHA-256 verified and cached — otherwise error.
The PATH walk skips node_modules/.bin, mirroring how pin discovery skips pin files under node_modules. An entry there is a bin some package declared, so a dependency shipping its own node never becomes your project's runtime.
nub node install
Provision a version into the cache now, instead of on the next nub <file>. The use cases are warming a CI cache in a setup step (the GitHub Action does this for you), or fetching a Node before you go offline.
nub node install 26 # newest 26.x
nub node install lts # newest LTS line
nub node install 20.11.0 # an exact version
nub node install 20 22 # several at onceAliases and ranges resolve the same way pins do — lts, latest, lts/<codename>, a bare major (26) or major.minor (22.13), or an exact version. With no argument, nub node install reads your project's pin and provisions that:
# installs whatever .node-version / .nvmrc / engines.node names
nub node installA version already in the cache is a no-op; a version already available on your PATH (a system install, nvm, fnm, …) is reported and skipped rather than re-downloaded.
$ nub node install 26.7.0
Node 26.7.0 is already available on PATH — skippednub node ls
List the versions in Nub's cache, newest first. The version your current directory resolves to is marked.
$ nub node ls
→ 26.7.0
22.13.0
20.11.0Only Nub's own download cache is shown — ls doesn't enumerate your nvm / fnm / system installs.
nub node uninstall
Reclaim disk by deleting a cached version. Nub guards against removing the version your current directory resolves to.
$ nub node uninstall 20.11.0
Removed Node 20.11.0 from the cacheLike ls, this operates on Nub's cache only.
nub node pin
Write a .node-version for the project so every later nub in that tree uses it. This is the explicit form of creating the pin file by hand.
$ nub node pin 26
pinned Node 26 → /path/to/project/.node-versionA few deliberate behaviors:
- It pins the project, not the subdirectory. Run from anywhere inside the project and the file lands at the project root — the nearest
package.jsondirectory — not in your current subfolder. - In a workspace, it pins the whole repo. A Node version is a property of the repository, not one package, so in a monorepo the pin is written at the workspace root.
- It edits the pin file you already have. If the project carries a
.nvmrcbut no.node-version,pinupdates the.nvmrcin place.
Whatever spec you give is written verbatim — including an alias like lts — and pin works offline. The .node-version file is the tool-agnostic convention (nvm, fnm, volta, asdf all read it), so the pin is portable to your other tools.
nub node which
Resolve which Node runs here and where it lives. The binary path goes to stdout; a » resolved from <source> explainer goes to stderr, so it never pollutes a captured value.
$ nub node which
/Users/you/.nvm/versions/node/v26.7.0/bin/node
» resolved from .node-version (26.7.0)The stdout/stderr split means nub node which composes in a shell — capture the path and the explainer stays out of the variable:
$ NODE=$(nub node which)
» resolved from .node-version (26.7.0)
$ echo "$NODE"
/Users/you/.nvm/versions/node/v26.7.0/bin/nodeWith no pin the explainer reads » resolved from node on PATH. Bare nub node prints a status block instead — the version, path, and resolution source on separate lines.
nub node shim
Make node itself resolve through Nub, so a machine with no Node installed can run node at all. This is the fresh-install story: brew install nubjs/tap/nub (or the curl script), nub node shim, and now a bare node file.js works — Nub resolves the right version, provisions it if it's missing, and runs it.
$ nub node shim
node shim in /Users/you/.nub/node-shim (created)
`node` now resolves through nub (version management only — no augmentation; run `nub` for that)
added /Users/you/.nub/node-shim to PATH in /Users/you/.zshrc
restart your shell, or run: source /Users/you/.zshrcThe shimmed node runs stock Node, unchanged. It resolves and provisions the version — the precedence above — but adds nothing else: no TypeScript, no injected globals, no automatic .env. So node app.ts type-strips exactly as stock Node does, while nub app.ts transpiles and runs it.
Like nub pm shim, it is opt-in and reversible. It installs a node link in ~/.nub/node-shim and adds that directory to your PATH; because that comes first, a Node you install later (brew install node) is shadowed until you unshim. Re-run nub node shim after nub upgrade to re-link.
$ nub node unshim
removed /Users/you/.nub/node-shim
PATH: removed the node-shim block from /Users/you/.zshrcMirrors and proxies
Node downloads honor HTTP(S)_PROXY and NO_PROXY, and TLS-intercepting corporate CAs work out of the box through the system trust store. To fetch Node from an internal mirror instead of nodejs.org, set the standard env var or the .npmrc key pnpm uses — Nub reads both from a project or user-level file, with no Nub-specific config:
node-mirror:release=https://artifactory.corp.example/node/The NODEJS_ORG_MIRROR env var (the nvm/n convention) takes precedence over the .npmrc key. A mirror is used for musl builds too — point it at one carrying the unofficial-builds layout on Alpine.
Related
- Running files — how provisioning fires during a normal
nub <file>run. - FAQ — supported Node floor and compatibility.
The virtual store
How Nub's default linker works — one symlink per package into a machine-global store, with install-time phantom detection and per-package ejection — its warm-install performance, and the flat and project-local opt-outs.
Package meta-manager
Provision and run the package manager your project pins — corepack's job, in native Rust. The exact pnpm, npm, or yarn release is fetched, verified, cached, and run on the project's Node.