Node.js
1. What's in the catalogue
Three published lines, 22.23.2, 24.19.0, 26.7.0; pin with @22, @24, @26. As of catalogue 2026.35.22 (2026-08-25):
| Tool | Caps | Requires | Notes |
|---|---|---|---|
node | rw | — | interpreter only; no network by default |
npm | net, rw | node | the default environment provider for package.json |
npx | net, rw | node | run a package's binary without a project |
yarn | net, rw | node | explicit opt-in provider (-e @yarn:…) |
Declared, not yet runnable: typescript (with its tsc binary). Its image is in the signed payload with no tool bound, so kapsl --info tsc reports unknown tool today. There is no published JS linter in the catalogue — no eslint, no prettier — so editor-side linting is the IDE use case's territory, not the catalogue's.
2. The five-minute version
kapsl node --version
v26.7.0
Run a script:
kapsl node hello.js
The inline package form — node:lodash installs that package into a scanned one-shot environment (via NODE_PATH, see below), then runs:
kapsl node:lodash -e "console.log('lodash', require('lodash/package.json').version)"
lodash 4.18.1
The file-based form — a package.json next to the script:
kapsl -e @npm:package.json node hello.js
lodash 4.18.1 [ [ 1, 2 ], [ 3, 4 ] ]3. The environment story: where packages go, and install scripts
require() does not search /usr/local/lib/node_modules by default. The npm provider installs globally into the environment image and sets NODE_PATH at build time, so globally installed packages are visible to require() with no runtime configuration and no ./node_modules in your working tree. Your project's working directory stays exactly as you left it; the dependency set lives in the scanned image.
The provider has one deliberate escalation worth knowing about. Modern npm blocks a dependency's preinstall/install/postinstall scripts by default and only warns about it. That is a footgun in this context: a native addon whose entire install script is node-gyp rebuild installs "successfully" (npm exits 0), silently produces no compiled .node file at all, and only fails later at require() time with a confusing Cannot find module '../build/Release/…'. The builder therefore passes --dangerously-allow-all-scripts. The name is npm's; the risk model here is not weaker — the install already runs as root inside an isolated podman build, the same "arbitrary code execution during env build" every other provider accepts. A loud compile error beats a silent no-op.
yarn is never guessed. package.json belongs to npm by default, so a project using yarn has to say so: kapsl -e @yarn:Yarn.lock node app.js. Auto-detecting from package.json alone is impossible — both tools read the same file. The node-yarn example is the same shape as the npm examples with the provider named.
4. Tooling
The catalogue's Node tooling is node, npm, npx, and yarn, and nothing else today. typescript/tsc is declared, not published — until it is, tsc-based builds run through the project's own environment (the NestJS example does exactly that) or wait for the publish. No JS linter is published either (no eslint, no prettier); editor-side linting is the IDE use case's.
One mechanism matters for framework projects: reaching a binary npm installed into the environment image. npx <pkg> resolves through the environment; when a framework's binary lives at a known path (NestJS installs nest under the global prefix), the project overlay can reach it with an explicit args splice — the NestJS example does exactly this with a bash -c invocation of the installed binary, and its README explains why.
5. Project patterns
The examples corpus covers the full framework spread; each README carries the captured transcript, including the failures:
express-quickstart— the canonical Express service:package.json, published port 3000,0.0.0.0binding, the security notes on whatnpm installdoes inside the build, and the full--watchinvestigation (below).node-simple— all three levels of integration with no dependencies: ports,0.0.0.0binding,subcommand_index, dev/prod alias pairs in one.kapslrc, andKAPSL_ARGSas the one-off escape hatch.node-package— the two dependency models (environment image vs a realnode_modules) andnpm installasnpm install.vite-tailwind-quickstart—npm run devkeyed onsubcommand_index: 1, port 5173, the-- --host 0.0.0.0splice.nextjs-quickstart— the scaffolding case:npx create-next-app@latestwith--cap net, and the real finding that scaffolding into.fails on the CLI's own writability check, not the sandbox (below).nestjs-quickstart— a full build-then-run service: the environment carries the installed toolchain,nest buildruns inside it, and the production path is plainnode dist/main.json a published port.svelte-quickstart—npm run devatsubcommand_index: 1, plus thenpxnetwork grant.
The overlay shape is the same for every npm run dev family project — from the vite-tailwind-quickstart .kapslrc:
tools:
npm:
subcommand_index: 1
subcommands:
dev:
ports: ["5173"]
args: ["--", "--host", "0.0.0.0"]
subcommand_index: 1 because args[0] is always the literal run; args splices --host 0.0.0.0 after the matched subcommand so the dev server binds an address the host can reach.
6. Known limitations
From the example transcripts:
--watchdoes not see host edits through the bind mount. The express example tried Node's native--watchfirst: it usesfs.watch(inotify on Linux), which does not reliably propagate across the podman bind mount, so host edits did not restart the server. The verified working alternative isnodemon --legacy-watch— but reached correctly:node -- --legacy-watch app.jssends the flag to node (bad option: --legacy-watch), so the example runs it through the absolute path withbash -c '/usr/local/lib/node_modules/.bin/nodemon --legacy-watch app.js'.- The
--pass-through is where the flag dies. npm's own--separates npm's arguments from the script's; a project overlay'sargssplice lands after the matched subcommand, so a flag meant for a watcher (or the app) must survive both boundaries. The express README's failing transcripts are the reference for what happens when one is crossed wrong. - Scaffolding into
.fails in a way that is not a capability problem.create-next-app .reports"The application path is not writable"— and the nextjs example proves that is not the sandbox: a directwriteFileSyncin the same directory succeeds, and an explicit--cap rwchanges nothing. The CLI's own writability check does not hold up against the target being.itself; scaffolding into a named subdirectory sidesteps it entirely (the vue and angular examples hit the same wall, for their own different reasons). Separately,npxneeds--cap netexplicitly, as in every othernpx-based example.