npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@beamhop/lightbox

v0.1.0

Published

Declarative microsandbox snapshot builder and sandbox launcher for Bun & Node.

Downloads

81

Readme

lightbox

Declarative snapshot builder and sandbox launcher built on the official microsandbox Node SDK.

You describe a snapshot — base image, resources, setup steps — and lightbox drives the SDK for you: create a builder VM, apply your steps, snapshot the disk, clean up. Later, you launch named sandboxes from that snapshot in under 100 ms with no per-boot install cost.

// Pull the three pieces we need: a typed-config helper, the snapshot
// builder, and the launcher that detaches the sandbox from this process.
import { buildSnapshot, defineSnapshot, launchSandboxDetached } from "@beamhop/lightbox";

// defineSnapshot is identity at runtime — it only exists so the object
// literal below gets full IntelliSense and type-checking.
const config = defineSnapshot({
  name: "py-data",                 // artifact name under ~/.microsandbox/snapshots/
  image: "python:3.12",            // base OCI image the builder VM boots from
  resources: { cpus: 2, memory: "1G" }, // 2 vCPUs + 1 GiB for the builder VM
  setup: [
    // Each setup step runs inside the builder before snapshotting.
    // kind:"shell" lets us use pipes/&&; cache-disabled keeps the snapshot small.
    { kind: "shell", script: "pip install --no-cache-dir numpy pandas" },
  ],
});

// Spin up the builder, run setup, sync, stop, snapshot, then clean up.
// force:true overwrites any prior snapshot with the same name.
await buildSnapshot(config, { force: true });

// Boot a fresh VM from the snapshot. Detached = keeps running after exit;
// replace:true wipes any stale sandbox named "analysis-1" first.
await launchSandboxDetached({ snapshot: "py-data", name: "analysis-1", replace: true });

Install

{
  // Consume the library from another package in this monorepo.
  // "workspace:*" tells bun to resolve to the in-repo version, never npm.
  "dependencies": { "@beamhop/lightbox": "workspace:*" }
}

microsandbox is the only runtime dep. It pulls the platform-appropriate msb + libkrunfw binaries on first use (cached under ~/.microsandbox/). No global tools required.

Concepts

  • Snapshot — a frozen disk image built from a base OCI image plus your setup steps. Stored under ~/.microsandbox/snapshots/<name>. Booting from a snapshot is fast because the upper layer is pre-populated.
  • Builder sandbox — the throwaway VM lightbox spins up to apply your setup steps. Named <snapshot>-builder; removed after snapshotting unless you pass { keepBuilder: true }.
  • Launched sandbox — a persistent VM booted from a snapshot via Sandbox.builder(name).fromSnapshot(...).createDetached(). Survives until you remove it.
  • Lifecycle ownership — when you call launchSandbox() you get back a live Sandbox handle whose lifecycle is tied to your process. Call launchSandboxDetached() instead (or sandbox.detach() manually) to let the sandbox survive process exit.

API

defineSnapshot(config) → SnapshotConfig

Identity helper for type inference and IDE autocomplete:

import { defineSnapshot } from "@beamhop/lightbox";

const cfg = defineSnapshot({
  name: "node-ci",                       // snapshot name on disk
  image: "node:22",                      // base image — Node 22 LTS
  resources: { cpus: 4, memory: "4G" },  // beefier builder for npm installs
  workdir: "/work",                      // default cwd for setup-step commands
  env: { CI: "true" },                   // env exported into every setup step
  setup: [
    // First step must create /work — msb won't mkdir workdir for you.
    { kind: "shell", description: "make workspace", script: "mkdir -p /work" },
    // Install global tooling that should bake into the snapshot.
    { kind: "shell", description: "tooling", script: "npm i -g pnpm@9" },
  ],
  // Labels are metadata stored in the snapshot manifest — useful for filtering.
  labels: { team: "platform", purpose: "ci" },
});

SnapshotConfig fields

| Field | Type | Notes | |-------|------|-------| | name | string | Snapshot artifact name. Must be unique unless you pass force. | | 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` | number | Memory for the builder VM. Strings are converted to MiB; raw numbers are MiB. | | workdir | string? | Working dir for setup steps. Must be created by a setup stepmsb 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. |

SetupStep

A tagged union — shell for one-liners that need pipes/redirects, exec for direct argv execution.

type SetupStep =
  // shell: passes `script` to `sh -lc`; supports pipes, redirects, &&, ||.
  | { kind: "shell"; script: string; description?: string }
  // exec: direct argv — no shell parsing. Safer for paths with spaces.
  | { kind: "exec"; cmd: string; args?: string[]; description?: string };

description is printed before the step runs when verbose is on.

buildSnapshot(config, opts?) → Promise<void>

Builds the snapshot. Workflow:

  1. Ensure the microsandbox runtime is installed (install() if needed).
  2. If a snapshot with the same name exists, error (or remove it if force: true).
  3. Sandbox.builder(<name>-builder).image(...).createDetached().
  4. Run each setup step via sandbox.shellStream(...) / sandbox.execStream(...). Output is piped to your stdout/stderr live when verbose: true.
  5. sandbox.exec("sync") to flush the page cache. Required — without sync, the snapshot can miss recent writes from the last setup step.
  6. sandbox.stopAndWait().
  7. Snapshot.builder(builderName).name(config.name).create().
  8. Remove the builder sandbox (unless keepBuilder: true).

BuildOptions

| Field | Default | Notes | |-------|---------|-------| | force | false | Overwrite an existing snapshot with the same name. | | keepBuilder | false | Keep the builder VM after snapshotting (for debugging). | | builderSuffix | "-builder" | Suffix appended to config.name for the builder VM. | | verbose | true | Stream step output live. |

launchSandbox(opts) → Promise<Sandbox>

Boots a sandbox from a snapshot in detached mode and returns the live Sandbox handle. The sandbox's lifecycle is tied to your process — when the process exits, the sandbox is stopped.

Use this when you want to drive the sandbox from the same script that launched it.

import { launchSandbox } from "@beamhop/lightbox";

// Returns a live Sandbox handle whose lifecycle is tied to THIS process —
// if we exit without stopping it, the SDK kills the VM.
const sb = await launchSandbox({
  snapshot: "lightbox",                  // snapshot to boot from
  name: "agent-1",                       // sandbox name (must be unique)
  resources: { cpus: 2, memory: "2G" },  // per-sandbox override of snapshot defaults
  replace: true,                         // tear down any existing "agent-1" first
});

// Run a command inside the VM. exec(cmd, args[]) takes argv directly.
const out = await sb.exec("copilot", ["--version"]);
// .stdout() decodes the captured stdout as UTF-8.
console.log(out.stdout());

await sb.stop();   // graceful shutdown; otherwise process exit would kill it.

launchSandboxDetached(opts) → Promise<void>

Same as launchSandbox, but immediately calls sandbox.detach() so the sandbox keeps running after your process exits. To control it later, look it up via Sandbox.get(name):

// Sandbox is re-exported from lightbox — no need to import "microsandbox".
import { launchSandboxDetached, Sandbox } from "@beamhop/lightbox";

// Boots the sandbox AND immediately releases lifecycle ownership, so the
// VM keeps running once this script exits.
await launchSandboxDetached({ snapshot: "lightbox", name: "agent-1", replace: true });
// ... process exits, sandbox keeps running ...

// In a separate process:
// Sandbox.get() is a cheap DB lookup — returns metadata, doesn't connect.
const handle = await Sandbox.get("agent-1");
// .connect() opens a control channel without re-taking lifecycle ownership.
const sb = await handle.connect();
// .shell() runs a script via `sh -lc`; .stdout() decodes the result.
console.log((await sb.shell("pi --version")).stdout());

LaunchOptions

| Field | Type | Notes | |-------|------|-------| | snapshot | string | Snapshot name to boot from. | | name | string | Sandbox name. | | resources | { cpus?, memory? } | Per-sandbox overrides. | | workdir | string? | Working dir inside the guest. Must exist in the snapshot. | | env | Record<string, string>? | Env vars for the sandbox. | | ports | Record<number\|string, number\|string>? | { host: guest } port mappings. | | replace | boolean? | Replace an existing sandbox with the same name. | | verbose | true | Log [lightbox] launching ... to stdout. |

Helpers

import {
  listSnapshots,    // enumerate all snapshots on disk
  snapshotExists,   // shortcut: does a snapshot with this name exist?
  removeSnapshot,   // delete a snapshot artifact + DB row
  removeSandbox,    // stop (if needed) and delete a sandbox
  ensureRuntime,    // memoized install() of the host msb + libkrunfw
} from "@beamhop/lightbox";

await listSnapshots();              // → SnapshotRecord[] (name, digest, size, ...)
await snapshotExists("lightbox");   // → boolean, true if "lightbox" is indexed
// force:true also removes if the snapshot has indexed children.
await removeSnapshot("lightbox", { force: true });
// force:true kills the sandbox first if it's still running.
await removeSandbox("agent-1",  { force: true });

// Manually trigger the one-time runtime install (otherwise it happens
// lazily on the first SDK call). Idempotent — safe to call repeatedly.
await ensureRuntime();

SDK re-exports

For convenience, the underlying SDK types are re-exported so callers don't need to import microsandbox directly:

// Runtime classes — Sandbox.builder / .get / .list, Snapshot.builder / .list, etc.
import { Sandbox, Snapshot } from "@beamhop/lightbox";
// Types only — used to annotate streaming reads and exec results.
import type { ExecHandle, ExecOutput } from "@beamhop/lightbox";

Anything not re-exported can still be imported from microsandbox (it's a direct dep).

Presets

codingAgentsPreset(opts?)

Importable from lightbox/presets. Builds a SnapshotConfig 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";
// Presets live at a subpath so tree-shaking can drop them if unused.
import { codingAgentsPreset } from "@beamhop/lightbox/presets";

// No-arg call uses every default: name="lightbox", image="oven/bun".
// force:true lets you re-run this script idempotently.
await buildSnapshot(codingAgentsPreset(), { force: true });
// Snapshot "lightbox" is now ready.

Override the defaults:

codingAgentsPreset({
  name: "agents-xl",                     // give the snapshot a non-default name
  image: "oven/bun",                     // (still the default — shown for clarity)
  resources: { cpus: 4, memory: "4G" },  // bigger builder for parallel npm work
  labels: { tier: "premium" },           // arbitrary metadata, merged with preset's labels
});

CodingAgentsPresetOptions:

| Field | Default | |-------|---------| | name | "lightbox" | | image | "oven/bun" | | resources | { cpus: 2, memory: "2G" } | | labels | undefined |

Recipes

Build a custom snapshot from scratch

import { buildSnapshot, defineSnapshot } from "@beamhop/lightbox";

await buildSnapshot(
  defineSnapshot({
    name: "rust-ci",                       // snapshot artifact name
    image: "rust:1.82",                    // official Rust toolchain image
    resources: { cpus: 4, memory: "4G" },  // cargo compiles benefit from extra RAM/cores
    setup: [
      // Add the linter and formatter components to the snapshot.
      { kind: "shell", script: "rustup component add clippy rustfmt" },
      // Pre-install cargo-nextest so CI runs don't pay the install cost each time.
      { kind: "shell", script: "cargo install cargo-nextest --locked" },
    ],
  }),
  { force: true },  // overwrite an existing "rust-ci" snapshot
);

Extend a preset

import { buildSnapshot } from "@beamhop/lightbox";
import { codingAgentsPreset } from "@beamhop/lightbox/presets";

// Start from the preset, but give the resulting snapshot a different name
// so we don't clobber the vanilla "lightbox" one.
const cfg = codingAgentsPreset({ name: "lightbox-plus" });
// Append an extra setup step. `setup` is always defined on a preset, so
// the non-null assertion is safe here.
cfg.setup!.push({
  kind: "shell",
  description: "install ripgrep",                            // shown in build logs
  script: "apt-get update && apt-get install -y ripgrep",    // runs in oven/bun (Debian-based)
});

await buildSnapshot(cfg, { force: true });

Launch a pool of sandboxes

import { launchSandboxDetached } from "@beamhop/lightbox";

// Launch three sandboxes in parallel. Each gets a unique name and boots
// from the same snapshot. Detached so they outlive this script.
await Promise.all(
  ["agent-1", "agent-2", "agent-3"].map((name) =>
    launchSandboxDetached({ snapshot: "lightbox", name, replace: true }),
  ),
);

Run code, then tear down

import { launchSandbox } from "@beamhop/lightbox";

// `await using` ties the sandbox's lifetime to this block. When `sb` goes
// out of scope, Sandbox[Symbol.asyncDispose]() runs and stops the VM.
await using sb = await launchSandbox({
  snapshot: "lightbox",
  name: "ephemeral",
  replace: true,
});

// Combine two commands in one shell invocation — saves a round-trip.
const r = await sb.shell("pi --version && copilot --version");
console.log(r.stdout());
// Sandbox is stopped/disposed when `sb` goes out of scope.

Debug a failed build

Pass keepBuilder: true to inspect the builder VM after a failure:

// keepBuilder:true skips the final `removeSandbox` cleanup, leaving the
// builder VM around so you can poke at it. .catch keeps the script alive
// long enough to investigate.
await buildSnapshot(cfg, { force: true, keepBuilder: true }).catch(console.error);
// Then attach with the SDK:
//   const h = await Sandbox.get("<name>-builder"); // find the builder
//   const sb = await h.connect();                  // open a control channel
//   await sb.attachShell();                        // drop into an interactive shell

Gotchas

  • Don't set workdir to a path that doesn't exist in the base image. The SDK validates the working dir at create time. lightbox doesn't pass workdir to the builder for this reason — create the dir as the first setup step.
  • The page cache must be synced before snapshotting. buildSnapshot runs sync for you; without it, the last setup step's writes can vanish.
  • launchSandbox() ties lifecycle to your process. Use launchSandboxDetached() (or call sandbox.detach() yourself) if the sandbox should outlive your script.
  • bun install -g postinstalls are blocked by default. That's a bun default. Most CLIs install their bin via package.json bin regardless. Run bun pm -g untrusted inside the sandbox to see what was skipped.

Types

All types are re-exported from the package root:

import type {
  SnapshotConfig,     // input to defineSnapshot / buildSnapshot
  SetupStep,          // tagged union for shell vs exec steps
  SnapshotResources,  // { cpus?, memory? }
  BuildOptions,       // second arg to buildSnapshot
  LaunchOptions,      // arg to launchSandbox / launchSandboxDetached
  SnapshotRecord,     // shape returned by listSnapshots()
  Memory,             // `${number}M` | `${number}G` template literal type
} from "@beamhop/lightbox";