v0.8.0
This release redesigns process spawning around two explicit use cases: ordinary long-lived commands with streams, and interactive terminal sessions with a PTY. This is a breaking API change because the old spawn shape blurred those two modes and made it hard to build SSH-like clients without special cases.
Separate APIs for streamed processes and PTYs
Use sandbox.spawn() for non-interactive commands that may still run for a long time. It returns a process handle immediately with Web streams for stdin, stdout, and stderr, plus lifecycle promises for readiness and exit.
const process = sandbox.spawn("node", ["server.js"], {
cwd: "/workspace",
});
await process.ready;
const logs = process.stdout
.pipeThrough(new TextDecoderStream())
.getReader();
await process.stdin.getWriter().write(new TextEncoder().encode("status\n"));
const exit = await process.exit;
console.log(exit);Use sandbox.pty() when the guest process should believe it is attached to a terminal. This is the right shape for shells, REPLs, TUIs, and SSH-like clients that need terminal behavior instead of plain pipes.
const shell = sandbox.pty("/bin/bash", [], {
cwd: "/workspace",
size: { rows: 30, cols: 120 },
});
await shell.ready;
shell.output
.pipeThrough(new TextDecoderStream())
.pipeTo(hostTerminalWritable);
hostTerminalReadable.pipeTo(shell.input);
shell.resize({ rows: 40, cols: 140 });Undefined args default to an empty argv
Callers can still omit the argument list for commands that do not need one. undefined args are treated as [] at the public boundary for exec, spawn, and pty.
await sandbox.exec("pwd");
const process = sandbox.spawn("sleep");
const shell = sandbox.pty("/bin/sh", {
size: { rows: 30, cols: 120 },
});Process lifecycle is now explicit
Spawned handles expose readiness separately from process completion. This lets callers wire up streams immediately while still detecting launch failures correctly. If a process exits before it reaches the ready state, ready rejects and exit still resolves with the process termination details.
const process = sandbox.spawn("false");
try {
await process.ready;
} catch (error) {
// The command failed before it became ready.
}
const exit = await process.exit;Signal exits are reported as signal exits instead of being collapsed into only numeric exit codes when the guest can report the signal.
const process = sandbox.spawn("sleep", ["60"]);
await process.kill("SIGTERM");
const exit = await process.exit;
// { exitCode: null, signal: "SIGTERM" }Abort and stream closure behavior is stricter
Already-aborted signals now prevent spawn() and pty() from launching guest commands. Once a spawned process or PTY has closed its input stream, later writes fail instead of being accepted and ignored.
const controller = new AbortController();
controller.abort();
try {
sandbox.spawn("touch", ["/tmp/should-not-exist"], {
signal: controller.signal,
});
} catch (error) {
// No guest command was launched.
}Choosing the right process API
Use pty() for programs that draw a terminal UI, check whether stdin is a TTY, use line editing, or expect job-control behavior. Shells, language REPLs, editors, pagers, and SSH-like clients should generally use this API.
const shell = sandbox.pty("/bin/bash", [], {
size: { rows: 30, cols: 120 },
});Use spawn() for services, build tools, test runners, package managers, and other commands where stdin, stdout, and stderr should remain separate streams.
const process = sandbox.spawn("npm", ["test"]);
process.stdout
.pipeThrough(new TextDecoderStream())
.pipeTo(logWritable);
const exit = await process.exit;When piping host input into the guest, treat writable failures as the normal signal that the guest no longer accepts input. For long-lived clients, stop the host-side pipe when exit resolves or when the writable rejects.