Skip to content

chore(cli): replace the huh form library with a built-in forms package - #92

Merged
ThomasRooney merged 10 commits into
mainfrom
chore/cli-remove-huh
Oct 8, 2026
Merged

ThomasRooney merged 10 commits into
mainfrom
chore/cli-remove-huh

Conversation

@ThomasRooney

@ThomasRooney ThomasRooney commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

Why

Generated CLIs depended on charm.land/huh/v2 for their guided forms (configure, auth login, and the prompts for missing required inputs), yet used only a small part of it. Generated CLIs now carry a small forms package built around exactly those questions, which removes the dependency and its transitive modules.

What changed

  • New internal/forms package, generated when interactiveAuth or interactiveMode is enabled: forms.go (about 590 lines) shipped without tests. One Field type covers text input, secret input, multi-line JSON, single choice and yes/no. One Form model steps through pages of fields, built directly on bubbletea, bubbles (textinput, textarea) and lipgloss, using the CLI's theme colours.
  • API: forms.NewInput(&v).Title(…).Description(…).Placeholder(…).Password().Validate(fn), forms.NewText(&v), forms.NewSelect(&v, forms.NewOption(label, value)…), forms.NewConfirm(&b), forms.NewPage(title, fields…), forms.New(pages…).Accessible(b).IO(in, out).Run().
  • Keys:
    • enter and tab move forward. enter on the last field submits; tab on the last field does nothing.
    • shift+tab moves back, across pages too.
    • ctrl+c cancels with user aborted.
    • Selects use up/down, j/k, ctrl+p/ctrl+n, home/end (g/G) and ctrl+u/ctrl+d half pages. They wrap around and show a window of options that fits the terminal height. Long and multi-line labels wrap and stay aligned.
    • Confirms toggle with left/right/h/l, and y/n answers and moves on.
    • Text fields insert a newline with alt+enter or ctrl+j.
    • Pages taller than the terminal scroll to keep the focused field in view, with the page title kept alongside the first field. Selects size their option window to the space left after their heading. Narrow terminals (down to 8 columns) don't overflow. Dumb terminals always get line prompts.
    • Validation runs when moving forward and shows the error under the field.
  • Line-prompt (accessible) mode:
    • Fields are asked in order. A blank answer keeps the current value, non-blank answers are stored exactly as typed, and end of input stops prompting. Other read errors are returned from Run.
    • Prompts use the command's streams: configure and auth login write to stdout, and prompts for missing inputs write to stderr.
    • All answers go through one buffered reader, so piped input reaches every field.
    • Secrets are read without echo at a terminal, and from the next line otherwise.
  • Template call sites (auth.go, configure.go, interactive.go, includes/security.ts) use the new API. The per-file theme and width helpers are removed, as are the duplicated security-field emitters.
  • Dependencies: generated CLIs drop huh, catppuccin/go, hashstructure, go-humanize, x/exp/ordered and x/exp/strings. bubbletea and bubbles become direct dependencies whenever either interactive option is enabled.
  • Regenerated the review CLI (zSDKs/sdk-cli) and templates/perms.go, and added a patch changeset for cli.

Breaking for customised CLIs: custom code in the auth login #region custom-auth-* blocks that builds huh fields must move to the forms API above.

Testing

  • tests/primary/forms_test.go (33 tests) runs against the generated primary test CLI and isn't part of generated output. It mirrors the scenarios huh's own suite covers for these field types:

    • a multi-page walk-through with select validation
    • input typing, bound starting values, placeholders, password masking and paste
    • text newlines and paste
    • confirm toggles and y/Y/n/N
    • select cursor, wrap-around, home/end, half-page moves and multi-line labels
    • previous page, tab versus enter on the last field, and leading, middle and trailing empty pages
    • validation blocking forward moves only
    • titles and descriptions per field kind
    • rendering within the terminal width (including 8–20 columns), and reflow on resize
    • tall pages on short terminals in both directions, the page title with the first field, a field taller than the terminal, selects with headings on short terminals, and TERM=dumb
    • ctrl+c
    • line prompts for every field kind: defaults, invalid answers, surrounding whitespace, CRLF, piped multi-field input, end of input and read errors

    I checked the tests can fail by making deliberate behaviour-changing edits to forms.go: all 38 were caught.

  • Terminal matrix (out of tree). A configure-shaped form (8 credentials, global parameters, a select with a long label, a confirm) ran in a pseudo-terminal, with its output replayed into a terminal emulator of the same size. At every step the test checked that the user can see what they typed and that passwords stay masked, then checked every answer arrived:

    • sizes 8×4, 12×6, 16×5, 20×8, 30×10, 40×12, 60×15, 80×24, 100×30, 132×43, 200×50 and 300×80
    • shift+tab back up a page taller than the terminal
    • resizing mid-form to 40×12, 25×8, 200×60 and 80×24
    • TERM set to xterm-256color, xterm, vt100, screen and linux, plus NO_COLOR=1 and COLORTERM=truecolor
    • TERM=dumb
  • Out-of-tree pseudo-terminal smoke tests ran a built program through the interactive form (secret input, validation, the JSON newline, select, confirm, ctrl+c) and through line prompts at a terminal (secret not echoed).

  • TARGET=primary make test-cli: 1753 tests pass, including the forms suite, run with agent-detection environment variables unset.

  • TARGET=review make test-cli: 609 pass, 2 skipped.

  • Generated the review spec with each of the four interactiveAuth/interactiveMode combinations. Each builds and passes staticcheck, internal/forms contains only forms.go, and it and the charm modules appear only when an interactive option is enabled.

  • go generate ./..., prettier --check (3.0.3 from the lockfile) and make check-template-cli pass.

  • make lint steps 1–4 pass. The golangci-lint step didn't run locally because the installed binary was built with Go 1.25 and the repository targets Go 1.26.8.

Public-safety check

  • This change contains no credentials, customer documents, private repository URLs, private filesystem paths, or unredacted private logs.
  • Title, body, comments, and commit messages name no customers or customer-derived identifiers, private paths or trackers, or workflow provenance, and are understandable without private context (.claude/skills/public-repo-communication/SKILL.md).
  • Generated fixtures and review SDK changes are public-safe.
  • I reviewed git diff --check.

Generated CLIs built their guided forms (configure, auth login, and
missing-input prompts) on charm.land/huh/v2. They now generate
internal/forms, a reduced copy of huh v2.0.3 that keeps only what the
CLI calls: forms and groups, input, text, select and confirm fields,
the base theme, the keymap, and accessible line-prompt mode. File
pickers, multi-select, notes, spinners, dynamic *Func fields, extra
layouts and themes, and timeouts are dropped. The MIT notice is kept
in the package's doc.go.

Retained code is unchanged apart from the removed paths, with two
deliberate differences: accessible password input reads through
golang.org/x/term (already a dependency) instead of
charmbracelet/x/term, and program errors are wrapped as "form: ..."
instead of "huh: ...".

Generated CLIs drop huh, catppuccin, hashstructure, go-humanize and
the charmbracelet/x/exp modules. bubbletea and bubbles become direct
dependencies whenever interactiveAuth or interactiveMode is enabled.

Custom code in the auth login regions that calls huh must switch to
the forms package; the setter API is kept so the switch is a rename.
@ThomasRooney
ThomasRooney requested a review from a team as a code owner October 7, 2026 21:42

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 44 files

Shadow auto-approve: would not auto-approve because issues were found.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread templates/templates/cli/auxiliary/internal/forms/form.go.stmpl Outdated
Comment thread zSDKs/sdk-cli/internal/forms/field_text.go Outdated
Comment thread zSDKs/sdk-cli/internal/forms/accessible.go Outdated
Comment thread templates/templates/cli/auxiliary/internal/forms/form.go.stmpl Outdated
Comment thread zSDKs/sdk-cli/internal/forms/field_select.go Outdated
Comment thread templates/templates/cli/auth.go.stmpl Outdated
Comment thread zSDKs/sdk-cli/internal/forms/group.go Outdated
internal/forms is now a single generated file, about half its previous
size. Every call site rendered with the same theme, the terminal width,
and help hidden, so those become fixed behaviour of the package. The
theme, width and help options, and the copies of the theme and width
helpers in auth.go, configure.go and interactive.go, are removed.

Also removed because no generated code reaches them: select filtering,
the external-editor shortcut on text fields, inline layouts, custom
confirm labels, character limits, hidden groups, field keys, value
accessors, and the dark-background plumbing (the theme ignores it).
Select stores strings only, and Input.EchoMode(EchoModePassword) becomes
Input.Password().

Two key handling changes follow: "/" no longer opens a filter in
selects, and ctrl+e in an input moves to the end of the line instead of
being swallowed by an unused suggestion binding.

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 40 files (changes from recent commits).

Shadow auto-approve: would not auto-approve because issues were found.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread templates/templates/cli/auxiliary/internal/forms/forms.go.stmpl Outdated
internal/forms is rewritten around what generated CLIs ask for: pages
of text, secret, JSON, choice and yes/no fields. One Field type and one
Form model replace the per-field models, groups, viewports and themes
of the previous package, which was adapted from huh and is removed
along with its license notice.

The API binds each answer at construction (NewInput(&v), NewSelect(&v,
options...), NewConfirm(&b), NewText(&v)), groups fields with
NewPage(title, fields...), and runs with New(pages...).Accessible(b).Run().
Selects show a window of options sized to the terminal height.

Line-prompt mode now reads all answers through one buffered reader, so
piped input reaches every field instead of being consumed by the first,
and a secret read from a non-terminal is taken from the next line
rather than failing. Blank answers keep the current value and end of
input stops prompting, as before.

The package ships with unit tests covering navigation, validation,
selects, confirms, abort, and the line prompts.
@ThomasRooney ThomasRooney changed the title chore(cli): replace the huh form library with an in-repo forms package chore(cli): replace the huh form library with a built-in forms package Oct 7, 2026

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 12 files (changes from recent commits).

Shadow auto-approve: would not auto-approve because issues were found.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread templates/templates/cli/auxiliary/internal/forms/forms.go.stmpl Outdated
Comment thread zSDKs/sdk-cli/internal/forms/forms.go
Comment thread zSDKs/sdk-cli/internal/forms/forms.go Outdated
Comment thread zSDKs/sdk-cli/internal/forms/forms_test.go Outdated
Form fixes:
- Line prompts read and write through Form.IO, so configure and auth
  login use the command's streams; prompts for missing inputs write to
  stderr to keep stdout for command output.
- Read errors other than end of input are returned from Run instead of
  being treated as a finished form.
- Answers keep their surrounding whitespace; only blank answers fall
  back to the current value.
- Enter on the last field submits even when empty pages follow it.
- Page titles and option labels wrap to the terminal width, and
  multi-line option labels stay aligned under their first line.
- Selects window options by rendered height and support ctrl+d and
  ctrl+u half-page moves.

The tests move from internal/forms into tests/primary so they run
against the generated test CLI without shipping in generated output.
They cover the scenarios huh's own suite exercises for the fields used
here: multi-page walk-through with validation, input, password,
placeholder and paste, text with newlines, confirm keys, select
navigation including page moves and wrap-around, previous page,
titles and descriptions, width and resize, abort, and line prompts for
every field kind including defaults, invalid answers, piped multi-field
input, end of input and read errors.

@cubic-dev-ai cubic-dev-ai 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.

0 issues found across 13 files (changes from recent commits).

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Shadow auto-approve: would require human review. Replaces huh with a generated internal forms package and moves configure/auth call sites to it. The dismissed styling issue doesn't apply to single-select, but the PR breaks customized auth/configure regions that embedded huh fields; that breaking contract needs human sign-off.

View guided diff | Turn on auto-fix | Re-trigger cubic

Pages taller than the terminal now scroll: the form renders the window
of lines that contains the focused field, moving it as little as
possible, keeping the page title in view with the first field, and
showing the input end of a field that is itself taller than the
terminal. Previously the terminal showed only the bottom of the page, so
on an 80x24 terminal the first fields of a long configure page were off
screen while being edited.

Selects size their option window from the space left after their own
title and description, so the heading stays visible on short
terminals. Widths below the previous 10-column floor no longer overflow,
and confirm buttons drop their padding when the terminal is too narrow
for it. Dumb terminals always get line prompts, even when a caller asks
for the interactive form.

Tests cover tall pages at several heights in both directions, the page
title with the first field, a field taller than the terminal, selects
with headings on short terminals, narrow widths, and TERM=dumb.

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 4 files (changes from recent commits).

Shadow auto-approve: would not auto-approve because issues were found.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread zSDKs/sdk-cli/internal/forms/forms.go Outdated
Comment thread templates/templates/cli/auxiliary/internal/forms/forms.go.stmpl Outdated
APIs without global security scheme groups generate an auth login that
never builds a form, so the unconditional import failed to compile.

@cubic-dev-ai cubic-dev-ai 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.

0 issues found across 1 file (changes from recent commits).

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Shadow auto-approve: would not auto-approve. Auto-approval blocked by 2 unresolved issues from previous reviews.

View guided diff | Turn on auto-fix | Re-trigger cubic

The input width went negative below 8 columns, which panicked when the
field rendered. Clamp it, and show compact Y/N confirm buttons when the
full labels no longer fit.

@cubic-dev-ai cubic-dev-ai 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.

0 issues found across 4 files (changes from recent commits).

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Shadow auto-approve: would require human review. Replaces huh with a generated internal/forms package, rewires configure/auth/interactive prompts, and drops the huh dependency with added tests. The remaining human judgment is accepting the breaking change to custom auth/configure regions that embedded huh fields.

View guided diff | Turn on auto-fix | Re-trigger cubic

@2ynn 2ynn left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Three P2 correctness issues are detailed inline: textarea visibility on short terminals, unwrapped validation errors, and skipped accessible select validators. A P3 inline note covers the half-page navigation assertions. The remaining buffered-input issue across separate forms is documented as an inherited/incomplete-fix follow-up in the existing buffering thread, not a newly introduced regression.

One additional coverage gap: TestFormsCtrlCAborts checks the quit command and empty view but does not exercise Run returning ErrUserAborted. Retaining the cancellation smoke test in the suite would protect that public error contract.

All assertions in the 33 new test functions were inspected. Focused model and line-prompt probes reproduce the reported failures; CI-registered tests were not rerun. No additional issue was found in dependency gating or the remaining call-site changes.

Comment thread templates/templates/cli/auxiliary/internal/forms/forms.go.stmpl
Comment thread templates/templates/cli/auxiliary/internal/forms/forms.go.stmpl Outdated
Comment thread templates/templates/cli/auxiliary/internal/forms/forms.go.stmpl Outdated
Comment thread templates/templates/cli/tests/primary/forms_test.go.stmpl Outdated
…ompt input

- Wrap validation errors to the field width and budget their rows when
  sizing select windows and the multi-line editor, which now shrinks to fit
  short terminals instead of hiding its first line.
- Keep the focused input in view when a field and its error are taller
  than the terminal.
- Validate select answers in line prompts, including a kept default, and
  re-ask when the validator rejects them.
- Add forms.Reader so consecutive forms over one input share buffered
  answers; auth login and command prompts use one reader per command.
- Read interactive keys from the configured input when it is not stdin, so
  ctrl+c on scripted input returns ErrUserAborted.
@ThomasRooney

Copy link
Copy Markdown
Member Author

Ctrl-C note: added TestFormsRunReturnsErrUserAbortedOnCtrlC in 2cf1559. Run now reads keys from a non-stdin input passed to IO.

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 9 files (changes from recent commits).

Shadow auto-approve: would not auto-approve because issues were found.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread templates/templates/cli/tests/primary/forms_test.go.stmpl
Reject trailing characters in the integer validator used by the form
tests, and assert which authentication method the piped login picks.

@cubic-dev-ai cubic-dev-ai 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.

0 issues found across 2 files (changes from recent commits).

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.

Shadow auto-approve: would require human review. Swaps huh for an in-repo forms package across auth/configure/interactive prompts, with tests and dependency cleanup. Needs sign-off for the breaking change: custom auth/configure regions embedding huh fields must migrate to forms.

View guided diff | Turn on auto-fix | Re-trigger cubic

# Conflicts:
#	zSDKs/sdk-cli/.speakeasy/gen.lock
@ThomasRooney
ThomasRooney added this pull request to the merge queue Oct 8, 2026
Merged via the queue into main with commit 25b18ac Oct 8, 2026
162 checks passed
@ThomasRooney
ThomasRooney deleted the chore/cli-remove-huh branch October 8, 2026 18:05
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 8, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants