Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
215874a
docs: add phased implementation plan for the sites namespace
claude Jul 13, 2026
7828145
feat(sites): add the sites namespace — static-site hosting with atomi…
claude Jul 13, 2026
a35d6b5
fix(sites): handle the paginated /pullzone response envelope
claude Jul 13, 2026
b72795f
continue sites
jamie-at-bunny Jul 13, 2026
d682885
remove old plans
jamie-at-bunny Jul 13, 2026
c1c6ff8
apply code suggestions
jamie-at-bunny Jul 13, 2026
3e2b65a
apply code suggestions
jamie-at-bunny Jul 14, 2026
5c0588a
apply code suggestions
jamie-at-bunny Jul 14, 2026
b075942
provision deployment tweaks
jamie-at-bunny Jul 14, 2026
c59573b
update docs
jamie-at-bunny Jul 14, 2026
271bbd4
code fixes
jamie-at-bunny Jul 14, 2026
f1e3d21
refactor the poc
jamie-at-bunny Jul 15, 2026
ff7821d
refactorings
jamie-at-bunny Jul 15, 2026
2411bbd
Merge remote-tracking branch 'origin/main' into claude/bunny-sites-pl…
jamie-at-bunny Jul 15, 2026
343b1aa
fix(sites): pass gh secret via stdin, strip client x-bunny-preview he…
jamie-at-bunny Jul 15, 2026
6d1de46
fix(sites): expand trailing-slash URLs to index.html for path preview…
jamie-at-bunny Jul 15, 2026
2defb06
fmt
jamie-at-bunny Jul 15, 2026
fe8b557
fix(sites): reconcile current/previous pointers on concurrent state w…
jamie-at-bunny Jul 15, 2026
a067ab7
fix(sites): infer the framework output dir for --build deploys withou…
jamie-at-bunny Jul 15, 2026
0f6be4f
prefixes
jamie-at-bunny Jul 16, 2026
8b1d7eb
config update
jamie-at-bunny Jul 19, 2026
8f95164
update config
jamie-at-bunny Jul 19, 2026
b79951e
set config state
jamie-at-bunny Jul 19, 2026
de2e1b3
remote filter state
jamie-at-bunny Jul 20, 2026
64c877f
fix build flag
jamie-at-bunny Jul 20, 2026
3230dc7
Merge remote-tracking branch 'origin/main' into claude/bunny-sites-pl…
jamie-at-bunny Jul 20, 2026
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
5 changes: 5 additions & 0 deletions .changeset/config-package-sites.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@bunny.net/config": patch
---

rename `@bunny.net/app-config` to `@bunny.net/config` and add a top-level `sites` block (`name`, `dir`, `build`) to the bunny.jsonc schema; the root `BunnyConfigSchema` makes `app` and `sites` optional so app-only, sites-only, and combined files all validate against the generated JSON Schema
5 changes: 5 additions & 0 deletions .changeset/sites-namespace.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@bunny.net/cli": minor
---

feat(sites): new `bunny sites` namespace for static-site hosting. `sites create` provisions a storage zone + pull zone + middleware router per site (zones are named `sites-<name>-<random id>` so globally-taken names can't block the create; commands take the clean site name), prompting for the name (directory-name suggestion) and a custom domain when run interactively (Bunny DNS record, nameserver guidance, DNS wait + SSL); `sites deploy` uploads immutable deploys (git-sha or content-hash IDs, no-op when unchanged) to preview URLs, and `--production`/`--prod` publishes the live site by flipping the router's `CURRENT_DEPLOY` env var + purging the cache; `sites deployments list/publish/prune` cover rollback and cleanup; `sites domains` attaches custom domains plus a `*.preview.<domain>` wildcard for per-deploy preview URLs; `sites ci init` (also offered by `sites create` on GitHub repos) scaffolds a GitHub Actions workflow with framework detection: previews on PRs, production on merges to main; `sites link/unlink/show/upgrade/delete` round out the lifecycle. Concurrent deploys merge remote state records instead of overwriting. Site state lives at `_bunny/site.json` inside the storage zone (403-blocked by the router). The shared hostnames factory gains optional `onAdded`/`onRemoved` hooks.
2 changes: 1 addition & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ bsql
# Generated artifacts
packages/openapi-client/src/generated
packages/openapi-client/specs
packages/app-config/generated
packages/config/generated

# Changesets keeps its own format
.changeset/*.md
Expand Down
91 changes: 76 additions & 15 deletions AGENTS.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ test("example", () => {
This is a Bun workspace monorepo with five packages:

- `packages/openapi-client/` (`@bunny.net/openapi-client`) — standalone, type-safe OpenAPI client, zero CLI deps
- `packages/app-config/` (`@bunny.net/app-config`) — shared Zod schemas, types, and JSON Schema for `bunny.jsonc`
- `packages/config/` (`@bunny.net/config`) — shared Zod schemas, types, and JSON Schema for `bunny.jsonc`
- `packages/database-shell/` (`@bunny.net/database-shell`) — standalone SQL shell engine (REPL, formatting, masking)
- `packages/sandbox/` (`@bunny.net/sandbox`) — standalone sandbox SDK (create, file buffering, command exec, port exposure) over Magic Containers + SSH
- `packages/cli/` (`@bunny.net/cli`) — the CLI, depends on the other four
Expand All @@ -47,7 +47,7 @@ This is a Bun workspace monorepo with five packages:
- Import API clients from `@bunny.net/openapi-client`, not relative paths. Import generated types from `@bunny.net/openapi-client/generated/<spec>.d.ts`.
- Use `clientOptions(config, verbose)` from `packages/cli/src/core/client-options.ts` when creating API clients in command handlers.
- Database commands use v2 API endpoints (`/v2/databases/...`).
- Apps (Magic Containers) commands use `bunny.jsonc` as the single source of truth. App ID is stored in the config (no separate manifest file). Use `resolveAppId()` and `resolveContainerId()` from `packages/cli/src/commands/apps/config.ts`. Types and conversion functions come from `@bunny.net/app-config`.
- Apps (Magic Containers) commands use `bunny.jsonc` as the single source of truth. App ID is stored in the config (no separate manifest file). Use `resolveAppId()` and `resolveContainerId()` from `packages/cli/src/commands/apps/config.ts`. Types and conversion functions come from `@bunny.net/config`.
- Prefer generated schema types over inline primitives. Use `Pick<components["schemas"]["TypeName"], "field1" | "field2">` instead of `{ field1: string; field2: number }`. Only fall back to `string`, `any`, or `number` when no generated type exists.

## Documentation
Expand Down
36 changes: 25 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@ Monorepo for the [bunny.net](https://bunny.net) CLI and supporting packages.

## Packages

| Package | Name | Description |
| ------------------------------------------------------------------------ | ------------------------------------ | ------------------------------------------------------------ |
| [`packages/cli/`](packages/cli/) | `@bunny.net/cli` | Command-line interface for bunny.net |
| [`packages/openapi-client/`](packages/openapi-client/) | `@bunny.net/openapi-client` | Standalone, type-safe OpenAPI client for bunny.net |
| [`packages/sandbox/`](packages/sandbox/) | `@bunny.net/sandbox` | Standalone sandbox SDK over Magic Containers and SSH |
| [`packages/app-config/`](packages/app-config/) | `@bunny.net/app-config` | Shared Zod schemas, types, and JSON Schema for `bunny.jsonc` |
| [`packages/database-shell/`](packages/database-shell/) | `@bunny.net/database-shell` | Standalone interactive SQL shell for libSQL databases |
| [`packages/database-openapi/`](packages/database-openapi/) | `@bunny.net/database-openapi` | Generate OpenAPI 3.0 specs from a database schema |
| [`packages/database-rest/`](packages/database-rest/) | `@bunny.net/database-rest` | PostgREST-like REST API handler (database-agnostic) |
| [`packages/database-adapter-libsql/`](packages/database-adapter-libsql/) | `@bunny.net/database-adapter-libsql` | Bunny Database adapter for database-rest |
| [`packages/scriptable-dns-types/`](packages/scriptable-dns-types/) | `@bunny.net/scriptable-dns-types` | Ambient TypeScript types for the Scriptable DNS runtime |
| Package | Name | Description |
| ------------------------------------------------------------------------ | ------------------------------------ | -------------------------------------------------------------------------- |
| [`packages/cli/`](packages/cli/) | `@bunny.net/cli` | Command-line interface for bunny.net |
| [`packages/openapi-client/`](packages/openapi-client/) | `@bunny.net/openapi-client` | Standalone, type-safe OpenAPI client for bunny.net |
| [`packages/sandbox/`](packages/sandbox/) | `@bunny.net/sandbox` | Standalone sandbox SDK over Magic Containers and SSH |
| [`packages/config/`](packages/config/) | `@bunny.net/config` | Shared Zod schemas, types, and JSON Schema for `bunny.jsonc` (app + sites) |
| [`packages/database-shell/`](packages/database-shell/) | `@bunny.net/database-shell` | Standalone interactive SQL shell for libSQL databases |
| [`packages/database-openapi/`](packages/database-openapi/) | `@bunny.net/database-openapi` | Generate OpenAPI 3.0 specs from a database schema |
| [`packages/database-rest/`](packages/database-rest/) | `@bunny.net/database-rest` | PostgREST-like REST API handler (database-agnostic) |
| [`packages/database-adapter-libsql/`](packages/database-adapter-libsql/) | `@bunny.net/database-adapter-libsql` | Bunny Database adapter for database-rest |
| [`packages/scriptable-dns-types/`](packages/scriptable-dns-types/) | `@bunny.net/scriptable-dns-types` | Ambient TypeScript types for the Scriptable DNS runtime |

See each package's README for usage and API documentation.

Expand Down Expand Up @@ -56,8 +56,22 @@ bun ny dns records scan example.com # scan for the domain's existing rec
bun ny dns records preset list # list DNS record presets (email providers, verification, security)
bun ny dns records preset google-workspace example.com # apply a preset record set
bun ny dns records preset bluesky example.com --param did=did:plc:abc123 # apply a preset non-interactively
bun ny sites create my-site # provision a static site (storage zone + pull zone + edge router; zones are named sites-my-site-<suffix>, served at sites-my-site-<suffix>.b-cdn.net)
bun ny sites deploy # no linked site? offers to create one or pick an existing; detects the framework, offers to build, then deploys
bun ny sites deploy ./dist # deploy to an immutable preview URL (the site's b-cdn.net host + /deploys/<id>/); the router's HTMLRewriter keeps root-absolute assets working
bun ny sites deploy ./dist --production # deploy and publish as the live site (--prod works too)
bun ny sites deploy --build # run `sites.build` from bunny.jsonc (else the detected framework's build), then deploy `sites.dir` (or the detected output dir)
bun ny sites deployments list # list deploys with the live one marked
bun ny sites deployments publish --previous # instant rollback to the previous deploy
bun ny sites deployments prune # delete old deploys (keeps the newest 5, never current/previous)
bun ny sites domains add example.com # attach a custom domain (+ *.preview.example.com for previews)
bun ny sites ssl --no-force-ssl # stop forcing HTTPS on the site's b-cdn.net system host
bun ny sites open # open the site's live URL in the browser
bun ny sites ci init # add a GitHub Actions workflow (preview on PRs, production on main)
```

Preconfigure the `sites` block in `bunny.jsonc` (`name`, `build`, `dir`) so a deploy needs no flags: `bun ny sites deploy --build --prod`. See [`examples/sites/`](examples/sites/) for ready-to-copy configs (Vite, Astro, Next.js static export, Hugo, plain HTML, and a combined app + site file).

### Available Scripts

```bash
Expand Down
47 changes: 23 additions & 24 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

60 changes: 60 additions & 0 deletions examples/sites/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Sites config examples

Drop-in `bunny.jsonc` files for `bunny sites`. Each one preconfigures the
`sites` block so a deploy needs no flags: the CLI reads the site to target, the
build command to run, and the directory to upload straight from the file.

## The `sites` block

| Field | Purpose | Required |
| ------- | --------------------------------------------------------------------------------------------------- | -------- |
| `name` | Links the directory to a site (created with `bunny sites create <name>`), so deploys skip `--site`. | No |
| `build` | The command `bunny sites deploy --build` runs before uploading. | No |
| `dir` | The directory that gets uploaded. Defaults to the detected framework's output dir, then the cwd. | No |

Every field is optional, and the whole block is optional. A file with only a
`sites` block validates fine: `app` is not required. (`$schema` points at the
JSON Schema for editor autocompletion; it resolves once `@bunny.net/cli` is
installed.)

## What "preconfigured" buys you

With `name`, `build`, and `dir` set, the entire deploy is one command:

```bash
bun ny sites deploy --build # runs `build`, uploads `dir`, to a preview URL
bun ny sites deploy --build --prod # same, published as the live site
```

No `--site`, no build command, no directory argument. Without the config you'd
type them each time:

```bash
bun ny sites deploy dist --build "npm run build" --site acme-app --prod
```

## Examples

| Directory | Framework | `build` | `dir` |
| ----------------------------------- | ----------------------- | --------------- | -------- |
| [`vite/`](./vite) | Vite | `npm run build` | `dist` |
| [`astro/`](./astro) | Astro | `npm run build` | `dist` |
| [`nextjs-static/`](./nextjs-static) | Next.js (static export) | `npm run build` | `out` |
| [`hugo/`](./hugo) | Hugo | `hugo --minify` | `public` |
| [`static-html/`](./static-html) | Plain HTML (no build) | - | `.` |
| [`app-and-site/`](./app-and-site) | Magic Containers + site | `npm run build` | `dist` |

`app-and-site/` shows both blocks in one file: `bunny apps deploy` reads `app`,
`bunny sites deploy` reads `sites`, and each ignores the other.

## Typical setup

```bash
bun ny sites create acme-app # provision the site + set `sites.name`
# ...author bunny.jsonc from one of these examples...
bun ny sites deploy --build --prod
```

For other frameworks, set `dir` to the framework's output folder (Gatsby
`public`, SvelteKit `build`, Eleventy `_site`, ...); the CLI also detects most of
these automatically when `dir` is omitted.
27 changes: 27 additions & 0 deletions examples/sites/app-and-site/bunny.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"$schema": "./node_modules/@bunny.net/config/generated/schema.json",
"version": "2026-05-11",
// One bunny.jsonc can hold both a Magic Containers `app` and a static `sites` block.
// `bunny apps deploy` reads `app`; `bunny sites deploy` reads `sites`. Each ignores the other.
"app": {
"name": "acme-api",
"regions": ["DE"],
"containers": {
"api": {
"image": "ghcr.io/acme/api:latest",
"endpoints": [
{
"type": "cdn",
"ssl": true,
"ports": [{ "public": 443, "container": 8080 }]
}
]
}
}
},
"sites": {
"name": "acme-www",
"build": "npm run build",
"dir": "dist"
}
}
10 changes: 10 additions & 0 deletions examples/sites/astro/bunny.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"$schema": "./node_modules/@bunny.net/config/generated/schema.json",
"version": "2026-05-11",
"sites": {
"name": "acme-blog",
"build": "npm run build",
// Astro's default static output.
"dir": "dist"
}
}
10 changes: 10 additions & 0 deletions examples/sites/hugo/bunny.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"$schema": "./node_modules/@bunny.net/config/generated/schema.json",
"version": "2026-05-11",
"sites": {
"name": "acme-docs",
// Hugo builds directly (no package manager).
"build": "hugo --minify",
"dir": "public"
}
}
10 changes: 10 additions & 0 deletions examples/sites/nextjs-static/bunny.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"$schema": "./node_modules/@bunny.net/config/generated/schema.json",
"version": "2026-05-11",
"sites": {
"name": "acme-landing",
// Requires `output: "export"` in next.config.js so the build emits static HTML.
"build": "npm run build",
"dir": "out"
}
}
9 changes: 9 additions & 0 deletions examples/sites/static-html/bunny.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"$schema": "./node_modules/@bunny.net/config/generated/schema.json",
"version": "2026-05-11",
"sites": {
"name": "acme-status",
// No build step: plain HTML lives in this directory, so `dir` is the project root.
"dir": "."
}
}
9 changes: 9 additions & 0 deletions examples/sites/vite/bunny.jsonc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"$schema": "./node_modules/@bunny.net/config/generated/schema.json",
"version": "2026-05-11",
"sites": {
"name": "acme-app",
"build": "npm run build",
"dir": "dist"
}
}
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
],
"devDependencies": {
"@bunny.net/openapi-client": "workspace:*",
"@bunny.net/app-config": "workspace:*",
"@bunny.net/config": "workspace:*",
"@bunny.net/storage-sdk": "^0.3.1",
"@bunny.net/database-shell": "workspace:*",
"@bunny.net/sandbox": "workspace:*",
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { openCommand } from "./commands/open.ts";
import { registriesNamespace } from "./commands/registries/index.ts";
import { sandboxNamespace } from "./commands/sandbox/index.ts";
import { scriptsNamespace } from "./commands/scripts/index.ts";
import { sitesNamespace } from "./commands/sites/index.ts";
import { storageNamespace } from "./commands/storage/index.ts";
import { whoamiCommand } from "./commands/whoami.ts";
import { bunny } from "./core/colors.ts";
Expand All @@ -38,6 +39,7 @@ const commands: CommandModule[] = [
const experimentalCommands: CommandModule[] = [
appsNamespace,
registriesNamespace,
sitesNamespace,
storageNamespace,
];

Expand Down
Loading
Loading