userland.rundocs70Try the demo
SDK

Running commands

Four ways to start a process: run for one BusyBox applet, exec for an exact argv, shExec for a full shell line, node for Node.js. All take ExecOptions and resolve to an ExecResult.

Method
What it does
Shell features
Starts in
run(command)
splits on whitespace and runs one BusyBox applet
none (no quotes, pipes, $VAR)
/
exec(argv)
single-quotes every element and runs it through sh
none (argv is passed literally)
/
shExec(line)
writes the line to a temp script and runs sh <file>
all: pipes, redirects, &&, loops, variables
/
node(args)
runs Node.js with an explicit argv (without the leading node)
n/a
opts.cwd, else /

run

SDK · JavaScript

run(): one applet, no shell

The line is split on whitespace and handed to BusyBox, so | is just an argument here.

javascript
const r = await nano.run("echo a | b");
print(JSON.stringify(r));
loading…
Expected output
{"exitCode":0,"stdout":"a | b\n"}

exec

SDK · JavaScript

exec(): an exact argv

Each element is single-quoted, so spaces, $ and quotes reach the program unchanged. Use it for user-supplied arguments.

javascript
const r = await nano.exec(["printf", "[%s]\\n", "a b", "$HOME", "it's"]);
print(r.stdout.trim());
loading…
Expected output
[a b]
[$HOME]
[it's]

shExec

SDK · JavaScript

shExec(): a full shell line

Pipes, redirects and variables work. stderr is merged into stdout in write order.

javascript
nano.fs.writeFile("/work/data.txt", "3\n10\n7\n");
const r = await nano.shExec("sort -n /work/data.txt | tail -1; pwd; echo to-stderr >&2; exit 4");
print(JSON.stringify(r));
loading…
Expected output
{"exitCode":4,"stdout":"10\n/\nto-stderr\n"}

node

node(args, opts) runs on the runner chosen by engines; this page uses the host engine. cwd and env are honored by node() only. On the RISC-V runner they are applied through an sh wrapper that needs /usr/bin/node in the filesystem (true after installApp("node")).

SDK · JavaScript

node() with cwd and env

javascript
nano.fs.writeFile("/work/app/main.js", "console.log(process.cwd(), process.env.GREETING, require('path').basename(__filename))");
const r = await nano.node(["/work/app/main.js"], { cwd: "/work/app", env: { GREETING: "hi" } });
print(JSON.stringify(r));
loading…
Expected output
{"exitCode":0,"stdout":"/work/app hi main.js\n"}
Caution · On the host engine, pass the entry script as an absolute path. In SDK 0.2.10 a relative entry (node(["main.js"], { cwd })) exits with code 1 and no output.

ExecOptions

OptionTypeDefaultDescription
onData(chunk: string) => void—Streams combined stdout+stderr as the guest writes it.
maxStepsnumber2,000,000; 2e9 for VM `node()`Instruction budget. When it runs out the run ends with exitCode: -1.
cwdstring—Working directory. node() only; run/exec/shExec always start at / (use cd in the line).
envRecord<string, string>—Extra environment variables. node() only.
SDK · JavaScript

onData: stream output

javascript
const chunks = [];
const r = await nano.shExec("for i in 1 2 3; do echo line $i; done", { onData: (c) => chunks.push(c) });
print(JSON.stringify(chunks), "exit", r.exitCode);
loading…
Expected output
["line 1\n","line 2\n","line 3\n"] exit 0
SDK · JavaScript

maxSteps: bound a runaway program

An infinite loop with a 1,000,000-instruction budget. Raise the budget for heavy work, lower it for untrusted input.

javascript
const r = await nano.shExec("while true; do :; done", { maxSteps: 1_000_000 });
print(JSON.stringify(r));
loading…
Expected output
{"exitCode":-1,"stdout":""}
SDK · JavaScript

cwd/env don't apply to shExec

shExec ignores cwd and env; put cd and export in the line, or use a Shell, which tracks both for you.

javascript
print((await nano.shExec("pwd; echo [$GREETING]", { cwd: "/work", env: { GREETING: "hi" } })).stdout.trim());
print((await nano.shExec("cd /work && GREETING=hi && echo $(pwd) [$GREETING]")).stdout.trim());
loading…
Expected output
/
[]
/work [hi]

ExecResult

OptionTypeDefaultDescription
exitCodenumber—The process exit status. -1 when the instruction budget ran out or the run was cancelled.
stdoutstring—Combined stdout and stderr.
cancelledboolean—true when cancel() stopped the run.
snapshotReadyboolean—Set by snapshot runs (see NodeRuntime).

VM operations are serialized

A NanoVM runs one thing at a time: each run resets the CPU state. The SDK queues every VM-occupying call (run, exec, shExec, VM node(), fs mutations, snapshots) in FIFO order, so you can fire calls concurrently without corrupting each other. Host-engine node() doesn't occupy the VM. withVmLock(fn) runs your own code under the same lock, which you need when you call nano.raw directly. It is not re-entrant: don't call another queued nano method inside fn.

SDK · JavaScript

Concurrent calls queue in order

javascript
const order = [];
const a = nano.run("echo A").then(() => order.push("A"));
const b = nano.withVmLock(async () => {
  order.push("lock");
  return (await nano.raw.run("echo raw")).stdout.trim();
});
const c = nano.run("echo C").then(() => order.push("C"));
print("raw run:", await b);
await Promise.all([a, c]);
print("order:", order.join(" → "));
loading…
Expected output
raw run: raw
order: A → lock → C

While a server started with startServer() runs, the VM is held: other VM calls reject at once with "the VM is busy serving" until stop(). Use a second instance for concurrent work.

Standard input

OptionTypeDefaultDescription
setInteractiveStdin(on = true)voidoffInteractive mode: reads block until you writeStdin() more data or closeStdin().
writeStdin(data)string | Uint8Array—Append bytes to the guest's stdin.
closeStdin()void—Signal end of input (EOF).

A blocking read on the main thread blocks the page, so use interactive stdin with the worker transport (or the terminal, which does this for you). For one-shot input, redirect from a file instead.

SDK · JavaScript

One-shot input from a file

javascript
nano.fs.writeFile("/tmp/in.txt", "alpha\nbeta\ngamma\n");
print((await nano.shExec("sort -r < /tmp/in.txt")).stdout.trim());
loading…
Expected output
gamma
beta
alpha
SDK · JavaScript

Interactive stdin over a worker

The VM runs in a Web Worker, so the page keeps running while read waits. Input arrives 200 ms and 400 ms later; closeStdin() ends cat.

javascript
const vm = await sdk.createNanoWorker({ image: { wasm: "/nano/console.wasm" }, ramMB: 256 });
await vm.setInteractiveStdin(true);
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const run = vm.shExec("read name; echo hello $name; cat", { onData: (d) => print("[out]", d.trim()) });
await sleep(200);
await vm.writeStdin("nano\n");
await sleep(200);
await vm.writeStdin("more input\n");
await sleep(200);
await vm.closeStdin();
print("exit", (await run).exitCode);
vm.destroy();
loading…
Expected output
[out] hello nano
[out] more input
exit 0

Cancel and destroy

cancel() stops the current run; it resolves with { exitCode: -1, cancelled: true } and the VM stays usable. destroy() frees the VM; any later call on it fails.

SDK · JavaScript

cancel() a long run

javascript
const t0 = performance.now();
const running = nano.shExec("while true; do :; done", { maxSteps: 1e12 });
setTimeout(() => nano.cancel(), 300);
const r = await running;
print(JSON.stringify(r), "after ~" + Math.round((performance.now() - t0) / 100) * 100, "ms");
print((await nano.run("echo still usable")).stdout.trim());
loading…
Expected output
{"exitCode":-1,"stdout":"","cancelled":true} after ~300 ms
still usable
SDK · JavaScript

destroy() frees a VM

javascript
const vm = await sdk.createNano({ image: { wasm: "/nano/console.wasm" }, ramMB: 256 });
print((await vm.run("echo alive")).stdout.trim());
vm.destroy();
const after = await vm.run("echo again").then(() => "ran", () => "rejected");
print("after destroy:", after);
loading…
Expected output
alive
after destroy: rejected

Ready-made command-line tools to run this way are in the app catalog, for example ripgrep and jaq.

Error codes

Runs resolve with an exit code rather than throwing. Errors you can catch: ERR_NODE_HOST_UNSUPPORTED and ERR_NODE_HOST_RUNTIME_UNAVAILABLE (on err.code, see engines), "the VM is busy serving" while a server holds the VM, nano-sdk: fs op failed (exit N) from fs mutations, and catalog: … errors from installs.