Skip to content

Commit f4bc85d

Browse files
feat(scripts): manage custom hostnames for Edge Scripts (#89)
* feat(scripts): manage custom hostnames for Edge Scripts * switch to domains
1 parent aa0f44d commit f4bc85d

12 files changed

Lines changed: 1009 additions & 12 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@bunny.net/cli": minor
3+
---
4+
5+
feat(scripts): manage custom domains for Edge Scripts (`bunny scripts domains`, with `hostnames` as a hidden alias)

AGENTS.md

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,11 @@ bunny-cli/
158158
│ │ ├── errors.ts # Re-exports UserError/ApiError from @bunny.net/openapi-client + ConfigError
159159
│ │ ├── format.ts # Shared table/key-value rendering (text, table, csv, markdown)
160160
│ │ ├── format.test.ts # Tests for format utilities
161+
│ │ ├── hostnames/ # Reusable pull-zone hostname feature (mounted by scripts; apps next)
162+
│ │ │ ├── index.ts # Re-exports client helpers + createHostnamesCommands
163+
│ │ │ ├── client.ts # hostnameUrl(), fetchPullZoneHostnames(), enableSsl() + Hostname/ResolvedPullZone types
164+
│ │ │ ├── client.test.ts # Tests for hostnameUrl() scheme logic
165+
│ │ │ └── commands.ts # createHostnamesCommands(): add/ssl/list/remove factory parameterized by a pull-zone resolver
161166
│ │ ├── logger.ts # Chalk-based structured logger
162167
│ │ ├── manifest.ts # .bunny/ context file resolution (load, save, resolveManifestId)
163168
│ │ ├── types.ts # GlobalArgs, OutputFormat, and shared type definitions
@@ -271,15 +276,18 @@ bunny-cli/
271276
│ │ ├── index.ts # defineNamespace("scripts", ...) — registers all script commands
272277
│ │ ├── constants.ts # SCRIPT_MANIFEST, SCRIPT_TYPE_LABELS
273278
│ │ ├── create.ts # Create a remote Edge Script (exports shared `createScript` helper)
279+
│ │ ├── delete.ts # Delete an Edge Script (double confirmation or --force)
274280
│ │ ├── deploy.ts # Deploy code to an Edge Script (publishes by default)
275281
│ │ ├── docs.ts # Open Edge Script documentation in browser
276282
│ │ ├── init.ts # Scaffold a new Edge Script project from a template (calls `createScript`)
277283
│ │ ├── link.ts # Link directory to a remote Edge Script (.bunny/script.json)
278284
│ │ ├── list.ts # List all Edge Scripts (Standalone + Middleware)
279-
│ │ ├── show.ts # Show Edge Script details (supports manifest fallback)
285+
│ │ ├── show.ts # Show Edge Script details + hostnames (supports manifest fallback)
280286
│ │ ├── deployments/
281287
│ │ │ ├── index.ts # defineNamespace("deployments", ...)
282288
│ │ │ └── list.ts # List deployments for an Edge Script
289+
│ │ ├── hostnames/
290+
│ │ │ └── index.ts # Mounts core/hostnames factory: script pull-zone resolver + --id/--pull-zone, visible as "domains" with hidden "hostnames" alias
283291
│ │ └── env/
284292
│ │ ├── index.ts # defineNamespace("env", ...)
285293
│ │ ├── list.ts # List environment variables for a script
@@ -304,10 +312,16 @@ bunny-cli/
304312
- **Namespaces are directories** with an `index.ts` that calls `defineNamespace()`.
305313
- **Leaf commands** are individual `.ts` files that call `defineCommand()`.
306314
- **Top-level commands** (`login`, `logout`, `whoami`) are registered directly in `cli.ts` without a namespace.
307-
- **Shared internal code lives in `packages/cli/src/core/`** — command factories, errors, logger, format utilities, UI helpers, and shared types. Keep this flat (no nested subdirectories).
315+
- **Shared internal code lives in `packages/cli/src/core/`** — command factories, errors, logger, format utilities, UI helpers, and shared types. Keep this mostly flat; a cohesive, reusable feature spanning several files may use a subdirectory (e.g. `core/hostnames/` — the pull-zone hostname helpers + the `createHostnamesCommands` factory mounted by both `scripts` and, in future, `apps`).
308316
- **Config logic lives in `packages/cli/src/config/`** — schema, file resolution, and profile management.
309317
- **Error classes are split.** `UserError` and `ApiError` live in `@bunny.net/openapi-client` (the SDK needs them). `ConfigError` lives in the CLI and extends `UserError`. The CLI's `errors.ts` re-exports `UserError` and `ApiError` from `@bunny.net/openapi-client`.
310318
- **Import API clients from `@bunny.net/openapi-client`**, not relative paths. Import generated types from `@bunny.net/openapi-client/generated/<spec>.d.ts`.
319+
- **Pull-zone settings are exposed via "Hybrid D" across surfaces.** Scripts and apps are backed by a pull zone, which has a large settings surface (hostnames, caching, edge rules, origin, security, purge, CORS, optimizer, logging, …). To keep each owner's help legible:
320+
- **Flatten only first-class groups** directly into the owner — picked by user mental model, kept to one or two. `scripts domains` is the flattened group (a custom domain is "my site's address," not a CDN setting).
321+
- **Group the long tail** under a `pullzone` sub-namespace within the owner (e.g. `scripts pullzone <setting>`), so the owner's top-level help gains one line, not ten. Curate per owner — don't expose settings that don't apply (a script _is_ its pull zone's origin, so no origin-URL command under `scripts`).
322+
- **A standalone `bunny pullzone` command** (planned) is the canonical full surface for pull zones not backing a script/app, targeted by `--id`.
323+
- Each setting-area is a **mountable factory** like `createHostnamesCommands` (`core/hostnames/`): one `{ commandPath, target, resolve(args) => { pullZoneId, coreClient }, hiddenAliases }` mounted into the root `pullzone` (resolve from `--id`), `scripts` (resolve from the linked manifest), and `apps` (resolve from the CDN endpoint). The resolver is the only per-surface difference.
324+
- Canonical term is `pullzone` (matches the bunny.net dashboard/API); `pz` is a hidden alias (`defineNamespace(alias, false, …)`), the same pattern as `domains`'s hidden `hostnames` alias.
311325

312326
---
313327

@@ -830,9 +844,18 @@ bunny
830844
│ │ Create a remote Edge Script (use after init when --deploy was skipped)
831845
│ ├── deploy <file> [id] [--skip-publish]
832846
│ │ Deploy code to an Edge Script (publishes by default)
847+
│ ├── delete [id] [--force] Delete an Edge Script (double confirmation or --force)
833848
│ ├── deployments
834849
│ │ └── list [id] (alias: ls) List deployments for an Edge Script
835850
│ ├── docs Open Edge Script documentation in browser
851+
│ ├── domains (hidden alias: hostnames)
852+
│ │ ├── add <domain> [--ssl] [--no-force-ssl] [--id] [--pull-zone]
853+
│ │ │ Add a custom domain (SSL opt-in; HTTPS forced by default)
854+
│ │ ├── ssl <domain> [--no-force-ssl] [--id] [--pull-zone]
855+
│ │ │ Issue a free SSL certificate (HTTPS forced by default)
856+
│ │ ├── list (alias: ls) [--id] [--pull-zone] List pull zone domains
857+
│ │ └── remove <domain> (alias: rm) [--force] [--id] [--pull-zone]
858+
│ │ Remove a custom domain
836859
│ ├── env
837860
│ │ ├── list [id] List environment variables
838861
│ │ ├── set <key> <value> [id] Set environment variable

packages/cli/README.md

Lines changed: 162 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,14 @@ bunny open --print
8181
bunny open --print --output json
8282
```
8383

84+
### `bunny docs`
85+
86+
Open the bunny.net documentation in your default browser.
87+
88+
```bash
89+
bunny docs
90+
```
91+
8492
### `bunny config`
8593

8694
Manage CLI configuration and profiles.
@@ -477,6 +485,8 @@ bunny scripts deploy dist/index.js 12345
477485
| ---------------- | ------------------------------ |
478486
| `--skip-publish` | Upload code without publishing |
479487

488+
After publishing, the live URL and any custom domains are printed.
489+
480490
> **Note:** `bunny scripts deploy` works regardless of how the script was created or whether GitHub Actions is configured. The last deployment always wins — whether triggered by a GitHub Action or a manual CLI deploy.
481491
482492
#### `bunny scripts link`
@@ -503,13 +513,156 @@ bunny scripts list --output json
503513

504514
#### `bunny scripts show`
505515

506-
Show details for an Edge Script. Uses the linked script from `.bunny/script.json` if no ID is provided.
516+
Show details for an Edge Script. Uses the linked script from `.bunny/script.json` if no ID is provided. Output includes the script's hostnames (system and custom) with their SSL status.
507517

508518
```bash
509519
bunny scripts show <script-id>
510520
bunny scripts show
511521
```
512522

523+
#### `bunny scripts delete`
524+
525+
Delete an Edge Script. Uses the linked script if no ID is provided. Requires double confirmation (or `--force` to skip).
526+
527+
```bash
528+
bunny scripts delete <script-id>
529+
bunny scripts delete
530+
bunny scripts delete <script-id> --force
531+
```
532+
533+
| Flag | Description |
534+
| --------- | ------------------------- |
535+
| `--force` | Skip confirmation prompts |
536+
537+
#### `bunny scripts deployments`
538+
539+
Manage Edge Script deployments.
540+
541+
##### `bunny scripts deployments list`
542+
543+
List deployments for an Edge Script. Uses the linked script if no ID is provided.
544+
545+
```bash
546+
bunny scripts deployments list
547+
bunny scripts deployments ls
548+
bunny scripts deployments list <script-id>
549+
bunny scripts deployments list --output json
550+
```
551+
552+
#### `bunny scripts env`
553+
554+
Manage environment variables and secrets for an Edge Script. All subcommands default to the linked script; pass `--id <script-id>` to target another.
555+
556+
##### `bunny scripts env list`
557+
558+
List environment variables and secrets.
559+
560+
```bash
561+
bunny scripts env list
562+
bunny scripts env ls
563+
bunny scripts env list --output json
564+
```
565+
566+
##### `bunny scripts env set`
567+
568+
Set an environment variable or secret. Runs interactively when arguments are omitted. The variable name is uppercased.
569+
570+
```bash
571+
bunny scripts env set MY_VAR value
572+
bunny scripts env set # interactive
573+
bunny scripts env set API_KEY secret-value --secret
574+
```
575+
576+
| Flag | Description |
577+
| ---------- | --------------------------------------- |
578+
| `--secret` | Store as an encrypted secret |
579+
| `--id` | Edge Script ID (uses linked if omitted) |
580+
581+
##### `bunny scripts env remove`
582+
583+
Remove an environment variable or secret. Shows an interactive picker when no name is given; prompts for confirmation unless `--force`.
584+
585+
```bash
586+
bunny scripts env remove MY_VAR
587+
bunny scripts env rm MY_VAR -f
588+
```
589+
590+
##### `bunny scripts env pull`
591+
592+
Pull environment variables to a local `.env` file.
593+
594+
```bash
595+
bunny scripts env pull
596+
bunny scripts env pull <script-id>
597+
bunny scripts env pull --force
598+
```
599+
600+
| Flag | Description |
601+
| --------- | ---------------------------------------------- |
602+
| `--force` | Overwrite an existing `.env` without prompting |
603+
604+
#### `bunny scripts domains`
605+
606+
Manage custom domains for an Edge Script. A script's domains live on its linked pull zone, so these commands operate on that pull zone. All subcommands default to the linked script; pass `--id <script-id>` to target another, and `--pull-zone <id>` when a script has more than one linked pull zone. (`bunny scripts hostnames` is kept as a hidden alias.)
607+
608+
##### `bunny scripts domains add`
609+
610+
Add a custom domain. SSL is **not** requested by default — a free certificate can only be issued once your DNS points at bunny.net, so the command prints the `CNAME` record to create and the follow-up command to enable HTTPS. Pass `--ssl` to issue a certificate immediately; HTTP is redirected to HTTPS by default (opt out with `--no-force-ssl`).
611+
612+
```bash
613+
# Add a domain and get DNS instructions
614+
bunny scripts domains add shop.example.com
615+
616+
# Add and request SSL now (DNS must already be pointed at bunny.net) — HTTPS forced
617+
bunny scripts domains add shop.example.com --ssl
618+
619+
# Add and request SSL without forcing HTTPS
620+
bunny scripts domains add shop.example.com --ssl --no-force-ssl
621+
```
622+
623+
| Flag | Description |
624+
| ---------------- | ----------------------------------------------------------------------- |
625+
| `--ssl` | Issue a free SSL certificate now and force HTTPS (requires DNS pointed) |
626+
| `--no-force-ssl` | When issuing SSL, keep serving HTTP instead of redirecting to HTTPS |
627+
| `--id` | Edge Script ID (uses linked script if omitted) |
628+
| `--pull-zone` | Pull zone ID (required if the script has multiple linked zones) |
629+
630+
##### `bunny scripts domains ssl`
631+
632+
Request a free SSL certificate for a custom domain. Run this after the domain's DNS points at bunny.net (see the `CNAME` printed by `domains add`). HTTP is redirected to HTTPS by default; pass `--no-force-ssl` to keep plain HTTP.
633+
634+
```bash
635+
bunny scripts domains ssl shop.example.com
636+
bunny scripts domains ssl shop.example.com --no-force-ssl
637+
```
638+
639+
##### `bunny scripts domains list`
640+
641+
List the domains on a script's pull zone, with SSL and Force SSL status.
642+
643+
```bash
644+
bunny scripts domains list
645+
bunny scripts domains ls
646+
bunny scripts domains list --output json
647+
```
648+
649+
##### `bunny scripts domains remove`
650+
651+
Remove a custom domain. System hostnames controlled by bunny.net cannot be removed.
652+
653+
```bash
654+
bunny scripts domains remove shop.example.com
655+
bunny scripts domains remove shop.example.com --force
656+
```
657+
658+
#### `bunny scripts docs`
659+
660+
Open the Edge Scripts documentation in your browser.
661+
662+
```bash
663+
bunny scripts docs
664+
```
665+
513666
### `bunny api`
514667

515668
Make a raw authenticated HTTP request to any bunny.net API endpoint. Auth is handled automatically via your configured API key.
@@ -543,6 +696,14 @@ bunny api GET /pullzone --verbose
543696

544697
The method is case-insensitive (`get` and `GET` both work). Paths are relative to `https://api.bunny.net` — use `/database/...` for the Database API and `/mc/...` for Magic Containers.
545698

699+
### `bunny completion`
700+
701+
Generate a shell completion script. Add the output to your shell profile to enable tab completion.
702+
703+
```bash
704+
bunny completion >> ~/.zshrc
705+
```
706+
546707
## Global Options
547708

548709
| Flag | Alias | Description | Default |

packages/cli/src/commands/scripts/deploy.ts

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
import { existsSync } from "node:fs";
22
import { resolve } from "node:path";
3-
import { createComputeClient } from "@bunny.net/openapi-client";
3+
import {
4+
createComputeClient,
5+
createCoreClient,
6+
} from "@bunny.net/openapi-client";
47
import { resolveConfig } from "../../config/index.ts";
58
import { clientOptions } from "../../core/client-options.ts";
69
import { defineCommand } from "../../core/define-command.ts";
710
import { UserError } from "../../core/errors.ts";
11+
import {
12+
fetchPullZoneHostnames,
13+
type Hostname,
14+
hostnameUrl,
15+
} from "../../core/hostnames/index.ts";
816
import { logger } from "../../core/logger.ts";
917
import { resolveManifestId } from "../../core/manifest.ts";
1018
import { spinner } from "../../core/ui.ts";
@@ -92,7 +100,8 @@ export const scriptsDeployCommand = defineCommand<DeployArgs>({
92100
const code = await Bun.file(absPath).text();
93101

94102
const config = resolveConfig(profile, apiKey, verbose);
95-
const client = createComputeClient(clientOptions(config, verbose));
103+
const options = clientOptions(config, verbose);
104+
const client = createComputeClient(options);
96105

97106
const spin = spinner("Uploading code...");
98107
spin.start();
@@ -125,13 +134,57 @@ export const scriptsDeployCommand = defineCommand<DeployArgs>({
125134
return;
126135
}
127136

137+
if (!published) return;
138+
128139
const { data: script } = await client.GET("/compute/script/{id}", {
129140
params: { path: { id } },
130141
});
131142

132-
const hostname = script?.LinkedPullZones?.[0]?.DefaultHostname ?? undefined;
133-
if (hostname && published) {
134-
logger.info(`Live at: ${hostname}`);
143+
const zones = script?.LinkedPullZones ?? [];
144+
145+
// Pull the full hostname list (incl. custom domains) from the core API;
146+
// fall back to the script's system hostname if that lookup fails.
147+
const coreClient = createCoreClient(options);
148+
const hostnames: Hostname[] = [];
149+
for (const zone of zones) {
150+
if (zone.Id == null) continue;
151+
try {
152+
hostnames.push(...(await fetchPullZoneHostnames(coreClient, zone.Id)));
153+
} catch (err) {
154+
logger.debug(
155+
`Failed to fetch hostnames for pull zone ${zone.Id}: ${err}`,
156+
verbose,
157+
);
158+
}
159+
}
160+
161+
if (hostnames.length === 0) {
162+
const fallback = zones[0]?.DefaultHostname;
163+
if (fallback) logger.info(`Live at: ${fallback}`);
164+
return;
165+
}
166+
167+
const system = hostnames.find((h) => h.IsSystemHostname);
168+
const primary = system ?? hostnames[0];
169+
const customs = hostnames.filter((h) => h !== primary);
170+
171+
if (primary?.Value) {
172+
logger.info(
173+
`Live at: ${hostnameUrl(primary.Value, {
174+
hasCertificate: primary.HasCertificate,
175+
forceSSL: primary.ForceSSL,
176+
})}`,
177+
);
178+
}
179+
180+
for (const custom of customs) {
181+
if (!custom.Value) continue;
182+
logger.log(
183+
` ${hostnameUrl(custom.Value, {
184+
hasCertificate: custom.HasCertificate,
185+
forceSSL: custom.ForceSSL,
186+
})}`,
187+
);
135188
}
136189
},
137190
});

0 commit comments

Comments
 (0)