diff --git a/.changeset/spotty-onions-smile.md b/.changeset/spotty-onions-smile.md new file mode 100644 index 00000000..a6b7cb2c --- /dev/null +++ b/.changeset/spotty-onions-smile.md @@ -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-` so sandboxes are recognizable in the MC dashboard; default generated sandbox names dropped their `sandbox-` prefix accordingly. diff --git a/AGENTS.md b/AGENTS.md index 9f5e557d..8f2cd21e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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-` (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 diff --git a/README.md b/README.md index 0ab21879..3c4f1b6d 100644 --- a/README.md +++ b/README.md @@ -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 # 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 diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index fdc20598..d1c3af7a 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -27,6 +27,7 @@ const commands: CommandModule[] = [ dbNamespace, dnsNamespace, scriptsNamespace, + sandboxNamespace, configNamespace, docsCommand, openCommand, @@ -37,7 +38,6 @@ const commands: CommandModule[] = [ const experimentalCommands: CommandModule[] = [ appsNamespace, registriesNamespace, - sandboxNamespace, storageNamespace, ]; @@ -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"], ]; diff --git a/packages/sandbox/src/sandbox.test.ts b/packages/sandbox/src/sandbox.test.ts index 8afbb453..7eb4a58b 100644 --- a/packages/sandbox/src/sandbox.test.ts +++ b/packages/sandbox/src/sandbox.test.ts @@ -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"; @@ -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({ diff --git a/packages/sandbox/src/sandbox.ts b/packages/sandbox/src/sandbox.ts index d3e9ba05..cb9abb7e 100644 --- a/packages/sandbox/src/sandbox.ts +++ b/packages/sandbox/src/sandbox.ts @@ -37,6 +37,8 @@ type McClient = ReturnType; 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", @@ -92,7 +94,7 @@ export class Sandbox { const agentToken = generateToken(); const appId = await createApp(client, { - name, + name: appNameFor(name), region: options.region ?? DEFAULT_REGION, agentToken, volumeSize: options.volumeSize ?? DEFAULT_VOLUME_GB, @@ -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; return new Sandbox( { @@ -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-`. */ +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; }