Skip to content

fix(storage): convention parity#120

Merged
jamie-at-bunny merged 4 commits into
mainfrom
fix/storage-convention-parity
Jul 20, 2026
Merged

fix(storage): convention parity#120
jamie-at-bunny merged 4 commits into
mainfrom
fix/storage-convention-parity

Conversation

@jamie-at-bunny

Copy link
Copy Markdown
Member

No description provided.

@bogdan-at-bunny

Copy link
Copy Markdown

@codex review

@changeset-bot

changeset-bot Bot commented Jul 10, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ed84565

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 6 packages
Name Type
@bunny.net/cli Patch
@bunny.net/cli-linux-x64 Patch
@bunny.net/cli-linux-arm64 Patch
@bunny.net/cli-darwin-x64 Patch
@bunny.net/cli-darwin-arm64 Patch
@bunny.net/cli-windows-x64 Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@jamie-at-bunny
jamie-at-bunny changed the base branch from main to fix/storage-behavioral-consistency July 10, 2026 11:49

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9ca267cb30

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +36 to +39
if (!force) {
const confirmed = await confirm(
`Unlink from ${existing.name ?? existing.id}?`,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid prompting in JSON unlink mode

When --output json is used and .bunny/storage.json exists, this branch still opens a confirmation prompt unless --force is also supplied. In CI or other JSON consumers this can block or produce the plain-text Unlink cancelled. success path instead of a JSON payload, violating the command's JSON-output contract; require --force/return a JSON error before calling confirm in JSON mode.

Useful? React with 👍 / 👎.

@greptile-apps

greptile-apps Bot commented Jul 10, 2026

Copy link
Copy Markdown

Greptile Summary

This PR aligns the storage commands around a consistent set of conventions: a new unlink command mirrors link, the zone picker now offers to link the directory after a selection (offerLink), zone remove gains a type-to-confirm second step plus stale-manifest cleanup, replication confirmation defaults to no, and all command-path hints are corrected to the canonical plural storage zones … form. The shared writeStorageManifest / ignoreManifest refactor in interactive.ts removes duplicated picker logic from link.ts.

  • New unlink command (unlink.ts): removes .bunny/storage.json, guarded by isInteractive in non-TTY mode so it errors rather than silently no-ops without --force.
  • offerLink in zone picker (interactive.ts): after a user picks a zone interactively, they are offered to link the directory; the offer is skipped when the zone was resolved from an explicit ref or the existing manifest, and never fires non-interactively.
  • Double confirmation for zone deletion (zone/remove.ts): a yes/no prompt is followed by a type-to-confirm step unless --force; after deletion, a stale .bunny/storage.json pointing at the removed zone is automatically cleaned up.

Confidence Score: 5/5

Safe to merge; changes are additive convention fixes with no API contract breakage.

All changed code paths are interactive CLI flows. The destructive zone remove command now has stronger confirmation guards (type-to-confirm + stale manifest cleanup), the new unlink command correctly errors in non-interactive mode, and the offerLink offer only fires after the picker which already requires a TTY. The one small gap — unlinked absent from JSON output — is a cosmetic completeness issue with no impact on correctness.

packages/cli/src/commands/storage/zone/remove.ts — JSON output missing the unlinked field; packages/cli/src/commands/storage/file/remove.ts — offerLink: true on a destructive command (noted in previous review round)

Important Files Changed

Filename Overview
packages/cli/src/commands/storage/interactive.ts Refactored signature to opts object, added offerLink/ignoreManifest options, extracted writeStorageManifest helper, and added maybeLinkZone to offer directory linking after zone picker; logic is sound
packages/cli/src/commands/storage/unlink.ts New command to remove .bunny/storage.json; correctly guards non-interactive mode with isInteractive check before prompting, and emits structured JSON when --output json is set
packages/cli/src/commands/storage/zone/remove.ts Adds type-to-confirm second confirmation and stale-manifest cleanup after zone deletion; JSON output omits the unlinked field, so --output json callers cannot determine if the manifest was cleaned up
packages/cli/src/commands/storage/link.ts Simplified to delegate to resolveStorageZoneInteractive with ignoreManifest: true, eliminating duplicated picker logic and sharing writeStorageManifest
packages/cli/src/commands/storage/zone/update.ts Updated to new opts signature and added offerLink; --custom-404-path "" now correctly sends null to clear the field via the falsy-coalescing pattern
packages/cli/src/commands/storage/constants.ts Changed replication confirmation default from true (yes) to false (no) — more conservative since replication additions are permanent
packages/cli/src/commands/storage/zone/add.ts Changed replication-decline to log+return instead of throw UserError, and corrected command hints from singular zone to plural zones
packages/cli/src/commands/storage/zone/hostnames/index.ts Updated to new opts signature with offerLink, corrected commandPath and error hint from storage zone to storage zones (plural)
packages/cli/src/commands/storage/file/remove.ts Updated to new opts signature; passes offerLink: true on a destructive (delete) command — see existing outside-diff comment about this convention mismatch
packages/cli/src/commands/storage/index.ts Registers storageUnlinkCommand in the namespace alongside the existing link command

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A([resolveStorageZoneInteractive]) --> B{ref provided?}
    B -- yes --> C[resolveStorageZone by name/ID]
    C --> D([return zone — no link offer])

    B -- no --> E{ignoreManifest?}
    E -- no --> F{manifest.id present?}
    F -- yes --> G[fetchStorageZone by manifest ID]
    G --> D

    F -- no --> H{force OR not interactive?}
    E -- yes --> H
    H -- yes --> ERR([UserError: zone required])
    H -- no --> I[fetchStorageZones → picker prompt]
    I --> J{user cancelled?}
    J -- yes --> ERR2([UserError: zone required])
    J -- no --> K[fetchStorageZone by picked ID]
    K --> L{offerLink?}
    L -- no --> M([return zone])
    L -- yes --> N[confirm: Link this directory?]
    N -- yes --> O[writeStorageManifest]
    O --> M
    N -- no --> M
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A([resolveStorageZoneInteractive]) --> B{ref provided?}
    B -- yes --> C[resolveStorageZone by name/ID]
    C --> D([return zone — no link offer])

    B -- no --> E{ignoreManifest?}
    E -- no --> F{manifest.id present?}
    F -- yes --> G[fetchStorageZone by manifest ID]
    G --> D

    F -- no --> H{force OR not interactive?}
    E -- yes --> H
    H -- yes --> ERR([UserError: zone required])
    H -- no --> I[fetchStorageZones → picker prompt]
    I --> J{user cancelled?}
    J -- yes --> ERR2([UserError: zone required])
    J -- no --> K[fetchStorageZone by picked ID]
    K --> L{offerLink?}
    L -- no --> M([return zone])
    L -- yes --> N[confirm: Link this directory?]
    N -- yes --> O[writeStorageManifest]
    O --> M
    N -- no --> M
Loading

Reviews (3): Last reviewed commit: "fix(storage): close Greptile P2 gaps in ..." | Re-trigger Greptile

Comment thread packages/cli/src/commands/storage/zone/remove.ts
Comment thread packages/cli/src/commands/storage/unlink.ts
Base automatically changed from fix/storage-behavioral-consistency to main July 10, 2026 14:58
…tions

- Use the shared isInteractive() gate (stdin + stdout TTY, json-aware)
  instead of stdout-only checks in the zone resolver, link, and zone add
- zones update: require field flags whenever non-interactive (json,
  non-TTY, or --force) instead of only under --output json, and abort
  the whole interactive edit on a mid-flow cancel rather than applying
  partial answers
- zones update --force now matches zones add: skip prompts and
  confirmations, use flag values only
- storage zone domains commands now resolve the zone like every other
  storage command: explicit ref, then linked zone, then picker
- zones remove: double confirmation (yes/no, then type the zone name)
  unless --force, matching db and scripts delete; also removes a stale
  .bunny/storage.json that pointed at the deleted zone
- New storage unlink command mirroring dns zones unlink
- resolveStorageZoneInteractive takes DNS-style opts (output, force,
  offerLink, ignoreManifest); the picker now offers to link the
  directory on non-destructive commands, and link reuses the resolver
  instead of duplicating it
- The zone-required error now hints at bunny storage link
- Replication confirmation defaults to no: adding a replica is
  permanent and adds cost
- Hints and the domains factory commandPath use the canonical plural
  (storage zones add / storage zones domains)
- Declining the replication confirm in zones add prints Cancelled. and
  exits cleanly instead of throwing, matching the other declines
- --custom-404-path "" clears the custom 404 (null), matching the
  prompt's blank-for-none behavior
@jamie-at-bunny
jamie-at-bunny force-pushed the fix/storage-convention-parity branch from 9ca267c to d2bd230 Compare July 20, 2026 06:06
- remove: compare the type-to-confirm value against zone.Name ?? "" so a
  Ctrl+C (undefined) can never match an undefined zone name and skip the guard
- unlink: throw when run non-interactively without --force instead of
  silently cancelling and leaving the manifest, matching the other commands
@jamie-at-bunny
jamie-at-bunny merged commit 3c3373a into main Jul 20, 2026
2 checks passed
@jamie-at-bunny
jamie-at-bunny deleted the fix/storage-convention-parity branch July 20, 2026 09:45
@github-actions github-actions Bot mentioned this pull request Jul 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants