feat(ui): add huh-backed prompts + detector.ResolveInteractive#118
Merged
Conversation
Wraps huh in a small Prompt interface so internal/cli's
post-upgrade cleanup can prompt the user via huh.Form in
FancyMode and a numbered-line fallback in PlainMode. Adds
detector.ResolveInteractive + detector.ResolveManagerAuto so
the upgrade flow can pick a manager when multiple are detected
without the user pre-supplying a preference.
Adds github.com/charmbracelet/huh v1.0.0 to go.mod. The huh
dependency is contained to internal/ui/prompt.go — internal/cli
calls ui.NewPrompt + ui.Prompt.{Confirm,Select} and never imports
huh directly (same constraint as lipgloss / bubbletea per #105).
Implementation notes:
- prompt.go: Prompt interface (Confirm/Select/Mode);
plainPrompt uses line reads against io.Reader; fancyPrompt
builds a fresh huh.Form per call. Confirm supports a
defaultYes pre-fill so huh's toggle starts on the right side;
empty input / EOF fall back to the default for Confirm
(true) and the default label / first option for Select.
- prompt_test.go covers: y/n/empty/EOF/case-insensitive
Confirm paths, numeric/label/EOF Select paths, no-options
error path, bufio.Reader passthrough, nil-stream degradation,
and I/O error surfacing.
- detector/interactive.go: ResolveManagerAuto wraps ResolveManager
with an explicit ErrInteractiveRequired sentinel for the
multi-manager case. ResolveInteractive prompts the user (via
ui.Prompt or a fallback plain prompt against cmd streams)
with a single nonInteractive shortcut for CI / --yes runs.
single-manager registries short-circuit without prompting.
- detector/interactive_test.go covers: zero/single/multi/no-pref
paths, fallback plain prompt with numeric + EOF input,
nonInteractive ErrInteractiveRequired surfacing, and the
stubPrompt pattern that production code paths would use if
they ever need to mock the picker.
- cleanup.go: runCleanupPrompt signature now takes
(ui.Prompt, ui.Writer, ...); the old bufio-based prompts
(promptAllOrNothing, promptPerVersion) are rewritten on top
of the new abstraction. The "delete one specific version"
path is now a labeled Select option ("Delete v22.11.0")
instead of the old "type a version string" pattern — same
per-version confirm UX but a discoverable option list.
- cleanup_test.go rewires newCleanupIO into a
(prompt, writer, buf) triple-return helper. The number
inputs change from text ("y\n", "20.18.0\n") to numeric
indexes ("1\n" for "Delete all", "2\n" for "Skip cleanup",
"3\n" / "4\n" / "5\n" for per-version picks) since the new
prompt flow uses Select rather than free-form input.
- upgrade.go: ResolveManagerAuto + ResolveInteractive replace
the bare ResolveManager call. Resolved manager still goes
through the existing "Using manager: X" log line. The
ErrInteractiveRequired path hands off to ResolveInteractive
with yes=nonInteractive (a CI script that ran with --yes
surfaces "use --manager" rather than hanging on stdin).
Per #105 phasing, this is PR3 of 3. PR4 (next) migrates the
remaining call sites (check, list, packages, config) to
ui.Writer. The Prompt abstraction is now stable so PR4 can
focus on the Writer migrations without revisiting this PR's
surface.
Part of #105.
Co-Authored-By: puku-ai-2.8 <noreply@puku.sh>
|
✔️ d99215e - Conventional commits check succeeded. |
This was referenced Jul 5, 2026
dipto0321
added a commit
that referenced
this pull request
Jul 5, 2026
16 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds huh-backed interactive prompts (
internal/ui/prompt.go) anddetector.ResolveInteractiveto handle the "multiple Node version managers detected, no preference given" case. The post-upgrade cleanup decision point is migrated from raw stdin line-reads (promptAllOrNothing/promptPerVersion) to the newui.Promptabstraction so FancyMode gets huh's arrow-key navigation and PlainMode gets a numbered-line fallback.This is PR3 of 3 for #105 (PR1 was the scaffold via #74; PR2 was #117 spinners via bubbletea; PR4 will follow).
What this PR delivers
internal/ui/prompt.go(new)Promptinterface:Confirm(question, defaultYes),Select(prompt, options, defaultLabel),Mode().NewPrompt(mode, in, out)— FancyMode + non-nil streams →fancyPrompt(huh.Form per call); otherwiseplainPrompt.plainPromptreads one line per question. EOF / empty input fall back to the default (Confirm →defaultYes; Select →defaultLabel/options[0]). I/O errors propagate.fancyPromptbuilds a freshhuh.Formper call (no cached state) and pipes throughhuh.ErrUserAborted→ "use the default" mapping.prompt_test.gocovers Confirm y/n/Y/yes/empty/EOF/case-insensitive/variants, Select numeric/label/empty/EOF/no-options, bufio.Reader passthrough, nil-stream degradation under FancyMode, and I/O error surfacing. All tests pass under-race.internal/detector/interactive.go(new)ResolveManagerAuto(reg, preferred)— wrapper aroundResolveManagerthat returns a newErrInteractiveRequiredsentinel when multiple managers are detected and no preference is set. ExistingResolveManagersemantics preserved.ResolveInteractive(reg, prompt, nonInteractive, in, out)— for the multi-manager case. Single-manager registries short-circuit (no prompt).nonInteractive=truereturnsErrInteractiveRequiredimmediately (the--yes/ CI path) so the caller can surface "use --manager". Order is stable viaPriority.interactive_test.gocovers zero/single/multi/no-pref paths, the fallback plain-prompt against supplied in/out streams (EOF → first option), the nonInteractive shortcut, and astubPromptfor future test mocks.internal/cli/cleanup.go(rewritten prompts)runCleanupPrompt(prompt, writer, cfg, toInstall, installed, active, m)— new signature threads aui.Prompt+ui.Writerthrough. Plain-mode tests pass aui.NewPrompt(PlainMode, ...)frominternal/ui; production code callsui.NewPrompt(writer.Mode(), cmd.InOrStdin(), cmd.OutOrStdout())once per upgrade.promptAllOrNothingnow constructs a Select with two top-level options ("Delete all of the above", "Skip cleanup") plus one "Delete vX.Y.Z" entry per candidate. The "delete one specific version" path is now a labeled option rather than the old "type a version string" pattern — same UX, more discoverable.promptPerVersionusesui.Prompt.ConfirmwithdefaultYes=false. Empty input defaults to "no" — the safe direction for cleanup prompts.ui.Writer.{Info,Success,Warn}so Plain-mode gets unstyled text and Fancy-mode gets colored prefixes (✓/⚠/•).cleanupIOstruct (was:bufio.Reader+io.Writer) is gone — that's the migrated surface.internal/cli/cleanup_test.go(rewired)newCleanupIO(cleaned up; replaced bynewTestPromptandpromptWithInput).#76"sticky-up afteryat all-or-nothing",#58ForcePerVersion downgrade, excludes-new-and-active, no-candidates, prefiltered-only, uninstall-error-collected.internal/cli/upgrade.godetector.ResolveManager→detector.ResolveManagerAuto+detector.ResolveInteractivehandoff. TheErrInteractiveRequiredbranch hands off withyesmapped tononInteractiveso a CI script with-ybut no--managererrors out with the "use --manager" hint instead of hanging on stdin.runCleanupPromptcall site now constructs bothpromptandwriteronce and threads them through.go.mod/go.sumgithub.com/charmbracelet/huh v1.0.0+ transitive deps (charmbracelet/bubbles,charmbracelet/x/exp/strings,dustin/go-humanize,mitchellh/hashstructure/v2,atotto/clipboard,catppuccin/go,mattn/go-localereaderwas already a bubbletea transitive, plus a few more — seego.sum).Edge cases I considered before opening
defaultYesso a pipedecho "" | nodeup upgradewithout-ydoesn't accidentally mass-delete. Fancy prompt pre-fills the huh toggle with the default.prompt_test.gopins both paths.defaultLabeloroptions[0]. Fancy prompt's abort handler returns the default. Closed-stdin CI scripts still pick something — the safest available — and don't hang.ResolveInteractiveshort-circuits inlen(reg.Found) == 1before any prompt call. No spinner, no huh build, no cost.ErrNoManager(existing sentinel). Caller already handles this on the upgrade path.ErrInteractiveRequiredwithout reading stdin. The UI layer can surface "use --manager " cleanly.interactive_test.gopins this.options[0]per the Prompt.Select contract. User probably expected "fnm" (top-priority manager) anyway — matches the platform'sPriority()order.fancyPrompt.{Confirm,Select}builds a freshhuh.Formand runs.Run()to completion. No global state, no caching, no signal-handler installation (huh's default SIGINT handler is left in place because we trust the bubbletea stream handler we set in PR2 to never race against plain prompt paths).cmd.OutOrStdout()for the prompt line, separate for the writer). Different files = no buffered-output race; tests confirm withbytes.Buffershared between both.optionsto Select: Returns an error from both Plain and Fancy paths. Defensive — callers always pre-validate.#76regression test under the new prompt flow: A user who selects "Delete all" (option index 1) no longer gets per-version[y/N]re-prompts. The sticky-up logic still lives inrunCleanupPromptStep 4 (cfg.PerVersion = false unless ForcePerVersion). The test for this isTestCleanupPrompt_DeleteAllSkipsPerVersionPromptand was updated to check for the absence of "[y/N]" in the output rather than the old "Delete v" string (which now legitimately appears in the Select option list and isn't the regression marker).Verification
Plus a manual
make build && ./bin/nodeup upgrade --helpconfirming the binary still launches cleanly.Out of scope (deferred to PR4 per #105 phasing)
cmd.Printfcalls incheck.go,list.go,packages.go,config.go, and the remainingupgrade.golines toui.Writer.{Info,Success,Warn,Error,Println}. PR4 is purely mechanical at this point: thePromptandWritersurfaces are stable, so PR4 doesn't need to revisit this PR's code, just swap call sites in adjacent files.internal/ui/report.gofor the end-of-upgrade summary block (some of those lines go throughui.Writertogether).V2flips from aspirational to enforced (every user-facing string ininternal/cli/*flows throughinternal/ui).Part of #105. PR4 to follow in this same issue.