Styled CLI with charmbracelet/fang + Charm v2 stack#29
Merged
munezaclovis merged 15 commits intomainfrom Mar 7, 2026
Merged
Conversation
- Migrate lipgloss v1 → v2 (charm.land/lipgloss/v2) - Migrate huh v0.8 → v2 (charm.land/huh/v2) - Add fang v2 (charm.land/fang/v2) for styled help/usage/errors - Replace manual Execute() error handling with fang.Execute() - Add pv color scheme using charmtone palette
- Move examples from install.go Long field to proper Example field - Clean up env.go Long field, add Example - Add examples to: link, unlink, log, php:install, php:use, service:add
- Convert 16 files from ui.Fail/ErrAlreadyPrinted sandwich to plain fmt.Errorf returns; fang's DefaultErrorHandler renders the ERROR badge - Add custom pvErrorHandler that skips ErrAlreadyPrinted (for ui.Step failures where the spinner already displayed the error) - Change ui.Step/StepVerbose/StepProgress to return ErrAlreadyPrinted on failure since they display the error inline - Remove all cmd.SilenceUsage = true (fang sets it globally) - Remove installCmd.SilenceUsage from init - Clean up unused imports (os, ui) in simplified files
- Remove bare fmt.Fprintln(os.Stderr) padding from 9 command files - Simplify daemon enable/disable to return ui.Step() directly - Clean up unused os imports after spacing removal
Document the fang/huh/lipgloss/ui layering, error handling patterns, and hard don'ts now that the Charm v2 stack is in place.
lipgloss v2 always emits ANSI escape codes in Render(), even when stderr is piped or captured. This broke "8.4 (default)" matching in start-curl.sh because the two words use different styles with ANSI reset/re-style codes between them.
- Guard ErrAlreadyPrinted in update.go, install.go, service_add.go to avoid printing empty error messages after ui.Step already displayed them - Run go mod tidy to mark fang/v2 and charmtone as direct dependencies - Fix duplicate Example in env.go - Remove double blank lines in service_destroy.go, service_remove.go - Fix CLAUDE.md: Footer signature, soften fmt.Print rule to match reality
Split the vague "prefer ui.* helpers" rule into two clear rules: errors always return to fang, status output uses ui.* helpers.
- setup.go: 6x ui.Red.Render("!") → ui.Fail() with ErrAlreadyPrinted guards
- uninstall.go: 4x sub-command errors → ui.Fail() with guards,
6x post-ui.Step error prints removed (were printing empty sentinel)
- link.go, unlink.go, php_use.go: ui.Red.Render("!") → ui.Fail()
- update.go: add missing ErrAlreadyPrinted guard on self-update
- doctor.go: all output was going to stdout instead of stderr
- service_logs.go: fmt.Fprintf → ui.Subtle()
- Add Cobra command groups (Core, Server, PHP, Composer, Mago, Colima, Services, Daemon) with GroupID on every command so `pv -h` renders organized, readable sections instead of a flat list. - Refactor uninstall.go: replace bufio.Scanner with huh interactive prompts, replace all raw fmt.Fprint* with ui.Subtle/ui.Success/ui.Fail per CLAUDE.md guidelines.
…packages
Restructure flat cmd/ files into grouped subpackages under
internal/commands/{php,mago,composer,colima,service,daemon}/.
Each group exports Register() for command registration and Run*()
wrappers for orchestrator access. Thin shims in cmd/ call Register().
Also fix UI guideline violations in service status/remove commands:
replace raw fmt.Fprintf with ui.Table, remove blank-line spacers.
huh opens /dev/tty directly, so piping stdin doesn't work in CI. Add -f/--force flag that skips the confirmation and auth backup prompts. Update e2e script to use --force instead of piping.
- Fix nil pointer dereference in uninstall when settings file is missing - Eliminate command injection in runSudo by using exec.Command with separate argv instead of shell interpolation via sudo sh -c - Tighten copyFile permissions from 0644 to 0600 for auth token backup - Replace raw fmt.Fprintf in doctor.go with new ui.SectionHeader helper - Deduplicate sanitizeProjectName into services.SanitizeProjectName - Add ui.Subtle message when service:env skips unknown services - Improve ErrAlreadyPrinted sentinel with descriptive error message - Update CLAUDE.md to document internal/commands/<group>/ subpackages - Add registration tests for all 6 command subpackages - Add ui.Step/StepVerbose/StepProgress contract tests
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
Execute()error handling withfang.Execute()— all help pages, usage text, and errors are now beautifully styled with syntax-highlighted examples, colored sections, and anERRORbadgeui.Fail()+ErrAlreadyPrinted+SilenceUsagesandwich pattern (~45 lines each) to plainreturn fmt.Errorf()— fang renders them automaticallyExample:fields to 8 key commands (link, install, env, log, php:install, php:use, unlink, service:add) — fang syntax-highlights program names, flags, args, and commentsui.Fatal)Before → After
Help output: default ugly Cobra → styled sections (USAGE, EXAMPLES, COMMANDS, FLAGS) with colored code blocks
Error output: manual
ui.Fail()+FailDetail()+ blank lines +SilenceUsage→ justreturn fmt.Errorf(), fang rendersERRORbadge + contextual "Try --help" hintWhat stays
internal/ui/spinners, progress bars, tables,Success(),Subtle()— these handle runtime output that fang doesn't touch.ErrAlreadyPrintedremains for spinner failures (custom fang error handler skips them).Follow-up work
status.go,service_status.go,php_list.gowithui.Table()Example:fields to more commands (incremental)Test plan
go build ./...passesgo test ./...passes (all packages)pv --helprenders styled outputpv link --helpshows syntax-highlighted examplespv --bogusshows fang ERROR badgepv --versionshows version