Shell
nano.shell() returns a stateful, renderer-agnostic line engine for your own terminal UI: it keeps the working directory and exported variables between lines, handles a few builtins in JavaScript and routes node and script lines to their runners.
A session
Each line is one shell.run() call. The prompt state (cwd, env) persists; the samples start in /work.
mkdir -p app/src && cd app/src export NAME=nano echo "hello $NAME" > greeting.txt cd .. find . -type f pwd
$ mkdir -p app/src && cd app/src $ export NAME=nano $ echo "hello $NAME" > greeting.txt $ cd .. $ find . -type f ./src/greeting.txt $ pwd /work/app
Options
| Option | Type | Default | Description |
|---|---|---|---|
cwd | string | "/root" | Initial working directory. |
env | Record<string, string> | {} | Initial environment, exported before every line. |
captureStderr | boolean | false | Capture stderr separately into result.stderr (adds a redirect and loses live interleaving). |
scriptCommand | string | "script" | Word that routes a line to the Boa sandbox instead of BusyBox. Active when the VM has scripting configured. |
scriptExpose | ExposeConfig | { fs: "readwrite", run: true, node: true } | Capabilities granted to script lines. |
cwd, env and captureStderr
const sh = nano.shell({ cwd: "/work", env: { LANG: "C.UTF-8" }, captureStderr: true });
const r = await sh.run('echo "out $LANG"; echo err >&2; exit 3');
print(JSON.stringify(r));
const plain = nano.shell();
print(plain.cwd, JSON.stringify(await plain.run("echo out; echo err >&2")));{"exitCode":3,"stdout":"out C.UTF-8\n","cwd":"/work","output":"out C.UTF-8\n","stderr":"err\n"}
/root {"exitCode":0,"stdout":"out\nerr\n","cwd":"/root","output":"out\nerr\n"}ShellResult
run(line, opts) takes the same ExecOptions as shExec and resolves to an ExecResult plus cwd (after the line), output (alias of stdout) and stderr (only with captureStderr).
Builtins and routing
pwdclearESC[2J ESC[3J ESC[Hexport KEY=value (one plain assignment)shell.envnode … without shell operatorsnano.node() with the session's cwd and env (host or VM per engines)script <source>nano.script(), when scripting is configuredsh via shExec, then the new cwd is read backBuiltins and their limits
Only a single, plain export KEY=value updates the tracked env. export A=1 B=2 stores the rest of the line as the value, and an assignment with $ runs in sh and is not kept.
const sh = nano.shell({ cwd: "/work" });
print(JSON.stringify((await sh.run("pwd")).stdout));
print(JSON.stringify((await sh.run("clear")).stdout));
await sh.run("export NAME=nano");
await sh.run("export A=1 B=2");
await sh.run("export X=$NAME-2");
print(JSON.stringify(sh.env));"/work\n"
"\u001b[2J\u001b[3J\u001b[H"
{"NAME":"nano","A":"1 B=2"}node lines run on the Node runner
A node line without shell operators goes to nano.node() with the session's cwd and env. With a pipe it goes to sh, which on this image has no node binary.
nano.fs.writeFile("/work/hi.js", "console.log('node in', process.cwd(), 'as', process.env.WHO)");
const sh = nano.shell({ cwd: "/work", env: { WHO: "shell" } });
print((await sh.run("node /work/hi.js")).stdout.trim());
print((await sh.run("node /work/hi.js | tr a-z A-Z")).stdout.trim());node in /work as shell /tmp/.nano-exec-7.sh: line 3: node: not found
scriptCommand and scriptExpose
Lines starting with the script word run in the Boa sandbox with the granted capabilities. Here the word is js and the grant is read-only files, so a write fails.
const sh = nano.shell({ scriptCommand: "js", scriptExpose: { fs: "readonly" } });
print(JSON.stringify(await sh.run(`js "nano.fs.readText('/etc/hostname').trim() + ' via boa'"`)));
const denied = await sh.run(`js "nano.fs.writeFile('/tmp/x', 'y')"`);
print(denied.exitCode, denied.stdout.split("\n")[0]);{"exitCode":0,"stdout":"nanovm via boa\n","cwd":"/root","output":"nanovm via boa\n"}
1 TypeError: not a callable functiontokenize
tokenize(line) splits a line on spaces and tabs while respecting single and double quotes, the way the Shell parses node lines. It does not handle backslash escapes.
Quote-aware word splitting
print(JSON.stringify(sdk.tokenize(`grep -n "hello world" 'it''s' --x=1`)));
["grep","-n","hello world","its","--x=1"]
Wiring it to a terminal
const shell = nano.shell({ cwd: "/root" });
async function onEnter(line) {
await shell.run(line, { onData: (chunk) => term.write(chunk) });
term.write(`${shell.cwd} $ `);
}The embeddable terminal uses the same engine. The Shell works over Nano and over the worker client (client.shell()), so the same UI code runs in both. For a finished UI, use <nano-terminal>.