Skip to main content
commands.run() resolves with everything at once when the command ends, which is all a quick command needs. Commands that run longer are worth bounding, watching, or feeding input to while they run. These options cover that; they layer onto the same run() call and none of them change its return value.

Timeouts

Both methods take a timeout in seconds. When it is reached the guest work is interrupted and the call returns rather than waiting.

Interrupting a command

A command no longer has to run to its timeout. interrupt() stops whatever is in the foreground, and commands.run() takes an AbortSignal for callers who would rather set the deadline up front.
The two report differently, on purpose. interrupt() resolves with an ordinary result carrying exit code 130, because a terminal wants the exit code. A signal rejects with signal.reason, so your own controller throws AbortError and AbortSignal.timeout() throws TimeoutError. The command really stops. This is not the caller walking away while the guest keeps working, so the sandbox is yours again immediately.
code.run() cannot be interrupted yet. It accepts signal because it shares an options type with commands.run(), but ignores it, so use timeout there.A command that ignores the interrupt runs to its own deadline and is recovered the same way a timeout is.

Streaming output

commands.run() hands back everything at once, when the command ends. For a build or a test run that is a long wait followed by a wall of text, so it also takes onStdout and onStderr, called with each piece of output as it arrives:
The return value does not change. What the callbacks receive concatenates to exactly the stdout you get without them, so you can add one to existing code and nothing else moves. Chunks arrive as the guest produces output, not on a fixed schedule. A command printing two hundred lines with no pauses delivers them in one chunk; one printing a line a second delivers a chunk per line. A command that finishes quickly delivers a single chunk, which is indistinguishable from not streaming, and that is correct: there was nothing to stream. Streaming composes with interrupt() and signal. The chunks that arrived before the stop are kept, and interrupt() still resolves with exit code 130.
code.run() does not stream. It accepts the options because it shares an options type with commands.run(), but ignores them.

Sending input

stdin gives the command something to read. A string or a Uint8Array becomes that command’s entire input, ending with a real EOF:
Input is byte-exact, so binary works the same way:
Without stdin a command’s input is closed, which is why cat with no stdin returns at once rather than hanging.

Streaming input

Pass a ReadableStream or an AsyncIterable when what you send next depends on what came back. That puts the command on a terminal, so you write as it runs and writer.close() ends its input:
stdin accepts a string, a Uint8Array, an AsyncIterable, or a ReadableStream. Match on the output so far rather than the chunk you were handed, and step through once: the prompt comes back after every answer. Use tty: true for a terminal with no input of its own, and timeout: 0 for no deadline, which is what an interactive session wants.
Close the stream only once the command has read what you sent it, by writing in response to output as above. Closing early is ignored and the command keeps waiting. If you have all the input up front, pass a string instead.