Commands
Run commands in a microVM and wait for them, or detach them, stream their output, send input and signals, and open an interactive terminal.
A command is a process you start in a running microVM. Wait for it when you need its result before moving on, such as npm install, and detach it when it keeps running, such as a dev server.
Run a command and wait
vm.run starts a command and resolves when it exits. A non-zero exit code is a normal result; pass throwOnError: true to reject with CommandFailedError instead. Over REST, POST /microvms/{id}/commands waits by default and answers with the result.
import { MicroVM } from '@gigadrive/sdk';
await using vm = await MicroVM.create({ image: 'node22' });
const install = await vm.run('npm', ['install', 'left-pad'], {
cwd: '/tmp',
env: { CI: '1' },
timeout: '2m',
onStdout: (text) => process.stdout.write(text),
onStderr: (text) => process.stderr.write(text),
});
if (install.exitCode !== 0) {
throw new Error(`npm install failed: ${install.stderr}`);
}With args, the command runs directly and is looked up on PATH, with no shell involved. Without args, a cmd that contains a space runs through /bin/sh -c, so pipes, redirects and && work:
await vm.run('ls -la | wc -l');Waiting has a time limit
A command you wait for over REST runs for at most 60 seconds unless you set timeoutMs, and for at most 10 minutes whatever you set. The response carries at most 1 MiB of each of standard output and standard error, and sets truncated: true when it left output out.
vm.run makes that same waiting request unless you pass onStdout, onStderr, stdin or signal. With any of those, it starts the command detached and streams its output instead, so neither limit applies. For a job that can run longer than 10 minutes, pass an output callback, or start it with vm.spawn as described in Detached commands.
Options
| TypeScript | REST field | Default | Effect |
|---|---|---|---|
cwd | cwd | The image's working directory | Directory the command starts in |
env | env | None | Variables added on top of the image's and the microVM's environment |
user | user | The image's user, else root | A user name from the image, root, or a numeric user ID |
sudo | none | false | Run as root. Shorthand for user: 'root' |
timeout | timeoutMs | None, see the previous note | Kill the command with SIGKILL after this long |
onStdout | none | None | Called with each chunk of standard output as text, as it arrives |
onStderr | none | None | Called with each chunk of standard error as text, as it arrives |
stdin | stdin | None | See Standard input |
pty | pty | None | Run in a terminal of cols by rows characters. vm.spawn only |
throwOnError | none | false | Reject with CommandFailedError on a non-zero exit code. vm.run only |
signal | none | None | An AbortSignal. Aborting kills the command with SIGKILL. vm.run only |
| none | wait | true | false answers 201 as soon as the command starts |
In TypeScript, timeout takes milliseconds or a string such as '30s', '10m' or '2h'.
Commands run as the image's configured user unless you pass user. root is allowed, because the microVM itself is the isolation boundary.
Exit codes and timeouts
A finished command reports exitCode, and signal when a signal ended it. A process killed by a signal has exitCode: null. A command that reaches its timeout is killed with SIGKILL and reports signal: "SIGKILL" and timedOut: true.
A command that runs past the microVM's own timeout ends when the microVM stops. Extend the microVM first for long jobs; see Limits and pricing.
Detached commands
vm.spawn starts a command and returns a Command without waiting. Over REST, send "wait": false and the response is 201 with the command.
// An app you uploaded to /srv/app earlier, see Files.
const server = await vm.spawn('npm', ['run', 'dev'], { cwd: '/srv/app' });
await vm.waitForPort(3000);
// Later: stop the server and wait for it to exit.
await server.kill();
const finished = await server.wait();
console.log(finished.exitCode, finished.signal);A detached command belongs to the microVM, not to your connection. Closing your process, or losing the network, does not stop it. Reattach to one later with vm.commands.get(commandId), or list them with vm.commands.list().
Over REST, list a microVM's commands with GET /microvms/{id}/commands, read one with GET /microvms/{id}/commands/{commandId}, and wait for one with POST /microvms/{id}/commands/{commandId}/wait. A single wait call returns after at most 60 seconds, with status: "running" if the command has not exited yet; call it again to keep waiting.
The Command object carries id, cmd, args, cwd, user, status (running or exited), exitCode, signal, timedOut, pid, pty, startedAt and finishedAt.
Stream output
command.logs() streams a command's output as it is written, starting with what it has printed so far. Each chunk carries stream (stdout or stderr), text and offset. command.output() collects everything until the command exits.
for await (const { stream, text } of server.logs()) {
(stream === 'stdout' ? process.stdout : process.stderr).write(text);
}Over REST, GET /microvms/{id}/commands/{commandId}/logs is a Server-Sent Events stream:
curl -N \
-H "Authorization: Bearer $GIGADRIVE_TOKEN" \
-H "Accept: text/event-stream" \
"https://api.gigadrive.network/microvms/0199a7c4-5b1e-7c3d-9f20-333333333333/commands/cmd_0k3x9q2m7a1bz4yd/logs"| Event | Data | Meaning |
|---|---|---|
stdout | { data, offset } | A chunk of standard output. data is base64 |
stderr | { data, offset } | A chunk of standard error. data is base64 |
truncated | {} | Part of the requested output is no longer kept |
exit | { exitCode, signal, timedOut, finishedAt } | The command exited. The stream ends after this event |
end | { reason: "snapshot" } | With follow=false, the command is still running and the stream ends |
error | { retryable: true } | The stream failed partway. Reconnect from the last offsets |
offset is the position of the chunk's first byte in its stream. To resume after a dropped connection, pass the next byte's position as stdoutOffset and stderrOffset. follow=false sends what is available now and closes instead of waiting for the command to exit. The SDK reconnects on its own.
The number of log streams open at once is limited per credential and per organization. Past the limit, the call answers 429 with a retry-after header.
Output retention
Each command keeps the last 8 MiB of standard output and the last 8 MiB of standard error. Older output is discarded, so a stream that starts late, or an offset that is too old, begins at the oldest byte still kept.
A microVM keeps its 256 most recent commands. When it is full, the oldest exited command is forgotten to make room. If all 256 are still running, starting another one fails with 413.
Standard input
Pass stdin: true to vm.spawn to keep the command's input open, then write to it with write(). end() closes the input, which most programs read as the end of their data. For vm.run, stdin takes the data itself, writes it and closes the input.
const sort = await vm.spawn('sort', [], { stdin: true });
await sort.write('banana\napple\ncherry\n');
await sort.end();
console.log(await sort.stdout()); // "apple\nbanana\ncherry\n"Over REST, start the command with "stdin": true and "wait": false, then send raw bytes to POST /microvms/{id}/commands/{commandId}/stdin with Content-Type: application/octet-stream. Add ?eof=true to close the input after those bytes. A command started without stdin reads an empty input.
Signals
command.kill() sends SIGTERM, or the signal you pass it. Over REST, POST /microvms/{id}/commands/{commandId}/signal takes { "signal": "SIGINT" }. The signal goes to the command's whole process group, so the processes it started receive it too.
Allowed signals: SIGTERM, SIGKILL, SIGINT, SIGHUP, SIGQUIT, SIGUSR1 and SIGUSR2.
Interactive terminals
Pass pty to run the command in a terminal, for example to drive a shell or a program that only works interactively. Output then arrives on standard output only, and input written to the command is typed into the terminal. In TypeScript, pty: true opens an 80 by 24 terminal, and command.resize(cols, rows) changes its size.
curl -X POST https://api.gigadrive.network/microvms/0199a7c4-5b1e-7c3d-9f20-333333333333/commands \
-H "Authorization: Bearer $GIGADRIVE_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"cmd":"bash","pty":{"cols":120,"rows":30},"wait":false}'When the terminal window changes size, send { "cols": 160, "rows": 40 } to POST /microvms/{id}/commands/{commandId}/resize. Both values range from 1 to 1000. From your own terminal, gigadrive microvms shell opens one for you; see gigadrive microvms.
Errors
Every error response is { error, code }, where error explains the problem and code is stable.
| Status | code | When |
|---|---|---|
400 | invalid_request, guest_invalid_argument | An invalid option, or a resize of a command that has no terminal |
400 | guest_command_not_found | The program does not exist or is not on PATH |
403 | guest_permission_denied | The user may not run the program or enter the working directory |
404 | not_found, guest_not_found | The microVM or the command does not exist |
409 | microvm_not_running | The microVM is not running |
413 | guest_too_large | 256 commands are already running |
429 | microvm_busy | Too many operations run on this microVM at once. Retry shortly |
502 | microvm_unreachable | The microVM could not be reached. Retry shortly |
