userland.rundocs70Try the demo
SDK

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.

shell

A session

Each line is one shell.run() call. The prompt state (cwd, env) persists; the samples start in /work.

bash
mkdir -p app/src && cd app/src
export NAME=nano
echo "hello $NAME" > greeting.txt
cd ..
find . -type f
pwd
loading…
Expected output
$ 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

OptionTypeDefaultDescription
cwdstring"/root"Initial working directory.
envRecord<string, string>{}Initial environment, exported before every line.
captureStderrbooleanfalseCapture stderr separately into result.stderr (adds a redirect and loses live interleaving).
scriptCommandstring"script"Word that routes a line to the Boa sandbox instead of BusyBox. Active when the VM has scripting configured.
scriptExposeExposeConfig{ fs: "readwrite", run: true, node: true }Capabilities granted to script lines.
SDK · JavaScript

cwd, env and captureStderr

javascript
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")));
loading…
Expected output
{"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

Line
Handled by
pwd
JS: prints the tracked cwd, no process
clear
JS: emits ESC[2J ESC[3J ESC[H
export KEY=value (one plain assignment)
JS: updates shell.env
node … without shell operators
nano.node() with the session's cwd and env (host or VM per engines)
script <source>
the Boa sandbox via nano.script(), when scripting is configured
anything else
sh via shExec, then the new cwd is read back
SDK · JavaScript

Builtins 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.

javascript
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));
loading…
Expected output
"/work\n"
"\u001b[2J\u001b[3J\u001b[H"
{"NAME":"nano","A":"1 B=2"}
SDK · JavaScript

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.

javascript
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());
loading…
Expected output
node in /work as shell
/tmp/.nano-exec-7.sh: line 3: node: not found
SDK · JavaScript

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.

javascript
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]);
loading…
Expected output
{"exitCode":0,"stdout":"nanovm via boa\n","cwd":"/root","output":"nanovm via boa\n"}
1 TypeError: not a callable function

tokenize

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.

SDK · JavaScript

Quote-aware word splitting

javascript
print(JSON.stringify(sdk.tokenize(`grep -n "hello world" 'it''s' --x=1`)));
loading…
Expected output
["grep","-n","hello world","its","--x=1"]

Wiring it to a terminal

javascript
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>.