From e64ddc07d366f3868002aecda66a2cd4c035754a Mon Sep 17 00:00:00 2001 From: jamie-at-bunny Date: Tue, 28 Jul 2026 09:50:59 +0100 Subject: [PATCH 1/6] docs(storage): add skill reference, fix stale README examples --- packages/cli/README.md | 89 +++++----- skills/bunny-cli/SKILL.md | 5 + skills/bunny-cli/references/storage.md | 217 +++++++++++++++++++++++++ 3 files changed, 269 insertions(+), 42 deletions(-) create mode 100644 skills/bunny-cli/references/storage.md diff --git a/packages/cli/README.md b/packages/cli/README.md index dc22e66..97f79a9 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -477,66 +477,71 @@ Positional value ordering for `record add` follows the record type: `A`/`AAAA`/` > **Experimental**: hidden from `--help` and the landing page while it stabilizes. -Manage Edge Storage through two resource groups: **`bunny storage zone`** (the zone itself: create, list, inspect, update, delete) and **`bunny storage file`** (the files within a zone). Zone management uses the account API key; file operations use the zone's own password and a region-specific host, both resolved automatically from the zone. The `[zone]` argument accepts either the zone name or its numeric ID. `zone` aliases to `zones` (and `bucket`/`buckets`); `file` aliases to `files`. +Manage Edge Storage through two resource groups: **`bunny storage zones`** (the zone itself: create, list, inspect, update, delete; alias `zone`, plus hidden `bucket`/`buckets`) and **`bunny storage files`** (the files within a zone; alias `file`). Zone management uses the account API key; file operations use the zone's own password and a region-specific host, both resolved automatically from the zone. `zones` commands take the zone as an optional `[zone]` positional; `files` commands take it as the `--zone`/`-z` flag (their positional is the file path). Either accepts the zone name or its numeric ID. When the zone is omitted it resolves from the directory's linked zone (`bunny storage link`, stored in `.bunny/storage.json`), then an interactive picker, which offers to link the directory to the picked zone. -A storage zone only holds files; a **pull zone** is what serves them on the web. `zone add` offers to create one (origin set to the new storage zone) and then to add a custom domain, or pass `--pull-zone`/`--domain` to do it non-interactively. Custom domains live on the pull zone and are managed with `bunny storage zone domains`. +A storage zone only holds files; a **pull zone** is what serves them on the web. `zones add` offers to create one (origin set to the new storage zone) and then to add a custom domain, or pass `--pull-zone`/`--domain` to do it non-interactively. Custom domains live on the pull zone and are managed with `bunny storage zones domains`. ```bash # Zones (lifecycle) -bunny storage zone list -bunny storage zone add # interactive: prompts for name and region -bunny storage zone add my-zone --region DE -bunny storage zone add my-zone --region NY --replication LA,SG -bunny storage zone add my-zone --region DE --pull-zone # also create a pull zone to serve it on the web -bunny storage zone add my-zone --region DE --domain cdn.example.com # pull zone + custom domain -bunny storage zone show my-zone -bunny storage zone update my-zone # interactive: edit settings, pre-filled with current values -bunny storage zone update my-zone --custom-404-path /404.html -bunny storage zone remove my-zone +bunny storage zones list +bunny storage zones add # interactive: prompts for name and region +bunny storage zones add my-zone --region DE +bunny storage zones add my-zone --region NY --replication LA,SG +bunny storage zones add my-zone --region DE --pull-zone # also create a pull zone to serve it on the web +bunny storage zones add my-zone --region DE --domain cdn.example.com # pull zone + custom domain +bunny storage zones show my-zone +bunny storage zones update my-zone # interactive: edit settings, pre-filled with current values +bunny storage zones update my-zone --custom-404-path /404.html +bunny storage zones remove my-zone # confirms twice (yes/no, then type the zone name) + +# Link the working directory to a zone so commands can omit it +bunny storage link my-zone +bunny storage unlink # List the available storage regions bunny storage regions # S3-compatible credentials (for zones with S3 preview access) -bunny storage zone credentials my-zone # show endpoint + access key (secret masked) -bunny storage zone credentials my-zone --show-secret # reveal the secret access key -bunny storage zone credentials my-zone --read-only # use the read-only password as the secret -bunny storage zone credentials my-zone --format rclone >> ~/.config/rclone/rclone.conf -eval "$(bunny storage zone credentials my-zone --format env)" # AWS-compatible env vars +bunny storage zones credentials my-zone # show endpoint + access key (secret masked) +bunny storage zones credentials my-zone --show-secret # reveal the secret access key +bunny storage zones credentials my-zone --read-only # use the read-only password as the secret +bunny storage zones credentials my-zone --format rclone >> ~/.config/rclone/rclone.conf +eval "$(bunny storage zones credentials my-zone --format env)" # AWS-compatible env vars # Files: list, upload, download, delete (paths are relative to the zone root) -bunny storage file list my-zone -bunny storage file list my-zone images/ -bunny storage file upload my-zone ./photo.png --to images/ -bunny storage file upload my-zone ./photo.png --checksum --content-type image/png -bunny storage file download my-zone images/photo.png --out ./local.png -bunny storage file remove my-zone images/photo.png -bunny storage file remove my-zone images/ --force # trailing slash removes a directory +bunny storage files list --zone my-zone +bunny storage files list images/ # linked zone +bunny storage files upload ./photo.png --to images/ +bunny storage files upload ./photo.png --checksum --content-type image/png +bunny storage files download images/photo.png --out ./local.png +bunny storage files remove images/photo.png +bunny storage files remove images/ --force # trailing slash removes a directory # Custom domains on the zone's pull zone -bunny storage zone domains list my-zone -bunny storage zone domains add cdn.example.com my-zone -bunny storage zone domains ssl cdn.example.com my-zone -bunny storage zone domains remove cdn.example.com my-zone +bunny storage zones domains list my-zone +bunny storage zones domains add cdn.example.com my-zone +bunny storage zones domains ssl cdn.example.com my-zone +bunny storage zones domains remove cdn.example.com my-zone # Open the storage documentation bunny storage docs ``` -A trailing slash on a `file` path denotes a directory: `file list my-zone images/` lists that directory, and `file remove my-zone images/` deletes it and its contents recursively. Edge Storage file operations are powered by the [`@bunny.net/storage-sdk`](https://github.com/BunnyWay/edge-script-sdk/tree/main/libs/bunny-storage). - -bunny.net's S3-compatible API is in closed preview and is opt-in per zone (it cannot be enabled on an existing zone). When a zone has access, `bunny storage zone show` surfaces its S3 endpoint, and `bunny storage zone credentials` emits the endpoint, region, access key (the zone name), and secret (the zone password) as a table, as JSON (`--output json`), or as ready-to-use config for `rclone`, the AWS CLI, `s3cmd`, or your shell (`--format`). The table masks the secret by default; pass `--show-secret` to reveal it (`--output json` and `--format` always emit it in full, since they're meant to be consumed by tools). The access key and secret are the zone's existing name and password, so there's nothing new to rotate beyond the zone's own credentials. - -| Flag | Commands | Description | -| ---------------------------------------------------------------------------------- | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | -| `--region`, `--replication` | `zone add` | Primary region code, plus optional replication regions (any storage region except the primary; run `storage regions` to list them) | -| `--pull-zone`, `--pull-zone-name`, `--domain` | `zone add` | Also create a pull zone (what serves the stored files on the web) and optionally a custom domain; interactively, `add` offers both | -| `--custom-404-path`, `--rewrite-404-to-200`, `--replication` | `zone update` | Edit zone settings (see `bunny storage zone update --help`) | -| `--format` (`rclone` \| `aws` \| `s3cmd` \| `env`), `--read-only`, `--show-secret` | `zone credentials` | Emit S3 config for a tool; use the read-only password as the secret; reveal the masked secret in the table | -| `--to` | `file upload` | Remote path; a trailing slash uploads into that directory | -| `--checksum`, `--content-type` | `file upload` | Send a SHA256 checksum for server-side verification; set the stored content type | -| `--out` | `file download` | Local destination path (defaults to the file name) | -| `--force` | `zone remove`, `file remove` | Skip the confirmation prompt | +A trailing slash on a `files` path denotes a directory: `files list images/` lists that directory, and `files remove images/` deletes it and its contents recursively. Edge Storage file operations are powered by the [`@bunny.net/storage-sdk`](https://github.com/BunnyWay/edge-script-sdk/tree/main/libs/bunny-storage). + +bunny.net's S3-compatible API is in closed preview and is opt-in per zone (it cannot be enabled on an existing zone). When a zone has access, `bunny storage zones show` surfaces its S3 endpoint, and `bunny storage zones credentials` emits the endpoint, region, access key (the zone name), and secret (the zone password) as a table, as JSON (`--output json`), or as ready-to-use config for `rclone`, the AWS CLI, `s3cmd`, or your shell (`--format`). The table masks the secret by default; pass `--show-secret` to reveal it (`--output json` and `--format` always emit it in full, since they're meant to be consumed by tools). The access key and secret are the zone's existing name and password, so there's nothing new to rotate beyond the zone's own credentials. + +| Flag | Commands | Description | +| ---------------------------------------------------------------------------------- | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | +| `--region`, `--replication` | `zones add` | Primary region code, plus optional replication regions (any storage region except the primary; run `storage regions` to list them) | +| `--pull-zone`, `--pull-zone-name`, `--domain` | `zones add` | Also create a pull zone (what serves the stored files on the web) and optionally a custom domain; interactively, `add` offers both | +| `--custom-404-path`, `--rewrite-404-to-200`, `--replication` | `zones update` | Edit zone settings; replication is additive since replicas can't be removed (see `bunny storage zones update --help`) | +| `--format` (`rclone` \| `aws` \| `s3cmd` \| `env`), `--read-only`, `--show-secret` | `zones credentials` | Emit S3 config for a tool; use the read-only password as the secret; reveal the masked secret in the table | +| `--zone`, `-z` | all `files` commands | Storage zone name or ID (defaults to the linked zone) | +| `--to` | `files upload` | Remote path; a trailing slash uploads into that directory | +| `--checksum`, `--content-type` | `files upload` | Send a SHA256 checksum for server-side verification; set the stored content type | +| `--out` | `files download` | Local destination path (defaults to the file name) | +| `--force` | `zones remove`, `files remove`, `unlink` | Skip the confirmation prompts | ### `bunny scripts` diff --git a/skills/bunny-cli/SKILL.md b/skills/bunny-cli/SKILL.md index a5483b0..b417e0f 100644 --- a/skills/bunny-cli/SKILL.md +++ b/skills/bunny-cli/SKILL.md @@ -45,6 +45,10 @@ bunny sandbox create my-sandbox -e ANTHROPIC_API_KEY=sk-ant-... bunny sandbox exec my-sandbox -- bun install bunny sandbox url add my-sandbox 3000 +# manage Edge Storage +bunny storage zones add my-zone --region DE +bunny storage files upload ./photo.png --to images/ --zone my-zone + # manage DNS bunny dns zones add example.com bunny dns zones nameservers example.com # is the registrar delegated to bunny yet? @@ -65,6 +69,7 @@ Use this to route to the correct reference file: - **Authenticate or switch profiles** -> `references/auth.md` - **Database management (create, list, show, link, delete, shell, studio, regions, tokens)** -> `references/database.md` - **DNS (zones, delegation checks, records, presets, BIND import/export, DNSSEC, logging, Scriptable DNS scripts)** -> `references/dns.md` +- **Edge Storage (zones, replication, S3 credentials, file upload/download, custom domains)** -> `references/storage.md` - **Edge Scripts (init, create, deploy, link, stats, deployments/rollback, env vars, custom domains)** -> `references/scripts.md` - **Static sites (create, deploy, rollback, previews, custom domains)** -> `references/sites.md` - **Sandboxes (create, exec, ssh, cp, files, public URLs, persistent env vars, Claude Code auth)** -> `references/sandbox.md` diff --git a/skills/bunny-cli/references/storage.md b/skills/bunny-cli/references/storage.md new file mode 100644 index 0000000..cecc666 --- /dev/null +++ b/skills/bunny-cli/references/storage.md @@ -0,0 +1,217 @@ +# Storage Commands + +`bunny storage` manages Edge Storage: the zones that hold files and the files within them. The namespace is hidden from top-level help while experimental, but the commands work. Use `bunny storage --help` for full flag details. + +Two resource groups plus directory-level helpers: + +- `bunny storage zones` (alias: `zone`; hidden: `bucket`, `buckets`): zone lifecycle, settings, S3 credentials, custom domains +- `bunny storage files` (alias: `file`): the files within a zone (list, upload, download, delete) +- `bunny storage link` / `unlink`: pin the current directory to a zone +- `bunny storage regions`: list the available region codes +- `bunny storage docs`: open the Edge Storage documentation + +Two credential layers, both resolved automatically: zone management uses the account API key; file operations use the zone's own password and a region-specific host, fetched from the zone itself. Nothing extra to configure. + +## Zone resolution + +Zone and domains commands take an optional `[zone]` positional; file commands take a `--zone` (`-z`) flag instead (their positional is the file path). Either accepts the zone name or its numeric ID. When omitted, the zone is resolved in this order: + +1. Explicit zone reference +2. `.bunny/storage.json` manifest (written by `bunny storage link`) +3. Interactive picker (errors when non-interactive: `--output json`, no TTY, or `--force`; pass a zone or link the directory in CI) + +When a zone is chosen via the picker on a non-destructive command, the command offers to link the directory to it. + +## Typical workflows + +```bash +# Create a zone, link the directory, work with files +bunny storage zones add my-zone --region DE +bunny storage link my-zone +bunny storage files upload ./photo.png --to images/ +bunny storage files list images/ +bunny storage files download images/photo.png --out ./local.png + +# Create a zone that is also served on the web (pull zone + custom domain) +bunny storage zones add my-zone --region DE --pull-zone --domain cdn.example.com + +# S3-compatible access (zones with S3 preview access) +bunny storage zones credentials my-zone --format rclone >> ~/.config/rclone/rclone.conf +eval "$(bunny storage zones credentials my-zone --format env)" +``` + +**A storage zone only holds files; a pull zone is what serves them on the web.** `zones add` offers to create one (origin set to the new storage zone) and then to add a custom domain, or pass `--pull-zone` / `--domain` to do it non-interactively. Custom domains live on the pull zone and are managed with `bunny storage zones domains`. + +--- + +# Zones + +## `bunny storage zones add`: Create a zone + +```bash +bunny storage zones add # interactive: prompts for name and region +bunny storage zones add my-zone --region DE +bunny storage zones add my-zone --region NY --replication LA,SG +bunny storage zones add my-zone --region DE --pull-zone +bunny storage zones add my-zone --region DE --domain cdn.example.com # implies --pull-zone +``` + +| Flag | Description | +| ------------------ | ----------------------------------------------------------------------------- | +| `--region` | Main region code (e.g. `DE`, `NY`, `LA`, `SG`); run `storage regions` to list | +| `--replication` | Replication region codes (comma-separated or repeated) | +| `--pull-zone` | Also create a pull zone to serve the storage zone over the web | +| `--pull-zone-name` | Name for the pull zone (defaults to the storage zone name) | +| `--domain` | Custom domain to add to the pull zone (implies `--pull-zone`) | +| `--force` | Skip prompts and confirmations (use flag values only) | + +**Gotchas:** + +- The main region cannot be changed after creation. +- Replication regions are **permanent once added** (there is no API to remove one) and each adds storage cost, so the command confirms before creating a zone with any. The confirmation defaults to no. +- Region and replication both affect pricing. +- With `--domain`, the interactive flow walks through DNS setup and SSL; under `--output json` the domain is attached without prompts and SSL is issued later via `domains ssl` once DNS points at bunny. + +## `bunny storage zones list` / `show` + +```bash +bunny storage zones list # alias: ls +bunny storage zones show my-zone +bunny storage zones show # linked zone, or pick interactively +``` + +When the zone has S3 preview access, `show` surfaces the S3 endpoint and hints at `credentials` for the keys. + +## `bunny storage zones update`: Edit zone settings + +Interactive editor pre-filled with current values when run without flags; flags make it non-interactive (a partial set of flags is a partial update). Non-interactive contexts require at least one flag. + +```bash +bunny storage zones update my-zone # interactive editor +bunny storage zones update my-zone --custom-404-path /404.html +bunny storage zones update my-zone --custom-404-path "" # clear the custom 404 +bunny storage zones update my-zone --replication LA,SG +``` + +| Flag | Description | +| ---------------------- | ----------------------------------------------------- | +| `--custom-404-path` | File returned for missing files (empty string clears) | +| `--rewrite-404-to-200` | Rewrite 404 responses to 200 for extensionless URLs | +| `--replication` | Replication region codes to add (additive; see below) | +| `--force` | Skip prompts and confirmations | + +Replication is **additive only**: existing replicas can never be removed, so the final set is always the existing regions plus any new picks. Omitting an existing region from `--replication` prints a warning and keeps it; adding new regions confirms first (permanent, adds cost). + +## `bunny storage zones remove`: Delete a zone + +Deletes the zone **and all of its files**. Double confirmation unless `--force`: a yes/no prompt, then typing the zone name. Also removes a `.bunny/storage.json` that pointed at the deleted zone. + +```bash +bunny storage zones remove my-zone # alias: rm +bunny storage zones remove my-zone --force +``` + +## `bunny storage zones credentials`: S3-compatible credentials + +Alias: `creds`. bunny.net's S3-compatible API is in closed preview and opt-in per zone (it cannot be enabled on an existing zone); the command warns when the zone lacks access. The endpoint is `https://-s3.storage.bunnycdn.com`, the access key is the zone name, and the secret is the zone password, so there is nothing new to rotate beyond the zone's own credentials. + +```bash +bunny storage zones credentials my-zone # table: endpoint + keys (secret masked) +bunny storage zones credentials my-zone --show-secret # reveal the secret +bunny storage zones credentials my-zone --read-only # use the read-only password as the secret +bunny storage zones credentials my-zone --format rclone >> ~/.config/rclone/rclone.conf +bunny storage zones credentials my-zone --format aws # AWS CLI profile snippet +bunny storage zones credentials my-zone --format s3cmd +eval "$(bunny storage zones credentials my-zone --format env)" # AWS_* env vars +``` + +| Flag | Description | +| --------------- | --------------------------------------------------------------------------- | +| `--format` | Emit ready-to-use config: `rclone`, `aws`, `s3cmd`, or `env` | +| `--read-only` | Use the zone's read-only password as the secret access key | +| `--show-secret` | Reveal the secret (masked by default in both the table and `--output json`) | + +`--format` output always contains the full secret (it is meant to be consumed by tools). + +## `bunny storage zones domains`: Custom domains + +Hidden alias: `hostnames`. Domains attach to the storage zone's **pull zone**; if the zone has none, create one with `zones add --pull-zone`. When the zone has multiple pull zones, pass `--pull-zone ` to choose. + +```bash +bunny storage zones domains list my-zone +bunny storage zones domains add cdn.example.com my-zone +bunny storage zones domains ssl cdn.example.com my-zone +bunny storage zones domains remove cdn.example.com my-zone +``` + +`add` walks through DNS (CNAME to the pull zone's system hostname) and offers free SSL; `--ssl`/`--wait` control the follow-up, `--force-ssl` redirects HTTP to HTTPS. `ssl` issues the certificate later, once DNS resolves to bunny. `remove` confirms unless `--force`. + +--- + +# Files + +File commands use the zone's storage password against a region-specific host (both resolved from the zone automatically) and are powered by `@bunny.net/storage-sdk`. Paths are relative to the zone root. **A trailing slash denotes a directory**: `--to images/` uploads into it, `remove images/` deletes it and its contents recursively. + +## `bunny storage files list` + +```bash +bunny storage files list # linked zone's root (alias: ls) +bunny storage files list images/ # a directory +bunny storage files list --zone my-zone +``` + +## `bunny storage files upload` + +```bash +bunny storage files upload ./photo.png # to the zone root, same name +bunny storage files upload ./photo.png --to images/ # into a directory +bunny storage files upload ./photo.png --to images/renamed.png +bunny storage files upload ./photo.png --checksum --content-type image/png +``` + +| Flag | Description | +| ---------------- | --------------------------------------------------------- | +| `--to` | Remote path; a trailing slash uploads into that directory | +| `--content-type` | Override the stored content type | +| `--checksum` | Send a SHA256 checksum so the server verifies the upload | +| `--zone`, `-z` | Storage zone name or ID (defaults to the linked zone) | + +## `bunny storage files download` + +```bash +bunny storage files download images/photo.png # to ./photo.png +bunny storage files download images/photo.png --out ./local.png +bunny storage files download images/photo.png --zone my-zone +``` + +## `bunny storage files remove` + +Confirms unless `--force`. A trailing slash deletes the directory recursively. + +```bash +bunny storage files remove images/photo.png # alias: rm +bunny storage files remove images/ --force # directory + contents, no prompt +``` + +--- + +# Directory linking and regions + +## `bunny storage link` / `unlink` + +Link the current directory to a zone (`.bunny/storage.json`) so storage commands resolve it without an explicit zone. + +```bash +bunny storage link my-zone # by name or ID +bunny storage link # pick interactively +bunny storage unlink # confirms unless --force +``` + +## `bunny storage regions` + +Lists the available storage region codes (e.g. `DE`, `NY`, `LA`, `SG`). Replication uses these same regions, minus the zone's primary. + +```bash +bunny storage regions +bunny storage regions --output json +``` From 269c01d4de8009a82da43c17ed24d1e6129d53a6 Mon Sep 17 00:00:00 2001 From: jamie-at-bunny Date: Tue, 28 Jul 2026 09:56:55 +0100 Subject: [PATCH 2/6] provide explicit zone flag as options --- skills/bunny-cli/references/storage.md | 38 ++++++++++++++++---------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/skills/bunny-cli/references/storage.md b/skills/bunny-cli/references/storage.md index cecc666..117c4b5 100644 --- a/skills/bunny-cli/references/storage.md +++ b/skills/bunny-cli/references/storage.md @@ -22,15 +22,20 @@ Zone and domains commands take an optional `[zone]` positional; file commands ta When a zone is chosen via the picker on a non-destructive command, the command offers to link the directory to it. +Prefer passing the zone explicitly (`--zone` / `[zone]`); it works from any directory and in non-interactive contexts. Linking is a convenience for a directory dedicated to one zone, letting you drop the zone from every command. + ## Typical workflows ```bash -# Create a zone, link the directory, work with files +# Create a zone and work with files (zone passed explicitly) bunny storage zones add my-zone --region DE +bunny storage files upload ./photo.png --to images/ --zone my-zone +bunny storage files list images/ --zone my-zone +bunny storage files download images/photo.png --out ./local.png --zone my-zone + +# Or link the directory once, then drop the zone from file commands bunny storage link my-zone bunny storage files upload ./photo.png --to images/ -bunny storage files list images/ -bunny storage files download images/photo.png --out ./local.png # Create a zone that is also served on the web (pull zone + custom domain) bunny storage zones add my-zone --region DE --pull-zone --domain cdn.example.com @@ -152,21 +157,24 @@ bunny storage zones domains remove cdn.example.com my-zone File commands use the zone's storage password against a region-specific host (both resolved from the zone automatically) and are powered by `@bunny.net/storage-sdk`. Paths are relative to the zone root. **A trailing slash denotes a directory**: `--to images/` uploads into it, `remove images/` deletes it and its contents recursively. +Every file command takes `--zone` (`-z`); it can be omitted only in a directory linked with `bunny storage link` (or interactively via the picker). + ## `bunny storage files list` ```bash -bunny storage files list # linked zone's root (alias: ls) -bunny storage files list images/ # a directory -bunny storage files list --zone my-zone +bunny storage files list --zone my-zone # zone root (alias: ls) +bunny storage files list images/ --zone my-zone # a directory +bunny storage files list images/ # linked zone ``` ## `bunny storage files upload` ```bash -bunny storage files upload ./photo.png # to the zone root, same name -bunny storage files upload ./photo.png --to images/ # into a directory -bunny storage files upload ./photo.png --to images/renamed.png -bunny storage files upload ./photo.png --checksum --content-type image/png +bunny storage files upload ./photo.png --zone my-zone # to the zone root, same name +bunny storage files upload ./photo.png --to images/ --zone my-zone +bunny storage files upload ./photo.png --to images/renamed.png --zone my-zone +bunny storage files upload ./photo.png --checksum --content-type image/png --zone my-zone +bunny storage files upload ./photo.png --to images/ # linked zone ``` | Flag | Description | @@ -179,9 +187,9 @@ bunny storage files upload ./photo.png --checksum --content-type image/png ## `bunny storage files download` ```bash -bunny storage files download images/photo.png # to ./photo.png -bunny storage files download images/photo.png --out ./local.png -bunny storage files download images/photo.png --zone my-zone +bunny storage files download images/photo.png --zone my-zone # to ./photo.png +bunny storage files download images/photo.png --out ./local.png --zone my-zone +bunny storage files download images/photo.png # linked zone ``` ## `bunny storage files remove` @@ -189,8 +197,8 @@ bunny storage files download images/photo.png --zone my-zone Confirms unless `--force`. A trailing slash deletes the directory recursively. ```bash -bunny storage files remove images/photo.png # alias: rm -bunny storage files remove images/ --force # directory + contents, no prompt +bunny storage files remove images/photo.png --zone my-zone # alias: rm +bunny storage files remove images/ --force --zone my-zone # directory + contents, no prompt ``` --- From 2794424dcc06ad8b97940ff8f826250f11622c0b Mon Sep 17 00:00:00 2001 From: jamie-at-bunny Date: Tue, 28 Jul 2026 09:59:19 +0100 Subject: [PATCH 3/6] docs(storage): credentials JSON output masks the secret without --show-secret --- packages/cli/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/README.md b/packages/cli/README.md index 97f79a9..c6b5530 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -529,14 +529,14 @@ bunny storage docs A trailing slash on a `files` path denotes a directory: `files list images/` lists that directory, and `files remove images/` deletes it and its contents recursively. Edge Storage file operations are powered by the [`@bunny.net/storage-sdk`](https://github.com/BunnyWay/edge-script-sdk/tree/main/libs/bunny-storage). -bunny.net's S3-compatible API is in closed preview and is opt-in per zone (it cannot be enabled on an existing zone). When a zone has access, `bunny storage zones show` surfaces its S3 endpoint, and `bunny storage zones credentials` emits the endpoint, region, access key (the zone name), and secret (the zone password) as a table, as JSON (`--output json`), or as ready-to-use config for `rclone`, the AWS CLI, `s3cmd`, or your shell (`--format`). The table masks the secret by default; pass `--show-secret` to reveal it (`--output json` and `--format` always emit it in full, since they're meant to be consumed by tools). The access key and secret are the zone's existing name and password, so there's nothing new to rotate beyond the zone's own credentials. +bunny.net's S3-compatible API is in closed preview and is opt-in per zone (it cannot be enabled on an existing zone). When a zone has access, `bunny storage zones show` surfaces its S3 endpoint, and `bunny storage zones credentials` emits the endpoint, region, access key (the zone name), and secret (the zone password) as a table, as JSON (`--output json`), or as ready-to-use config for `rclone`, the AWS CLI, `s3cmd`, or your shell (`--format`). The table and JSON output mask the secret by default; pass `--show-secret` to reveal it (`--format` always emits it in full, since it's meant to be consumed by tools). The access key and secret are the zone's existing name and password, so there's nothing new to rotate beyond the zone's own credentials. | Flag | Commands | Description | | ---------------------------------------------------------------------------------- | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | `--region`, `--replication` | `zones add` | Primary region code, plus optional replication regions (any storage region except the primary; run `storage regions` to list them) | | `--pull-zone`, `--pull-zone-name`, `--domain` | `zones add` | Also create a pull zone (what serves the stored files on the web) and optionally a custom domain; interactively, `add` offers both | | `--custom-404-path`, `--rewrite-404-to-200`, `--replication` | `zones update` | Edit zone settings; replication is additive since replicas can't be removed (see `bunny storage zones update --help`) | -| `--format` (`rclone` \| `aws` \| `s3cmd` \| `env`), `--read-only`, `--show-secret` | `zones credentials` | Emit S3 config for a tool; use the read-only password as the secret; reveal the masked secret in the table | +| `--format` (`rclone` \| `aws` \| `s3cmd` \| `env`), `--read-only`, `--show-secret` | `zones credentials` | Emit S3 config for a tool; use the read-only password as the secret; reveal the secret masked in the table and JSON | | `--zone`, `-z` | all `files` commands | Storage zone name or ID (defaults to the linked zone) | | `--to` | `files upload` | Remote path; a trailing slash uploads into that directory | | `--checksum`, `--content-type` | `files upload` | Send a SHA256 checksum for server-side verification; set the stored content type | From f47a16079ec7327b6e21401f76469dba7a51fe9f Mon Sep 17 00:00:00 2001 From: jamie-at-bunny Date: Tue, 28 Jul 2026 10:00:45 +0100 Subject: [PATCH 4/6] fmt --- packages/cli/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/README.md b/packages/cli/README.md index c6b5530..535acce 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -536,7 +536,7 @@ bunny.net's S3-compatible API is in closed preview and is opt-in per zone (it ca | `--region`, `--replication` | `zones add` | Primary region code, plus optional replication regions (any storage region except the primary; run `storage regions` to list them) | | `--pull-zone`, `--pull-zone-name`, `--domain` | `zones add` | Also create a pull zone (what serves the stored files on the web) and optionally a custom domain; interactively, `add` offers both | | `--custom-404-path`, `--rewrite-404-to-200`, `--replication` | `zones update` | Edit zone settings; replication is additive since replicas can't be removed (see `bunny storage zones update --help`) | -| `--format` (`rclone` \| `aws` \| `s3cmd` \| `env`), `--read-only`, `--show-secret` | `zones credentials` | Emit S3 config for a tool; use the read-only password as the secret; reveal the secret masked in the table and JSON | +| `--format` (`rclone` \| `aws` \| `s3cmd` \| `env`), `--read-only`, `--show-secret` | `zones credentials` | Emit S3 config for a tool; use the read-only password as the secret; reveal the secret masked in the table and JSON | | `--zone`, `-z` | all `files` commands | Storage zone name or ID (defaults to the linked zone) | | `--to` | `files upload` | Remote path; a trailing slash uploads into that directory | | `--checksum`, `--content-type` | `files upload` | Send a SHA256 checksum for server-side verification; set the stored content type | From 7e202155f96eb67bf560e7cfac56be620b44df1f Mon Sep 17 00:00:00 2001 From: jamie-at-bunny Date: Tue, 28 Jul 2026 10:07:02 +0100 Subject: [PATCH 5/6] update comments around force --- AGENTS.md | 8 ++++---- packages/cli/README.md | 2 +- packages/cli/src/commands/storage/file/remove.ts | 3 ++- packages/cli/src/commands/storage/zone/hostnames/index.ts | 2 +- packages/cli/src/commands/storage/zone/remove.ts | 6 +++++- skills/bunny-cli/references/storage.md | 2 +- 6 files changed, 14 insertions(+), 9 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 7d088b9..426525f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -367,7 +367,7 @@ bunny-cli/ │ │ │ │ ├── list.ts # List all storage zones (alias: ls) │ │ │ │ ├── add.ts # Create a storage zone (prompts for name + region when omitted; offers/--pull-zone creates a pull zone via core/hostnames createPullZone, then offers/--domain a custom domain via setupHostname). Under --output json it stays non-interactive: --domain is attached via addHostname (no DNS/SSL prompts) and reported in the CustomDomain field (with cnameTarget or error) │ │ │ │ ├── show.ts # Show zone details (region, replication, hostname, usage; adds S3 endpoint rows when S3-enabled) -│ │ │ │ ├── credentials.ts # S3 credentials / tool config for the zone (alias: creds; --format, --read-only, --show-secret); table masks the secret unless --show-secret, JSON/--format always emit it in full +│ │ │ │ ├── credentials.ts # S3 credentials / tool config for the zone (alias: creds; --format, --read-only, --show-secret); table and JSON mask the secret unless --show-secret, --format always emits it in full │ │ │ │ ├── update.ts # Update zone settings (custom 404, rewrite 404->200, replication); replication is additive (replicas can't be removed, so existing ones are kept and the prompt only offers new regions, confirming before adding); interactive pre-filled editor when no flags (a mid-flow cancel aborts the whole edit); --output json/non-TTY/--force require flags and error "No changes requested." without them │ │ │ │ ├── remove.ts # Delete a storage zone and its files (alias: rm); double confirmation (yes/no + type the zone name) unless --force, and removes a stale .bunny/storage.json that pointed at the deleted zone │ │ │ │ └── hostnames/index.ts # Mounts core/hostnames createHostnamesCommands as "storage zones domains" (alias hostnames); resolver maps a storage zone (name/ID positional, else linked zone, else picker via resolveStorageZoneInteractive; --pull-zone) to its linked pull zone @@ -485,7 +485,7 @@ bunny-cli/ - **Config logic lives in `packages/cli/src/config/`** — schema, file resolution, and profile management. - **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`. - **Import API clients from `@bunny.net/openapi-client`**, not relative paths. Import generated types from `@bunny.net/openapi-client/generated/.d.ts`. -- **Mask secrets in human output; reveal only behind an explicit flag.** Any sensitive value (API keys, passwords, S3 secret keys, auth tokens) must be masked in the default table/text output and shown in full only when the user opts in with a flag (e.g. `--show-secret`). Use `maskSecret()` from `core/format.ts` for the masked form (it keeps the last 4 characters for identification). Machine-readable output is the exception because it exists to be consumed by tools: `--output json` and tool-config `--format` emit full values. Never print a secret the user did not explicitly ask to see. Reference: `storage zones credentials` masks the S3 secret access key by default and reveals it with `--show-secret`, while never leaking it from inspect/list commands (see `toSafeStorageZone`). +- **Mask secrets in human output; reveal only behind an explicit flag.** Any sensitive value (API keys, passwords, S3 secret keys, auth tokens) must be masked in the default table/text output and shown in full only when the user opts in with a flag (e.g. `--show-secret`). Use `maskSecret()` from `core/format.ts` for the masked form (it keeps the last 4 characters for identification). Tool-config output (`--format rclone|aws|...`) is the exception because it exists to be consumed by tools: it always emits full values. `--output json` masks like the table; `--show-secret` reveals there too. Never print a secret the user did not explicitly ask to see. Reference: `storage zones credentials` masks the S3 secret access key by default in both the table and JSON and reveals it with `--show-secret`, while never leaking it from inspect/list commands (see `toSafeStorageZone`). - **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: - **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). - **Group the long tail** under a `pullzone` sub-namespace within the owner (e.g. `scripts pullzone `), 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`). @@ -1023,7 +1023,7 @@ bunny │ │ Enable DNS query logging │ └── disable [domain] [--force] Disable DNS query logging ├── storage (experimental, hidden from help and landing page) -│ │ Two resource groups: `zones` (the zone, via core API + account key) and `files` (zone contents, via @bunny.net/storage-sdk + the zone password/region host, resolved automatically). The zone is a name or numeric ID; `zones` commands take it as the `[zone]` positional, `files` commands as the `--zone`/`-z` flag (the positional is the file/path). When the zone is omitted it resolves from a linked zone (`bunny storage link`) then an interactive picker, which offers to link the directory to the picked zone (except on destructive commands). +│ │ Two resource groups: `zones` (the zone, via core API + account key) and `files` (zone contents, via @bunny.net/storage-sdk + the zone password/region host, resolved automatically). The zone is a name or numeric ID; `zones` commands take it as the `[zone]` positional, `files` commands as the `--zone`/`-z` flag (the positional is the file/path). When the zone is omitted it resolves from a linked zone (`bunny storage link`) then an interactive picker (errors instead of prompting under --output json, no TTY, or --force), which offers to link the directory to the picked zone (except on destructive commands). │ ├── zones (canonical; aliases: zone; hidden: bucket, buckets) │ │ ├── list List all storage zones (alias: ls) │ │ ├── add [name] [--region] [--replication] [--pull-zone] [--pull-zone-name] [--domain] [--force/-f] Create a storage zone (prompts for name + region when omitted; offers/--pull-zone creates a pull zone to serve it on the web, then offers/--domain a custom domain via setupHostname; replicas are permanent so adding any is confirmed; --force/--output json skip all prompts and use flag values only) @@ -1031,7 +1031,7 @@ bunny │ │ ├── update [zone] [--custom-404-path] [--rewrite-404-to-200] [--replication] [--force/-f] Update zone settings (edits interactively pre-filled when no flags; replication is additive and adding a replica is confirmed unless --force; --force/--output json/non-TTY require flags and error "No changes requested." without them) │ │ ├── remove [zone] [--force] Delete a storage zone and its files (alias: rm); double confirmation (yes/no + type the zone name) unless --force; cleans up a stale .bunny/storage.json │ │ ├── credentials [zone] [--format rclone|aws|s3cmd|env] [--read-only] [--show-secret] (alias: creds) -│ │ │ S3 credentials for the zone (name = access key, password = secret); --format emits tool config, else table/--output json; table masks the secret unless --show-secret +│ │ │ S3 credentials for the zone (name = access key, password = secret); --format emits tool config, else table/--output json; table and JSON mask the secret unless --show-secret │ │ └── domains (canonical; alias: hostnames) custom domains on the zone's pull zone; mounts core/hostnames createHostnamesCommands; resolver maps the storage zone (positional, else linked zone, else picker) to its linked pull zone │ ├── files (canonical; aliases: file) [--zone|-z] defaults to the linked zone on every file command │ │ ├── list [path] [--zone] (alias: ls) List files in a directory (trailing slash on path) diff --git a/packages/cli/README.md b/packages/cli/README.md index 535acce..66e875a 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -477,7 +477,7 @@ Positional value ordering for `record add` follows the record type: `A`/`AAAA`/` > **Experimental**: hidden from `--help` and the landing page while it stabilizes. -Manage Edge Storage through two resource groups: **`bunny storage zones`** (the zone itself: create, list, inspect, update, delete; alias `zone`, plus hidden `bucket`/`buckets`) and **`bunny storage files`** (the files within a zone; alias `file`). Zone management uses the account API key; file operations use the zone's own password and a region-specific host, both resolved automatically from the zone. `zones` commands take the zone as an optional `[zone]` positional; `files` commands take it as the `--zone`/`-z` flag (their positional is the file path). Either accepts the zone name or its numeric ID. When the zone is omitted it resolves from the directory's linked zone (`bunny storage link`, stored in `.bunny/storage.json`), then an interactive picker, which offers to link the directory to the picked zone. +Manage Edge Storage through two resource groups: **`bunny storage zones`** (the zone itself: create, list, inspect, update, delete; alias `zone`, plus hidden `bucket`/`buckets`) and **`bunny storage files`** (the files within a zone; alias `file`). Zone management uses the account API key; file operations use the zone's own password and a region-specific host, both resolved automatically from the zone. `zones` commands take the zone as an optional `[zone]` positional; `files` commands take it as the `--zone`/`-z` flag (their positional is the file path). Either accepts the zone name or its numeric ID. When the zone is omitted it resolves from the directory's linked zone (`bunny storage link`, stored in `.bunny/storage.json`), then an interactive picker, which offers to link the directory to the picked zone (except on destructive commands). Non-interactive runs (`--output json`, no TTY, or `--force`) error instead of prompting; pass a zone or link the directory. A storage zone only holds files; a **pull zone** is what serves them on the web. `zones add` offers to create one (origin set to the new storage zone) and then to add a custom domain, or pass `--pull-zone`/`--domain` to do it non-interactively. Custom domains live on the pull zone and are managed with `bunny storage zones domains`. diff --git a/packages/cli/src/commands/storage/file/remove.ts b/packages/cli/src/commands/storage/file/remove.ts index 39b3f41..f8dfb8a 100644 --- a/packages/cli/src/commands/storage/file/remove.ts +++ b/packages/cli/src/commands/storage/file/remove.ts @@ -60,9 +60,10 @@ export const storageFileRemoveCommand = defineCommand({ const config = resolveConfig(profile, apiKey, verbose); const client = createCoreClient(clientOptions(config, verbose)); + // Destructive: --force must not silently delete from a picked zone, and no link offer. const zone = await resolveStorageZoneInteractive(client, ref, { output, - offerLink: true, + force, }); const connection = connectStorageZone(zone); diff --git a/packages/cli/src/commands/storage/zone/hostnames/index.ts b/packages/cli/src/commands/storage/zone/hostnames/index.ts index 30d7b33..af97ec1 100644 --- a/packages/cli/src/commands/storage/zone/hostnames/index.ts +++ b/packages/cli/src/commands/storage/zone/hostnames/index.ts @@ -27,7 +27,7 @@ function resolvePullZoneId(zone: StorageZoneModel, flag?: number): number { if (zones.length === 0) { throw new UserError( `Storage zone ${zone.Name} has no pull zone.`, - 'Create one with "bunny storage zones add --pull-zone".', + "The CLI only creates one during `zones add --pull-zone`; for an existing zone, create a pull zone with it as the origin in the dashboard (https://dash.bunny.net/cdn/add).", ); } diff --git a/packages/cli/src/commands/storage/zone/remove.ts b/packages/cli/src/commands/storage/zone/remove.ts index 923c90e..3ec4f57 100644 --- a/packages/cli/src/commands/storage/zone/remove.ts +++ b/packages/cli/src/commands/storage/zone/remove.ts @@ -41,7 +41,11 @@ export const storageZoneRemoveCommand = defineCommand({ const config = resolveConfig(profile, apiKey, verbose); const client = createCoreClient(clientOptions(config, verbose)); - const zone = await resolveStorageZoneInteractive(client, ref, { output }); + // Destructive: --force must not silently delete a picked zone, so it disables the picker. + const zone = await resolveStorageZoneInteractive(client, ref, { + output, + force, + }); const confirmed = await confirm( `Delete storage zone ${zone.Name} and all ${zone.FilesStored ?? 0} file(s)? This cannot be undone.`, diff --git a/skills/bunny-cli/references/storage.md b/skills/bunny-cli/references/storage.md index 117c4b5..1413b41 100644 --- a/skills/bunny-cli/references/storage.md +++ b/skills/bunny-cli/references/storage.md @@ -140,7 +140,7 @@ eval "$(bunny storage zones credentials my-zone --format env)" # AWS_* env var ## `bunny storage zones domains`: Custom domains -Hidden alias: `hostnames`. Domains attach to the storage zone's **pull zone**; if the zone has none, create one with `zones add --pull-zone`. When the zone has multiple pull zones, pass `--pull-zone ` to choose. +Hidden alias: `hostnames`. Domains attach to the storage zone's **pull zone**. The CLI only creates that pull zone during zone creation (`zones add --pull-zone`); for an existing zone without one, create a pull zone with the storage zone as its origin in the dashboard (or `bunny api POST /pullzone --body '{"Name":"...","StorageZoneId":,"OriginType":2}'`). When the zone has multiple pull zones, pass `--pull-zone ` to choose. ```bash bunny storage zones domains list my-zone From 60da2affa471237f521be2281d18a0aa054c8137 Mon Sep 17 00:00:00 2001 From: jamie-at-bunny Date: Tue, 28 Jul 2026 10:07:19 +0100 Subject: [PATCH 6/6] fix(storage): --force disables the zone picker on destructive removes --- .changeset/storage-force-picker.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/storage-force-picker.md diff --git a/.changeset/storage-force-picker.md b/.changeset/storage-force-picker.md new file mode 100644 index 0000000..b6b6d22 --- /dev/null +++ b/.changeset/storage-force-picker.md @@ -0,0 +1,5 @@ +--- +"@bunny.net/cli": patch +--- + +storage: `--force` on `zones remove` and `files remove` now errors without an explicit or linked zone instead of opening the picker