Commands
The userland — 60+ lazily-loaded coreutils plus node, npm, and git — and how to write your own.
Commands are Lifo's userland: ls, grep, sed, curl, node, npm, git,
and dozens more. Each is a plain async function over a CommandContext, held in
a CommandRegistry.
The command contract
A command is just:
type Command = (ctx: CommandContext) => Promise<number>; // resolves to an exit codeCommandContext gives it everything it needs and nothing it doesn't:
interface CommandContext {
args: string[]; // argv (args[0] is the command name)
env: Record<string, string>;
cwd: string;
vfs: VFS; // the filesystem
stdout: { write(t: string): void };
stderr: { write(t: string): void };
stdin?: { read(): Promise<string | null>; readAll(): Promise<string> };
signal: AbortSignal; // cancellation / kill
setRawMode?: (on: boolean) => void; // interactive TTY only
executeCapture?: (line: string, o?) => Promise<string>; // shell out
executeCaptureResult?: (line: string, o?) => Promise<{ stdout; stderr; code }>;
}Because a command only touches ctx, it's isolated and testable — it reads
args/stdin, writes stdout/stderr, honors signal, and returns an exit
code. executeCapture / executeCaptureResult let a command run other commands,
which is how child_process shells out.
The registry (lazy by default)
Commands are registered on a CommandRegistry, almost always lazily — the
implementation is a dynamic import() that only loads when the command first
runs, so unused commands never enter the bundle:
registry.registerLazy("ls", () => import("./fs/ls.js"));
registry.register("my-tool", async (ctx) => {
ctx.stdout.write("hello\n");
return 0;
});createDefaultRegistry() wires up the standard set. Sandbox.create() builds
on it and adds the system commands.
What's built in
Roughly 60+ commands, grouped by area:
- Filesystem —
ls,cat,cp,mv,rm,mkdir,touch,find,tree,stat,ln,du,df,chmod,chown,realpath,file, … - Text —
grep,sed,awk,head,tail,wc,sort,uniq,cut,tr,diff,nl,rev,tac,base64,seq,nano,less, … - Archive —
tar,gzip/gunzip,zip/unzip. - I/O —
tee,xargs,yes,printf. - System —
env,uname,date,whoami,which,free,uptime,ps/top/kill,node,npm/npx,systemctl,lifo, … - Network —
curl,ifconfig,route,netstat,host,ip,ports,tunnel,forward. - Git — a full
gitvia isomorphic-git, in every shell.
Adding your own
Register a handler on the sandbox and it's immediately available in the shell:
sandbox.commands.register("greet", async (ctx) => {
const who = ctx.args[1] ?? "world";
ctx.stdout.write(`hello, ${who}\n`);
return 0;
});
await sandbox.commands.run("greet there"); // → "hello, there"