feat(bonfire): Artisan help, examples, JSON listing and error hints

- help <command> and <command> --help print Description, Usage,
  Arguments, Options with defaults, Global options, Help and Examples
- Command.Examples and Command.Hidden (appended, additive); list --all
  shows hidden commands, list --format=json prints a bonfire.Listing
- CheckDocs reports commands lacking description, long help, examples or
  argument and option descriptions
- Error block with the ERROR badge, WithHint explanation and Try lines
  ({bin} replaced), cobra suggestions as Try, -v error chain, messages
  scrubbed by sunscreen; Reported errors print nothing and exit 1
This commit is contained in:
Jakub Zych
2026-10-21 12:21:24 +02:00
parent 80fae5c279
commit fb32886775
12 changed files with 1130 additions and 52 deletions

View File

@@ -12,7 +12,7 @@ bonfire is the console layer of SummerCMS. Plugins and framework modules describ
## Role in the framework
A base layer for console commands with no framework dependencies. `pact` uses it so plugins can contribute commands, and `lagoon`, `conga`, `cabana`, `lighthouse`, `flare` and `surf` build their own commands (migrate, workers, serve, admin tools) on it. Keeping it separate means every module describes commands the same way without depending on the CLI binary.
A base layer for console commands; its only framework dependency is `sunscreen`, which scrubs credentials from error messages. `pact` uses it so plugins can contribute commands, and `lagoon`, `conga`, `cabana`, `lighthouse`, `flare` and `surf` build their own commands (migrate, workers, serve, admin tools) on it. Keeping it separate means every module describes commands the same way without depending on the CLI binary.
Use cases:
@@ -32,7 +32,11 @@ Use cases:
- Passthrough commands (`bonfire.Command.Passthrough`) skip flag parsing, so every argument, including `--help`, reaches `bonfire.Input.Args` unchanged; this suits commands that forward to another program. `bonfire.Command.Long` adds extended help text that the help command shows instead of the description.
- Artisan-style command list: a bare binary (or `--help`, or `list`) prints a version header (`bonfire.Version.Header`, read from the build information by `bonfire.BuildVersion`), the usage line, the global options and the commands, top-level names first and then one block per namespace, with aligned descriptions.
- Global options on every command: `-v`/`-vv`/`-vvv` (`--verbose`), `-q` (`--quiet`, implies `-n`), `--ansi`/`--no-ansi` (last wins), `-n` (`--no-interaction`) and `--env`. `bonfire.PeekGlobals` reads them from the raw arguments before the root exists, so the generated main can pick the configuration environment and the log verbosity before plugins boot; `bonfire.OnGlobals` hooks receive the parsed `bonfire.Globals` before every command runs (roots built by `bonfire.Call` run no hook).
- Exit codes: `bonfire.Execute` runs the root through fang and returns `bonfire.ExitOK`, `bonfire.ExitFailure`, or `bonfire.ExitUsage` for an unknown command or flag and a wrong number of arguments (`bonfire.UsageError`). Errors print as a block on the error stream (`bonfire.RenderError`); `bonfire.Fail` prints errors that happen before the root exists.
- Artisan-style help: `help <command>` and `<command> --help` print Description, Usage, Arguments, Options with `[default: ...]` and "(multiple values allowed)", the global options, Help (`bonfire.Command.Long`) and Examples (`bonfire.Command.Examples`, printed after `bin/<name>` or `summer`).
- Hidden commands (`bonfire.Command.Hidden`) run but stay out of the list unless `list --all`.
- JSON catalog: `list --format=json` prints every command, hidden ones flagged, with arguments, options, defaults and examples as a `bonfire.Listing` (`bonfire.Describe` builds it from a command slice).
- Documentation coverage: `bonfire.CheckDocs` reports commands lacking a description, long help, examples, or argument and option descriptions.
- Error block and exit codes: `bonfire.Execute` runs the root through fang and returns `bonfire.ExitOK`, `bonfire.ExitFailure`, or `bonfire.ExitUsage` for an unknown command or flag and a wrong number of arguments (`bonfire.UsageError`). Errors print on the error stream as a block (`bonfire.RenderError`): the ERROR badge and the message, the explanation and Try lines of `bonfire.WithHint` (`{bin}` becomes `bin/<name>` or `summer`), cobra's "did you mean" suggestions as Try lines, and "Run with -v for details." or, at `-v`, the wrapped error chain; every message passes through `sunscreen.Scrub`. Errors wrapped by `bonfire.Reported` print nothing. `bonfire.Fail` prints errors that happen before the root exists.
- Name checks at boot: two commands with the same name, or a command named `list` or `help`, make `bonfire.NewRoot` fail with `bonfire.ErrDuplicateCommand`; a command flag that reuses a global option's name or shorthand fails with `bonfire.ErrReservedFlag` instead of panicking.
- Injectable streams (`bonfire.NewRootIO`, `bonfire.NewOutput`) so commands can be tested against buffers.
- In-process calls: `bonfire.Call` runs a named command with arguments against any writer (Laravel `Artisan::call`), and `bonfire.Catalog` holds an application binary's final command list so code outside the Cobra root, such as the conga scheduler, can call any registered command.
@@ -98,7 +102,8 @@ if catalog.Has("blog:import") {
| Identifier | Description |
|------------|-------------|
| `bonfire.Command` | A console command: name, description, optional extended help (`bonfire.Command.Long`), flags, arguments, the `bonfire.Command.Passthrough` switch that hands every argument to Run unparsed, and the `bonfire.Command.Run` function. |
| `bonfire.Command` | A console command: name, description, optional extended help (`bonfire.Command.Long`), flags, arguments, the `bonfire.Command.Passthrough` switch that hands every argument to Run unparsed, the `bonfire.Command.Run` function, usage examples (`bonfire.Command.Examples`) and the `bonfire.Command.Hidden` switch. |
| `bonfire.Example` | One usage example: the command line without the binary (`Command`) and a short `Description`. |
| `bonfire.Flag` | A string flag; `bonfire.Flag.Bare` allows the flag without a value, `bonfire.Flag.Repeatable` makes it an ordered multi-value flag. |
| `bonfire.Arg` | A positional argument with a name, description and required marker. |
| `bonfire.Input` | Parsed view handed to `bonfire.Command.Run`: `bonfire.Input.Args`, `bonfire.Input.Argument`, `bonfire.Input.Flag` and `bonfire.Input.Flags`. |
@@ -121,6 +126,19 @@ if catalog.Has("blog:import") {
| `bonfire.RenderError` | Writes an error as the console error block. |
| `bonfire.UsageError` | Marks a usage mistake (exit code 2); `bonfire.Usage` wraps an error in it. |
| `bonfire.Usage` | Wraps an error as a `bonfire.UsageError`. |
| `bonfire.HintError` | An error with an `Explanation` and a `Try` text that the error block prints. |
| `bonfire.WithHint` | Wraps an error in a `bonfire.HintError`; `{bin}` in the Try text becomes `bin/<name>` or `summer`. |
| `bonfire.ErrReported` | Marks an error whose details the command already printed. |
| `bonfire.Reported` | Wraps an error so it matches `bonfire.ErrReported`: exit code 1, no error block. |
| `bonfire.Listing` | The JSON catalog of `list --format=json`: `Application` (`bonfire.ApplicationInfo`), `GlobalOptions` and `Commands`. |
| `bonfire.ApplicationInfo` | Name, version and framework version of a binary in a `bonfire.Listing`. |
| `bonfire.CommandInfo` | One command of a `bonfire.Listing`: name, description, long help, usage, hidden and passthrough switches, `Arguments`, `Options` and `Examples`. |
| `bonfire.ArgumentInfo` | One argument of a `bonfire.CommandInfo`. |
| `bonfire.OptionInfo` | One flag of a `bonfire.CommandInfo`, or a global option, with its default. |
| `bonfire.ExampleInfo` | One example of a `bonfire.CommandInfo`. |
| `bonfire.Describe` | Builds the `bonfire.Listing` of a binary from its commands and `bonfire.Version`. |
| `bonfire.CheckDocs` | Reports commands lacking a description, long help, examples or argument and option descriptions, as `bonfire.DocProblem` values. |
| `bonfire.DocProblem` | One command's documentation gaps; `bonfire.DocProblem.String` reads "<command>: missing description, examples". |
| `bonfire.ErrDuplicateCommand` | Returned by `bonfire.NewRoot` when two commands share a name or a command is named `list` or `help`. |
| `bonfire.ErrReservedFlag` | Returned by `bonfire.NewRoot` when a command flag reuses a global option's name or shorthand. |
| `bonfire.NewOutput` | Builds a `bonfire.Output` over the given streams, applying the terminal and color policy. |
@@ -148,9 +166,9 @@ Without these variables, color is enabled only when stdout is a terminal.
## Dependencies
- SummerCMS modules: none.
- SummerCMS modules: `sunscreen` (scrubs error messages).
- Third-party: `github.com/spf13/cobra`, `github.com/spf13/pflag`, `golang.org/x/term`, `charm.land/lipgloss/v2`, `charm.land/fang/v2` and `github.com/charmbracelet/colorprofile` (decided in `.planning/notes/2026-10-21-console-charm-stack.md`).
- Standard library: `bufio`, `context`, `errors`, `fmt`, `io`, `os`, `runtime/debug`, `slices`, `strconv`, `strings`, `sync`, `time`, `unicode/utf8`.
- Standard library: `bufio`, `context`, `encoding/json`, `errors`, `fmt`, `io`, `os`, `runtime/debug`, `slices`, `strconv`, `strings`, `sync`, `time`, `unicode/utf8`.
## Testing