Create a snapshot
buildSnapshot(), defineSnapshot(), setup steps, and presets.
buildSnapshot(config, opts?) → Promise<void>
function buildSnapshot(
config: SnapshotConfig,
opts?: BuildOptions,
): Promise<void>;
Build a snapshot from a declarative config. Idempotent by default — re-running overwrites any existing snapshot of the same name. Pass { overwrite: false } to make collisions throw.
import { buildSnapshot } from "@beamhop/lightbox";
await buildSnapshot({
name: "rust-ci",
image: "rust:1.82",
resources: { cpus: 4, memory: "4G" },
setup: [
"rustup component add clippy rustfmt", // string shorthand → shell step
"cargo install cargo-nextest --locked",
],
});
How it works
- Ensure the microsandbox runtime is installed.
- If a snapshot with the same name exists, remove it (or throw if
overwrite: false). - Create a throwaway builder sandbox from
config.image. - Run each
setupstep in order. Output is streamed live whenverbose: true. syncto flush the page cache. Required — withoutsync, the last step’s writes can vanish.- Stop the builder, snapshot it under
config.name, remove the builder.
BuildOptions
| Field | Default | Notes |
|---|---|---|
overwrite | true | Overwrite an existing snapshot. Pass false to make collisions throw. |
debugBuilder | false | Keep the builder VM after snapshotting so you can attach to it. See Debug a failed build. |
verbose | false | Stream setup-step output to host stdout/stderr. |
defineSnapshot(config) → SnapshotConfig
function defineSnapshot<T extends SnapshotConfig>(config: T): T;
Identity helper for type inference. Returns its argument unchanged at runtime — the value of using it is that the literal gets inferred precisely (discriminated unions stay narrow, defaults stay literal) so your IDE gives accurate autocomplete and cfg.setup retains its element types.
import { defineSnapshot } from "@beamhop/lightbox";
const cfg = defineSnapshot({
name: "node-ci",
image: "node:22",
resources: { cpus: 4, memory: "4G" },
workdir: "/work",
env: { CI: "true" },
setup: [
"mkdir -p /work",
"npm i -g pnpm@9",
],
labels: { team: "platform", purpose: "ci" },
});
Use it when you want to hold the config in a variable separately from the buildSnapshot() call (e.g. to mutate it before building). For a one-shot build you can pass the literal directly to buildSnapshot() — the inference is just slightly looser without it.
SnapshotConfig
| Field | Type | Notes |
|---|---|---|
name | string | Snapshot artifact name. Stored under ~/.microsandbox/snapshots/<name>. |
image | string | Base OCI image (e.g. "oven/bun", "python:3.12"). |
resources.cpus | number? | vCPUs for the builder VM. |
resources.memory | `${number}M` | `${number}G` | Builder memory, e.g. "512M" or "2G". |
workdir | string? | Working dir for setup steps. Must be created by a setup step — msb does not auto-mkdir. |
env | Record<string, string>? | Env vars during setup. |
setup | SetupStep[]? | Steps run in order in the builder before snapshot. |
labels | Record<string, string>? | Labels stored on the snapshot artifact. |
Setup steps
A SetupStep is one of:
type SetupStep =
| string // shell shorthand
| { kind: "shell"; script: string; description?: string }
| { kind: "exec"; cmd: string; args?: string[]; description?: string };
Most steps are shell one-liners — use the string form. Switch to the object form when you want a description (printed before the step when verbose: true) or argv-form exec (no shell interpretation; safer for paths with spaces).
shell(script, description?) / exec(cmd, args?, description?)
Helpers for the object forms:
import { shell, exec } from "@beamhop/lightbox";
shell("npm i -g pnpm", "install pnpm");
exec("cargo", ["install", "cargo-nextest", "--locked"], "tooling");
Presets
codingAgentsPreset(opts?)
Importable from @beamhop/lightbox/presets. Builds a PresetConfig that installs four coding-agent CLIs on top of oven/bun:
| CLI | npm package | bin |
|---|---|---|
| GitHub Copilot | @github/copilot | copilot |
| Gemini | @google/gemini-cli | gemini |
| Codex | @openai/codex | codex |
| Pi coding agent | @earendil-works/pi-coding-agent | pi |
import { buildSnapshot } from "@beamhop/lightbox";
import { codingAgentsPreset } from "@beamhop/lightbox/presets";
await buildSnapshot(codingAgentsPreset());
// Snapshot "lightbox" is now ready.
Override the defaults:
codingAgentsPreset({
name: "agents-xl",
image: "oven/bun",
resources: { cpus: 4, memory: "4G" },
labels: { tier: "premium" },
});
CodingAgentsPresetOptions:
| Field | Default |
|---|---|
name | "lightbox" |
image | "oven/bun" |
resources | { cpus: 2, memory: "1G" } |
labels | undefined |
Extending a preset
The return type is PresetConfig — a SnapshotConfig with setup guaranteed defined — so you can append steps without a non-null assertion:
import { buildSnapshot, shell } from "@beamhop/lightbox";
import { codingAgentsPreset } from "@beamhop/lightbox/presets";
const cfg = codingAgentsPreset({ name: "lightbox-plus" });
cfg.setup.push(shell("apt-get update && apt-get install -y ripgrep", "ripgrep"));
await buildSnapshot(cfg);