@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-sdkUsage
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 ASRunning 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 CANCELLEDA 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: RUNNINGThe waiters accept an explicit interval, which pins polling to that fixed value instead of backing off:
await sandbox.waitUntilRunning(500); // check every 500msApps
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 insteadSet 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.
