Skip to content

Fix/dns argument handling#116

Merged
jamie-at-bunny merged 4 commits into
mainfrom
fix/dns-argument-handling
Jul 9, 2026
Merged

Fix/dns argument handling#116
jamie-at-bunny merged 4 commits into
mainfrom
fix/dns-argument-handling

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 9, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 528a73b

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

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Can't wait for the next one!

Reviewed commit: 923899f6b2

ℹ️ 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".

@jamie-at-bunny jamie-at-bunny merged commit aaba878 into main Jul 9, 2026
2 checks passed
@jamie-at-bunny jamie-at-bunny deleted the fix/dns-argument-handling branch July 9, 2026 13:48
@github-actions github-actions Bot mentioned this pull request Jul 9, 2026
@greptile-apps

greptile-apps Bot commented Jul 9, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes argument-handling across all DNS commands: it stops interactive prompts from firing in non-TTY/JSON output contexts by centralising the TTY check in a new isInteractive(output) helper, adds per-type positional-argument validation (rejectExtraValues) to the add command, introduces an interactive field editor for the update command, and simplifies dns zones link by delegating to the existing resolveZoneInteractive with the new ignoreManifest flag.

  • isInteractive helper (core/ui.ts): single source of truth that checks output !== "json" + both stdin.isTTY and stdout.isTTY; replaces six different per-file inline expressions that each checked a different combination.
  • add command: rejectExtraValues now enforces the exact positional arity for each record type, and the catch-and-prompt fallback means omitting values in an interactive session triggers a guided prompt rather than a bare error.
  • update command: new promptFieldChanges multiselect lets users pick and edit individual fields without needing to remember flag names; resolveRecordInteractive and resolveZoneInteractive both receive output so they error cleanly in non-interactive contexts.

Confidence Score: 4/5

Safe to merge; the two findings are confined to the new interactive update editor and affect only UX, not correctness of the non-interactive path.

The core fixes — centralising the interactivity check, rejecting extra positional values, and threading output into resolvers — are clean and consistent. The new interactive field editor in update.ts has a gap for PULLZONE and SCRIPT record types: it surfaces a Value field that doesn't map to the right API field for those types, and it has no path to change the PullZoneId or ScriptId. Additionally, the CAA Tag picker doesn't pre-select the existing value. Neither of these blocks users of the non-interactive flag-based path.

packages/cli/src/commands/dns/record/update.ts — the promptFieldChanges function needs type-aware field filtering for PULLZONE/SCRIPT records.

Important Files Changed

Filename Overview
packages/cli/src/core/ui.ts Adds the shared isInteractive(output) helper that gates all interactive prompts; checks both stdin and stdout TTY.
packages/cli/src/commands/dns/interactive.ts Adds ignoreManifest flag and isInteractive guard to resolveZoneInteractive; passes output to resolveRecordInteractive; removes stale json-output guard from offerLink.
packages/cli/src/commands/dns/record/add.ts Adds rejectExtraValues to police per-type positional argument counts; falls back to promptRecord when buildRecord throws and no values were given in an interactive session.
packages/cli/src/commands/dns/record/update.ts Adds interactive field editor (promptFieldChanges) for the update command; does not filter available fields by record type, so PULLZONE/SCRIPT records show misleading Value field without a path to update PullZoneId/ScriptId.
packages/cli/src/commands/dns/zone/link.ts Replaces ~40 lines of inline zone-picker logic with a single resolveZoneInteractive call using ignoreManifest: true; no behaviour changes for the resolved zone writing.
packages/cli/src/commands/dns/record-types.ts Exports CAA_TAGS constant, centralising the three valid CAA tag values shared between add.ts and update.ts.
packages/cli/src/commands/dns/scripts/attach.ts Replaces inline output !== json && process.stdout.isTTY check with shared isInteractive(output); renames local shadow variable.
packages/cli/src/commands/dns/zone/remove.ts Passes { output } to resolveZoneInteractive so the non-interactive guard fires correctly when no domain argument is given.
packages/cli/src/commands/dns/record/remove.ts Threads output into resolveRecordInteractive so the non-interactive guard works for the remove flow.
packages/cli/src/core/hostnames/commands.ts Replaces inline args.output !== json && process.stdout.isTTY check with isInteractive(args.output).

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Command invoked] --> B{is ref given?}
    B -- Yes --> C[resolveZone via API]
    C --> D[Return zone]
    B -- No --> E{ignoreManifest?}
    E -- No --> F{manifest.id set?}
    F -- Yes --> G[Load linked zone]
    G --> D
    F -- No --> H{isInteractive output?}
    E -- Yes --> H
    H -- No --> I[throw UserError: zone required]
    H -- Yes --> J[Fetch all zones + show picker]
    J --> K{offerLink?}
    K -- Yes --> L[maybeLinkZone]
    L --> D
    K -- No --> D

    subgraph update.ts
        M[dns records update] --> N{hasFieldFlags?}
        N -- Yes --> O[Apply flag overrides to API call]
        N -- No --> P{isInteractive?}
        P -- No --> Q[throw UserError: no changes]
        P -- Yes --> R[promptFieldChanges]
        R --> O
    end

    subgraph add.ts
        S[dns records add] --> T{args.type given?}
        T -- No --> U{isInteractive?}
        U -- No --> V[throw UserError: type required]
        U -- Yes --> W[Full interactive wizard]
        T -- Yes --> X[buildRecord]
        X -- Success --> Y[writeAndReport]
        X -- UserError + no values + interactive --> Z[promptRecord]
        Z --> Y
    end
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[Command invoked] --> B{is ref given?}
    B -- Yes --> C[resolveZone via API]
    C --> D[Return zone]
    B -- No --> E{ignoreManifest?}
    E -- No --> F{manifest.id set?}
    F -- Yes --> G[Load linked zone]
    G --> D
    F -- No --> H{isInteractive output?}
    E -- Yes --> H
    H -- No --> I[throw UserError: zone required]
    H -- Yes --> J[Fetch all zones + show picker]
    J --> K{offerLink?}
    K -- Yes --> L[maybeLinkZone]
    L --> D
    K -- No --> D

    subgraph update.ts
        M[dns records update] --> N{hasFieldFlags?}
        N -- Yes --> O[Apply flag overrides to API call]
        N -- No --> P{isInteractive?}
        P -- No --> Q[throw UserError: no changes]
        P -- Yes --> R[promptFieldChanges]
        R --> O
    end

    subgraph add.ts
        S[dns records add] --> T{args.type given?}
        T -- No --> U{isInteractive?}
        U -- No --> V[throw UserError: type required]
        U -- Yes --> W[Full interactive wizard]
        T -- Yes --> X[buildRecord]
        X -- Success --> Y[writeAndReport]
        X -- UserError + no values + interactive --> Z[promptRecord]
        Z --> Y
    end
Loading

Fix All in Claude Code

Reviews (1): Last reviewed commit: "style: reformat dns.test.ts after placeh..." | Re-trigger Greptile

Comment on lines +71 to +136
async function promptFieldChanges(
existing: DnsRecordModel,
): Promise<Partial<UpdateDnsRecordModel>> {
const fields: { title: string; value: PromptableField | "Disabled" }[] = [
{ title: `Value (${existing.Value ?? ""})`, value: "Value" },
{ title: `Name (${recordName(existing.Name)})`, value: "Name" },
{ title: `TTL (${existing.Ttl ?? "default"})`, value: "Ttl" },
];
if (existing.Type === RECORD_TYPES.MX || existing.Type === RECORD_TYPES.SRV)
fields.push({
title: `Priority (${existing.Priority ?? 0})`,
value: "Priority",
});
if (existing.Type === RECORD_TYPES.SRV) {
fields.push({ title: `Weight (${existing.Weight ?? 0})`, value: "Weight" });
fields.push({ title: `Port (${existing.Port ?? 0})`, value: "Port" });
}
if (existing.Type === RECORD_TYPES.CAA) {
fields.push({ title: `Flags (${existing.Flags ?? 0})`, value: "Flags" });
fields.push({ title: `Tag (${existing.Tag ?? ""})`, value: "Tag" });
}
fields.push({
title: `Comment (${existing.Comment || "none"})`,
value: "Comment",
});
fields.push({
title: existing.Disabled ? "Enable the record" : "Disable the record",
value: "Disabled",
});

const { picked } = await prompts({
type: "multiselect",
name: "picked",
message: "Fields to change:",
choices: fields,
hint: "Space to toggle, Enter to confirm",
instructions: false,
});
if (!picked || picked.length === 0) {
throw new UserError("No changes requested.");
}

const changes: Partial<UpdateDnsRecordModel> = {};
for (const field of picked as (PromptableField | "Disabled")[]) {
if (field === "Disabled") {
changes.Disabled = !existing.Disabled;
continue;
}
const spec = FIELD_PROMPTS[field];
const { value } = await prompts({
type: spec.kind === "tag" ? "select" : spec.kind,
name: "value",
message: spec.message,
...(spec.kind === "tag"
? { choices: CAA_TAGS.map((t) => ({ title: t, value: t })) }
: {
initial:
(field === "Name"
? recordName(existing.Name)
: existing[field]) ?? undefined,
}),
});
if (value === undefined) throw new UserError(`${field} is required.`);
changes[field] = field === "Name" && value === "@" ? "" : value;
}
return changes;

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 Interactive update offers misleading fields for PULLZONE/SCRIPT records

promptFieldChanges always includes Value (and never PullZoneId or ScriptId) regardless of record type. For a PULLZONE or SCRIPT type record the meaningful editable field is PullZoneId/ScriptId, not Value. If a user selects the "Value" option in the picker and enters text, the Object.assign at line 232 sets body.Value on a record that was never supposed to carry that field, while the PullZoneId/ScriptId is absent from the seeded body (the accelerated-zone guard won't match these types). The result sent to the API has no link ID and a spurious Value, which is at minimum confusing and potentially corrupts the record depending on API behaviour.

Fix in Claude Code

Comment on lines +124 to +126
...(spec.kind === "tag"
? { choices: CAA_TAGS.map((t) => ({ title: t, value: t })) }
: {

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 The Tag field for CAA records is displayed with the current value in the multiselect list title (Tag (issue)), but the subsequent select prompt doesn't set an initial to pre-select it. The user has to remember the value they just saw and pick it again manually. Passing the existing tag as the initial selection index would make the UX consistent with the numeric fields.

Suggested change
...(spec.kind === "tag"
? { choices: CAA_TAGS.map((t) => ({ title: t, value: t })) }
: {
...(spec.kind === "tag"
? {
choices: CAA_TAGS.map((t) => ({ title: t, value: t })),
initial: CAA_TAGS.indexOf(
(existing as { Tag?: string }).Tag as (typeof CAA_TAGS)[number],
),
}
: {

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code

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