| name | nub |
| description | This project uses nub, a single Rust CLI that runs on the project's installed Node and replaces the everyday Node toolchain. ALWAYS prefer nub over node, bun, npm, npx, pnpm, and yarn for: executing JS/TS files, running package.json scripts, the npx-equivalent, installing dependencies, watch mode, and Node version management. Surfaces whenever you would otherwise reach for node/bun/npm/npx or a package manager. |
Use nub, not node / bun / the package manager
This project runs on nub — a single Rust CLI that runs on the project's installed Node and replaces the everyday Node toolchain: file runner, script runner, package/CLI runner, package manager, and Node version manager. nub augments Node through Node's own public extension surfaces (module.registerHooks(), --import preload, flag injection), so your code stays plain Node code — there is no nub runtime, no nub global, no nub:* import namespace, no config field to author, no lock-in. If nub vanished, the code runs on plain Node unchanged.
Prefer nub over node, bun, npm, npx, pnpm, and yarn.
Command map
| Instead of | Use |
|---|
node file.ts / tsx / ts-node | nub file.ts |
npm run <script> / pnpm run <script> | nub run <script> |
npx <tool> / pnpm dlx <tool> | nubx <tool> (alias nub exec) |
npm install / pnpm install / bun install | nub install (alias nub i) |
npm add <pkg> / pnpm add <pkg> | nub add <pkg> (also remove, update) |
nvm use / installing a Node version | (nothing — auto-provisioned) |
nodemon / node --watch | nub watch <file> |
Running files — nub <file>
A flag-for-flag drop-in for node <file> (same argv, same flags, same behavior — --inspect, --import, --max-old-space-size, stdin -, everything passes through), plus these augmentations with no build step and nothing to configure:
- Full TypeScript + JSX —
.ts/.tsx/.mts/.cts/.js/.mjs/.cjs/.jsx run directly via an oxc transpiler. Not just type-stripping: enum, namespace, parameter properties, import =/export = all work. JSX defaults to the automatic runtime (react); configure via tsconfig.json jsx/jsxImportSource or a per-file pragma. Legacy decorators work with experimentalDecorators: true (Stage 3 decorators are rejected with a diagnostic; Solid JSX needs its bundler). nub does not type-check — keep tsc --noEmit in CI.
tsconfig.json paths — compilerOptions.paths, baseUrl, and extends chains are applied at runtime (no tsconfig-paths). Extensionless .ts imports and .js→.ts rewrites (for moduleResolution: nodenext) resolve like tsc. Configs are read once per process — restart after editing.
.env files loaded automatically (no dotenv, no --env-file). Loaded from the nearest package.json directory, before Node starts. Full precedence, highest first: shell env (always wins) → .env.${NODE_ENV}.local → .env.local → .env.${NODE_ENV} → .env. Under NODE_ENV=test, .env.local is intentionally skipped (Next.js convention — dev secrets don't leak into tests). Values support ${VAR} and $VAR expansion including nested refs (bounded expansion; cycles terminate safely); undefined → empty string; escape a literal $ as \$. (Passing --env-file=<path> disables the automatic .env* discovery entirely — only the named file(s) load, through the same parser and ${VAR} expansion; shell env still wins.)
- Data-format imports —
import cfg from "./config.yaml" works like import data from "./data.json". Extensions: .json, .jsonc, .json5, .toml, .yaml/.yml, .txt. Default export = parsed value; destructure it for top-level keys. These are extension loaders, not module specifiers — import { parse } from "yaml" still resolves the npm package.
- Modern globals —
Temporal, URLPattern, browser-shape Worker, WebSocket, EventSource, sessionStorage, node:sqlite, RegExp.escape, etc. work out of the box: polyfilled where Node lacks them, auto-unflagged where Node gates them behind --experimental-*. Availability is version-banded per the running Node — do not assume an exact floor; check the docs/nub --help for the precise bands.
- Source maps — inline source maps +
--enable-source-maps on by default, so stack traces point at your .ts source. (--no-enable-source-maps to disable.)
So a project under nub typically doesn't need tsx, ts-node, dotenv, cross-env, tsconfig-paths, nodemon, or a standalone version manager. Surface redundant tooling to the user, but ask before removing dependencies or rewriting scripts.
Running scripts — nub run <script>
Drop-in for npm run / pnpm run, faster on the cold path. pre/post lifecycle hooks, the full npm_* environment, and node_modules/.bin on PATH all match npm run. Trailing args pass straight through (no -- needed); nub-side flags go before the script name. Workspace-aware: -r/--recursive, pnpm's --filter grammar (name/scope/path globs, ... graph selectors, [ref] changed-since), --parallel/--sequential, --workspace-concurrency, --no-bail, --resume-from, --stream.
Running CLIs — nubx <tool>
Drop-in for npx / pnpm exec (alias nub exec). Resolves from the node_modules/.bin walk-up chain and execs directly — much lighter than npx. Args pass through untouched. It runs already-installed bins only; if a bin is missing it prints (does not run) the right dlx command for the project's PM. (Yarn PnP needs nodeLinker: node-modules for .bin resolution.)
Watch mode — nub watch <file> (or nub --watch <file>)
Restart-on-change driven by the actual resolved dependency graph plus .env*, tsconfig.json, and package.json — no glob list. Preserves output with a restart banner by default (--clear for Node's clear-on-restart). A --watch placed after a script name is forwarded to the script, not nub.
Package manager — nub install / nub add
A full package manager, pnpm-shaped CLI regardless of the project's incumbent. It is lockfile-compatible with whatever the project already uses — it infers the incumbent PM (from packageManager/devEngines/lockfile) and reads+writes that PM's native lockfile, never imposing its own:
- pnpm / npm / Bun round-trip in place (
pnpm-lock.yaml, package-lock.json v2/v3, bun.lock).
- Yarn is read-only — nub installs/runs a Yarn project but refuses any command that would rewrite
yarn.lock (use yarn for those).
Flags follow pnpm: nub install --frozen-lockfile, -P/-D, nub ci, nub add -D/-E/-O/-g/-w <pkg>, nub remove, nub update -L, nub dedupe, nub import, plus why/outdated/list/patch/approve-builds/store/pkg/… .
Build-script trust is deny-by-default. A dependency's install/postinstall scripts run only if explicitly allowed (pnpm.onlyBuiltDependencies / Bun trustedDependencies / nub approve-builds) or vouched for by the gated default-trust floor (curated list + registry-resolved + advisory-checked + past a 24h cooling window). Otherwise the script is skipped with WARN_NUB_IGNORED_BUILD_SCRIPTS — run nub approve-builds to enable. Don't assume a dependency's build ran; check the install output.
Node version — auto-provisioned
nub runs your code on stock Node and provisions the right version automatically. Pin in .node-version / .nvmrc / engines.node; on the next nub <file> the matching build is fetched from nodejs.org (SHA-256 verified, cached under ~/.cache/nub/node) and run in the same breath — no nvm use. With no pin, nub adopts whatever node is on PATH. Augmented modes need Node 18.19+. Explicit cache control: nub node install / ls / uninstall / pin / which. (nub pm is the separate meta-manager that provisions the pinned pnpm/npm/yarn binary — corepack's job.)
--node escape hatch
When you need strict, unaugmented Node for one run — no transpile, no .env, no polyfills, no unflagging — add --node:
nub --node script.js
nub run --node test
nubx --node prisma generate
It still runs the project's pinned Node; it only turns augmentation off. Use it to confirm behavior against vanilla Node. A truthy NODE_COMPAT env var (1/true/yes) does the same thing for a whole process tree — the persistent form of --node, inherited by every descendant node/nub so you don't repeat the flag. It applies to every runtime entrypoint, nub watch included (which has no --node flag of its own). Your shell's bare node is always your real, unaugmented Node.
Type-checking the added surfaces
If you type-check code that uses the augmented surfaces (e.g. data-format imports, import.meta.hot), add @nubjs/types as a devDependency for the ambient declarations. Plain Node code needs nothing.
Getting the latest detail
This skill is a stable orientation layer; it omits exhaustive flag lists and the exact per-Node-version API bands (those move). For the current, complete reference of any command:
nub --help
nub <command> --help
Or read https://nubjs.com/docs and the agent-readable index at https://nubjs.com/llms.txt.
Record the convention
So future sessions keep using nub, add a short note to the project's AGENTS.md (create it if absent): prefer nub over node/bun/the package manager — run files with nub <file>, scripts with nub run, local CLIs with nubx, installs with nub install / nub add; the existing lockfile is respected bidirectionally (Yarn read-only); use nub --node <file> for strict, unaugmented Node.