userland.rundocs70Try the demo
SDK

Headless

createHeadless() boots a VM and returns the terminal's features as an API with no UI: files, catalog installs, commands and server previews.

javascript
import { createHeadless } from "@userland-run/nano-sdk";

const s = await createHeadless({ wasmUrl: "/nano/console.wasm", ramMB: 512, serviceWorkerUrl: "/nano-sw.js" });
OptionTypeDefaultDescription
wasmUrlstring"/nano.wasm"The image to boot. Use console.wasm for BusyBox without a catalog install.
ramMBnumber1800VM memory. 256–512 is enough without Node on the VM.
serviceWorkerUrlstring"/nano-sw.js"Service worker for serve() previews.

The session

OptionTypeDefaultDescription
fsVfs—The filesystem.
run(command, opts?)Promise<ExecResult>—One BusyBox applet (as nano.run).
sh(line, opts?)Promise<ExecResult>—A full shell line (as nano.shExec).
installApp(ref, opts?)Promise<Manifest>—Install a signed catalog app.
catalog()Catalog—The catalog client.
serve(launch, opts?)Promise<{ url(path?), stop() }>port 8080, /listening/iStart a server (startServer) and get preview URLs. Options: port, readyPattern, onData.
vmNano—The underlying instance.
dispose()void—Stop servers, detach the preview bridge, destroy the VM. Idempotent; later serve() calls throw.
SDK · JavaScript

Files, commands and a catalog app

javascript
const s = await sdk.createHeadless({ wasmUrl: "/nano/console.wasm", ramMB: 256 });
s.fs.writeFile("/work/list.txt", "b\na\nc\n");
print(JSON.stringify(await s.run("sort /work/list.txt")));
print(JSON.stringify(await s.sh("sort -r /work/list.txt | head -1")));
const m = await s.installApp("hexyl");
print("installed", m.name, "| same Vfs:", s.fs === s.vm.fs);
print((await s.sh("printf AB | hexyl --border none --color never")).stdout.trim());
s.dispose();
loading…
Expected output
{"exitCode":0,"stdout":"a\nb\nc\n"}
{"exitCode":0,"stdout":"c\n"}
installed hexyl | same Vfs: true
00000000  41 42                                              AB
SDK · JavaScript

serve() with a preview URL

serve() starts the server, registers the preview bridge and returns url(path). A hidden iframe loads it through the service worker.

javascript
const s = await sdk.createHeadless({ wasmUrl: "/nano/console.wasm", ramMB: 256, serviceWorkerUrl: "/nano-sw.js" });
s.fs.writeFile("/www/index.html", "<h1>served headless</h1>");
const srv = await s.serve({ command: "echo listening on 8090; exec busybox httpd -f -p 8090 -h /www" }, { port: 8090 });
const frame = Object.assign(document.createElement("iframe"), { hidden: true, src: srv.url("/") });
const loaded = new Promise((r) => (frame.onload = r));
document.body.append(frame);
await loaded;
print(srv.url("/"), "→", frame.contentDocument.body.textContent);
frame.remove();
srv.stop();
s.dispose();
s.dispose(); // idempotent
print(await s.serve({ command: "true" }).catch((e) => e.message));
loading…
Expected output
/sw/8090/ → served headless
nano-sdk: headless session is disposed

See the embeddable terminal feature and the json-api example for the UI side. Display vs. headless: <nano-terminal> renders these features; createHeadless() exposes them as an API. For a VM off the main thread, use the worker transport or provision().