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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tame-pears-lick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@bunny.net/cli": minor
---

feat(storage): add storage zone and file commands with S3-compatible credentials
51 changes: 49 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,11 +161,11 @@ bunny-cli/
│ │ ├── format.test.ts # Tests for format utilities
│ │ ├── hostnames/ # Reusable pull-zone hostname feature (mounted by scripts; apps next)
│ │ │ ├── index.ts # Re-exports client helpers, DNS/flow helpers + createHostnamesCommands
│ │ │ ├── client.ts # hostnameUrl(), normalizeHostname(), addHostname(), fetchPullZoneHostnames(), enableSsl() + Hostname/ResolvedPullZone types
│ │ │ ├── client.ts # hostnameUrl(), normalizeHostname(), addHostname(), fetchPullZoneHostnames(), enableSsl(), createPullZone() (storage-zone origin) + Hostname/ResolvedPullZone types
│ │ │ ├── client.test.ts # Tests for hostnameUrl() scheme logic
│ │ │ ├── dns.ts # dnsPointsAt()/anyResolverPointsAt(): DNS checks (CNAME or flattened A records) via system + public (1.1.1.1/8.8.8.8) resolvers, injectable for tests
│ │ │ ├── dns.test.ts # Tests for DNS matching + multi-resolver checks with fake resolvers
│ │ │ ├── flow.ts # offerDnsWaitAndSsl(): poll DNS + opportunistically attempt SSL issuance (~30s) since bunny's resolvers decide validation; printSslHint(). dnsAlreadyLive skips the poll (Bunny DNS record already live). offerBunnyDnsThenSsl() takes an optional onBunnyDnsZone(zone) callback fired when the hostname is on Bunny DNS (lets the command layer link the directory)
│ │ │ ├── flow.ts # offerDnsWaitAndSsl(): poll DNS + opportunistically attempt SSL issuance (~30s) since bunny's resolvers decide validation; printSslHint(). dnsAlreadyLive skips the poll (Bunny DNS record already live). offerBunnyDnsThenSsl() takes an optional onBunnyDnsZone(zone) callback fired when the hostname is on Bunny DNS (lets the command layer link the directory). setupHostname(): resource-agnostic add-hostname -> DNS -> SSL orchestration (caller supplies sslHint/retryHint); used by scripts setupCustomDomain and storage zone add
│ │ │ ├── bunny-dns.ts # findBunnyDnsZone()/offerBunnyDnsRecord(): detect a hostname inside an account Bunny DNS zone, then add/repoint a PullZone record (always confirmed) so SSL can issue immediately
│ │ │ ├── bunny-dns.test.ts # Tests for longest-suffix zone matching + record-name derivation with a fake core client
│ │ │ └── commands.ts # createHostnamesCommands(): add/ssl/list/remove factory parameterized by a pull-zone resolver
Expand Down Expand Up @@ -304,6 +304,33 @@ bunny-cli/
│ │ │ ├── index.ts # defineNamespace("logging", ...)
│ │ │ ├── enable.ts # Enable DNS query logging (optional IP anonymization)
│ │ │ └── disable.ts # Disable DNS query logging (with confirmation)
│ │ ├── storage/ # Experimental (hidden from help and landing page)
│ │ │ ├── index.ts # defineNamespace("storage", ...): registers zone + file groups + link + regions + docs (+ hidden bucket aliases)
│ │ │ ├── api.ts # CoreClient type, fetchStorageZones/fetchStorageZone, resolveStorageZone (name-or-ID to zone, re-fetched by ID), toSafeStorageZone (strips Password/ReadOnlyPassword; used by every command that emits a raw zone as JSON: show/list/add)
│ │ │ ├── constants.ts # STORAGE_REGIONS (from SDK enum; /storagezone/regions API endpoint is not reliable) + replicationChoices/normalizeReplicationRegions (replication uses the same regions minus the primary; the SDK file ZoneSchema is the physical footprint, NOT the create input) + STORAGE_MANIFEST/StorageZoneManifest (.bunny/storage.json, written by storage link)
│ │ │ ├── interactive.ts # resolveStorageZoneInteractive: explicit name/ID arg → linked manifest (.bunny/storage.json, fetched by ID even when non-interactive) → zone picker
│ │ │ ├── link.ts # Link the current directory to a storage zone (.bunny/storage.json); bunny storage link [zone]
│ │ │ ├── files-api.ts # Adapter over @bunny.net/storage-sdk: connectStorageZone (zone → SDK connection, Region→StorageRegion enum + password), listFiles/uploadFile/downloadFile/deleteFile (deleteFile translates the SDK's boolean return into a UserError)
│ │ │ ├── files-api.test.ts # Tests for region mapping + delete error translation (NOT the SDK's URL building)
│ │ │ ├── s3.ts # S3 (closed preview): isS3Enabled (StorageZoneType===1), s3Endpoint (<region>-s3.storage.bunnycdn.com), s3Credentials (name=access key, password=secret), renderS3ToolConfig (rclone/aws/s3cmd/env formatters)
│ │ │ ├── s3.test.ts # Tests for S3 derivation + tool-config formatters
│ │ │ ├── docs.ts # Open storage docs (bunny storage docs)
│ │ │ ├── regions.ts # List available storage regions (bunny storage regions)
│ │ │ ├── zone/ # `bunny storage zones` (canonical: zones; aliases: zone; hidden: bucket, buckets)
│ │ │ │ ├── index.ts # defineNamespace("zones", ...) + storageZoneHiddenAliases (bucket/buckets)
│ │ │ │ ├── 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
│ │ │ │ ├── update.ts # Update zone settings (custom 404, rewrite 404->200, replication); interactive pre-filled editor when no flags, non-interactive under --output json
│ │ │ │ ├── remove.ts # Delete a storage zone and its files (alias: rm)
│ │ │ │ └── hostnames/index.ts # Mounts core/hostnames createHostnamesCommands as "storage zone domains" (alias hostnames); resolver maps a storage zone (name/ID positional, --pull-zone) to its linked pull zone
│ │ │ └── file/ # `bunny storage files` (canonical: files; aliases: file); zone is the --zone/-z flag (defaults to linked zone), the positional is the file/path
│ │ │ ├── index.ts # defineNamespace("files", ...)
│ │ │ ├── list.ts # List files in a directory (alias: ls; directories first; [path] positional, --zone flag)
│ │ │ ├── upload.ts # Upload a local file (<file> positional, --zone, --to, --checksum streams a SHA256, --content-type)
│ │ │ ├── download.ts # Download a file to disk (<path> positional, --zone, --out)
│ │ │ └── remove.ts # Delete a file or directory (alias: rm; <path> positional, --zone, trailing slash = recursive)
│ │ ├── registries/
│ │ │ ├── index.ts # Manual CommandModule (not defineNamespace) — default handler runs list
│ │ │ ├── list.ts # List container registries
Expand Down Expand Up @@ -360,6 +387,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/<spec>.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`).
- **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 <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`).
Expand Down Expand Up @@ -883,6 +911,25 @@ bunny
│ ├── enable [domain] [--anonymize-ip] [--anonymization onedigit|drop]
│ │ 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.
│ ├── zones (canonical; aliases: zone; hidden: bucket, buckets)
│ │ ├── list List all storage zones (alias: ls)
│ │ ├── add [name] [--region] [--replication] [--pull-zone] [--pull-zone-name] [--domain] 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; non-interactive under --output json)
│ │ ├── show [zone] Show zone details (region, replication, hostname, usage)
│ │ ├── update [zone] [--custom-404-path] [--rewrite-404-to-200] [--replication] Update zone settings (edits interactively pre-filled when no flags; non-interactive under --output json)
│ │ ├── remove [zone] [--force] Delete a storage zone and its files (alias: rm)
│ │ ├── 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
│ │ └── domains (canonical; alias: hostnames) custom domains on the zone's pull zone; mounts core/hostnames createHostnamesCommands; resolver maps the storage zone 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)
│ │ ├── upload <file> [--zone] [--to] [--checksum] [--content-type] Upload a local file
│ │ ├── download <path> [--zone] [--out] Download a file
│ │ └── remove <path> [--zone] [--force] (alias: rm) Delete a file or directory (trailing slash = recursive)
│ ├── link [zone] Link the current directory to a storage zone (.bunny/storage.json); interactive picker when omitted
│ ├── regions List available storage regions (replication uses the same set minus the primary)
│ └── docs Open storage documentation in browser
├── db
│ ├── create [--name] [--primary] [--replicas] [--storage-region]
│ │ Create a new database
Expand Down
33 changes: 20 additions & 13 deletions bun.lock

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

Loading