userland.run70Try the demo
Example · cli-toolkit

An npm-package CLI (commander + chalk) on the real Node.js

riscv · RISC-V VM

A small command-line tool on two everyday npm packages: commander parses the arguments and chalk colours the report. The dependency tree is pinned by the example's package-lock.json; npm run demos:build installs it with npm ci and ships the installed node_modules/ as bundle.json.

The live sample is the integration code you would write yourself: fetch the bundle, write every file into the VFS with nano.fs.writeFile(), install the riscv64 node binary from the signed catalog, and run cli.js with it. Node's own require() resolves commander and chalk from node_modules/ next to the script.

Why the RISC-V VM and not the faster host engine? commander 12 starts with const process = require("node:process"), and the host-engine tier (nodert, as shipped in nano-sdk 0.2.6) passes process into its module wrapper as a parameter, so that line is a SyntaxError there. The unmodified Node binary on the VM has no such gap — which is what the VM tier is for. Expect the first run to take a while: it downloads Node and boots V8 on an emulated CPU.

Run it

SDK · JavaScript

Install the bundle and run the CLI on the VM

An async function body with nano (the page's booted Nano, 1.8 GB guest) and print() in scope.

javascript
const dir = "/work/cli-toolkit";

// 1 · install: the node_modules/ that `npm ci` produced from package-lock.json
const bundle = await (await fetch("/demo-files/cli-toolkit/bundle.json")).json();
for (const f of bundle) nano.fs.writeFile(`${dir}/${f.path}`, f.content);
nano.fs.writeFile(`${dir}/cli.js`, await (await fetch("/demo-files/cli-toolkit/cli.js")).text());
print(`wrote ${bundle.length} files into ${dir}/node_modules`);

// 2 · the real Node.js binary (riscv64) from the signed catalog
const node = await nano.installApp("node");
print(`installed ${node.name} ${node.version}`);

// 3 · run it on the RISC-V VM
const r = await nano.shExec(`cd ${dir} && node cli.js`, { maxSteps: 1e12 });
print(r.stdout);
loading…
Expected output
wrote 42 files into /work/cli-toolkit/node_modules
installed node 25.4.0

  nano cli-toolkit  ·  npm packages, running in NanoVM

  PACKAGE         VERSION     ROLE
  ────────────────────────────────────────
  commander       12.1.0      args
  chalk           4.1.2       color
  ansi-styles     4.3.0       color
  supports-color  7.2.0       tty
  color-convert   2.0.1       color

  ✓ 5 packages resolved by require() from node_modules

How it runs

  1. Fetch /demo-files/cli-toolkit/bundle.json (the installed node_modules/, 42 files) and cli.js, and write them under /work/cli-toolkit/ with nano.fs.writeFile().
  2. nano.installApp("node") installs the riscv64 Node.js v25.4.0 ELF from the signed catalog (verified, cached in OPFS).
  3. nano.shExec("cd /work/cli-toolkit && node cli.js") runs it on the RISC-V VM; the output is printed.

Source

From demos/cli-toolkit/.

cli.js
javascript
// A CLI built on npm packages — commander for arg parsing, chalk for color —
// resolved from package-lock.json by `npm run demos:build` and running
// unmodified inside NanoVM. We pass an explicit argv to program.parse() so the
// demo is self-contained.
const { Command } = require("commander");
const chalk = require("chalk");
const fs = require("fs");
const path = require("path");

const versionOf = (name) =>
  JSON.parse(fs.readFileSync(path.join(__dirname, "node_modules", name, "package.json"), "utf8")).version;

const program = new Command();
program.name("nano").description("a tiny build-report CLI on commander + chalk").version("1.0.0");

program
  .command("report")
  .description("print a colorized dependency report")
  .option("-l, --limit <n>", "rows to show", "5")
  .action((opts) => {
    // Versions are read from each installed package's own package.json.
    const deps = [
      ["commander", "args"],
      ["chalk", "color"],
      ["ansi-styles", "color"],
      ["supports-color", "tty"],
      ["color-convert", "color"],
    ].map(([name, role]) => [name, versionOf(name), role]);
    const limit = parseInt(opts.limit, 10) || deps.length;

    console.log(chalk.cyan.bold("\n  nano cli-toolkit") + chalk.gray("  ·  npm packages, running in NanoVM\n"));
    console.log(
      "  " +
        chalk.gray("PACKAGE".padEnd(16)) +
        chalk.gray("VERSION".padEnd(12)) +
        chalk.gray("ROLE"),
    );
    console.log("  " + chalk.gray("─".repeat(40)));
    for (const [name, version, role] of deps.slice(0, limit)) {
      console.log(
        "  " +
          chalk.white(name.padEnd(16)) +
          chalk.yellow(version.padEnd(12)) +
          chalk.magenta(role),
      );
    }
    console.log(chalk.green("\n  ✓ ") + chalk.gray(deps.length + " packages resolved by require() from node_modules\n"));
  });

program.parse(["node", "cli.js", "report"]);
package.json
json
{
  "name": "nano-cli-toolkit",
  "version": "1.0.0",
  "private": true,
  "dependencies": {
    "commander": "^12.1.0",
    "chalk": "^4.1.2"
  }
}

Further reading

Other examples