wizard-commands
SkillAI & modelsUse when adding a prompt or a multi-step wizard to a command: internal/prompter questions, wizard TUI components, WizardModel, RunWizard, and the project init reference implementation.
Available today. Use it from your connected AI after setup.
No other account needed.
Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.
Then ask your AI: use the wizard-commands skill
What this skill tells your AI
The instructions your AI receives, as published by schmitthub/clawker in .agents/skills/wizard-commands/SKILL.md and read by ahel’s review.
Package contracts: internal/prompter/AGENTS.md, internal/tui/AGENTS.md.
Comprehensive reference for the two prompting layers in clawker: line-based prompter (Scenario 2) and BubbleTea wizard (Scenario 4).
1. Overview & Decision Tree
Clawker has two distinct prompting mechanisms, each living in its own package with separate import boundaries:
| Layer | Package | Technology | Scenario |
|---|---|---|---|
| Prompter | internal/prompter | Line-based stdin/stderr | Scenario 2 (static-interactive) |
| Wizard | internal/tui | BubbleTea alt screen | Scenario 4 (live-interactive) |
Decision Tree
Need user input?
│
├── Single y/n confirmation? ──────── f.Prompter().Confirm()
├── Single text input? ────────────── f.Prompter().String()
├── Single list selection? ────────── f.Prompter().Select()
├── Multi-step form with navigation? ─ f.TUI.RunWizard(fields)
├── Multi-step form + progress? ───── wizard → f.TUI.RunProgress (init pattern)
└── No input needed? ─────────────── Direct execution; --yes flag to skip
Key rule: Single prompt → prompter; multi-step form with back-navigation → wizard.
2. Prompter Package (internal/prompter/)
Construction & Factory Wiring
type Prompter struct { ios *iostreams.IOStreams }
func NewPrompter(ios *iostreams.IOStreams) *Prompter
Access via Factory: f.Prompter() (lazy accessor, returns *prompter.Prompter).
Three Methods
String — text input with default/validation:
func (p *Prompter) String(cfg PromptConfig) (string, error)
type PromptConfig struct {
Message string
Default string
Required bool
Validator func(string) error
}
Confirm — y/n toggle:
func (p *Prompter) Confirm(message string, defaultYes bool) (bool, error)
Select — numbered list, returns 0-based index:
func (p *Prompter) Select(message string, options []SelectOption, defaultIdx int) (int, error)
type SelectOption struct {
Label string
Description string
}
Non-Interactive Fallback
In CI/non-TTY (checked via ios.IsInteractive()):
String→ returnsDefault(or error ifRequiredwith no default)Confirm→ returnsdefaultYesSelect→ returnsdefaultIdx
Output Behavior
All prompts write to ios.ErrOut (stderr) — keeps stdout clean for data output.
Deprecated
PromptForConfirmation(in io.Reader, message string) bool — writes to os.Stderr directly, not testable. Use Prompter.Confirm() instead.
Real Callers
| Command | Method | Purpose |
|---|---|---|
image/prune | Confirm | "Remove all unused images?" |
volume/prune | Confirm | "Remove all unused volumes?" |
network/prune | Confirm | "Remove all unused networks?" |
project/register | Select, String | Project selection/naming |
project/init | Various | Project initialization prompts |
container/run | Confirm | Runtime confirmations |
container/create | Confirm | Creation confirmations |
Test Pattern
tio, _, _, _ := iostreams.Test()
tio.SetInteractive(true)
tio.InBuf.SetInput("y\n")
p := prompter.NewPrompter(tio)
result, err := p.Confirm("Continue?", false)
// result == true, errOut.String() contains "Continue?"
3. Wizard TUI Components (internal/tui/)
Three composable layers, each independently usable:
Layer 1 — Field Models (fields.go)
Standalone BubbleTea models. Value semantics (setters return copies). Each works alone or composed in a wizard.
FieldOption — shared type for select options:
type FieldOption struct { Label string; Description string }
SelectField — arrow-key selection wrapping ListModel:
func NewSelectField(id, prompt string, options []FieldOption, defaultIdx int) SelectField
func (f SelectField) Value() string // returns selected option's Label (NOT index)
func (f SelectField) SelectedIndex() int
func (f SelectField) IsConfirmed() bool // true after Enter
func (f SelectField) SetSize(w, h int) SelectField
Keys: Up/Down (or j/k) navigate, Enter confirms, Ctrl+C quits.
TextField — text input wrapping bubbles/textinput:
func NewTextField(id, prompt string, opts ...TextFieldOption) TextField
// Options: WithPlaceholder, WithDefault, WithValidator, WithRequired
func (f TextField) Value() string
func (f TextField) IsConfirmed() bool
func (f TextField) Err() string // validation error message
func (f TextField) SetSize(w, h int) TextField
Keys: Type to input, Enter validates + confirms, Ctrl+C quits. Required check runs before custom validator.
ConfirmField — yes/no toggle:
func NewConfirmField(id, prompt string, defaultYes bool) ConfirmField
func (f ConfirmField) Value() string // "yes" or "no" (lowercase)
func (f ConfirmField) BoolValue() bool
func (f ConfirmField) IsConfirmed() bool
func (f ConfirmField) SetSize(w, h int) ConfirmField
Keys: Left/Right/Tab toggle, y/n set directly, Enter confirms, Ctrl+C quits.
Layer 2 — StepperBar (stepper.go)
Pure render function for horizontal step progress indicator.
type StepState int
const (
StepPendingState // ○ (MutedStyle)
StepActiveState // ◉ (TitleStyle)
StepCompleteState // ✓ (SuccessStyle)
StepSkippedState // hidden
)
type Step struct {
Title string
Value string // shown after ": " on complete steps
State StepState
}
func RenderStepperBar(steps []Step, width int) string
Rendering: StepState.String() returns icon. Skipped steps hidden entirely. Steps joined by → separator (MutedStyle). Truncates to width.
Example output: ✓ Build Image: Yes → ◉ Packages → ○ Submit
Layer 3 — WizardModel (wizard.go)
Composes fields + stepper into multi-step flow with navigation. Runs in alt screen via TUI.RunWizard.
4. WizardModel Internals
Public Types
type WizardFieldKind int
const (
FieldSelect WizardFieldKind = iota
FieldText
FieldConfirm
)
type WizardField struct {
ID, Title, Prompt string
Kind WizardFieldKind
// Select-specific
Options []FieldOption
DefaultIdx int
// Text-specific
Placeholder string
Default string
Validator func(string) error
Required bool
// Confirm-specific
DefaultYes bool
// Conditional: skip when predicate returns true
SkipIf func(WizardValues) bool
}
type WizardValues map[string]string
type WizardResult struct {
Values WizardValues
Submitted bool
}
Two-Layer Design
WizardField is an immutable spec defining Kind/Options/SkipIf. At runtime, the wizard creates actual field instances (SelectField/TextField/ConfirmField) stored in maps by step index. When navigating backward, fields are recreated (fresh state).
Constructor Validation
newWizardModel(fields) panics on:
- Empty field ID
- Duplicate field ID
FieldSelectwith no options
These are programming errors caught at construction, not runtime.
After validation: creates field instances, finds first visible step, auto-completes if all steps are skipped.
Init/Update/View
Init(): delegates tocurrentFieldInit()(e.g., textinput blink cursor)Update(msg):WindowSizeMsg→ stores dimensions, callsupdateAllFieldSizesKeyMsg:Ctrl+C→ cancel, returntea.QuitEsc→goBack()(cancel if first step)Enter→confirmAndAdvance()(submit if last step)- Other keys →
delegateToCurrentField()(Up/Down, Left/Right, y/n, typing)
- Non-key messages (blink ticks) →
delegateNonKeyToCurrentField()
View():" " + stepperBar + "\n\n" + fieldView + "\n\n" + " " + helpBar
Navigation
nextVisibleStep(from)/prevVisibleStep(from)— skip SkipIf-true stepsactivateStep(idx)— recreates field when going backward (fresh state)confirmAndAdvance()— stores current field value, finds next visible step; if none, submitsgoBack()— finds previous visible step; if none (at first step), cancels
SkipIf Re-Evaluation
SkipIf predicates are re-evaluated on every navigation check. Changing an earlier answer can show or hide later steps. Both forward and backward navigation respects SkipIf.
filterQuit
filterQuit(cmd tea.Cmd) tea.Cmd wraps field commands to suppress tea.QuitMsg. Individual fields send tea.Quit on Enter/Ctrl+C (designed for standalone use), but the wizard needs to intercept these to manage its own lifecycle.
The implementation uses deferred evaluation — wraps the cmd in a closure that only calls cmd() during BubbleTea's runtime, not eagerly outside it.
Size Management
updateAllFieldSizes reserves 4 lines for chrome (stepper + gaps + help bar), distributes the remainder to all field instances:
fieldHeight = height - 4 // 1 stepper + 1 gap + 1 gap + 1 help
fieldWidth = width - 4 // 2-char indent on each side
Help Bar
Context-sensitive per field kind:
- SelectField:
↑/↓ navigate • enter confirm • esc back - ConfirmField:
←/→ toggle • y/n set • enter confirm • esc back - TextField:
type to enter • enter confirm • esc back
5. TUI.RunWizard Integration
func (t *TUI) RunWizard(fields []WizardField) (WizardResult, error)
Flow:
- Validates non-empty fields (
len(fields) == 0→ error) - Creates
wizardModelvianewWizardModel(fields) - Runs via
RunProgram(ios, &model, WithAltScreen(true)) - Alt-screen mode: wizard takes full terminal, restores on exit
- Extracts
submitted/cancelled/valuesfrom final model - Returns explicit error on unexpected model type
Return semantics:
result.Submitted == true+result.Valuespopulated → user completed the wizardresult.Submitted == false→ user cancelled (Esc on first step or Ctrl+C)err != nil→ BubbleTea runtime error or empty fields
6. Reference Implementation: clawker project init
See internal/cmd/project/init/init.go: runInteractive → opts.TUI.RunWizard(buildInitWizardSteps(wctx)), with performProjectSetup(ctx, performSetupInput{...}) as the shared testable core called by both interactive and non-interactive paths (opts.Yes || !IsInteractive() dispatch). A second inline RunWizard drives the post-init customize prompt.
Hybrid Scenario 3+4
- Wizard phase (alt screen) —
TUI.RunWizardcollects user choices - Progress phase (inline/alt) —
TUI.RunProgressfor image build - Static next steps —
fmt.Fprintf(ios.ErrOut, ...)guidance
Error handling:
- Build errors → graceful message + next steps + return nil (not fatal)
- Progress errors (Ctrl+C) → returned as error
- Settings save warnings → user-visible warning to stderr
Testing
// Test shared logic directly — no BubbleTea needed
performSetup(ctx, opts, true, "bookworm")
// Test with mock config + mocks.FakeClient
cfg := config.NewBlankConfig()
fake := mocks.NewFakeClient(cfg)
fake.SetupLegacyBuild()
7. Guidelines for Future Commands
| Need | Tool | Example |
|---|---|---|
| Single y/n | f.Prompter().Confirm() | image prune, volume prune |
| Single text input | f.Prompter().String() | Project name entry |
| Single list pick | f.Prompter().Select() | Base image selection |
| Multi-step form | f.TUI.RunWizard(fields) | clawker init |
| Multi-step + progress | Wizard then f.TUI.RunProgress | clawker init (build phase) |
| No input needed | Direct execution | --yes flag bypasses interactive |
Non-interactive mode: Check ios.IsInteractive() or --yes flag early and bypass wizard entirely. Both the prompter and wizard have non-interactive fallback, but the recommended pattern is to have a separate runNonInteractive path (like init) for clarity.
8. Key Gotchas
-
Separate packages:
internal/prompter(line-based, stdin/stderr) vsinternal/tui(BubbleTea, alt screen). Don't mix them in a single flow. -
Import boundary: Only
internal/tuiimportsbubbletea/bubbles. Onlyinternal/iostreamsimportslipgloss. Onlyinternal/prompterreads fromios.Indirectly. -
Prompter writes to stderr directly: Prompts render to
ios.ErrOut. Wizard uses BubbleTea's alt screen (writes toios.ErrOutviaRunProgram). -
Wizard panics on invalid definitions: Empty ID, duplicate ID, FieldSelect with no options → panic (programming errors, not runtime errors).
-
Value semantics per kind:
SelectField.Value()returns the label string, not the indexConfirmField.Value()returns lowercase"yes"or"no"(not"Yes"/"No")TextField.Value()returns the text as entered
-
SkipIf re-evaluates dynamically: Changing an earlier answer can show/hide later steps. Both forward and backward navigation respects SkipIf.
-
filterQuit prevents premature exit: Individual fields send
tea.Quiton Enter (designed for standalone use), but the wizard intercepts this. Uses deferred evaluation to avoid callingcmd()outside BubbleTea runtime. -
Testing wizards: Use
newWizardModel(fields)+ directUpdate()calls — no BubbleTea program runtime needed. Test shared logic (likeperformSetup) directly, bypassing BubbleTea entirely. -
Non-interactive bypass: Check
ios.IsInteractive()or--yesflag early. Don't attempt to run a wizard in non-interactive mode — have a separate code path. -
Prompter.Select returns index, wizard SelectField returns label: These are different APIs with different return types. The prompter returns
(int, error), the wizard stores the label string inWizardValues.
Signals
- GitHub stars
- 55
- Forks
- 6
- Last commit
- Sep 2026
Advanced
- Item type
- skill
- Key
wizard-commands- Source
- github.com/schmitthub/clawker