Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/spotty-onions-smile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@bunny.net/sandbox": patch
"@bunny.net/cli": patch
---

Sandbox is now visible on the CLI root help and landing page, with create examples in the README and root help. The backing Magic Containers app is now named `sandbox-<name>` so sandboxes are recognizable in the MC dashboard; default generated sandbox names dropped their `sandbox-` prefix accordingly.
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ bunny-cli/
│ │ ├── tsconfig.json
│ │ └── src/
│ │ ├── index.ts # Barrel export: Sandbox, Command, types
│ │ ├── sandbox.ts # Sandbox class: create/get/fromHandle, runCommand (timeout/signal/onStdout/onStderr), writeFiles, readFile, listFiles, deleteFile, rename, exists, mkDir, exposePort, domain, getEnv/setEnv/unsetEnv (persisted env), delete, disconnect, Symbol.dispose/asyncDispose
│ │ ├── sandbox.ts # Sandbox class: create/get/fromHandle, runCommand (timeout/signal/onStdout/onStderr), writeFiles, readFile, listFiles, deleteFile, rename, exists, mkDir, exposePort, domain, getEnv/setEnv/unsetEnv (persisted env), delete, disconnect, Symbol.dispose/asyncDispose; backing MC app is named `sandbox-<name>` (appNameFor/sandboxNameFor)
│ │ ├── provision.ts # Magic Containers app create/poll/endpoints + auth helpers + container env read/replace
│ │ ├── transport.ts # ssh2 SSH/SFTP transport (exec with limits, file IO, reachability)
│ │ ├── command.ts # Command (detached, logs()) and CommandFinished
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ bun ny apps deploy # first run? Imports docker-compose.
bun ny apps link # interactive: pick from existing apps on the account
bun ny apps link <app-id> # link a specific app to this directory (writes .bunny/app.json)
bun ny apps unlink # remove .bunny/app.json
bun ny sandbox create my-sandbox # create an ephemeral dev sandbox (backed by a Magic Containers app)
bun ny dns zones add example.com # create a zone, then choose how to add records (scan existing / upload a BIND zone file / add manually) before registrar setup steps
bun ny dns zones nameservers example.com # live-check whether the registrar delegates to bunny
bun ny dns records scan example.com # scan for the domain's existing records and import them
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const commands: CommandModule[] = [
dbNamespace,
dnsNamespace,
scriptsNamespace,
sandboxNamespace,
configNamespace,
docsCommand,
openCommand,
Expand All @@ -37,7 +38,6 @@ const commands: CommandModule[] = [
const experimentalCommands: CommandModule[] = [
appsNamespace,
registriesNamespace,
sandboxNamespace,
storageNamespace,
];

Expand Down Expand Up @@ -135,6 +135,8 @@ export const cli = instance
const examples = [
["Create a database", "bunny db create"],
["Create an edge script", "bunny scripts init"],
["Add a domain to manage DNS", "bunny dns zones add example.com"],
["Create a dev sandbox", "bunny sandbox create my-sandbox"],
// ["Deploy an app", "bunny apps deploy"],
];

Expand Down
21 changes: 21 additions & 0 deletions packages/sandbox/src/sandbox.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import {
splitHost,
} from "./provision.ts";
import {
appNameFor,
buildRemoteCommand,
resolvePath,
Sandbox,
sandboxNameFor,
shellQuote,
} from "./sandbox.ts";
import { collectExec, fileEntryFromAttrs } from "./transport.ts";
Expand All @@ -30,6 +32,25 @@ describe("Sandbox.create", () => {
});
});

describe("app naming", () => {
test("appNameFor prefixes the sandbox name", () => {
expect(appNameFor("my-box")).toBe("sandbox-my-box");
});

test("appNameFor always prefixes, even names that already start with sandbox-", () => {
expect(appNameFor("sandbox-my-box")).toBe("sandbox-sandbox-my-box");
});

test("sandboxNameFor strips one prefix, round-tripping appNameFor", () => {
expect(sandboxNameFor(appNameFor("my-box"))).toBe("my-box");
expect(sandboxNameFor(appNameFor("sandbox-my-box"))).toBe("sandbox-my-box");
});

test("sandboxNameFor leaves unprefixed app names alone", () => {
expect(sandboxNameFor("legacy-app")).toBe("legacy-app");
});
});

describe("Sandbox.runCommand validation", () => {
// fromHandle connects lazily, so validation errors surface with no network.
const sandbox = Sandbox.fromHandle({
Expand Down
21 changes: 18 additions & 3 deletions packages/sandbox/src/sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ type McClient = ReturnType<typeof createMcClient>;

const DEFAULT_REGION = "AMS";
const DEFAULT_VOLUME_GB = 10;
/** Prefix that marks the backing MC app as sandbox-owned. */
const APP_NAME_PREFIX = "sandbox-";
const SSH_REACHABLE_TIMEOUT_MS = 120_000;
const DEFAULT_IMAGE = {
registryId: "1156",
Expand Down Expand Up @@ -92,7 +94,7 @@ export class Sandbox {
const agentToken = generateToken();

const appId = await createApp(client, {
name,
name: appNameFor(name),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reserve prefix length before creating the MC app

When callers pass a sandbox name between 93 and 100 characters, the name previously fit the MC app schema, but adding sandbox- here sends a value over the 100-character API limit (AddApplicationRequest.name in packages/openapi-client/specs/magic-containers.json caps it at 100). This makes otherwise valid Sandbox.create({ name }) calls fail at create time; validate the user-facing sandbox name against the reduced limit or otherwise account for the prefix before calling createApp.

Useful? React with 👍 / 👎.

region: options.region ?? DEFAULT_REGION,
agentToken,
volumeSize: options.volumeSize ?? DEFAULT_VOLUME_GB,
Expand Down Expand Up @@ -148,7 +150,8 @@ export class Sandbox {
"Could not recover sandbox credentials from the app.",
);
}
const name = (app as { name?: string }).name ?? options.appId;
const appName = (app as { name?: string }).name;
const name = appName ? sandboxNameFor(appName) : options.appId;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve legacy sandbox-prefixed app names

When Sandbox.get() is used on sandboxes created by earlier SDK versions without an explicit name, their MC app name is already sandbox-<hex> because the old generated name included the prefix. This unconditionally strips that prefix, so reconnecting by app ID changes sandbox.name/toHandle().name from sandbox-<hex> to <hex>, which can break callers that key persisted handles or UI state by the sandbox name. Please distinguish newly prefixed app names from legacy names, or preserve the app name when that distinction is unavailable.

Useful? React with 👍 / 👎.


return new Sandbox(
{
Expand Down Expand Up @@ -448,5 +451,17 @@ function generateToken(): string {
}

function generateName(): string {
return `sandbox-${randomBytes(4).toString("hex")}`;
return randomBytes(4).toString("hex");
}

/** MC app name for a sandbox: always `sandbox-<name>`. */
export function appNameFor(name: string): string {
return `${APP_NAME_PREFIX}${name}`;
}

/** Recover the sandbox name from an MC app name by stripping one prefix. */
export function sandboxNameFor(appName: string): string {
return appName.startsWith(APP_NAME_PREFIX)
? appName.slice(APP_NAME_PREFIX.length)
: appName;
}