lightbox

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

  1. Ensure the microsandbox runtime is installed.
  2. If a snapshot with the same name exists, remove it (or throw if overwrite: false).
  3. Create a throwaway builder sandbox from config.image.
  4. Run each setup step in order. Output is streamed live when verbose: true.
  5. sync to flush the page cache. Required — without sync, the last step’s writes can vanish.
  6. Stop the builder, snapshot it under config.name, remove the builder.

BuildOptions

FieldDefaultNotes
overwritetrueOverwrite an existing snapshot. Pass false to make collisions throw.
debugBuilderfalseKeep the builder VM after snapshotting so you can attach to it. See Debug a failed build.
verbosefalseStream 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

FieldTypeNotes
namestringSnapshot artifact name. Stored under ~/.microsandbox/snapshots/<name>.
imagestringBase OCI image (e.g. "oven/bun", "python:3.12").
resources.cpusnumber?vCPUs for the builder VM.
resources.memory`${number}M` | `${number}G`Builder memory, e.g. "512M" or "2G".
workdirstring?Working dir for setup steps. Must be created by a setup stepmsb does not auto-mkdir.
envRecord<string, string>?Env vars during setup.
setupSetupStep[]?Steps run in order in the builder before snapshot.
labelsRecord<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:

CLInpm packagebin
GitHub Copilot@github/copilotcopilot
Gemini@google/gemini-cligemini
Codex@openai/codexcodex
Pi coding agent@earendil-works/pi-coding-agentpi
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:

FieldDefault
name"lightbox"
image"oven/bun"
resources{ cpus: 2, memory: "1G" }
labelsundefined

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);