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

@buddy-works/sandbox-sdk

v0.3.0

Published

TypeScript SDK for managing sandboxes through the Buddy API

Readme

Buddy Sandbox SDK

TypeScript SDK for managing Buddy sandboxes - isolated Ubuntu environments for running commands.

Installation

npm install @buddy-works/sandbox-sdk

Usage

import { Sandbox } from "@buddy-works/sandbox-sdk";

const identifier = "my-sandbox";

let sandbox: Sandbox;

try {
    sandbox = await Sandbox.getByIdentifier(identifier);
} catch {
    sandbox = await Sandbox.create({
        identifier,
        name: "My Sandbox",
        os: "ubuntu:24.04",
    });
}

await sandbox.start();

await sandbox.runCommand({
    command: "ping -c 5 buddy.works",
});

await sandbox.stop();

Set required environment variables:

export BUDDY_TOKEN="your-api-token"
export BUDDY_WORKSPACE="your-workspace"
export BUDDY_PROJECT="your-project"
export BUDDY_REGION="US"  # Optional: US (default), EU, or AS

Running commands

exec() runs a command and resolves when it finishes, with its exit code and output:

const result = await sandbox.exec({ command: "npm test" });

console.log(result.exit_code, result.stdout, result.stderr);

A command that exits non-zero still resolves, so check exit_code.

runtime picks the interpreter. It defaults to BASH, and also takes JAVASCRIPT, TYPESCRIPT or PYTHON:

await sandbox.exec({ command: "print(1 + 1)", runtime: "PYTHON" });

The request stays open while the command runs, and the API fails it after 60 seconds. Whatever it does, a command run this way leaves no entry in the sandbox command history, streams no logs, and cannot be terminated.

Use runCommand() for anything longer than a minute, to follow the output as it arrives, or to leave a command running after the call returns:

// streams to process.stdout as it runs, resolves once it finishes
const command = await sandbox.runCommand({ command: "npm test" });
console.log(command.data.status, command.data.exit_code);

// returns immediately, leaving the command running
const detached = await sandbox.runCommand({
    command: "npm run build",
    detached: true,
    output: null,
});

for await (const line of detached.logs({ follow: true })) {
    console.log(line);
}

const finished = await detached.wait(); // SUCCESSFUL, FAILED or CANCELLED

A detached command still streams to process.stdout by default, which keeps the Node process alive until the command ends - pass output: null to let it exit sooner.

The log of a command holds stdout and stderr interleaved, in the order the command wrote them: logs() yields its lines and output() returns it as one string. Use exec() when you need the two apart.

kill() cancels a running command and resolves once it has stopped, with the status CANCELLED in data, or after 30 seconds if it is slow to stop - then data.status is still INPROGRESS. listCommands() returns the sandbox's command history, newest first - the API keeps the 200 most recent.

wait(), logs() and output() take an AbortSignal that stops waiting, which gives a command a deadline:

try {
    await detached.wait({ signal: AbortSignal.timeout(5 * 60_000) });
} catch (error) {
    if (!(error instanceof DOMException && error.name === "TimeoutError")) {
        throw error;
    }
    await detached.kill(); // the signal only stops waiting, not the command
}

The API runs at most 3 commands at once per member in one sandbox, and 10 per workspace across sandboxes and targets. A command over either limit is rejected with HTTP 409, as is one that would exceed the 100 the API node runs at once; runCommand() then throws a BuddySDKError with the code CONCURRENCY_LIMIT, so you can wait for a running command to finish and try again.

A command may be up to 16 KB long and is stopped after 90 minutes, so run servers and other long-lived processes as apps. Its log is capped at 10 MiB, past which data.truncated is set.

Starting and cancelling commands needs the SANDBOX_USE token scope, which SANDBOX_WRITE includes.

Waiting for readiness

Sandbox.create() blocks until the sandbox has finished setup and reached RUNNING, so the instance it returns is ready to use. While waiting it polls the API on a backoff starting at 100ms and growing to 500ms.

Pass wait: false to skip the wait and drive it yourself. The instance you get back carries everything the create call returned - id, identifier, url, resources - but connection details are not settled yet.

const sandbox = await Sandbox.create({ identifier: "my-sandbox", wait: false });

// ... do other work while the sandbox boots ...

await sandbox.waitUntilReady();   // setup_status: SUCCESS
await sandbox.waitUntilRunning(); // status: RUNNING

The waiters accept an explicit interval, which pins polling to that fixed value instead of backing off:

await sandbox.waitUntilRunning(500); // check every 500ms

Apps

Sandboxes can run multiple apps simultaneously. Each app is a long-running process defined by a run command, an optional identifier (generated from the command when omitted) and an optional path it runs in (defaults to app_dir).

const sandbox = await Sandbox.create({
    identifier: "my-sandbox",
    name: "My Sandbox",
    os: "ubuntu:24.04",
    first_boot_commands: "apt-get update && apt-get install -y curl",
    apps: [
        { identifier: "api", command: "node server.js" },
        { identifier: "worker", command: "python worker.py", path: "/app/jobs" },
    ],
    timeout: 600, // auto-stop after 10 minutes of inactivity
});

// List apps
for (const app of sandbox.data.apps ?? []) {
    console.log(`${app.identifier}: "${app.command}" -> ${app.app_status}`);
}

// Control individual apps - by identifier or by the generated id
await sandbox.stopApp("api");
await sandbox.startApp("api");

const { logs } = await sandbox.getAppLogs("api");
console.log(logs);

Fetching repositories and artifacts

Use fetch to clone repositories or download artifacts into the sandbox on first boot. Each entry sets a type (PROJECT_REPO, PUBLIC_REPO, or ARTIFACT) plus the fields relevant to it.

await Sandbox.create({
    identifier: "my-sandbox",
    fetch: [
        {
            type: "PUBLIC_REPO",
            repository: "https://github.com/octocat/Hello-World",
            ref: "master",
            path: "/workspace/hello",
            build_command: "echo built",
        },
    ],
});

Updating a sandbox

Use sandbox.update() to change configuration after creation - timeout, apps, endpoints, variables, tags, etc. The internal state is updated with the API's response.

const sandbox = await Sandbox.getByIdentifier("my-sandbox");

await sandbox.update({
    timeout: 1200,
    tags: ["staging", "feature-x"],
});

Snapshots

Take point-in-time snapshots of a sandbox and restore from them later. Each snapshot is returned as a Snapshot instance with its own methods.

const sandbox = await Sandbox.getByIdentifier("my-sandbox");

// Create a snapshot. Returns immediately with status "CREATING".
const snapshot = await sandbox.createSnapshot({ name: "before-deploy" });

// Wait until the snapshot is "CREATED" before using it.
await snapshot.waitUntilReady();

// Create a new sandbox from the snapshot.
const restored = await Sandbox.createFromSnapshot(snapshot.id, {
    name: "restored-sandbox",
});

// List all snapshots for this sandbox.
const snapshots = await sandbox.listSnapshots();

// Delete a snapshot.
await snapshot.delete();

If you already have a snapshot ID from elsewhere (e.g. persisted in your own storage), get the entity directly:

const snapshot = await Sandbox.getSnapshotById(sandboxId, snapshotId);

Equivalent convenience methods on Sandbox are available when you only have an ID and don't want to fetch the Snapshot first:

await sandbox.waitForSnapshotReady(snapshotId);
await sandbox.deleteSnapshot(snapshotId);

Regions

Configure the API region:

# Via environment variable (recommended)
export BUDDY_REGION="EU"
// Or via connection config
const sandbox = await Sandbox.create({
    identifier: "my-sandbox",
    name: "My Sandbox",
    os: "ubuntu:24.04",
    connection: {
        region: "EU"  // US, EU, or AS
    }
});

Connection overrides

Override workspace/auth per call:

await Sandbox.create({
    identifier: "my-sandbox",
    name: "My Sandbox",
    os: "ubuntu:24.04",
    connection: {
        workspace: "different-workspace",
        project: "different-project",
        token: "custom-token",
        region: "EU"
    }
});

HTTP/2

The SDK sends API requests over HTTP/2, using a pool of 16 connections per process shared by all sandboxes. To change the pool size, or to use fetch (HTTP/1.1) instead:

export BUDDY_HTTP2_SESSIONS="32"  # 1-64, default 16
export BUDDY_HTTP2="0"            # use fetch instead

Set them before the first request. If a proxy variable is set (HTTPS_PROXY, HTTP_PROXY, ALL_PROXY), the SDK uses fetch: Node's http2 module never connects through a proxy, while fetch does when Node runs with --use-env-proxy or NODE_USE_ENV_PROXY=1.