Appearance
Shell
Spawn child processes and stream their output to the frontend in real time.
| Config key | shell |
| JS namespace | Shell |
| Core | No |
| Phases | Bindable · Lifecycle |
| Hard deps | stream |
| Platforms | macOS · Linux · Windows |
Shell lets you run commands and pipe their stdout/stderr to the browser over the WebSocket stream. Use it for build pipelines, log tailing, long-running tools, or anything that writes to standard output.
Disabling stream automatically disables this plugin.
Enabling
yaml
plugins:
enabled:
- shell
- stream # requiredOr omit plugins: entirely.
Spawning a process
lune.Shell.spawn starts the process immediately and returns a pid (a random hex string). Use the pid to subscribe to output and kill the process.
js
import { lune } from "../lunejs/runtime/runtime.js";
const pid = await lune.Shell.spawn({
command: "ping",
args: ["-c", "5", "127.0.0.1"],
});
lune.Shell.listen(pid, {
stdout: ({ line }) => console.log("out:", line),
stderr: ({ line }) => console.error("err:", line),
exit: ({ code }) => console.log("exited with", code),
});listen auto-unsubscribes all three channels once the exit event fires.
Collecting all output
lune.Shell.run is an async binding that captures all output and resolves with { stdout, stderr, code } once the process exits. Use it for short-lived commands where you want all output at once.
js
const { stdout, stderr, code } = await lune.Shell.run({
command: "uname",
args: ["-a"],
});
console.log(stdout); // Darwin …Killing a process
js
const pid = await lune.Shell.spawn({ command: "sleep", args: ["60"] });
await lune.Shell.kill({ pid }); // sends SIGTERMCalling lune.Shell.kill on an already-exited pid is a no-op.
Listing running processes
lune.Shell.list returns the pids of all processes currently alive. Use it to hydrate state in secondary windows that didn't spawn the processes.
js
const pids = await lune.Shell.list();
// ["a1b2c3d4...", ...]
for (const pid of pids) {
lune.Shell.listen(pid, {
stdout: ({ line }) => console.log(line),
exit: ({ code }) => console.log("done", code),
});
}Writing to stdin
lune.Shell.write sends text to the standard input of a running process. Use it for interactive programs that read commands from stdin — shells, REPLs, password prompts, etc.
js
const pid = await lune.Shell.spawn({ command: "/bin/sh", args: ["-i"] });
lune.Shell.listen(pid, {
stdout: ({ line }) => console.log(line),
exit: ({ code }) => console.log("exited", code),
});
await lune.Shell.write({ pid, text: "echo hello\n" });
await lune.Shell.write({ pid, text: "exit\n" });lune.Shell.close_stdin closes the stdin pipe, which sends EOF to the process. Many programs (e.g. sort, cat, wc) only flush their output once stdin is closed:
js
const pid = await lune.Shell.spawn({ command: "sort", args: [] });
await lune.Shell.write({ pid, text: "banana\n" });
await lune.Shell.write({ pid, text: "apple\n" });
lune.Shell.closeStdin({ pid }); // EOF → sort prints sorted output and exitslune.Shell.kill also closes stdin automatically.
Unsubscribing early
js
const pid = await lune.Shell.spawn({
command: "tail",
args: ["-f", "/var/log/system.log"],
});
lune.Shell.listen(pid, { stdout: ({ line }) => render(line) });
// Stop receiving output but let the process keep running
lune.Shell.unlisten(pid);JavaScript API
| Method | Signature | Description |
|---|---|---|
spawn | ({ command, args }) → Promise<string> | Start a process; returns pid |
run | ({ command, args? }) → Promise<{stdout, stderr, code}> | Spawn and collect all output |
kill | ({ pid }) → Promise<void> | Send SIGTERM to a running process |
list | () → Promise<string[]> | List pids of all currently live processes |
write | ({ pid, text }) → Promise<void> | Write text to a process's stdin |
closeStdin | ({ pid }) → Promise<void> | Close stdin, sending EOF to the process |
listen | (pid, opts) → void | Subscribe to output channels |
unlisten | (pid) → void | Remove all listeners for a pid |
listen options:
| Key | Type | Description |
|---|---|---|
stdout | (data: { line: string }) => void | Called per stdout line |
stderr | (data: { line: string }) => void | Called per stderr line |
exit | (data: { code: number }) => void | Called once on exit; auto-unsubscribes all listeners |
How it works
Each spawned process gets three Stream channels keyed by its pid:
shell:<pid>:stdout— one message per stdout lineshell:<pid>:stderr— one message per stderr lineshell:<pid>:exit— single message with{ code }after both pipes are drained
Crystal reads stdout and stderr in parallel async fibers, then waits for both to close before sending the exit event — so the exit message always arrives after all output.
Notes
- Output is line-buffered. Each
{ line }payload is one line. Processes that don't flush until exit produce no output until they exit or flush. - Shell metacharacters are not expanded. Pass the binary as the first argument and flags as separate array elements. For pipes or globs:
spawn({ command: "/bin/sh", args: ["-c", "ls | grep foo"] }). - Auto-cleanup on window close. The
Lifecycleshutdown hook sends SIGTERM to all running processes when the app quits. - Windows cmd builtins and
.cmd/.batshims work transparently. WhenCreateProcessraisesFile::NotFoundErrorfor a name likeecho,dir,type,npm.cmd, oryarn.cmd, the plugin auto-retries viacmd /c <name> …. No manual wrapping required.
Platform notes
- macOS — Verified.
- Linux — Untested.
- Windows — Verified. cmd builtins (echo, dir, type, etc.) and
.cmd/.batshims (npm.cmd, yarn.cmd) work — the plugin auto-retries viacmd /cwhen directProcess.newraisesFile::NotFoundError.
Disabling
yaml
plugins:
disabled:
- shell