Expand Huh forms usage for interactive CLI operations#14357
Conversation
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR expands the console layer’s interactive capabilities by introducing reusable Huh-based prompt helpers (input/select/form) and refactoring secrets set to use masked secret entry in interactive terminals while preserving non-interactive stdin behavior.
Changes:
- Added new console helpers for interactive input, secret input, select, multi-select, and multi-field forms.
- Added basic unit tests for the new console helper APIs/validation.
- Updated
pkg/cli/secret_set_command.goto use masked interactive secret entry with a non-interactive stdin fallback.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| pkg/console/input.go | Adds interactive input and secret input helpers using Huh forms. |
| pkg/console/input_test.go | Adds basic tests validating API presence and non-TTY behavior. |
| pkg/console/select.go | Adds interactive single- and multi-select helpers. |
| pkg/console/select_test.go | Adds basic tests for select helper validation and non-TTY behavior. |
| pkg/console/form.go | Adds a higher-level form runner to compose multiple field types with pre-validation. |
| pkg/console/form_test.go | Adds tests for form field validation and non-TTY behavior. |
| pkg/cli/secret_set_command.go | Refactors secret value input to use masked Huh prompt when interactive, otherwise reads from stdin. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Check if stdin is a TTY - if not, we can't show interactive forms | ||
| if !tty.IsStderrTerminal() { | ||
| return "", fmt.Errorf("interactive input not available (not a TTY)") | ||
| } |
There was a problem hiding this comment.
The code comment says this is checking whether stdin is a TTY, but the actual check is tty.IsStderrTerminal() (stderr). This can incorrectly attempt an interactive Huh prompt when stdin is piped but stderr is still a TTY, which can block or fail. Consider adding a stdin TTY check (e.g., term.IsTerminal(int(os.Stdin.Fd())) or os.Stdin.Stat()), and/or updating the tty package with IsStdinTerminal() and using that here (potentially requiring both stdin and stderr to be terminals).
| // Check if stdin is a TTY - if not, we can't show interactive forms | ||
| if !tty.IsStderrTerminal() { | ||
| return "", fmt.Errorf("interactive selection not available (not a TTY)") | ||
| } |
There was a problem hiding this comment.
Same issue as input helpers: the code says it checks whether stdin is a TTY, but it only checks tty.IsStderrTerminal(). If stdin is redirected/piped while stderr remains a TTY, this can still attempt to run an interactive selector and block waiting for terminal input. Recommend checking stdin TTY status (and possibly requiring both stdin+stderr to be terminals) before running Huh forms.
| // Check if stdin is a TTY - if not, we can't show interactive forms | ||
| if !tty.IsStderrTerminal() { | ||
| return fmt.Errorf("interactive forms not available (not a TTY)") | ||
| } |
There was a problem hiding this comment.
RunForm gates interactivity on tty.IsStderrTerminal() only, but Huh forms require interactive stdin for key events. If stdin is not a terminal (e.g., piped input) while stderr is a terminal, RunForm can still try to run interactively and block/fail. Consider checking stdin TTY status (and updating the tty package with an IsStdinTerminal() helper) before running the form.
| // If we're in an interactive terminal, use Huh for a better UX with password masking | ||
| if isTerminal && tty.IsStderrTerminal() { | ||
| secretSetLog.Print("Using interactive password prompt with Huh") | ||
| value, err := console.PromptSecretInput( | ||
| "Enter secret value", | ||
| "The value will be encrypted and stored in the repository", | ||
| ) | ||
| if err != nil { | ||
| if errors.Is(err, io.EOF) { | ||
| break | ||
| } | ||
| return "", err | ||
| secretSetLog.Printf("Interactive prompt failed: %v", err) | ||
| return "", fmt.Errorf("failed to read secret value: %w", err) | ||
| } | ||
| return value, nil | ||
| } |
There was a problem hiding this comment.
Switching interactive input from multi-line stdin read (Ctrl+D) to a single-line Huh password prompt removes the ability to enter multi-line secrets interactively (e.g., PEM keys/certificates). If multi-line secrets are a supported use case, consider preserving the previous Ctrl+D multi-line path (or offering an explicit multiline mode) in interactive terminals, while still supporting masked single-line entry when appropriate.
| // Check if stdin is connected to a terminal (interactive mode) | ||
| info, err := os.Stdin.Stat() | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
|
|
||
| if info.Mode()&os.ModeCharDevice != 0 { | ||
| fmt.Fprintln(os.Stderr, "Enter secret value, then press Ctrl+D:") | ||
| } | ||
|
|
||
| reader := bufio.NewReader(os.Stdin) | ||
| var b strings.Builder | ||
| isTerminal := (info.Mode() & os.ModeCharDevice) != 0 | ||
|
|
||
| for { | ||
| line, err := reader.ReadString('\n') | ||
| b.WriteString(line) | ||
| // If we're in an interactive terminal, use Huh for a better UX with password masking | ||
| if isTerminal && tty.IsStderrTerminal() { | ||
| secretSetLog.Print("Using interactive password prompt with Huh") | ||
| value, err := console.PromptSecretInput( | ||
| "Enter secret value", | ||
| "The value will be encrypted and stored in the repository", | ||
| ) | ||
| if err != nil { | ||
| if errors.Is(err, io.EOF) { | ||
| break | ||
| } | ||
| return "", err | ||
| secretSetLog.Printf("Interactive prompt failed: %v", err) | ||
| return "", fmt.Errorf("failed to read secret value: %w", err) | ||
| } | ||
| return value, nil | ||
| } | ||
|
|
||
| // Fallback to non-interactive stdin reading (piped input or non-TTY) | ||
| secretSetLog.Print("Using non-interactive stdin reading") | ||
| if isTerminal { | ||
| fmt.Fprintln(os.Stderr, "Enter secret value, then press Ctrl+D:") | ||
| } | ||
|
|
||
| reader := io.Reader(os.Stdin) | ||
| data, err := io.ReadAll(reader) | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
|
|
||
| value := strings.TrimRight(b.String(), "\r\n") | ||
| value := strings.TrimRight(string(data), "\r\n") |
There was a problem hiding this comment.
resolveSecretValueForSet now has additional branches for interactive prompting and stdin ReadAll fallback, but the existing tests (in secret_set_command_test.go) only cover the env/flag cases. Adding unit tests for the stdin paths (e.g., temporarily swapping os.Stdin with an os.Pipe/temp file and asserting trimming/empty handling) would help prevent regressions in non-interactive and terminal scenarios.
Huh forms library was underutilized (only confirmation dialogs). Manual
bufio.Scannerinput left secrets visible on screen, lacked validation, and had inconsistent UX.Changes
New Console Helpers (
pkg/console/)Input functions:
PromptInput()- text input with validationPromptSecretInput()- password-masked input usingEchoMode(huh.EchoModePassword)PromptInputWithValidation()- custom validation supportSelection functions:
PromptSelect()- single-select menuPromptMultiSelect()- multi-select with configurable limitForm builder:
RunForm()- compose multi-field forms (input/password/confirm/select)Refactored Commands
pkg/cli/secret_set_command.go:bufio.ReadString()loop withconsole.PromptSecretInput()io.ReadAll()for piped/non-TTY input (backward compatible)Example Usage
Technical Details
tty.IsStderrTerminal()+os.Stdin.Stat()IsAccessibleMode()(ACCESSIBLE, NO_COLOR, TERM=dumb)EchoMode(huh.EchoModePassword)not deprecatedPassword()Original prompt
This section details on the original issue you should resolve
<issue_title>[Code Quality] Expand Huh forms usage for interactive CLI operations</issue_title>
<issue_description>## Description
The Huh forms library is currently underutilized - only confirmation dialogs are implemented. Expanding Huh usage to other interactive CLI operations (secret input, workflow selection, configuration forms) will improve user experience and reduce input errors.
Current Problem
From the Terminal Stylist Report (Jan 31, 2026):
pkg/console/confirm.go(confirmation dialogs)fmt.Scanfand text-based promptsCurrent State
Suggested Changes
Priority 1: Secret Input (High Impact)
Replace
fmt.Scanfinpkg/cli/secret_set_command.go:Benefits:
Priority 2: Workflow Multi-Select (Medium Impact)
Add workflow selection for compilation:
Use cases:
gh aw compile --interactive(select multiple workflows)gh aw validate --interactive(select workflows to validate)gh aw run --interactive(select workflow to run)Priority 3: Configuration Forms (Lower Impact)
Interactive workflow scaffolding:
Use cases:
gh aw init(create new workflow interactively)gh aw config(configure global settings)Files Affected
New functionality:
pkg/console/input.go(input helpers)pkg/console/select.go(select/multi-select helpers)pkg/console/form.go(form composition helpers)Refactor existing:
pkg/cli/secret_set_command.go(secret input)pkg/cli/compile_command.go(workflow selection)pkg/cli/init_command.go(interactive scaffolding)Success Criteria
ACCESSIBLE,NO_COLOR)Estimated Effort
Medium (1-2 days)
Source
Extracted from Terminal Stylist Report: Console Output Analysis (Discussion #12889)
Priority
Medium - Imp...
💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more Copilot coding agent tips in the docs.