diff --git a/.planning/phases/19.8-console-commands-overhaul/19.8-02-SUMMARY.md b/.planning/phases/19.8-console-commands-overhaul/19.8-02-SUMMARY.md new file mode 100644 index 00000000..13eddc44 --- /dev/null +++ b/.planning/phases/19.8-console-commands-overhaul/19.8-02-SUMMARY.md @@ -0,0 +1,346 @@ +--- +phase: 19.8-console-commands-overhaul +plan: 02 +subsystem: console +tags: [bonfire, cobra, fang, bubbletea, bubbles, huh, lipgloss, colorprofile, diary, cli] + +requires: + - phase: 19.8-01 + provides: "diary (Bootstrap, Setup, SetVerbosity, console handler) and bonfire ColorMode, Palette, DefaultPalette, DetectProfile" +provides: + - "Global options -v/-vv/-vvv, -q, --ansi/--no-ansi, -n, --env on every command; PeekGlobals before boot, OnGlobals after parsing" + - "Artisan list (bare binary, --help, list, list --all, list --format=json) with the BuildVersion header" + - "Artisan per-command help (help , --help) with defaults, long help and Examples" + - "Command.Examples, Command.Hidden, Listing, Describe, CheckDocs" + - "Error block with WithHint, Reported, Usage; Execute and Fail return exit codes 0, 1, 2" + - "Duplicate command names and reserved global flags rejected by NewRoot" + - "SSU spinner, progress, process view and selector on charm v2; huh prompts; plain-line degradation" + - "Helpers GlobalsOf, VerbosityOf, IsQuiet, Interactive, ConfirmProceed, Process, Current, Failed, PrintAbove" + - "Generated main: PeekGlobals, diary.FromGlobals, compass.Open with --env, bonfire.Execute exit code" +affects: [19.8-03, 19.8-04, console, plugins, generated main] + +actuals: + tokens: 34921 + tasks: 3 + commits: 5 +plan_head_before: b7c07591cb3906d5b83403d18837f17af9189ffa +plan_head_after: 3f388629e14ea2e46dc5c97e9cfb01a1f8bad58f + +tech-stack: + added: + - "charm.land/fang/v2 v2.0.1" + - "charm.land/bubbletea/v2 v2.1.0" + - "charm.land/bubbles/v2 v2.2.1" + - "charm.land/huh/v2 v2.0.3" + - "github.com/spf13/pflag v1.0.9 (was indirect, now imported directly)" + - "github.com/charmbracelet/x/ansi v0.11.9 (direct, test only: stripping frames)" + patterns: + - "Root state (rootState) shared by list, help, the help command and per-subcommand help funcs; cobra prefers a command's own help func, so fang's root help func never reaches them" + - "Live views run as bubbletea programs with tea.WithInput(nil), tea.WithoutSignalHandler(), tea.WithContext; one at a time; PrintAbove sends lines to the running program with Program.Send (a no-op after it ends), so callers never block on a finished program" + - "Optional interface (consoleState) behind GlobalsOf/Interactive instead of new Output methods" + +key-files: + created: + - modules/bonfire/globals.go + - modules/bonfire/version.go + - modules/bonfire/execute.go + - modules/bonfire/list.go + - modules/bonfire/help.go + - modules/bonfire/docs.go + - modules/bonfire/errors.go + - modules/bonfire/live.go + - modules/bonfire/selector.go + - modules/bonfire/helpers.go + - modules/bonfire/list_smoke_test.go + - modules/bonfire/help_smoke_test.go + - modules/bonfire/widgets_smoke_test.go + modified: + - modules/bonfire/root.go + - modules/bonfire/command.go + - modules/bonfire/output.go + - modules/bonfire/widgets.go + - modules/bonfire/prompts.go + - modules/bonfire/example_test.go + - modules/bonfire/README.md + - modules/diary/diary.go + - modules/diary/console.go + - modules/diary/README.md + - internal/build/build.go + - internal/build/build_test.go + - internal/build/testdata/new/main.go.golden + - examples/hello/main.go + - examples/hello/hello_test.go + - cmd/summer/main.go + - cmd/summer/docs.go + - cmd/summer/docs_test.go + - internal/docsite/check_commands.go + - internal/docsite/checks_test.go + - docs/console/introduction.md + - docs/console/writing-commands.md + - docs/services/logging.md + - go.mod + - go.sum + +key-decisions: + - "NewRoot sends the error block to stderr when its output is the process stdout (root.SetErr); Output.Error keeps writing where it did, so today's stdout bytes do not move" + - "Execute resets the root's persistent flags before each run and pre-routes a call without a command word (only globals, -h, --help) to the list by dropping -h/--help; fang runs with WithoutVersion, WithoutManpage, WithoutCompletions and WithErrorHandler" + - "Passthrough commands (summer's delegates) parse no flags, so the root reads their globals with PeekGlobals over the raw args, as the generated main did before boot" + - "UsageError carries an unexported command name set by the flag error func and the argument-count check, so the error block's Try line is ' help ' (or ' list' at the root)" + - "The builtin list and help commands carry Long and Examples, so plan 3's framework CheckDocs test can include them" + - "--no-ansi on an interactive terminal uses the plain line prompts instead of huh and the selector (D-03: no styled widget under --no-ansi); -n and -q return defaults" + - "While a live view runs, Println, Printf (complete lines), status lines and tables print above it; a widget started inside a running view prints plain lines (one live view at a time)" + - "diary.ApplyGlobals rebuilds the channels only when the color mode differs from the one they were built with; otherwise it is SetVerbosity" + +patterns-established: + - "Command examples are stored without the binary and rendered after bin/ or summer" + - "Commands that already printed their problems return bonfire.Reported(err); usage checks wrap with bonfire.Usage" + - "Destructive steps ask through bonfire.ConfirmProceed, which never asks off an interactive terminal" + +requirements-completed: [SC-2, SC-3, D-01, D-02, D-03, D-04, D-13, D-14, D-15, D-16, D-17, D-18, D-27, D-28, D-30] + +coverage: + - id: D1 + description: "A bare binary, --help and list print the Artisan list with the version header, usage, global options and namespace blocks" + requirement: D-13 + verification: + - kind: unit + ref: "modules/bonfire/list_smoke_test.go#TestListTracerSmoke" + status: pass + - kind: integration + ref: "examples/hello/hello_test.go#TestBuiltBinaryListsCommands" + status: pass + human_judgment: false + - id: D2 + description: "Global options reach the OnGlobals hook (diary) and agree with PeekGlobals; Call runs no hook" + requirement: SC-2 + verification: + - kind: unit + ref: "modules/bonfire/list_smoke_test.go#TestListTracerSmoke" + status: pass + - kind: unit + ref: "internal/build/build_test.go#TestGenerateMainInstallsDiary" + status: pass + human_judgment: false + - id: D3 + description: "Exit codes 0, 1 and 2 from Execute in the generated main; migrate without a DSN prints database.dsn on stderr and exits 1" + requirement: D-18 + verification: + - kind: integration + ref: "examples/hello/hello_test.go#TestBuiltBinaryUnknownCommandExitsNonZero" + status: pass + - kind: integration + ref: "examples/hello/hello_test.go#TestMigrateWithoutDSNFailsLoudly" + status: pass + human_judgment: false + - id: D4 + description: "Per-command help in the Artisan layout with defaults, repeatable marker, long help and examples; help equals --help" + requirement: D-14 + verification: + - kind: unit + ref: "modules/bonfire/help_smoke_test.go#TestHelpSmoke" + status: pass + - kind: unit + ref: "modules/bonfire/example_test.go#ExampleExample" + status: pass + human_judgment: false + - id: D5 + description: "Hidden commands, list --all and list --format=json (Listing); CheckDocs reports documentation gaps" + requirement: D-16 + verification: + - kind: unit + ref: "modules/bonfire/help_smoke_test.go#TestListJSONSmoke" + status: pass + - kind: unit + ref: "modules/bonfire/example_test.go#ExampleCheckDocs" + status: pass + human_judgment: false + - id: D6 + description: "Error block: hint and Try with {bin}, -v chain, Reported silent, unknown command suggestion, scrubbed Bearer token" + requirement: D-18 + verification: + - kind: unit + ref: "modules/bonfire/help_smoke_test.go#TestErrorRenderSmoke" + status: pass + - kind: unit + ref: "modules/bonfire/example_test.go#ExampleWithHint" + status: pass + human_judgment: false + - id: D7 + description: "Duplicate command names and reserved global flags rejected at boot; todo closed" + verification: + - kind: unit + ref: "modules/bonfire/list_smoke_test.go#TestListTracerSmoke" + status: pass + human_judgment: false + - id: D8 + description: "Degradation matrix: -q keeps data and errors, -n returns defaults and Secret fails with a hint on a terminal stdin, ConfirmProceed never asks off a terminal, non-TTY bytes unchanged; ported models render frames" + requirement: D-03 + verification: + - kind: unit + ref: "modules/bonfire/widgets_smoke_test.go#TestWidgetMatrixSmoke" + status: pass + - kind: unit + ref: "modules/bonfire/output_test.go#TestNonTTYSpinner, TestNonTTYProgress, TestNonTTYTable, TestColorPolicy; prompts_test.go#TestSecretReadsPlainStdinLine, TestClosedStdinDoesNotHang (unchanged files)" + status: pass + - kind: other + ref: "go test -race -short ./modules/bonfire/... ./modules/diary/..." + status: pass + human_judgment: false + - id: D9 + description: "Live spinner, progress bar, process view, huh prompts and the arrow-key selector look and behave like SSU on a real terminal; diary log lines print above a running view" + requirement: D-01 + verification: + - kind: manual_procedural + ref: "throwaway program run under script(1) on a pty: spinner, progress, Process with diary logs, table; exit 0, logs above the view" + status: pass + human_judgment: true + rationale: "Animation, colors and keyboard interaction on a real terminal cannot be asserted by the short suite; the pty run only showed no hang and correct ordering. Plan 4 adds goldens; a person should try Choice/Ask/Secret on a terminal." + +duration: 28min +completed: 2026-10-21 +status: complete +--- + +# Phase 19.8 Plan 02: bonfire console layer Summary + +**bonfire reaches the Artisan bar: global -v/-q/--ansi/-n/--env flags that reach diary before plugins boot, the grouped list and per-command help with examples, a JSON catalog and CheckDocs, a styled error block with Try hints and exit codes 0/1/2, duplicate-name rejection, and SSU's spinner, progress, process view and selector on charm v2 that fall back to today's plain lines, with the Output interface unchanged.** + +## Performance + +- **Duration:** 28 min +- **Started:** 2026-10-21T10:02:41Z +- **Completed:** 2026-10-21T10:31:06Z +- **Tasks:** 3 +- **Files modified:** 38 (3,505 insertions, 353 deletions) + +## Accomplishments + +- Tracer: `./bin/hello` prints `SummerCMS hello (framework devel)`, usage, the D-13 global option lines and the commands grouped by namespace; `-vv list` works; an unknown command exits 2; a missing DSN exits 1 with `database.dsn` on stderr. +- Generated main and `summer`: `bonfire.PeekGlobals` first, `diary.Bootstrap(os.Stderr, diary.FromGlobals(g))`, `compass.Open(compass.Options{Dir: "config", Env: g.Env})`, `bonfire.Fail` for errors before the root exists, `bonfire.NewRoot(..., bonfire.WithVersion(bonfire.BuildVersion()), bonfire.OnGlobals(diary.ApplyGlobals))`, `os.Exit(...)` of `bonfire.Execute`. +- Help: Description, Usage, Arguments, Options (`[default: "*"]`, `[default: 90]`, "(multiple values allowed)"), Global options, Help, Examples; `help` command replaces cobra's. +- JSON catalog (`list --format=json`, `bonfire.Describe`), `Hidden` with `list --all`, `bonfire.CheckDocs`. +- Error block: ` ERROR ` badge, scrubbed message, explanation and Try lines from `WithHint` (`{bin}` replaced), cobra suggestions as Try, "Run with -v for details." or the `Caused by:` chain at `-v`; `Reported` prints nothing. +- Widgets: SSU's spinner, gradient progress bar and process view ported to bubbletea v2; lipgloss boxed tables; huh Ask/Confirm/Secret and a single-select selector with SSU's confirmation screen; `-q`, `-n`, `--no-ansi` and non-TTY degrade to plain lines; `ConfirmProceed`, `Process`, `Current`, `Failed`, `PrintAbove`; diary's styled log lines print above a running view. +- Docs: `docs/console/introduction.md` (list, global options, exit codes, help commands), `docs/console/writing-commands.md` (duplicates, Examples and hidden commands, Errors and hints, Documentation coverage, the degradation table, ConfirmProceed and Process), `docs/services/logging.md` (log lines above live views); bonfire and diary READMEs. + +## Task Commits + +1. **Task 1: tracer (global flags, list, exit codes)** - `8d4d7ce` feat(bonfire): global flags, Artisan list and exit codes +2. **Folded todo** - `80fae5c` docs(19.8): close the duplicate command names todo +3. **Task 2: help, examples, JSON listing, error hints** - `fb32886` feat(bonfire): Artisan help, examples, JSON listing and error hints +4. **Task 3: widgets on charm v2** - `3f38862` feat(bonfire): SSU widgets on charm v2 behind the Output interface + +`commits: 5` is measured from the ledger (`b7c0759..3f38862`); one of them, `cb38c49` docs(notes), belongs to another session that committed in between. This plan made four commits before this SUMMARY. + +The tracer feedback gate re-ran Task 1's automated verify after the commit (all green, no human check), so expansion continued. + +## Charm versions installed + +`charm.land/fang/v2` v2.0.1 (Task 1), `charm.land/bubbletea/v2` v2.1.0, `charm.land/bubbles/v2` v2.2.1 and `charm.land/huh/v2` v2.0.3 (Task 3), the versions of `.planning/notes/2026-10-21-console-charm-stack.md`. `go mod tidy` also raised `charmbracelet/ultraviolet`, `x/ansi` (v0.11.9) and `mattn/go-runewidth` (v0.0.30) as the new modules require, and pulled fang's man-page helpers (`muesli/mango*`, `muesli/roff`) as indirect requirements. Nothing imports `gopkg.in/yaml.v3`. + +## Exported names for plan 3 + +- **Commands and help:** `Command.Examples []Example`, `Command.Hidden bool`, `Example{Command, Description}`; `CheckDocs(commands []Command) []DocProblem`, `DocProblem{Command, Missing}` with `String()`; `Describe(name, commands, v) Listing`, `Listing`, `ApplicationInfo`, `CommandInfo`, `ArgumentInfo`, `OptionInfo`, `ExampleInfo`. +- **Errors:** `WithHint(err, explanation, try) error`, `HintError{Err, Explanation, Try}`, `Reported(err) error`, `ErrReported`, `Usage(err) error`, `UsageError{Err}`, `RenderError(w, name, err, g)`, `Fail(w, name, err, g) int`, `Execute(ctx, root, args) int`, `ExitOK`, `ExitFailure`, `ExitUsage`, `ErrDuplicateCommand`, `ErrReservedFlag`. +- **Globals:** `Globals{Verbosity, Quiet, Color, NoInteraction, Env}`, `PeekGlobals(args)`, `(Globals).Args()`, `RootOption`, `WithVersion`, `OnGlobals`, `Version{App, Framework, Local}`, `BuildVersion()`, `(Version).Header(name)`; diary `FromGlobals`, `ApplyGlobals`. +- **Output helpers:** `ConfirmProceed(out, question) (bool, error)`, `Process(ctx, out, operation, items, fn) error`, `GlobalsOf`, `VerbosityOf`, `IsQuiet`, `Interactive`, `Current`, `Failed`, `ItemProgress`, `PrintAbove`, `ErrAborted`. + +All names match the plan. `summer dev`'s child binary can get the verbosity forwarded with `bonfire.GlobalsOf(out).Args()`. + +## g15office + +Task u4rtpcl1 (the duplicate command names todo) was **not** moved to Live: no g15office tool is available in this executor session. The todo is closed locally (`80fae5c`); the orchestrator or the user should move u4rtpcl1 to Live with a comment such as "Done in Phase 19.8 plan 02 (8d4d7ce): bonfire.NewRoot rejects duplicate command names". + +## Decisions Made + +See `key-decisions` in the front matter. The two worth a second look: + +- **Error stream.** The error block goes to stderr in real binaries (`NewRoot` sets the cobra error stream to `os.Stderr` when its output is `os.Stdout`, as the old `main` printed errors there). `Output.Error` still writes where it did before (stdout for `NewRoot`), so no command's output moves between streams. +- **`--no-ansi` prompts.** On an interactive terminal, `--no-ansi` keeps prompts working but uses the plain line prompts instead of huh and the selector. `-n` and `-q` are the switches that turn prompts into defaults. + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 2 - Correctness] Error block on stderr** +- **Found during:** Task 1 +- **Issue:** `NewRoot(name, commands, out)` passed `out` as the error stream, so fang's error handler would have printed errors on stdout in every binary, against D-18 ("on stderr") and the plan's own `TestMigrateWithoutDSNFailsLoudly` (stderr must name `database.dsn`). +- **Fix:** `NewRoot` calls `root.SetErr(os.Stderr)` when `out` is the process stdout; the console's `Output.Error` stream is unchanged. +- **Files modified:** modules/bonfire/root.go +- **Commit:** 8d4d7ce + +**2. [Rule 1 - Bug] Re-executed roots kept the previous run's globals** +- **Found during:** Task 1 (smoke test) +- **Issue:** pflag count flags add to their current value, so a root executed twice kept `-vvv`. +- **Fix:** `Execute` resets the root's persistent flags to their defaults before each run. Command-local flags still persist between executions of one root (pflag behaviour, unchanged); the Task 2 smoke tests build a fresh root per run. +- **Files modified:** modules/bonfire/globals.go, modules/bonfire/execute.go, modules/bonfire/help_smoke_test.go +- **Commits:** 8d4d7ce, fb32886 + +**3. [Rule 3 - Blocking] Go fences in docs need runnable `src=` examples** +- **Found during:** Task 2 (docs checker) +- **Fix:** added `ExampleExample`, `ExampleWithHint` and `ExampleCheckDocs` to `modules/bonfire/example_test.go` (with `// Output:`) and referenced them from `docs/console/writing-commands.md`; `summer docs:sync` filled the copies. +- **Commit:** fb32886 + +**4. [Rule 2 - Docs checker] Commands of the docs checker** +- **Found during:** Task 1 +- **Fix:** `TestCommandWord` cases for the global flags (`bin/acme -v serve`, `--env staging`, `--env=dev`, `summer -q --no-ansi -n list`) and `TestDocsCommandNames` wants for `list` and `help`. Files not in the plan's list: `internal/docsite/checks_test.go`, `cmd/summer/docs_test.go`. +- **Commit:** 8d4d7ce + +**5. [Rule 2 - Correctness] Output printed during a live view** +- **Found during:** Task 3 (pty run) +- **Issue:** the plan routes only diary lines through `PrintAbove`; a command that prints with `Println`, a status line or a table inside `Spinner` (the README's own example does) would tear the view. +- **Fix:** while a live view runs, complete lines for stdout print above it; a widget started inside a running view prints plain lines (one live view at a time). +- **Files modified:** modules/bonfire/output.go, modules/bonfire/live.go +- **Commit:** 3f38862 + +**6. [Interpretation - D-03] `--no-ansi` on a terminal uses the line prompts** +- The plan defines interactive as "stdin and stdout are terminals and not noInteraction", which would run huh under `--no-ansi`. D-03 lists `--no-ansi` among the cases with no styled widgets, so huh and the selector need `Color != ColorNever` too; the line prompts (unchanged code path) serve `--no-ansi`. +- **Commit:** 3f38862 + +### Recorded differences + +**7. Acceptance check `go list -deps ./modules/bonfire | grep -cE '^charm.land/(bubbletea|bubbles|huh|fang|lipgloss)/v2$'` prints 4, not 5.** The check counts packages; `charm.land/bubbles/v2` has only a doc package at its root, and bonfire imports `bubbles/v2/spinner`, `bubbles/v2/progress` and `bubbles/v2/key` (as the plan asks). Counted per module (`go list -deps -f '{{.Module.Path}}' ./modules/bonfire | sort -u`), the result is 5. No blank import was added to satisfy the literal grep. + +**8. examples/hello go.mod/go.sum unchanged.** As in plan 01, the example modules build and test inside the repository's go.work without changes (deferred-items.md, "examples/hello go.mod is stale outside the workspace"). + +**9. `TestMigrateWithoutDSNFailsLoudly` builds the binary** (`runSummerBuild`) to check stderr, so it takes about half a second more. + +**10. Builtin `list` and `help` carry `Long` and `Examples`** (not specified), so plan 3's framework `CheckDocs` test can cover them without exceptions. + +--- + +**Total deviations:** 6 auto-fixed or interpreted (2 correctness, 1 bug, 1 blocking, 1 docs checker, 1 D-03 interpretation), 4 recorded differences. **Impact:** no change to the plan's exported names; the Output, Input and Progress interfaces are byte-identical to the plan's base commit. + +## Issues Encountered + +None beyond the deviations. `go test -count=1 -short ./...` was green after every task (no wristband flake this time). + +## Known Stubs + +None. + +## Threat Flags + +None beyond the plan's threat model. The only new terminal input path is huh and the selector, which run only when stdin and stdout are terminals, `-n`/`-q` are off and `--no-ansi` is not set (T-19.8-08); live views never read stdin (T-19.8-11); error text is scrubbed and the chain shows only at `-v` (T-19.8-07). + +## Verification + +- `go vet ./...`: clean. +- `go test -count=1 -short ./...`: green after each task. +- `go test -count=1 -race -short ./modules/bonfire/... ./modules/diary/...`: green. +- `go -C examples/hello test -count=1 ./...`: green. +- By name: `TestListTracerSmoke`, `TestHelpSmoke`, `TestListJSONSmoke`, `TestErrorRenderSmoke`, `TestHelpUsesSharedAdapter`, `TestWidgetMatrixSmoke`, `TestNonTTYSpinner`, `TestNonTTYProgress`, `TestNonTTYTable`, `TestColorPolicy`, `TestSecretReadsPlainStdinLine`, `TestClosedStdinDoesNotHang`, `TestBuiltBinaryListsCommands`, `TestHelpListsServe`, `TestBuiltBinaryUnknownCommandExitsNonZero`: `--- PASS`. +- `TestDocsTree` and `summer docs:build --check`: no problems. +- Acceptance criteria of all three tasks: pass, except the package-count check recorded above (module count 5). +- `modules/bonfire/output_test.go` and `prompts_test.go`: no commits since `cdddaa3`. +- No consuming application named in changed files. Nothing pushed or tagged. + +## User Setup Required + +None - no external service configuration required. + +## Next Phase Readiness + +Ready for 19.8-03: commands can use `Examples`, `Hidden`, `WithHint`, `Reported`, `Usage`, `ConfirmProceed`, `Process` and `GlobalsOf`; the framework coverage test can call `bonfire.CheckDocs`. Before the sweep's apps boot with the new main, Pitfall 15 still applies: build each app and run `bin/ list` to catch duplicate command names. A person should try the live widgets and prompts on a real terminal (coverage D9). + +## Self-Check: PASSED