diff --git a/.planning/phases/19.8-console-commands-overhaul/19.8-RESEARCH.md b/.planning/phases/19.8-console-commands-overhaul/19.8-RESEARCH.md new file mode 100644 index 00000000..33221ae4 --- /dev/null +++ b/.planning/phases/19.8-console-commands-overhaul/19.8-RESEARCH.md @@ -0,0 +1,794 @@ +# Phase 19.8: Console commands overhaul - Research + +**Researched:** 2026-10-21 +**Domain:** Go CLI UX (cobra + charm v2 stack), log/slog architecture, cross-repo command sweep +**Confidence:** HIGH for stack, APIs and inventory (read from source and probed); MEDIUM for UX semantics the CONTEXT leaves open (flagged in Open Questions) + + +## User Constraints (from CONTEXT.md) + +### Locked Decisions + +#### Console stack and look +- **D-01:** The console look follows SSU's terminal UX (`/media/nvme/dev/pxpx/ssu/internal/cli/tui`): spinner with counter and current item, multi-row live process view, progress model, arrow-key selector with confirmation screen, lipgloss status styles. The earlier Scala `summer-bonfire` design was itself modelled on SSU; the braille spinner and cyan-to-green gradient bar already in Go `bonfire` came from it. — **Reversibility:** costly — every command's output and the widget API depend on it +- **D-02:** Adopt the charm stack in `bonfire`: `charmbracelet/lipgloss` (styling, tables), `charmbracelet/bubbles` + `bubbletea` (live views, spinner, progress), `charmbracelet/huh` (prompts), `charmbracelet/fang` (cobra styling and error rendering). A decision note naming these dependencies and versions lands before any code (project rule: new dependency needs a decision note). Port SSU's widgets rather than inventing new ones. The existing `bonfire.Output` interface stays source-compatible for plugins (no breaking changes in shared plugins). — **Reversibility:** costly — adds dependencies to every application binary; removing them later means rewriting the widgets +- **D-03:** Every widget degrades on non-TTY, `-q`, `--no-ansi` and `-n`: no animation, plain lines, prompts return defaults (or fail with a hint when no default exists). + +#### What `serve` shows +- **D-04:** At default verbosity `serve` prints a startup summary and then **all INFO logs**, restyled through `diary`'s console handler (consistent prefixes per component instead of today's raw `slog` text mixed with styled lines). `-q` reduces to warnings/errors; `-v`/`-vv`/`-vvv` add debug detail. +- **D-05:** The output format follows the TTY: styled console output on a terminal; structured logs (JSON or logfmt per channel config) when stdout is not a TTY (systemd, Docker). The startup summary is also emitted as log records, so journald keeps it. +- **D-06:** Startup summary is a compact aligned block of about 8–12 lines: app name and version (plus framework version), environment, URL(s), database and migration state ("up to date" / "3 pending"), plugins (count and names), queue and scheduler, mail, cache and realtime drivers. `-v` adds per-plugin details. +- **D-07:** Shutdown (Ctrl+C / SIGTERM) prints a short report: draining, requests drained, workers stopped with job counts (done, in flight returned to the queue), uptime, and any drain errors. The same pattern applies to other long-running commands (`queue:work`, `dev`). + +#### Logging (`diary`) +- **D-08:** `config/log.yaml` mirrors Laravel's `config/logging.php` shape: `log.default` names the default channel, `log.level` the global level, `log.channels` is a map of named channels with `driver` (`console`, `stderr`, `daily`, `stack`, `null`), `level`, `format`, `path`, `days`, and `channels` for a stack. `SUMMER_LOG_*` env overrides work through `compass` as for any key. This replaces the `log.level`/`log.channel` pair in the roadmap text with `log.level`/`log.default`. — **Reversibility:** costly — published config keys in every application's `config/log.yaml` + ```yaml + # config/log.yaml + default: stack + level: info + channels: + stack: + driver: stack + channels: [console, daily] + console: + driver: console # styled on TTY, json/logfmt otherwise + format: auto + daily: + driver: daily + path: storage/logs/summer.log + days: 14 + format: json + level: warn + components: + river: warn + surf: info + ``` +- **D-09:** Per-component levels through a `log.components` map (`river: warn`, `journal: debug`). Plugins and modules get a named logger from `diary` (for example `diary.For("journal")`) instead of `slog.Default`; verbosity flags raise every component. +- **D-10:** Daily log files are **on everywhere** by default, like Laravel: `summer new` and the framework defaults use a `stack` of `console` + `daily` at `storage/logs/`, with retention (14 days default). Console output stays on as well. +- **D-11:** The request log is INFO from `surf` under a named `http` component (method, path, status, duration, request id), so it shows by default and can be silenced with `components.http: warn`. +- **D-12:** `sunscreen` redaction wraps every channel; `sunscreen.Install`/`InstallDefault` keep working as compatibility shims. + +#### Help and `list` +- **D-13:** Bare `` / `summer` prints an Artisan-style list: version header (`SummerCMS acme v1.4.0 (framework v0.3.2)`), usage, global options, then commands grouped by namespace (top-level first, then `admin`, `make`, `migrate`…) with aligned descriptions. Styled with lipgloss in the same palette as the rest of the console. + ``` + SummerCMS acme v1.4.0 (framework v0.3.2) + + Usage: + bin/acme [options] [arguments] + + Options: + -h, --help Display help for the given command + -q, --quiet Do not output any message + -v|vv|vvv Increase verbosity + --ansi|no-ansi Force (or disable) ANSI output + -n, --no-interaction Do not ask any interactive question + --env The environment the command runs under + + Available commands: + list List commands + migrate Run the database migrations + serve Serve the application + admin + admin:create Create a backend administrator + admin:token:create Create a personal API token + make + make:command Create a new console command + make:model Create a new model and migration + ``` +- **D-14:** Per-command help uses the Artisan layout: Description / Usage / Arguments / Options (with defaults) / Examples. fang is used for styling and error rendering, not for its own help layout. + ``` + Description: + Create a personal API token for a backend admin + + Usage: + admin:token:create [options] + + Arguments: + login The admin's login + + Options: + --scopes=SCOPES Comma-separated scopes [default: "*"] + --expires=DAYS Days until expiry [default: 90] + --json Print the token as JSON + + Examples: + # Token for the MCP server, journal only + bin/acme admin:token:create jane --scopes=journal.* + + # Script-friendly output + bin/acme admin:token:create jane --json + ``` +- **D-15:** New additive `bonfire.Command` fields: `Examples []Example` (command line + description, rendered as a section) and `Hidden bool` (runnable, left out of `list`; `list --all` shows it). Additive only, so existing plugins compile unchanged. +- **D-16:** `list --format=json` outputs every command with arguments, options and examples, for docs checks and AI agents. +- **D-17:** Documentation coverage check: `bonfire` exports a helper that reports commands lacking a description or examples. It **fails** for `summercms.go`'s own commands and **warns only** in plugin repos. Roadmap success criterion 3 is reworded to match. + +#### Errors +- **D-18:** Errors render as a styled block: the cause, a short explanation, then a `Try:` hint with the exact command that fixes it, then "Run with -v for details". `-v` adds the wrapped error chain. Exit codes: 1 for failure, 2 for usage errors. + ``` + ERROR No application found in /home/jane/acme/plugins. + + summer make:command needs an application root (summer.yaml). + + Try: + cd ..; bin/acme make:command acme.blog:publish + + Run with -v for details. + ``` +- **D-19:** Hints for known failure modes are part of the sweep: run from the wrong directory (suggest `cd ..; bin/ `), pending migrations (suggest `bin/ migrate`), missing config or env, database unreachable. Detecting the database when run from the wrong directory is **not** fixed; only the message improves. + +#### Sweep +- **D-20:** The sweep covers `summercms.go` and every shared plugin repo with commands: `sm-paymentgateway-plugin`, `sm-oauthserver-plugin`, `sm-journal-plugin`, `sm-sitemap-plugin`, `sm-chat-plugin`, `sm-chatchannels-plugin`, `sm-chatvideo-plugin`, `sm-faq-plugin`, `sm-menu-plugin`, `sm-herald-plugin`, `sm-forum-plugin`, `sm-pgstripe-plugin`, and `sm-user-plugin`. Changes per plugin: description, long help, examples, named loggers, output polish. One commit per plugin repo, pushed first, then pointer bumps in the apps as separate commits. No breaking changes to command names or flags. +- **D-21:** The Artisan checklist applies where it fits, not uniformly: `--force` confirmation on destructive commands, `--pretend` on migrate-like and bulk writers, `--json` on commands whose output scripts read (lists, tokens, status), progress for bulk loops. The plan records each command's checklist result so the gaps are explicit. +- **D-22:** One-shot commands end with a result line and a meaningful exit code. + +### Claude's Discretion +- Exact palette, symbols and spacing, as long as they match SSU's styles and stay consistent between `list`, help, widgets and log lines. +- Console handler choice inside `diary` (lipgloss-styled handler of our own, or `charmbracelet/log`), to be settled in the dependency decision note. +- Exact semantics of `--env` and `-n` defaults, following Artisan. + +### Folded Todos +- **Reject duplicate console command names in bonfire.NewRoot** (`.planning/todos/pending/bonfire-duplicate-command-names.md`): `NewRoot` fails with an error naming the duplicated command; update the bonfire README and `docs/console/writing-commands.md`. Fits plan 2. +- **Scaffolded files carry a DO NOT EDIT header, and make:command cannot reach the application** (`.planning/todos/pending/scaffold-generated-header-and-command-deps.md`): folded **partially**. The scaffolder itself is not changed; the errors these commands throw get proper messages with actionable hints (D-18, D-19). The todo stays open for the scaffolder fix. + +### Deferred Ideas (OUT OF SCOPE) +None; the discussion stayed within phase scope. + +#### Reviewed Todos (not folded) +- The 20 other keyword matches (benchmark, cache purge, uploads XSS, plugin ports, OIDC parity, etc.) are unrelated to the console and stay in their own phases or todos. + + + +## Phase Requirements + +No requirement IDs are mapped (null). The sources are the ROADMAP success criteria (SC1..SC5) and D-01..D-22. Related completed requirement: CLI-01 (rich output with non-TTY degradation, summer-bonfire design), which this phase must not regress. + +| Source | Description | Research support | +|--------|-------------|------------------| +| SC1 | `diary` module: `log.default/level/channels/components`, stderr, stack, daily+retention+JSON, styled console handler, component loggers, sunscreen on every channel, daily files on by default; charm stack with decision note first | Standard Stack, Pattern 1-3, Code Examples (diary), Pitfalls 4-8 | +| SC2 | Global flags `-v/-vv/-vvv`, `-q`, `--ansi/--no-ansi`, `-n`, `--env`; verbosity sets diary level at runtime (River, plugins) | Pattern 4 (globals pre-scan + persistent flags), probe results, Pitfalls 1-3 | +| SC3 | Description + long help + examples + documented args/options with defaults; namespaced `list`; `help `; coverage check fails in framework, warns in plugins | Pattern 5 (help/list), Command Inventory, Validation Architecture | +| SC4 | Sweep with Artisan checklist; serve/queue:work startup summary + shutdown report; result lines and exit codes | Command Inventory with per-command checklist, Pattern 6 (summary/shutdown), Pitfalls 9-14 | +| SC5 | READMEs, docs console+logging pages, landing site, roadmap presentation; unit tests in last plan | Docs Locations, Validation Architecture | + + +## Project Constraints (from CLAUDE.md) + +- Standard library first; a new dependency needs a research doc or a decision note naming it (`.planning/notes/`). The decision note for the charm stack lands before code (D-02). [VERIFIED: CLAUDE.md] +- YAML via `goccy/go-yaml`, never `gopkg.in/yaml.v3`. (fang/huh pull `gopkg.in/yaml.v3` only as an indirect test dependency of testify; nothing in this repo may import it.) [VERIFIED: CLAUDE.md; proxy go.mod of fang v2.0.1] +- `go vet ./...` and `go test ./...` green at every commit; Docker tests skip under `-short` with `flag.Parse()` + `testing.Short()` in TestMain. [VERIFIED: CLAUDE.md] +- New module: beach name, README in the standard structure (H1, summary sentence, import line, `**Name:**`, Overview, Features, Usage, API reference, Configuration, CLI commands, Dependencies, Testing), and a row in the root `README.md` table reusing the summary sentence. [VERIFIED: CLAUDE.md] +- Exported API, config key, CLI command or dependency change updates the module README and the affected `docs/` pages in the same commit; every identifier in README/docs must exist (docs checker); config keys are checked by hand. [VERIFIED: CLAUDE.md] +- Framework code and docs never name a consuming application (use `acme`, `blog`). Gate scripts in this repo split app names (`"fono""teka.go"`) to dodge the checker. [VERIFIED: CLAUDE.md; scripts/check-phase19.4.sh:62] +- Core plugins (user, blog, pages, payment...) must not get breaking changes unless asked. [VERIFIED: ~/.claude/CLAUDE.md, ../CLAUDE.md] +- Plugins: commit and push the plugin first, then the pointer bump in the app as a separate commit. No push/tag/deploy without the user's go-ahead. No co-author tags. Planning docs and code in separate commits. Stage files by path; never `git add -A`; several sessions run concurrently. [VERIFIED: CLAUDE.md] +- MSD overrides: lean plans (plan count confirmed with the user: 4); unit tests are the last plan; STATE.md via `msd_run query state.*`; g15office comment when the phase advances. [VERIFIED: CLAUDE.md] +- Release tags get a summercms.io changelog post the same day (only if this phase is tagged). [VERIFIED: CLAUDE.md] + +## Summary + +The console layer today is a thin cobra adapter (`modules/bonfire`) with hand-written ANSI widgets, and logging is a single `slog.NewTextHandler(os.Stderr)` wrapped by `sunscreen` and installed by the generated `main` before config loads. About 60 call sites across framework modules log through `slog.Default()` or a per-module `loggerFromApp(app)` helper that falls back to `slog.Default()`; River gets that logger through `river.Config.Logger`; GORM uses its own default logger that writes straight to stdout. That is exactly why `serve` output looks mixed today. + +The charm stack must be the **v2 line on `charm.land/...` import paths**, not the v1 line SSU pins (`github.com/charmbracelet/lipgloss v1.0.0`, `bubbletea v1.3.0`, `bubbles v0.20.0`). fang v1.0.0 already depends on `charm.land/lipgloss/v2` (beta), so mixing SSU's v1 versions with any fang would compile two lipgloss majors into every binary. All five v2 modules (lipgloss v2.0.6, bubbletea v2.1.0, bubbles v2.2.1, huh v2.0.3, fang v2.0.1) declare `go` ≤ 1.26 and work with this repo's `go 1.27.0`. Porting SSU's widgets means translating v1 APIs: `View() string` becomes `View() tea.View`, `tea.KeyMsg` becomes `tea.KeyPressMsg`, `progress.WithDefaultGradient/WithGradient` become `WithDefaultBlend/WithColors`, and `bar.Width = n` becomes `SetWidth(n)`. lipgloss v2 `Render()` always emits ANSI, so `--no-ansi`/`NO_COLOR` must be applied at the writer through `colorprofile.Writer`. A probe showed the full stack adds about 2.1 MB to a stripped binary. + +fang is the weakest fit. `fang.Execute` always calls `root.SetHelpFunc`, sets `root.Version` (unless `WithoutVersion()`), adds a hidden `man` command (unless `WithoutManpage()`), and reads the color profile from the environment only. Its exported value for us is `ErrorHandler`/`WithErrorHandler` and the `Styles`/`ColorScheme` types. Errors that occur before the root exists (config load in the generated `main`) never pass through it. Recommendation: keep fang (D-02 locks it) but use it narrowly: `fang.Execute(ctx, root, fang.WithoutVersion(), fang.WithoutManpage(), fang.WithoutCompletions(), fang.WithErrorHandler(bonfire.renderError))`, set the Artisan help func on **every subcommand** (cobra resolves a command's own `helpFunc` before its parent's), and render the root's help/list through root `RunE` plus our own `help` command. The bonfire error renderer also has to be callable directly from `main`. For `diary`, use stdlib `slog.NewMultiHandler` (stack, Go 1.26+), `slog.JSONHandler`/`TextHandler` (json/logfmt), `slog.DiscardHandler` (null), a stdlib daily-rotating writer, and **our own lipgloss console handler**. `charm.land/log/v2` fails `testing/slogtest` on group semantics (probe output below), so it is the wrong choice for a handler that sits under `sunscreen` and every module. + +**Primary recommendation:** Plan 1 writes the decision note (charm v2 set + "no charm log, no lumberjack"), then builds `diary` (proxy-backed `diary.For`, component filter, channels, daily writer) and rewires the generated `main`. Plan 2 adds globals pre-scan + persistent flags, Artisan help/list/JSON, error renderer with hints, duplicate rejection and charm-backed widgets behind the unchanged `Output` interface. Plan 3 sweeps 52 framework commands (18 summer tool, 15 delegates, 19 app binary) plus the 2 in-repo examples, and 26 plugin commands with a recorded checklist, startup summary and shutdown report, and the docs/landing/roadmap updates. Plan 4 adds goldens, slogtest, TTY/non-TTY matrix and the phase gate script. + +## Architectural Responsibility Map + +| Capability | Primary Tier | Secondary Tier | Rationale | +|------------|-------------|----------------|-----------| +| Global flag parsing (-v/-q/-n/--env/--ansi) | `bonfire` (cobra persistent flags + `PeekGlobals` pre-scan) | generated `main` (applies env before `compass`) | Flags must be known before config load and plugin boot, which happen before cobra parses | +| Log routing, levels, channels, rotation | `diary` (new framework module) | `sunscreen` (redaction leaf wrapper) | Single owner of slog default; modules ask it for named loggers | +| Console styling palette | `bonfire` (exported styles) | `diary` console handler imports them | One palette for list/help/widgets/log lines (D-01 discretion) | +| Help, list, JSON catalog, coverage check | `bonfire` | `cmd/summer` docs checker consumes JSON/coverage | Commands are bonfire values; bonfire owns rendering | +| Error rendering + hints + exit codes | `bonfire` (`Hint` error type, renderer, `Execute`) | modules wrap known failures (`lagoon`, `internal/build`) | Hints are domain knowledge; rendering is console concern | +| Startup summary / shutdown report | `surf` (serve), `conga` (queue:work, schedule:run), `cmd/summer` (dev) | `lagoon` (pending-migration count), `conga` (job counters) | Each long-running command knows what it booted | +| Request log | `surf` (`http` component) | `diary` | Already in `surf/accesslog.go`; only the logger and defaults change | +| Third-party loggers (River, GORM) | `conga`, `lagoon` | `diary.For("river")`, `diary.For("db")` | Owners of the client configs | +| Plugin command help/loggers | each `sm-*-plugin` repo | apps (pointer bumps) | D-20 one commit per plugin repo | + +## Standard Stack + +### Core (new direct dependencies, all need the decision note) + +| Library (import path) | Version | Purpose | Why standard | +|---|---|---|---| +| `charm.land/lipgloss/v2` | v2.0.6 (2026-08-11) | Styles, `lipgloss/v2/table` for boxed tables, layout for list/help/errors | De facto Go terminal styling; v2 is the only line fang/huh v2 build on [VERIFIED: proxy.golang.org @latest + go.mod] | +| `charm.land/bubbletea/v2` | v2.1.0 (2026-10-08) | Program runtime for live spinner/process/progress views | SSU's runtime; v2 API (`View() tea.View`, `KeyPressMsg`) [VERIFIED: proxy + UPGRADE_GUIDE_V2.md] | +| `charm.land/bubbles/v2` | v2.2.1 (2026-08-24) | `spinner` (MiniDot), `progress` (gradient bar), `key`/`help` | SSU's components [VERIFIED: proxy + module source] | +| `charm.land/huh/v2` | v2.0.3 (2026-03-10) | Ask/Confirm/Choice/Secret on an interactive TTY; `WithAccessible` | Charm's prompt library; `Form.WithInput/WithOutput/WithAccessible`, `ErrUserAborted` exist [VERIFIED: module source form.go:54-61, 235-351] | +| `charm.land/fang/v2` | v2.0.1 (2026-03-11) | Error rendering hook (`WithErrorHandler`) and Windows VT enabling around cobra | Locked by D-02; use narrowly (see Pattern 5) [VERIFIED: module source fang.go:21-170] | +| `github.com/charmbracelet/colorprofile` | v0.4.3 (transitive, imported directly) | `colorprofile.Writer` to apply `--ansi/--no-ansi/NO_COLOR` to lipgloss v2 output | lipgloss v2 `Render()` always emits ANSI; downsampling is the writer's job [CITED: lipgloss v2 UPGRADE_GUIDE_V2.md "Printing and Color Downsampling"] | + +Module go directives: lipgloss `go 1.25.0`, bubbletea `go 1.26.0`, bubbles `go 1.25.0`, huh `go 1.25.8`, fang `go 1.25.0`; repo `go 1.27.0`; toolchain here `go1.27.2`. [VERIFIED: proxy .mod files; go.mod:3; `go version`] +MVS side effects: fang requires `spf13/cobra v1.9.1`/`pflag v1.0.6`, repo keeps `cobra v1.10.2`/`pflag v1.0.9`; `golang.org/x/text` stays at repo's v0.42.0. [VERIFIED: proxy go.mod + repo go.mod] + +### Supporting (stdlib, no new dependency) + +| Package | Purpose | When | +|---|---|---| +| `log/slog` `NewMultiHandler` | `stack` driver fan-out; calls only enabled handlers, clones the record, joins errors | Go 1.26+ [VERIFIED: $GOROOT/src/log/slog/multi_handler.go:35-45] | +| `log/slog` `JSONHandler`, `TextHandler`, `DiscardHandler`, `LevelVar` | json / logfmt / null / runtime levels | [VERIFIED: `go doc log/slog.DiscardHandler`] | +| `testing/slogtest` | Conformance test for the console and daily handlers | Plan 4 | +| `runtime/debug.ReadBuildInfo` | App and framework versions for the list header | See Pattern 5 [VERIFIED: probe below] | +| `gorm.io/gorm/logger.NewSlogLogger` | Route GORM's logger through `diary.For("db")` | Already in gorm v1.31.2 [VERIFIED: gorm logger/slog.go:24-32] | + +### Alternatives Considered + +| Instead of | Could use | Tradeoff / verdict | +|---|---|---| +| charm v2 (`charm.land/*/v2`) | SSU's v1 pins (`github.com/charmbracelet/*` v1) | Rejected: fang v1.0.0 requires `charm.land/lipgloss/v2 v2.0.0-beta.3...` → two lipgloss majors; v1 is maintenance-only. Port SSU code with the v2 API renames. [VERIFIED: proxy go.mod of fang v1.0.0] | +| Own lipgloss console handler | `charm.land/log/v2` v2.0.1 | Rejected: fails `testing/slogtest` (WithGroup treated as a prefix, empty attrs printed, empty-key groups not inlined). It also brings its own level and prefix model that duplicates diary's. [VERIFIED: probe output in Code Examples] | +| Stdlib daily writer (~120 lines) | `gopkg.in/natefinch/lumberjack.v2` (SSU uses it) | Rejected: rotates by **size**, not by day; no `name-YYYY-MM-DD.log` naming; would need a decision note anyway. [VERIFIED: SSU internal/logging/logging.go uses `MaxSizeMB`/`MaxBackups`] | +| `slog.NewMultiHandler` | hand-written fan-out (SSU `MultiHandler`) | Use stdlib; SSU's version does not clone records. | +| huh for prompts on TTY | keep line prompts only | huh gives the arrow-key selector D-01 asks for; keep the line-based path for non-TTY (existing tests and piped-stdin contracts). | + +**Installation (plan 1, after the decision note):** +```bash +go get charm.land/lipgloss/v2@v2.0.6 charm.land/bubbletea/v2@v2.1.0 charm.land/bubbles/v2@v2.2.1 charm.land/huh/v2@v2.0.3 charm.land/fang/v2@v2.0.1 github.com/charmbracelet/colorprofile@v0.4.3 +go mod tidy +``` +Only `modules/bonfire` (and `modules/diary` for lipgloss/colorprofile) may import them. + +## Package Legitimacy Audit + +The MSD seam only supports npm/pypi/crates (`package-legitimacy check --ecosystem go` is rejected with a usage error), so Go modules were checked by hand against proxy.golang.org and the `charm.land` vanity server. + +| Module | Registry | Age (first tag of repo) | Source repo (go-import meta / proxy Origin) | Verdict | Disposition | +|---|---|---|---|---|---| +| charm.land/lipgloss/v2 v2.0.6 | proxy.golang.org | 2021-04-02 (v0.1.0) | `charm.land/lipgloss git https://github.com/charmbracelet/lipgloss`, hash 733ce535 | OK | Approved | +| charm.land/bubbletea/v2 v2.1.0 | proxy.golang.org | 2020-04-20 | github.com/charmbracelet/bubbletea, hash f9df43c4 | OK | Approved | +| charm.land/bubbles/v2 v2.2.1 | proxy.golang.org | 2020-06-08 | github.com/charmbracelet/bubbles, hash 49094810 | OK | Approved | +| charm.land/huh/v2 v2.0.3 | proxy.golang.org | 2023-12-08 | github.com/charmbracelet/huh, hash 3c0116c6 | OK | Approved | +| charm.land/fang/v2 v2.0.1 | proxy.golang.org | 2025-06-18 | github.com/charmbracelet/fang, hash d89b30af | OK (young, 2 v2 tags) | Approved; note in decision note | +| github.com/charmbracelet/colorprofile v0.4.3 | proxy.golang.org | transitive of all above | github.com/charmbracelet/colorprofile | OK | Approved | +| charm.land/log/v2 | — | — | — | not recommended (slogtest failures) | Not added | +| gopkg.in/natefinch/lumberjack.v2 | — | — | — | not needed | Not added | + +All names come from the projects' own go.mod/README files in the module cache and the `charm.land` go-import meta tags. They were not taken from web search. [VERIFIED: curl proxy.golang.org `@latest`, `.mod`, `@v/list`; curl `https://charm.land//v2?go-get=1`] +**Packages removed due to [SLOP]:** none. **Flagged [SUS]:** none. There are no postinstall hooks (Go modules have none). + +## Architecture Patterns + +### System Architecture Diagram + +``` + argv ──► generated main ──► bonfire.PeekGlobals(args) ──► {env, verbosity, quiet, ansi, noInteraction} + │ │ + │ ├─► compass.Open({Dir:"config", Env: env}) + │ └─► diary.Setup(cfg, {Verbosity, Quiet, Color, Stdout/Stderr}) + │ │ slog.SetDefault(root logger) + ▼ ▼ + backpack.New(cfg) ─► party.Activate ─► plugins log via diary.For("") ─┐ + │ │ + ▼ ▼ + commands = lagoon/conga/surf/cabana + plugins diary root handler (proxy, swappable) + │ │ component filter (log.components + verbosity floor) + bonfire.NewRoot(name, commands, out, opts...) │ sunscreen.Wrap per channel + │ rejects duplicate names ▼ + │ persistent flags (-v -q -n --env --ansi) slog.NewMultiHandler(stack) + │ help func on every subcommand ├─ console: TTY → lipgloss lines (stderr) + │ list / help commands │ non-TTY → logfmt|json + ▼ ├─ daily: storage/logs/summer-YYYY-MM-DD.log (json), retention + bonfire.Execute(ctx, root) ─► fang.Execute(... WithErrorHandler(render)) + │ │ └─ null / stderr + │ └─ error → styled block (cause, explanation, Try:, -v chain) → exit 1|2 + ▼ + Command.Run(ctx, Input, Output) + │ Output: console impl; TTY & !quiet & !noAnsi & !noInteraction → bubbletea views (spinner/process/progress) + │ otherwise → plain lines (existing fallbacks) + │ prompts: interactive → huh; piped stdin → line read; -n → default or hinted error + ▼ + serve / queue:work / dev: startup summary (rendered block + log records) … signal … shutdown report +``` + +### Recommended Project Structure + +``` +modules/diary/ # NEW (beach name: a beach diary/logbook; README "**Name:**" line) +├── diary.go # Setup, For, SetVerbosity, Root(), Install compat +├── config.go # log.* parsing (tolerant of env strings: "14", "console,daily") +├── component.go # component filter handler + proxy handler +├── console.go # lipgloss console handler (TTY) using bonfire styles +├── daily.go # daily rotating writer + retention +├── channels.go # driver factory: console, stderr, daily, stack, null +├── *_test.go # slogtest, rotation with injected clock, env overrides +└── README.md +modules/bonfire/ +├── command.go # + Examples, Hidden (additive) +├── root.go # NewRoot(name, cmds, out, opts ...RootOption); duplicates; persistent flags +├── globals.go # Globals, PeekGlobals(args), Verbosity +├── help.go / list.go # Artisan help + list (txt/json), Catalog JSON, CheckDocs +├── errors.go # Hint, UsageError, ErrReported, render, Execute → exit code +├── styles.go # palette (ported from SSU tui/styles.go + current ANSI set) +├── output.go / widgets.go # same Output interface; charm-backed TTY path +├── live.go # ported SSU SpinnerModel / ProcessModel / ProgressModel (v2 API) +├── prompts.go # huh on TTY; line fallback unchanged +└── testdata/*.golden # list/help/error goldens +``` + +### Pattern 1: Proxy-backed named loggers (init-order safe) +**What:** `diary.For(name)` returns `slog.New(&componentHandler{name: name})`. The handler resolves the current root handler at `Enabled`/`Handle` time from an `atomic.Pointer`, so loggers created in package `var` blocks or before `diary.Setup` still route correctly after setup and after `SetVerbosity`. +**When:** All framework modules (replace `loggerFromApp` fallbacks and bare `slog.Default()`), River (`diary.For("river")`), GORM (`diary.For("db")`), plugins (`diary.For("journal")`). +**Component filter:** `Enabled(ctx, lvl)` = `lvl >= floor(name)` && `root.Enabled(ctx, lvl)`, where `floor(name)` = `components[name]` else `log.level`, lowered by verbosity. Records from plain `slog.Default()` carry no component and use `log.level`. `WithAttrs` that sets key `component` rebinds the name, so `slog.Default().With("component","x")` also works. The console handler prints the component as a fixed-width prefix; JSON/logfmt get a `component` attribute. +**Example:** +```go +// Source: design for this phase (stdlib log/slog semantics) +type componentHandler struct { + name string + attrs []slog.Attr // pre-bound; replayed onto the live root at Handle time + group []string +} + +func (h *componentHandler) Enabled(ctx context.Context, l slog.Level) bool { + return l >= current().floor(h.name) && current().handler.Enabled(ctx, l) +} + +func (h *componentHandler) Handle(ctx context.Context, r slog.Record) error { + r.AddAttrs(slog.String("component", h.name)) + return current().bind(h.attrs, h.group).Handle(ctx, r) +} +``` +Note: rebinding `WithAttrs/WithGroup` onto a swapped root needs care. Keep `attrs`/`group` on the proxy and apply them to the live handler lazily (cache per root generation) so a swap after `With(...)` keeps the attributes. + +### Pattern 2: Channels as sunscreen-wrapped leaves under a stdlib MultiHandler +**What:** Each configured channel builds a leaf handler, which is wrapped with `sunscreen.Wrap` (literal D-12; `Wrap` returns an already-wrapped handler unchanged [VERIFIED: modules/sunscreen/sunscreen.go:82-87]). A `stack` is `slog.NewMultiHandler(children...)`. Each leaf owns a `slog.LevelVar` for its `level`, and the console leaf's LevelVar is what `-q` raises to WARN. +**Level rule (recommendation):** a record reaches a channel when `level >= max(floor(component), channel.level)`. Verbosity: `-v` sets every component floor to at most INFO (so `river: warn` shows INFO), `-vv` sets DEBUG everywhere, and `-vvv` adds DEBUG plus `AddSource` on the console and GORM `Info` (SQL traces, parameterized). `-q` sets the console leaf to WARN; files are unaffected. + +### Pattern 3: Daily writer (stdlib) +`path: storage/logs/summer.log` → files `storage/logs/summer-2026-10-21.log` (Laravel naming). Under a mutex: pick the file by `now()` date, reopen on date change with `O_APPEND|O_CREATE|O_WRONLY` (0o640), `MkdirAll` (0o750), then delete `summer-*.log` whose date is older than `days` at open and on each rotation. Each record is buffered and written with one `Write` call (append writes of a whole line from `serve` and `queue:work` processes do not interleave on a local filesystem). The path is relative to the **application root** (the parent of the config dir), not the process CWD. If opening fails, warn once on the console and disable the channel. **Boot never fails because of a log file.** Inject `now func() time.Time` for tests. + +### Pattern 4: Globals before boot, flags after +The generated `main` loads config and activates plugins **before** cobra parses flags [VERIFIED: internal/build/build.go:119-148]. So: +1. `g := bonfire.PeekGlobals(args)`: a tolerant scan (stops at `--`, recognises `-v/-vv/-vvv/--verbose`, `-q/--quiet`, `-n/--no-interaction`, `--ansi/--no-ansi`, `--env X`/`--env=X`), never errors. +2. `compass.Open(compass.Options{Dir: "config", Env: g.Env})` (`Options.Env` wins over `SUMMER_ENV` [VERIFIED: modules/compass/config.go:193-199]). +3. `diary.Setup(cfg, diary.Options{Verbosity: g.Verbosity, Quiet: g.Quiet, ...})`. +4. `bonfire.NewRoot(...)` registers the same flags as cobra persistent flags so help, validation and `Input` all see them. A `RootOption` callback (`bonfire.OnGlobals(func(Globals))`) lets `main` re-apply them to `diary` in `PersistentPreRunE` (same values; keeps `bonfire` free of a `diary` import). +Cobra probe: `-vvv migrate` → 3, `migrate -vv -q -n --env staging x` → all set, `--env staging migrate` works, bare root with `RunE` runs (list), `-v` alone runs root, and **passthrough commands receive `-v` as a raw arg** (`-v serve --addr :1 -vv` → args `["-v","--addr",":1","-vv"]`), so `summer serve -v` forwards verbosity to the app binary unchanged. [VERIFIED: probe in scratchpad, output quoted in Code Examples] + +### Pattern 5: Artisan help/list with fang narrowly +- `wrap()` sets `cmd.SetHelpFunc(artisanHelp)` on every subcommand. Cobra's `HelpFunc()` returns the command's own `helpFunc` before walking to the parent, so fang's root-level `SetHelpFunc` never reaches subcommands. [VERIFIED: cobra@v1.10.2 command.go:484-501; fang v2 fang.go:129-139] +- Root: `RunE` renders the list (bare binary). `bin --help` hits the root help func, which fang has set, so either (a) skip `fang.Execute` and call `root.ExecuteContext` + our renderer directly (fang then contributes only `Styles`/`DefaultErrorHandler`/types), or (b) call `fang.Execute` and pre-route `-h/--help` with no command to the list in `bonfire.Execute` before delegating. **Recommendation: (b)**, which honours D-02 and D-14 and stays small. Always pass `fang.WithoutVersion()` (fang's `--version`/`-v` would collide with verbosity; cobra only adds `-v` for version when no `v` shorthand exists [VERIFIED: cobra command.go:1238-1258]), `fang.WithoutManpage()` (else a hidden `man` command appears in our list/duplicate checks), `fang.WithoutCompletions()`. +- fang wraps stderr with `colorprofile.NewWriter(root.ErrOrStderr(), os.Environ())` before calling the error handler [VERIFIED: fang.go:166-169]. To honour `--no-ansi/--ansi`, the handler type-asserts `*colorprofile.Writer` and overrides `.Profile`. +- Version header: `debug.ReadBuildInfo()`. The app version is `Main.Version` (Go ≥1.24 stamps the VCS tag/pseudo-version, `+dirty` when modified [CITED: go.dev/doc/go1.24]). For the framework version, find the `Deps` entry for `git.golem15.com/golem15/summercms`. Probe results: with `replace => ../fw` the dep reads `version="v0.3.2"`, `Replace={"../fw","(devel)"}`; under go.work the dep reads `version="(devel)"`, `Replace=nil`. Render `framework v0.3.2 (local)` / `framework devel`. [VERIFIED: probe] +- Grouping: top-level names (no colon) first, then by the segment before the first colon (`admin:token:create` → `admin`), sorted; `Hidden` skipped unless `list --all`; include cobra's `help` and our `list`. +- `list --format=json`: `{"application":{name,version,framework},"global_options":[...],"commands":[{name,description,long,usage,hidden,passthrough,arguments:[{name,description,required}],options:[{name,shorthand,description,default,bare,repeatable}],examples:[{command,description}]}]}`. +- `bonfire.CheckDocs(cmds []Command) []DocProblem` (name, missing: description|long|examples|arg description|flag description). The framework test fails on any problem; plugin repos call it from a test with `t.Log` (warn only). + +### Pattern 6: Startup summary and shutdown report +- Summary values are collected after binding and rendered twice: as an aligned lipgloss block on a TTY, and as INFO log records (component `serve`) so journald and files keep them (D-05). Keep one record whose message contains **`listening on `** verbatim: app smoke scripts grep `listening on 127.0.0.1:$PORT` [VERIFIED: ../sm-grzybyfunkcjonalne-app/scripts/smoke.sh:306, ../sm-summercmsio-app/scripts/smoke.sh:125; modules/surf/serve.go:245]. +- Migration state: new additive `lagoon.Pending(gdb, plugins) (int, error)` comparing each plugin's `Migrations()` IDs with the history table (`lagoon.Status` only returns applied IDs [VERIFIED: modules/lagoon/migrations.go:169-205]). +- Drivers: read from config keys seen via grep in modules (`mail.driver`, `cache.driver`, `realtime.driver`, `search.driver`, `queue.work_in_serve`, `queue.queues.*`, `app.name`, `app.url`). [ASSUMED: grep-observed only; the planner opens each defining file before use] +- Shutdown counters: `surf` adds an atomic in-flight/served counter next to `accessLog`. `conga` counts done/failed/in-flight in `Manager.runAttempt` [VERIFIED: modules/conga/worker.go:243] and exposes an additive `Worker.Stats()`. Uptime comes from the start time. `Worker.Stop` already escalates to `StopAndCancel` after the context deadline [VERIFIED: worker.go:172-192]. Report in-flight-at-stop as "returned to the queue" (River retries cancelled jobs). + +### Anti-Patterns to Avoid +- **Adding methods to `bonfire.Output`.** Add optional interfaces plus helpers instead (`bonfire.VerbosityOf(out)`, `bonfire.Process(out, items, op, fn)`, `bonfire.ItemProgress` for `Current(item)`/`Fail(item, err)`). No external implementers exist today (only `modules/bonfire/widgets.go` defines `Spinner(message string`), but D-02 asks for source compatibility. +- **Starting a bubbletea program without `tea.WithInput(nil)` for spinners/progress.** With input enabled, bubbletea puts stdin in raw mode (Ctrl+C becomes a key, not SIGINT) and, if stdin is not a TTY, opens `/dev/tty` or fails with `bubbletea: error opening TTY` [VERIFIED: bubbletea v2 tea.go:1034-1044]. +- **Letting bubbletea own signals inside commands.** Use `tea.WithoutSignalHandler()` and `tea.WithContext(ctx)`; the command's own `signal.NotifyContext` stays authoritative (serve/conga/docs:serve/parity use it). +- **Writing log lines to the terminal while a live view renders.** The view redraws over them. During a live view, the console handler must route lines through `Program.Println` (prints above the view [VERIFIED: tea.go:1412-1434]). It must never call it after the program has quit (`p.msgs` send would block). +- **Logging URL paths in the request log.** See Pitfall 9. + +## Don't Hand-Roll + +| Problem | Don't build | Use instead | Why | +|---|---|---|---| +| Fan-out handler | custom MultiHandler | `slog.NewMultiHandler` | Clones records, honours Enabled, joins errors [VERIFIED: GOROOT multi_handler.go] | +| JSON / logfmt | custom encoders | `slog.JSONHandler` / `slog.TextHandler` | Correct escaping, groups, time format | +| Color downsampling / NO_COLOR | env checks around every Render | `colorprofile.Writer{Forward, Profile}` | lipgloss v2 always emits ANSI | +| Box tables | the current `boxLine/boxRow` | `lipgloss/v2/table` with `lipgloss.NormalBorder()` (same ┌┬┐ glyphs) | Width-aware (East Asian, emoji); keep the non-TTY tab-separated path as is | +| Spinner/progress animation | goroutine + `\r` loops | bubbles `spinner.MiniDot` (same frames as today's `spinnerFrames` [VERIFIED: widgets.go:11; SSU spinner.go]) + `progress.New(WithColors(cyan, green))` | Already SSU's look | +| Arrow-key prompts | raw-mode key handling | `huh.NewSelect/NewConfirm/NewInput().EchoMode(huh.EchoModePassword)` | Accessible mode, aborts | +| "Did you mean" | Levenshtein | `cmd.SuggestionsFor(arg)` (cobra, exported) | Cobra already formats suggestions into the error text (probe) | +| GORM → slog | custom gorm logger | `logger.NewSlogLogger(diary.For("db"), logger.Config{SlowThreshold: 200*time.Millisecond, LogLevel: logger.Warn, IgnoreRecordNotFoundError: true, ParameterizedQueries: true})` | Exists in gorm v1.31.2; parameterized avoids secrets in SQL traces | +| slog conformance | ad-hoc assertions | `testing/slogtest.TestHandler` | Catches group/attr bugs (it caught charm log) | + +**Key insight:** most of `diary` is configuration and policy over stdlib slog. The genuinely new code is the component filter/proxy, the console renderer and the daily writer, and each is small and testable. + +## Runtime / Deployed-State Impact + +This is not a rename phase; the full Runtime State Inventory is omitted. Deployed state still changes: +- **Files on disk:** daily logs appear under `/storage/logs/` on every deployed instance once binaries are rebuilt (D-10). `/storage/` is already git-ignored in new apps [VERIFIED: internal/build/testdata/new/dot-gitignore.golden]. Hosts must have the directory writable (supervisor/systemd users on rome; Docker volumes); the channel must degrade, not crash. +- **Config:** new `config/log.yaml` in `summer new` output and goldens. Existing apps rely on framework defaults (no file needed). `SUMMER_LOG__*` env vars use the `__` separator (`SUMMER_LOG__COMPONENTS__RIVER=warn`), **not** `SUMMER_LOG_*` as written in D-08 [VERIFIED: modules/compass/env.go:31-44]. +- **Generated code:** every app's committed `main.go` (generated) changes on the next `summer build`; `examples/hello/main.go` is committed generated code and must be regenerated. +- **Deploy scripts:** production runs `./bin/.new migrate` over non-interactive SSH [VERIFIED: ../sm-bm-app/DEPLOY.md:35; ../sm-demo-app/DEPLOY.md:359]. Any new confirmation on `migrate` must not block non-TTY runs (Pitfall 11). + +## Common Pitfalls + +### Pitfall 1: Global shorthands vs command flags +**What goes wrong:** pflag panics (`unable to redefine "v" shorthand`) when a command's flag reuses `v`, `q` or `n` under another name; same-name local flags silently shadow persistent ones. [VERIFIED: pflag@v1.0.9 flag.go:898-917] +**Status:** No command in summercms.go, the D-20 plugins or the user plugin declares any `Shorthand`, and none uses the names `verbose/quiet/ansi/no-ansi/no-interaction/env/format/all`. [VERIFIED: grep across repos] +**How to avoid:** `NewRoot` validates command flags against reserved names/shorthands and returns an error (not a panic) naming the command. `list` owns `--format/--all` locally. + +### Pitfall 2: The docs checker's flag model +`internal/docsite.commandWord` assumes the root has only `--help/-h` and that "any other --name or two-character -x consumes the next token". With bool globals, docs lines like `bin/acme -v serve` would be misread (it would take `serve` as the value of `-v`). Update `commandWord` to know the bool globals (`-v -q -n --ansi --no-ansi --quiet --verbose --no-interaction`) and the value-taking `--env`. [VERIFIED: internal/docsite/check_commands.go:96-110] +Also add `list` and `help` to `docsCommands()` (they come from bonfire, not `toolCommands()`), and keep the generated-main lines in the `commands = append(commands, pkg.Ctor(app...` shape that `TestDocsCommandsMirrorGeneratedMain` parses with a regex. [VERIFIED: cmd/summer/docs_test.go:415-470] + +### Pitfall 3: Scheduler `Call` and process-global verbosity +`bonfire.Call` builds a fresh root per call (used by the scheduler through `Catalog`) [VERIFIED: call.go:17-34]. Global flags will exist there too, but the `OnGlobals` hook that mutates `diary` must be installed only on the binary's main root, not in `Call`. Otherwise a scheduled entry with `-v` changes logging for the whole process. `Call` output is non-TTY with empty stdin, so prompts take defaults and confirmations must treat it as non-interactive. + +### Pitfall 4: `compass` map keys and env strings +- koanf uses `.` as delimiter, so component names must not contain dots (`acme.blog` would nest). Use plugin codes like `journal`, `oauthserver`. Document it. +- Env overrides arrive as strings: `SUMMER_LOG__CHANNELS__DAILY__DAYS=30` → `"30"`, `SUMMER_LOG__CHANNELS__STACK__CHANNELS=console,daily` → a string, not a list. diary's parser must accept both (strconv for ints, comma split for lists). The `summer new` http.yaml stub warns that env strings are rejected for numeric keys there [VERIFIED: internal/build/stubs/new.tmpl]; diary must not copy that strictness. + +### Pitfall 5: Loggers captured before setup +`sunscreen.InstallDefault` runs first today, so later `slog.Default()` calls are already redacted. With diary, `main` must call `diary.Setup` (or an early `diary.Bootstrap(stderr)` before config) **before** `compass`/`party`, because config errors and plugin `Register` logs happen there. The proxy pattern (Pattern 1) removes ordering bugs for `diary.For`. Calls to `slog.Default()` made before setup still get the bootstrap handler. + +### Pitfall 6: GORM bypasses slog entirely +`lagoon` opens GORM with `&gorm.Config{}` [VERIFIED: modules/lagoon/connection.go:57], so GORM uses `logger.Default = New(log.New(os.Stdout, "\r\n", log.LstdFlags), Config{SlowThreshold: 200 * time.Millisecond, LogLevel: Warn, IgnoreRecordNotFoundError: false, Colorful: true})` [VERIFIED: gorm@v1.31.2 logger/logger.go:76-81]. That is colored raw text on **stdout**, including every "record not found". Route it through `logger.NewSlogLogger` with `ParameterizedQueries: true` (SQL values can carry tokens/PII). Setting `IgnoreRecordNotFoundError: true` changes output and is intended. + +### Pitfall 7: Handler concurrency and the shared terminal +slog handlers are called concurrently (HTTP handlers, River workers). The console handler holds a `*sync.Mutex` shared across `WithAttrs/WithGroup` clones and writes each record in one `Write`. The daily writer locks around rotate+write. Bonfire output (stdout) and logs (stderr) share one terminal: the live-view rule in Anti-Patterns applies. Run `go test -race ./modules/diary/... ./modules/bonfire/...` (CGO/gcc available here). + +### Pitfall 8: Daily files in production +The path resolves against the app root. Unwritable dirs (read-only container FS) → warn + disable channel. Retention deletes only files matching `-YYYY-MM-DD`, never other files in the directory. File mode 0o640 because logs carry user identifiers. + +### Pitfall 9: D-11 vs the request log's security contract +D-11 asks for "method, **path**, status, duration, request id" and "shows by default". Today: +- the URL path is **deliberately never logged** because share/invitation tokens travel in paths, including for 404/405 before routing (documented in `docs/services/logging.md` "Request log" and `modules/surf/accesslog.go:42-60`); +- the attributes are `slog.String("method", req.Method)`, `slog.String("pattern", entry.pattern)`, status, `duration_ms`, optional `client_gone` [VERIFIED: accesslog.go:75-76]; +- the log is **off by default** (`const accessLogKey = "http.access_log"`) [VERIFIED: accesslog.go:14, 38]. +**Recommendation:** keep `pattern` instead of `path`. Add `request_id` from `X-Request-Id`, trimmed and capped at 128 bytes the way `surf/errorpage.go` already does (`errorPageMaxRequestID = 128`). Turning the log on by default needs a user decision: either flip `http.access_log` default to true with `components.http` as the silencer, or keep the key and document it. This is in Open Questions; do not silently log paths. + +### Pitfall 10: `-q` must not swallow data +Scripts read `admin:token:create` output via `out.Println(plain)` [VERIFIED: modules/cabana/token_commands.go:117], `key:generate` via `out.Success(key)`, `herald:vapid-generate` env lines. Artisan's `-q` silences everything. Recommendation: under `-q`, suppress `Info/Success/Warning`, banners, spinners and progress; keep `Println/Printf/Write` (data), `--json` output and `Error`. For `key:generate`, print the bare key when stdout is not a TTY (or add `--json`). This is in Open Questions. + +### Pitfall 11: `--force`/`--pretend` semantics already taken, and deploys +- In the importers and `pg:update-currencies`, `--force` already means "replace existing data"/"force update", not "skip confirmation" [VERIFIED: plugin console files]. Do not repurpose it. +- Importers already have `--dry-run`. Adding `--pretend` aliases is additive but optional; `--pretend` fits `migrate`/`migrate:rollback` (Artisan name). +- Artisan's ConfirmableTrait asks in production. Production deploys run `migrate` over non-TTY SSH, so a production confirmation that fails without `--force` on non-TTY would break every deploy. Recommendation: confirm only on an interactive TTY. Non-TTY keeps today's behaviour, so the change stays non-breaking (D-20); `--force` skips the prompt. + +### Pitfall 12: Prompts and piped stdin +`admin:create`/`admin:reset-password` read the password from piped stdin through `out.Secret` (test "WR-13" in `modules/cabana/commands_test.go:241-262`). `Secret` reads a plain line when stdin is not a terminal [VERIFIED: prompts.go:88-106]. Keep the line path whenever stdin is not a TTY. Use huh only when both stdin and stdout are TTYs and `-n` is unset. With `-n` and no default (Secret), fail with a hint ("pipe the password on stdin"). + +### Pitfall 13: Errors printed twice and odd exit codes +- `flare` returns sentinel errors after printing its own message: `errGenerateFailed = errors.New(GenerateVAPIDKeysCommandName + ": failed")`, `errTestPushFailed = ...` [VERIFIED: modules/flare/commands.go:20-21]. `docs:build --check` does the same (`errors.New(cmd + " failed")`). Add `bonfire.ErrReported` (and `bonfire.Reported(err)`) so the renderer exits 1 without a second message. Migrate those sites. +- `summer dev` returns `ctx.Err()` on Ctrl+C, and `summer main` prints it and exits 1. Map `context.Canceled` from a signal to a clean shutdown (exit 0 after the report, or 130). Decide one convention for dev/serve/queue:work. +- Usage errors → exit 2: wrap `positionalArgs` failures and `root.SetFlagErrorFunc` errors in `bonfire.UsageError`. Detect `unknown command` by prefix (fang's own approach). Turn cobra's appended "Did you mean this?" text into the `Try:` hint. + +### Pitfall 14: `summer dev` starts the binary with no arguments +`startBin` runs `exec.Command(binPath)` [VERIFIED: internal/dev/watch.go:201-210]. A bare app binary prints help (today) or the list (after this phase), then exits, so `summer dev` never serves. D-07 asks for a shutdown report for `dev`. Recommendation: start `bin/ serve` and forward `-v/-q`. This is a behaviour fix and must be confirmed (Open Questions). + +### Pitfall 15: Duplicate-name rejection can stop apps from starting +Each app has exactly one plugin appending `centrifugo.Commands` (demo plugin, bm plugin, the Płytarium domain plugin, which also appends `flare.Commands`) [VERIFIED: grep]. No duplicates exist today. After the change, a second plugin appending the same shared command would make `NewRoot` fail at boot. Plan 2 verification: build each app in the meta repo and run `bin/ list`. + +### Pitfall 16: Cross-repo ordering and concurrency +- Plugins compile against the working framework through `replace ... => ../summercms.go` in their go.mod/go.work, but tagged consumers ignore dependency replaces. Plugin commits that use `Examples`, `Hidden` or `diary` need a framework release tag before their `require summercms vX` bump is meaningful. Tagging needs the user's go-ahead. [VERIFIED: sibling go.mod files] +- Phase 19.5 is executing in `sm-user-plugin` (canonical checkout `../fonoteka.go/plugins/golem15/user`, per the 19.4/19.5 gate scripts) and `sm-sociallogin-plugin` right now [VERIFIED: STATE.md; scripts/check-phase19.5.sh:65-66]. Sweep the user plugin after 19.5 lands or coordinate. + +## Code Examples + +### Generated main after this phase (shape; keep the `commands = append(commands, …(app…` lines) +```go +// Source: derived from internal/build/build.go:94-151 (current template) +func run(args []string, out io.Writer) int { + g := bonfire.PeekGlobals(args) + diary.Bootstrap(os.Stderr, g) // redacting console until config is read + cfg, err := compass.Open(compass.Options{Dir: "config", Env: g.Env}) + if err != nil { + return bonfire.Fail(os.Stderr, err, g) // styled block, exit 1 + } + if err := diary.Setup(cfg, diary.Options{Globals: g}); err != nil { + return bonfire.Fail(os.Stderr, err, g) + } + app := backpack.New(cfg) + plugins, err := party.Activate(app, PluginIDs) + // ... unchanged command assembly ... + root, err := bonfire.NewRoot("acme", commands, out, + bonfire.WithVersion(bonfire.BuildVersion()), bonfire.OnGlobals(diary.Apply)) + if err != nil { + return bonfire.Fail(os.Stderr, err, g) + } + return bonfire.Execute(context.Background(), root, args) // 0, 1 or 2 +} +``` +Keep `func main() { os.Exit(run(os.Args[1:], os.Stdout)) }`. The `internal/build` goldens (`main.go.golden`) are updated with `go test ./internal/build -run Golden -update` (flag defined at `internal/build/new_golden_test.go:13`). + +### Porting SSU's spinner to bubbletea v2 (API translation) +```go +// Source: SSU internal/cli/tui/spinner.go (v1) translated per charm v2 upgrade guides +func (m spinnerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case progressMsg: // {Done, Total, Item, Err} + m.done, m.total, m.current = msg.Done, msg.Total, msg.Item + return m, nil + case doneMsg: + m.err = msg.Err + return m, tea.Quit + case spinner.TickMsg: + var cmd tea.Cmd + m.spin, cmd = m.spin.Update(msg) + return m, cmd + } + return m, nil +} + +func (m spinnerModel) View() tea.View { // v1: View() string + line := fmt.Sprintf("%s %s %d/%d", m.spin.View(), m.label, m.done, m.total) + if m.current != "" { + line += " " + styles.Muted.Render(m.current+"…") + } + return tea.NewView(line) +} + +// Run with: tea.NewProgram(model, tea.WithInput(nil), tea.WithOutput(w), +// tea.WithoutSignalHandler(), tea.WithContext(ctx)) +``` +Progress bar: `progress.New(progress.WithColors(lipgloss.Color("#00afaf"), lipgloss.Color("#00af00")), progress.WithoutPercentage())` (cyan→green, matching today's `progressColor` 256-color codes 37/36/34 [VERIFIED: widgets.go:133-142]); `bar.SetWidth(40)` replaces SSU's `bar.Width = 40`. + +### Error with hint +```go +// Source: design for D-18/D-19 +return bonfire.WithHint(err, + "summer make:command needs an application root (summer.yaml).", + "cd ..; {bin} make:command acme.blog:publish") // {bin} → "bin/" or "summer" +``` +Known-failure sites for hints: `lagoon: database.dsn is empty (set SUMMER_DATABASE__DSN)` [VERIFIED: modules/lagoon/connection.go:25] → Try: `cp .env.example .env` / `docker compose up -d --wait`; `lagoon: ping postgres:` (unreachable) → Try: `docker compose up -d --wait`; `build: no summer.yaml found from ` [VERIFIED: internal/build/manifest.go:215] → Try: `cd ; summer `. For the wrong directory: `compass` silently returns an empty config when `config/` is missing [VERIFIED: config.go:237-242]. So when a command fails **and** `config/` is absent in CWD **and** a parent holds `summer.yaml`, decorate the error with `cd ; bin/ ` (D-19: message only). The generated main cannot import `internal/build`, so the parent-walk lives in `bonfire` or `compass`. + +### Probe evidence (quoted) +charm log v2 against `testing/slogtest` (abridged): +``` +missing key "time": this test expects slog.TimeKey, slog.LevelKey and slog.MessageKey +unexpected key "": a Handler should ignore an empty Attr +missing key "c": a Handler should inline the Attrs of a group with an empty key +unexpected key "a": a Handler should handle the WithGroup method +missing group "G": a Handler should handle the WithGroup method +missing group "G": a Handler should handle multiple WithGroup and WithAttr calls +FAIL probe 0.004s +``` +(The "time" lines come from `ReportTimestamp: false`; the group and empty-attr failures are real.) + +Cobra global flags: +``` +["-vvv" "migrate"] => migrate v=3 q=false n=false env="" args=[] +["migrate" "-vv" "-q" "-n" "--env" "staging" "x"] => migrate v=2 q=true n=true env="staging" args=[x] +["-v" "serve" "--addr" ":1" "-vv"] => serve(passthrough) v=0 args=["-v" "--addr" ":1" "-vv"] +["--env" "staging" "migrate"] => migrate v=0 q=false n=false env="staging" args=[] +["migrat"] => ERR: unknown command "migrat" for "app"\n\nDid you mean this?\n\tmigrate +[] => ROOT list v= 0 +``` +BuildInfo: +``` +main="v1.4.0" +dep example.com/fw version="v0.3.2" replace={"../fw" "(devel)"} +--- go.work mode +main="v1.4.0+dirty" +dep example.com/fw version="(devel)" replace= +``` +Binary size (stripped, `-trimpath -s -w`): cobra+x/term baseline 2,527,495 B; + lipgloss/table, bubbletea, bubbles spinner+progress, huh, fang 4,649,223 B (≈ +2.1 MB). + +## Command Inventory (sweep input, D-20/D-21) + +Legend: **D** description, **L** long help, **E** examples (no command has E today; the field does not exist yet). Checklist columns hold proposed outcomes for the plan to confirm or decline: Force = `--force` confirmation, Pretend = `--pretend`/dry-run, Json = `--json`, Prog = progress/table for bulk, Res = result line + exit code. + +### summercms.go: `summer` tool (`cmd/summer`, 18 own + 15 delegates) + +| Command | File | D | L | Notes / checklist | +|---|---|---|---|---| +| build | main.go:51 | ✓ | – | Res ✓ (`built X in Y`); Prog: spinner around `go build` on TTY | +| make:plugin, make:model (`--no-migration`), make:migration, make:command, make:job, make:admin-controller | main.go:65-211 | ✓ | – | Res: `created ` lines; D-19 hints for wrong dir (folded todo); E needed | +| plugin:add `` | main.go:269 | ✓ | – | Res ✓ | +| new `` (`--framework --no-plugin --no-git`) | main.go:296 | ✓ | – | Writes `config/log.yaml` (D-10); golden update | +| dev | main.go:255 | ✓ | – | Long-running: summary + shutdown report (D-07); Pitfall 14; output via raw `fmt.Fprintf` in internal/dev/watch.go:79-150 | +| parity:proxy/record/replay/broadcasts/upstream | parity.go:19-104 | ✓ | – | Many `(true/false)` string flags: show defaults; E needed | +| docs:build (`--check`), docs:sync, docs:serve | docs.go:25-99 | ✓ | – | `ErrReported` for problem reports; docs:serve long-running | +| 15 delegates (migrate, migrate:rollback, migrate:status, serve, queue:work, queue:clear, schedule:run, route:list, key:generate, admin:create, admin:reset-password, admin:token:create/list/revoke/scopes) | runtime.go:27-43 | ✓ | ✓ (generated) | Passthrough; add E per row; `deferred:purge` and `websockets:*` are not delegated (by design, documented) | + +### summercms.go: application binary (framework, 19) + +| Command | File | D | L | Force | Pretend | Json | Prog | Res | +|---|---|---|---|---|---|---|---|---| +| migrate | lagoon/commands.go:26 | ✓ | – | TTY+production only (Pitfall 11) | add `--pretend` | – | per-plugin progress | ✓ "migrations applied" → count | +| migrate:rollback `--plugin` | lagoon/commands.go:39 | ✓ | – | TTY confirm | `--pretend` | – | – | ✓ | +| migrate:status | lagoon/commands.go:60 | ✓ | – | – | – | add | table ✓ | – | +| deferred:purge `--days` | lagoon/commands.go:82 | ✓ | – | TTY confirm | add (reports counts) | – | progress | ✓ | +| key:generate | lagoon/keygen.go:26 | ✓ | – | – | – | bare key on non-TTY / `--json` | – | ✓ | +| queue:work `--queue`* | conga/commands.go:31 | ✓ | – | – | – | – | summary + shutdown | – | +| schedule:run `--once` | conga/commands.go:45 | ✓ | – | – | – | – | summary + shutdown | ✓ (once) | +| queue:clear `[queue]` | conga/commands.go:64 | ✓ (ends with "."; fix) | – | TTY confirm | – | – | – | ✓ "Cleared N jobs" | +| serve `--addr` | surf/serve.go:30 | ✓ | – | – | – | – | summary + shutdown | – | +| route:list | surf/routelist_command.go:18 | ✓ | – | – | – | add | table ✓ | – | +| admin:create `--email --password --login --superuser --role` | cabana/commands.go:21 | ✓ | – | – | – | – | – | ✓ | +| admin:reset-password `` | cabana/commands.go:35 | ✓ | – | – | – | – | – | ✓ | +| admin:token:create ` --name --scope* --expires=90d` | cabana/token_commands.go:21 | ✓ | – | – | – | add | – | ✓ (token on stdout) | +| admin:token:list `[identifier] --revoked` | token_commands.go:36 | ✓ | – | – | – | add | table ✓ | – | +| admin:token:revoke `` | token_commands.go:49 | ✓ | – | – | – | – | – | ✓ | +| admin:token:scopes | token_commands.go:59 | ✓ | – | – | – | add | table ✓ | – | +| websockets:health | lighthouse/centrifugo/commands.go:31 | ✓ | – | – | – | add | – | exit 1 on failure | +| websockets:generate-vapid-keys `--update --show-current` | flare/commands.go:50 | ✓ | – | – | – | – | – | `ErrReported` | +| websockets:test-push ` --show-config` | flare/commands.go:212 | ✓ | – | – | – | – | – | `ErrReported`; Confirm default true under `-n` | +| examples: greeter:hello, blog:publish | examples/hello/plugins/greeter/plugin.go:80; docs/examples/blog/console/publish.go:18 | ✓ | – | – | – | – | – | include in the framework coverage check | + +### Shared plugins (D-20) + +| Repo | Commands (file) | Notes | +|---|---|---| +| sm-paymentgateway-plugin | pg:cancel-created-payments, pg:cancel-sent-payments, pg:cancel-placed-orders, pg:finish-orders (bulk writers, **scheduled daily**), pg:update-currencies (`--force` = force update; **every 10 min**), paymentgateway:import-winter (`--settings --force --dry-run`) (console/*.go; schedule.go:17-21) | Pretend candidates for the bulk writers; run under scheduler `Call` (non-interactive); 16 slog sites | +| sm-oauthserver-plugin | oauthserver:prune (scheduled daily), oauthserver:rotate-keys (`--prune`, scheduled interval), oauthserver:import-winter (`--file --key-dir --settings --uploads-dir --force --dry-run`) | Pretend for prune | +| sm-journal-plugin | journal:export-posts (`--path --dry-run`), journal:import-posts (`--path --force --dry-run`) | Progress for bulk; `--force` = update existing | +| sm-sitemap-plugin | sitemap:import `` (`--theme --append`), sitemap:import-winter (`--definitions --settings --theme --map* --page-url* --force --dry-run`) | | +| sm-chat-plugin / sm-chatchannels-plugin / sm-forum-plugin | chat:/chatchannels:/forum:rewrite-context-types (idempotent bulk rewrite; description ends with ".") | Pretend candidate | +| sm-chatvideo-plugin | chatvideo:reconcile-sessions (console/reconcile.go:14) | | +| sm-faq-plugin | faq:import-winter (`--file --force --dry-run`) | | +| sm-menu-plugin | menu:import-winter (`--menus --entries --map* --force --dry-run`) | | +| sm-herald-plugin | herald:vapid-generate (prints env lines with a private key) | Data on stdout under `-q` | +| sm-pgstripe-plugin | pgstripe:import-winter (`--settings --app-key --force --dry-run --webhook-secret-target`) | | +| sm-user-plugin (`../fonoteka.go/plugins/golem15/user`) | user:token:create `` (`--name=api --scopes=read,write --expires`), user:token:list, user:token:revoke ``, user:require-password-change ``, user:backfill-handles (bulk) | Json for token create/list; descriptions carry planning IDs ("(D-15)", "(D-21)"): remove them | + +Total plugin commands: 26 (paymentgateway 6, user 5, oauthserver 3, journal 2, sitemap 2, one each in chat, chatchannels, chatvideo, faq, forum, herald, menu, pgstripe). **No plugin command has a long help or examples today.** Descriptions mix trailing periods and capitalised verbs ("Cancels…", "Updates…"); normalise to imperative, no trailing period. + +**Apps that need pointer bumps after each plugin commit** [VERIFIED: app .gitmodules]: +- sm-user-plugin → sm-bm-app, sm-demo-app, sm-grzybyfunkcjonalne-app, sm-summercmsio-app, fonoteka.go +- sm-journal-plugin → sm-demo-app, sm-grzybyfunkcjonalne-app, sm-summercmsio-app +- sm-sitemap-plugin, sm-menu-plugin, sm-faq-plugin → sm-demo-app, sm-grzybyfunkcjonalne-app +- sm-paymentgateway-plugin, sm-pgstripe-plugin, sm-oauthserver-plugin → sm-demo-app +- sm-chat, sm-chatchannels, sm-chatvideo, sm-forum, sm-herald → no app in `..` mounts them (nothing to bump) + +Not in D-20 but carrying commands: `sm-golem-plugin`, `sm-feedback-plugin` (`ImportSettings`, mounted in sm-demo-app/fonoteka.go). App-local plugins (demo:seed/reset, the Płytarium domain plugin, bm) are out of scope. + +## Docs and Presentation Locations + +| What | Path | +|---|---| +| Logging page (rewrite: diary, config/log.yaml, channels, components, daily, request log) | `docs/services/logging.md` | +| Console pages | `docs/console/introduction.md` ("Getting help" → list/help/global options), `writing-commands.md` (Examples, Hidden, duplicates now rejected, hints, ErrReported, named loggers, -q/-v semantics), `setup-and-maintenance.md` (serve/queue:work summary, new `--json/--pretend`), `scaffolding.md` (`summer new` writes config/log.yaml; dev report), `utilities.md` | +| Configuration | `docs/services/configuration.md`, `docs/setup/configuration.md` (log keys, `SUMMER_LOG__*`) | +| Upgrade notes | `docs/setup/upgrading.md` (daily logs on, duplicate names fail, GORM log routing, request log) | +| Module READMEs | new `modules/diary/README.md`; `bonfire`, `sunscreen` (shim wording: `InstallDefault` is the only install function; `sunscreen.Install` in the roadmap does not exist), `surf`, `conga`, `lagoon`, `cabana`, `flare`, `lighthouse` | +| Root module table | `README.md` (new diary row; bonfire row at line 97, sunscreen at 117) | +| Example app | `examples/hello/main.go` (committed generated main; separate workspace module) | +| Landing site | `../sm-summercmsio-app/vue-summercmsio-app/i18n/locales/en.json` (`features.console`, `winter.concepts.commands`), `app/components/FeaturesSection.vue`, `TerminalCard.vue`, `app/data/terminal.json` (checked by `../sm-summercmsio-app/terminal_check_test.go`) | +| Roadmap presentation | Lives in **`../sm-demo-app`**, not the summercms.io app: `plugins/demo/content/ROADMAP.md` (snapshot, refresh with `scripts/sync-roadmap.sh`, `--check` fails when stale), curated text `plugins/demo/roadmap/curated.go` and `names.go` (sm-demo-app README "Roadmap deck") | + +## State of the Art + +| Old approach | Current approach | When changed | Impact | +|---|---|---|---| +| `github.com/charmbracelet/{lipgloss,bubbletea,bubbles,huh,fang}` v1 | `charm.land/*/v2` vanity paths | lipgloss v2 GA / bubbletea v2 GA in 2026 | SSU code must be translated | +| lipgloss Renderer downsampling | `Render()` emits full ANSI; `colorprofile.Writer`/`lipgloss.Fprintln` downsample | lipgloss v2 | Apply `--no-ansi` at the writer | +| `View() string`, `tea.KeyMsg` struct | `View() tea.View`, `tea.KeyPressMsg` | bubbletea v2 | Port all SSU models | +| `progress.WithGradient/WithDefaultGradient`, `.Width` field | `WithColors/WithDefaultBlend`, `SetWidth()` | bubbles v2 | | +| Hand-rolled multi handler | `slog.NewMultiHandler` | Go 1.26 | | +| Binary version via `-ldflags -X` | `debug.ReadBuildInfo().Main.Version` VCS stamp | Go 1.24 | No ldflags in `summer build` needed | + +## Assumptions Log + +| # | Claim | Section | Risk if wrong | +|---|---|---|---| +| A1 | Config keys for the summary (`mail.driver`, `cache.driver`, `realtime.driver`, `search.driver`, `queue.work_in_serve`, `queue.queues.*`) are the canonical ones | Pattern 6 | Wrong summary lines; planner reads the defining files first | +| A2 | River retries jobs cancelled by `StopAndCancel`, so in-flight-at-stop is "returned to the queue" | Pattern 6 | Report wording wrong; verify against River docs/tests | +| A3 | koanf's `LoadSection`/unmarshal is weakly typed for the `log` subtree | Pitfall 4 | Parse diary config manually from `Lookup` to avoid depending on it | +| A4 | Verbosity mapping (-v INFO floor, -vv DEBUG, -vvv DEBUG+source+SQL) | Pattern 2 | User may want a different mapping (D-04 only fixes -q and "debug detail") | +| A5 | `-q` keeps data output (Println/--json/Error) | Pitfall 10 | Deviates from Artisan; needs confirmation | +| A6 | Logfmt (`TextHandler`) as the `format: auto` non-TTY default | Pattern 2 | D-05 allows JSON or logfmt; logfmt keeps today's text and smoke greps | +| A7 | Daily file dates use the process local time zone (not `app.timezone`) | Pattern 3 | File boundaries differ from the scheduler's | + +## Open Questions + +1. **Request log path and default (D-11 vs the security contract).** What we know: paths are never logged by design, and the log is off by default via `http.access_log`. Recommendation: log `pattern` + `request_id` (no path), and ask whether to flip `http.access_log` to default-on with `components.http` as the silencer. +2. **`-q` semantics for data output.** Recommendation: keep data/`--json`/errors (Pitfall 10). +3. **Production confirmation on `migrate`/destructive commands.** Recommendation: TTY-only confirmation, non-TTY unchanged, `--force` skips (Pitfall 11). +4. **`summer dev` should run `serve`.** Recommendation: yes, with forwarded verbosity (Pitfall 14). +5. **Exit code after a signal-initiated graceful shutdown** (0 or 130). Recommendation: 0 when the shutdown report has no drain errors. +6. **`sm-golem-plugin` / `sm-feedback-plugin` commands** are outside the D-20 list. Include them in the sweep or leave them. +7. **Framework release tag for plugin `require` bumps.** Plugin commits using new APIs compile via replace, but tagged consumers need a framework tag; the user's go-ahead is needed for tag and push. +8. **Selector widget scope.** No command needs multi-select today. Recommendation: huh Select for `Choice`; port SSU's SelectorModel only when a command needs it (keeps plan 2 lean). + +## Environment Availability + +| Dependency | Required by | Available | Version | Fallback | +|---|---|---|---|---| +| Go toolchain | everything | ✓ | go1.27.2 (go.mod `go 1.27.0`) | — | +| proxy.golang.org / charm.land | `go get` charm modules | ✓ | reachable this session | — | +| gcc / CGO | `go test -race` | ✓ | CGO_ENABLED=1 | skip -race | +| Docker | Postgres-backed serve/migrate tests (pending count) | client 29.9.1 present; daemon access not confirmed | — | `-short` tests; run Docker stage via `sudo -u golem` per CLAUDE.md | +| Node/npm/pnpm | landing site tests (`../sm-summercmsio-app/vue-summercmsio-app`) | ✓ | node v22.23.3, npm 12.2.1, pnpm 12.14.0 | — | +| SSU source | port reference | ✓ | `/media/nvme/dev/pxpx/ssu` @ 0061e3d (2026-09-11) | — | + +No blocking gaps. + +## Validation Architecture + +### Test Framework +| Property | Value | +|---|---| +| Framework | Go `testing` (+ `testing/slogtest`), golden files with an `-update` flag (pattern: `internal/build/new_golden_test.go:13`) | +| Config file | none (go test) | +| Quick run command | `go test -short ./modules/bonfire/... ./modules/diary/... ./modules/sunscreen/... ./internal/build/... ./internal/docsite/... ./cmd/summer/...` | +| Full suite command | `go vet ./... && go test ./... && (cd examples/hello && go test ./...)` | +| Phase gate | `scripts/check-phase19.8.sh --all` (new; pattern of `scripts/check-phase19.4.sh`: per-repo stages, required test names, fail on skip/zero tests, `NO_COLOR=1`, additive-export check for plugin repos) | + +### Phase Requirements → Test Map +| Ref | Behavior | Type | Automated command | Exists? | +|---|---|---|---|---| +| SC1 | diary console/daily handlers pass slogtest | unit | `go test ./modules/diary -run TestSlogtest` | ❌ Wave 0 | +| SC1 | component floors, verbosity changes, `-q` on console only | unit | `go test ./modules/diary -run 'TestComponent|TestVerbosity'` | ❌ | +| SC1 | daily rotation + retention with injected clock; unwritable dir degrades | unit | `go test ./modules/diary -run TestDaily` | ❌ | +| SC1 | `SUMMER_LOG__…` env strings (`"30"`, `"console,daily"`) parse | unit | `go test ./modules/diary -run TestConfigEnv` | ❌ | +| SC1 | every channel redacts (`sunscreen`), `InstallDefault` still works | unit | `go test ./modules/diary ./modules/sunscreen -run 'TestRedact|TestInstallDefault'` | partial (sunscreen ✓) | +| SC1 | River/GORM/plain `slog.Default()` records route through diary with components | unit | `go test ./modules/conga ./modules/lagoon -short -run TestLogger` | ❌ | +| SC2 | `PeekGlobals` table (stops at `--`, `-vvv`, `--env=x`) and cobra persistent flags agree | unit | `go test ./modules/bonfire -run 'TestPeekGlobals|TestGlobalFlags'` | ❌ | +| SC2 | reserved shorthand/name collision returns an error (no panic) | unit | `go test ./modules/bonfire -run TestReservedFlags` | ❌ | +| SC2 | `Call` does not trigger `OnGlobals` | unit | `go test ./modules/bonfire -run TestCallNoGlobalsHook` | ❌ | +| SC3 | list golden (namespaces, hidden, `--all`), help golden (Description/Usage/Arguments/Options defaults/Examples), passthrough help unchanged | golden | `go test ./modules/bonfire -run 'TestListGolden|TestHelpGolden'` (`-update` to rewrite) | ❌ | +| SC3 | `list --format=json` schema | unit | `go test ./modules/bonfire -run TestListJSON` | ❌ | +| SC3 | coverage: framework commands documented (fails) | unit | `go test ./cmd/summer -run TestCommandsDocumented` | ❌ | +| SC3 | duplicate names rejected with the name in the error | unit | `go test ./modules/bonfire -run TestDuplicateNames` | ❌ | +| SC3 | docs checker accepts `bin/acme -v serve`, `summer list`, `bin/acme help migrate` | unit | `go test ./internal/docsite -run TestCommandWord` + `go test ./cmd/summer -run TestDocsTree` | partial | +| D-18 | error block golden; exit 1 vs 2; `ErrReported` silent; hint `{bin}` substitution; `-v` chain | golden/unit | `go test ./modules/bonfire -run 'TestErrorGolden|TestExitCodes'` | ❌ | +| D-03 | widget matrix: TTY vs non-TTY vs `-q` vs `--no-ansi` vs `-n` (existing non-TTY fallbacks unchanged) | unit | `go test ./modules/bonfire -run 'TestNonTTY|TestWidgetMatrix'` | partial (non-TTY ✓) | +| D-03 | ported models: Update/View as pure functions (no terminal) | unit | `go test ./modules/bonfire -run TestLiveModels` | ❌ | +| D-03 | piped Secret still reads stdin; `-n` Secret without default fails with hint | unit | `go test ./modules/bonfire ./modules/cabana -short -run 'TestSecret|TestAdminPasswordWithoutFlag'` | partial | +| SC4 | serve summary lines (pure builder like `serveBanner`), `listening on` record kept | unit | `go test ./modules/surf -short -run TestServeSummary` | partial (`serve_test.go` checks banner) | +| SC4 | shutdown report counters (surf in-flight, conga Stats) | unit | `go test ./modules/surf ./modules/conga -short -run 'TestShutdownReport|TestWorkerStats'` | ❌ | +| SC4 | `lagoon.Pending` | integration (Docker) | `go test ./modules/lagoon -run TestPending` | ❌ | +| SC4 | per-plugin warn-only coverage | unit | `go test ./...` in each plugin repo (`TestCommandsDocumented` logs) | ❌ | +| SC5 | generated main + `summer new` goldens | golden | `go test ./internal/build -run Golden` | ✓ (update) | +| SC5 | landing terminal commands still valid | unit | `cd ../sm-summercmsio-app && go test -run Terminal` | ✓ | + +### Testing techniques +- **TTY simulation:** `newConsole(in, out, err, lookup, tty, interactive *bool)` already injects TTY/interactive flags [VERIFIED: modules/bonfire/output.go:65-91]. Extend it with a forced `colorprofile.Profile` so goldens contain no ANSI (or a pinned `ANSI` profile for one styled golden). +- **bubbletea:** test models by calling `Update`/`View` directly. For an end-to-end run use `tea.WithInput(nil)`, `tea.WithOutput(&buf)`, `tea.WithWindowSize(80, 24)`. Do not add `teatest` (another dependency). +- **slog handlers:** `slogtest.TestHandler` (JSON parse of the output) for the console handler's non-TTY/JSON modes and the daily writer; the race detector for concurrent writes. +- **Goldens:** `modules/bonfire/testdata/{list,help_*,error_*}.golden`, rewritten with `go test ./modules/bonfire -update`. + +### Sampling Rate +- **Per task commit:** quick run command above. +- **Per wave merge:** full suite + `summer docs:build --check`. +- **Phase gate:** `scripts/check-phase19.8.sh --all` green before `/msd-verify-work`; manual UAT on a real terminal (spinner/progress/huh prompts, Ctrl+C report) is required because animations cannot be asserted fully. + +### Wave 0 Gaps +- [ ] `modules/diary/*_test.go` (new module) +- [ ] `modules/bonfire/testdata/*.golden` + `-update` flag in bonfire tests +- [ ] `scripts/check-phase19.8.sh` (multi-repo gate with `--self-test`) +- [ ] `TestCommandsDocumented` in `cmd/summer` and in each D-20 plugin repo (warn-only) + +## Security Domain + +| ASVS category | Applies | Standard control | +|---|---|---| +| V2 Authentication | no (no auth changes) | — | +| V3 Session | no | — | +| V4 Access Control | no | — | +| V5 Input Validation | yes | `--env` value validated by `compass.sanitizeEnv` (letters, digits, `-`, `_`; rejects `..`, `/`, `\`) [VERIFIED: modules/compass/env.go:110-118]; diary config: whitelist drivers/formats, reject unknown levels | +| V6 Cryptography | no | — | +| V7 Error handling & logging | **yes** | sunscreen on every channel; no URL paths/query/headers in the request log; GORM SQL parameterized; `-v` error chains on stderr only; log files 0o640, dirs 0o750; never print secrets in hints/summaries (summary shows driver names, never DSNs or keys; centrifugo API key is already never printed) | +| V12 Files | yes | Daily writer path confined under the app root (clean + prefix check like `compass.Persist` does for overrides); retention deletes only `-YYYY-MM-DD` matches | + +| Threat pattern | STRIDE | Mitigation | +|---|---|---| +| Tokens in logged paths (share/invite links) | Information disclosure | Keep `pattern`, not path (Pitfall 9) | +| Secrets in SQL traces at `-vvv` | Information disclosure | `ParameterizedQueries: true`; sunscreen still applies | +| DSN/keys in the startup summary or error chain | Information disclosure | Summary prints driver/host only; run error text through `sunscreen.Scrub` in the renderer | +| Log path traversal via `log.channels.*.path` | Tampering | Clean and confine to the app root; refuse absolute paths outside it unless explicitly allowed | +| Terminal escape injection via logged user data | Tampering | The console handler strips control characters from message/attr values (stdlib Text/JSON handlers already quote) | +| Log disk exhaustion | DoS | Retention days; daily files only | + +## Sources + +### Primary (HIGH confidence) +- proxy.golang.org `@latest`/`.mod`/`@v/list` for charm.land/{lipgloss,bubbletea,bubbles,huh,fang,log}/v2 and github.com/charmbracelet/* v1; charm.land go-import meta tags +- Module source read this session (scratch module cache): fang v2.0.1 `fang.go`, `help.go`, `theme.go`; bubbletea v2.1.0 `tea.go`, `options.go`, `UPGRADE_GUIDE_V2.md`; bubbles v2.2.1 `progress`, `spinner`, `UPGRADE_GUIDE_V2.md`; huh v2.0.3 `form.go`, `field_input.go`; lipgloss v2.0.6 `query.go`, `table`, `UPGRADE_GUIDE_V2.md`; charm log v2.0.1 `logger_121.go` +- cobra v1.10.2 `command.go`; pflag v1.0.9 `flag.go`; gorm v1.31.2 `logger/logger.go`, `logger/slog.go`; Go stdlib `log/slog/multi_handler.go` +- Repo files read: modules/bonfire/*, modules/sunscreen/sunscreen.go, modules/compass/{config,env}.go + README, internal/build/build.go + stubs + goldens, internal/dev/watch.go, internal/docsite/check_commands.go, cmd/summer/{main,runtime,docs,parity}.go + docs_test.go, modules/{lagoon,conga,surf,cabana,flare,lighthouse/centrifugo} command files, surf/accesslog.go, conga/{client,worker}.go, lagoon/{connection,migrations}.go +- SSU `/media/nvme/dev/pxpx/ssu`: go.mod, internal/cli/tui/*.go, internal/cli/root.go, internal/logging/*.go +- Probes (scratchpad): slogtest vs charm log; cobra global flags; BuildInfo replace/go.work; binary size +- go.dev/doc/go1.24 (VCS version stamping) + +### Secondary (MEDIUM) +- `../.depreciated/modules/summer-bonfire/PLAN.md` (Scala design, non-TTY table) +- Sibling repos: plugin console files, app `.gitmodules`/go.mod/go.work, DEPLOY.md, smoke scripts, sm-demo-app README roadmap deck + +### Tertiary (LOW) +- None used for recommendations. + +## Metadata + +**Confidence breakdown:** +- Standard stack: HIGH (versions, go directives, dependency graph and APIs read from source; size probed) +- Architecture: HIGH for the flag/help/fang mechanics (source + probes); MEDIUM for level-mapping policy (A4) +- Pitfalls: HIGH (each tied to a file/line or a probe) +- Inventory: HIGH (grep + read of every command file across 14 repos) + +**Research date:** 2026-10-21 +**Valid until:** 2026-11-20 (charm v2 is moving; re-check `@latest` before `go get`) diff --git a/.planning/phases/19.8-console-commands-overhaul/19.8-VALIDATION.md b/.planning/phases/19.8-console-commands-overhaul/19.8-VALIDATION.md new file mode 100644 index 00000000..74a08196 --- /dev/null +++ b/.planning/phases/19.8-console-commands-overhaul/19.8-VALIDATION.md @@ -0,0 +1,78 @@ +--- +phase: "19.8" +slug: "console-commands-overhaul" +# status lifecycle: draft (seeded by plan-phase) → validated (set by validate-phase §6) +# audit-milestone §5.5 distinguishes NOT-VALIDATED (draft) from PARTIAL (validated + nyquist_compliant: false) (#2117) +status: draft +nyquist_compliant: false +wave_0_complete: false +created: "2026-10-21" +--- + +# Phase 19.8 — Validation Strategy + +> Per-phase validation contract for feedback sampling during execution. + +--- + +## Test Infrastructure + +| Property | Value | +|----------|-------| +| **Framework** | {pytest 7.x / jest 29.x / vitest / go test / other} | +| **Config file** | {path or "none — Wave 0 installs"} | +| **Quick run command** | `{quick command}` | +| **Full suite command** | `{full command}` | +| **Estimated runtime** | ~19.8 seconds | + +--- + +## Sampling Rate + +- **After every task commit:** Run `{quick run command}` +- **After every plan wave:** Run `{full suite command}` +- **Before `/msd-verify-work`:** Full suite must be green +- **Max feedback latency:** 19.8 seconds + +--- + +## Per-Task Verification Map + +| Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status | +|---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------| +| 19.8-01-01 | 01 | 1 | REQ-{XX} | T-19.8-01 / — | {expected secure behavior or "N/A"} | unit | `{command}` | ✅ / ❌ W0 | ⬜ pending | + +*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky* + +--- + +## Wave 0 Requirements + +- [ ] `{tests/test_file.py}` — stubs for REQ-{XX} +- [ ] `{tests/conftest.py}` — shared fixtures +- [ ] `{framework install}` — if no framework detected + +*If none: "Existing infrastructure covers all phase requirements."* + +--- + +## Manual-Only Verifications + +| Behavior | Requirement | Why Manual | Test Instructions | +|----------|-------------|------------|-------------------| +| {behavior} | REQ-{XX} | {reason} | {steps} | + +*If none: "All phase behaviors have automated verification."* + +--- + +## Validation Sign-Off + +- [ ] All tasks have `` verify or Wave 0 dependencies +- [ ] Sampling continuity: no 3 consecutive tasks without automated verify +- [ ] Wave 0 covers all MISSING references +- [ ] No watch-mode flags +- [ ] Feedback latency < 19.8s +- [ ] `nyquist_compliant: true` set in frontmatter + +**Approval:** {pending / approved YYYY-MM-DD}