- serve prints a startup summary on a terminal (version, environment, URL, admin, database host and name, pending migrations, plugins, drivers) and logs it as a 'startup summary' record plus 'listening on <addr>' - on SIGINT/SIGTERM serve drains, prints and logs a shutdown report and returns nil (exit 0), or the drain errors (exit 1) - the request log is on by default under diary component http with request_id; the route pattern, never the path, is logged - lagoon.PendingMigrations, conga.Worker.Stats, diary.HideOnTTY, bonfire.IsTerminal and bonfire.Details back the summary and report
203 lines
18 KiB
Markdown
203 lines
18 KiB
Markdown
# bonfire
|
|
|
|
Declarative console commands for the `summer` tool and application binaries, adapted to Cobra with typed input, prompts and styled output.
|
|
|
|
`import "git.golem15.com/golem15/summercms/modules/bonfire"`
|
|
|
|
**Name:** The evening fire where everyone gathers; the command line is where you sit down and fire off commands.
|
|
|
|
## Overview
|
|
|
|
bonfire is the console layer of SummerCMS. Plugins and framework modules describe commands as plain `bonfire.Command` values (name, flags, arguments and a run function), and `bonfire.NewRoot` turns a slice of them into a Cobra root command. Commands never touch Cobra directly: they read arguments through `bonfire.Input` and write through `bonfire.Output`, which also provides tables, spinners, progress bars and interactive prompts. It is the counterpart of WinterCMS's artisan console commands (`registerConsoleCommand` and Laravel's `Illuminate\Console\Command` output helpers).
|
|
|
|
## Role in the framework
|
|
|
|
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:
|
|
|
|
- Add a plugin command such as blog:import with typed arguments and options.
|
|
- Ask for confirmation or input interactively and print styled tables and progress.
|
|
- Ship operational commands (backfills, health checks) inside the same binary as the application.
|
|
|
|
## Features
|
|
|
|
- Command values with a description, positional arguments (`bonfire.Arg`) and string flags (`bonfire.Flag`), collected from plugins or the tool itself.
|
|
- Command name validation: plugin commands must use the `namespace:verb` form (for example `blog:import`); `build`, `dev`, `new`, `serve` and `migrate` are the only bare names accepted. Invalid names make `bonfire.NewRoot` fail with `bonfire.ErrCommandName`.
|
|
- Usage strings and argument-count checks derived from the declared arguments (`<name>` for required, `[name]` for optional).
|
|
- Scalar flags, bare flags (`bonfire.Flag.Bare`, so `--force` alone stores `true`) and ordered repeatable flags (`bonfire.Flag.Repeatable`, read back through `bonfire.Input.Flags`).
|
|
- Styled status lines: `bonfire.Output.Info`, `bonfire.Output.Success`, `bonfire.Output.Warning` and `bonfire.Output.Error` (the last one writes to the error stream).
|
|
- Widgets on the charm stack, ported from SSU's terminal UI: box-drawn tables (`bonfire.Output.Table`, lipgloss), a spinner around a function (`bonfire.Output.Spinner`), a gradient progress bar (`bonfire.Output.Progress`, whose `bonfire.Progress` also implements `bonfire.ItemProgress`) and a multi-row process view for bulk loops (`bonfire.Process`), each a bubbletea program that keeps stdin and signals to the command. Log lines and output printed while a view runs appear above it (`bonfire.PrintAbove`).
|
|
- Prompts: `bonfire.Output.Ask`, `bonfire.Output.Confirm` and `bonfire.Output.Secret` use huh fields on an interactive terminal, and `bonfire.Output.Choice` an arrow-key selector with a confirmation screen. Off a terminal they read lines from stdin as before (a piped password still works), return their defaults when input ends, and `bonfire.Output.Confirm` returns its default without asking. A cancelled prompt returns `bonfire.ErrAborted`. `bonfire.ConfirmProceed` asks before a destructive step only on an interactive terminal.
|
|
- Degradation: every widget follows the terminal and the global options, and the `bonfire.Output` interface is unchanged for plugins:
|
|
|
|
| Run | Animation | Decoration | Data | Prompts |
|
|
|-----|-----------|------------|------|---------|
|
|
| Terminal | spinner, progress bar, process view | colors, symbols, boxed tables | printed | huh fields and the selector |
|
|
| Not a terminal | none: `[...] message` and a line per tenth of progress | symbols, no colors unless `FORCE_COLOR` | printed, tables tab-separated | read a line from stdin; `Confirm` returns its default |
|
|
| `-q` | none | info, success and warning lines hidden; errors kept | printed (`Println`, `Printf`, tables) | defaults, as with `-n` |
|
|
| `--no-ansi` | none | symbols, no colors | printed | plain line prompts |
|
|
| `-n` | none | as on the terminal | printed | defaults; `Secret` reads piped stdin or fails with a hint |
|
|
- Command state for helpers: `bonfire.GlobalsOf`, `bonfire.VerbosityOf`, `bonfire.IsQuiet` and `bonfire.Interactive` read the running command's global options from its `bonfire.Output`.
|
|
- 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).
|
|
- 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.
|
|
|
|
## Usage
|
|
|
|
```go
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
|
|
"git.golem15.com/golem15/summercms/modules/bonfire"
|
|
)
|
|
|
|
func main() {
|
|
importPosts := bonfire.Command{
|
|
Name: "blog:import",
|
|
Description: "Import posts from a feed",
|
|
Args: []bonfire.Arg{{Name: "url", Description: "Feed URL", Required: true}},
|
|
Flags: []bonfire.Flag{
|
|
{Name: "dry-run", Description: "Report without writing", Bare: true},
|
|
{Name: "tag", Description: "Tag to apply (repeatable)", Repeatable: true},
|
|
},
|
|
Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
|
|
url, _ := in.Argument("url")
|
|
_, dryRun := in.Flag("dry-run")
|
|
return out.Spinner("Importing "+url, func() error {
|
|
out.Table([]string{"Tag"}, [][]string{{"news"}})
|
|
if dryRun {
|
|
out.Warning("dry run: nothing written")
|
|
}
|
|
return nil
|
|
})
|
|
},
|
|
}
|
|
|
|
args := os.Args[1:]
|
|
g := bonfire.PeekGlobals(args)
|
|
root, err := bonfire.NewRoot("acme", []bonfire.Command{importPosts}, os.Stdout,
|
|
bonfire.WithVersion(bonfire.BuildVersion()))
|
|
if err != nil {
|
|
os.Exit(bonfire.Fail(os.Stderr, "acme", err, g))
|
|
}
|
|
os.Exit(bonfire.Execute(context.Background(), root, args))
|
|
}
|
|
```
|
|
|
|
Running a command in-process, with empty stdin so prompts take their defaults:
|
|
|
|
```go
|
|
catalog := bonfire.NewCatalog(commands)
|
|
if catalog.Has("blog:import") {
|
|
err := catalog.Call(ctx, "blog:import", []string{"--dry-run", "https://example.com/feed"}, os.Stdout)
|
|
if errors.Is(err, bonfire.ErrUnknownCommand) {
|
|
// not registered in this binary
|
|
}
|
|
}
|
|
```
|
|
|
|
## API reference
|
|
|
|
| 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, 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`. |
|
|
| `bonfire.Output` | Injected console: printing, status lines, tables, spinner, progress bar and prompts. |
|
|
| `bonfire.Progress` | A progress bar advanced from inside `bonfire.Output.Progress`. |
|
|
| `bonfire.NewRoot` | Builds the Cobra root command for a binary from a slice of commands, using the process stdin, with the global options, the `list` command and the command list on a bare call; takes `bonfire.RootOption` values. When the output is the process stdout, the error block goes to stderr. |
|
|
| `bonfire.NewRootIO` | `bonfire.NewRoot` with injected stdin, stdout and stderr. |
|
|
| `bonfire.RootOption` | An option of `bonfire.NewRoot` and `bonfire.NewRootIO`. |
|
|
| `bonfire.WithVersion` | Sets the `bonfire.Version` shown in the list header; without it the root reads `bonfire.BuildVersion`. |
|
|
| `bonfire.OnGlobals` | Registers a function that receives the parsed `bonfire.Globals` before every command of the root runs. |
|
|
| `bonfire.Globals` | The global options: `Verbosity` (0 to 3), `Quiet`, `Color` (a `bonfire.ColorMode`), `NoInteraction` and `Env`. |
|
|
| `bonfire.PeekGlobals` | Reads `bonfire.Globals` from raw arguments without failing; stops at `--`. |
|
|
| `bonfire.Globals.Args` | Turns `bonfire.Globals` back into flags (`-vv`, `-q`, `-n`, `--ansi` or `--no-ansi`, `--env=<value>`) for forwarding. |
|
|
| `bonfire.Version` | Application and framework versions of a binary (`App`, `Framework`, `Local`). |
|
|
| `bonfire.BuildVersion` | Reads the `bonfire.Version` from the binary's build information. |
|
|
| `bonfire.Version.Header` | The list header: `SummerCMS acme v1.4.0 (framework v0.3.2)`. |
|
|
| `bonfire.Execute` | Runs a root with arguments through fang and returns the exit code; prints the list when no command is given. |
|
|
| `bonfire.Fail` | Renders an error that happened before the root exists and returns `bonfire.ExitFailure`. |
|
|
| `bonfire.ExitOK`, `bonfire.ExitFailure`, `bonfire.ExitUsage` | The exit codes 0, 1 and 2. |
|
|
| `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.ItemProgress` | The optional side of a `bonfire.Progress` that names the current item (`Current`) and records failures (`Fail`); `bonfire.Output.Progress` hands one over. |
|
|
| `bonfire.Current` | Names the current item on a `bonfire.Progress` that is an `bonfire.ItemProgress`. |
|
|
| `bonfire.Failed` | Records a failed item on a `bonfire.Progress` that is an `bonfire.ItemProgress`. |
|
|
| `bonfire.Process` | Runs a function per item with the process view on a terminal, or one `✓`/`✗` line per item; returns the joined failures. |
|
|
| `bonfire.PrintAbove` | Prints a line above the running live view and reports whether one was running. |
|
|
| `bonfire.GlobalsOf` | The `bonfire.Globals` of the command an Output belongs to (zero for other Output implementations). |
|
|
| `bonfire.VerbosityOf` | The `-v` level of the command an Output belongs to. |
|
|
| `bonfire.IsQuiet` | Whether the command runs with `-q`. |
|
|
| `bonfire.IsTerminal` | Whether the Output writes to a terminal; long-running commands print a summary block for a person there and log records otherwise. |
|
|
| `bonfire.Details`, `bonfire.Detail` | Prints a title and aligned label/value lines in the palette (a `Warn` value in the warning color); nothing under `-q`. Used for startup summaries and shutdown reports. |
|
|
| `bonfire.Interactive` | Whether the Output can ask a person: stdin and stdout are terminals and neither `-n` nor `-q` is set. |
|
|
| `bonfire.ConfirmProceed` | Asks before a destructive step on an interactive terminal; returns true without asking otherwise. |
|
|
| `bonfire.ErrAborted` | Returned by a prompt the person cancelled. |
|
|
| `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. |
|
|
| `bonfire.ErrCommandName` | Returned when a plugin command name is not in `namespace:verb` form. |
|
|
| `bonfire.Call` | Runs one command of a slice by exact name with arguments, writing output to a writer; stdin is empty. |
|
|
| `bonfire.ErrUnknownCommand` | Returned by `bonfire.Call` when no command has the name. |
|
|
| `bonfire.Catalog` | An immutable copy of a binary's command list; the generated app main publishes one on the app. |
|
|
| `bonfire.NewCatalog` | Builds a `bonfire.Catalog` from a command slice. |
|
|
| `bonfire.ColorMode` | How console output decides on color: `bonfire.ColorAuto` (terminal and environment), `bonfire.ColorAlways` or `bonfire.ColorNever`. |
|
|
| `bonfire.Palette` | The console styles (`Success`, `Current`, `Warning`, `Error`, `ErrorBold`, `Muted`, `Header`), shared with the `diary` console log lines. |
|
|
| `bonfire.DefaultPalette` | Returns the palette ported from SSU's status styles: green success, cyan current, yellow warning, red and bold red errors, gray muted text, bold headers. |
|
|
| `bonfire.DetectProfile` | Returns the color profile for a writer under a `bonfire.ColorMode`, honouring `NO_COLOR`, `TERM=dumb` and `FORCE_COLOR`; render palette styles through a `colorprofile.Writer` with it. |
|
|
|
|
## Configuration
|
|
|
|
bonfire reads no config keys. Output color follows these environment variables:
|
|
|
|
| Variable | Effect |
|
|
|----------|--------|
|
|
| `NO_COLOR` | Any non-empty value disables color. |
|
|
| `TERM` | The value `dumb` disables color. |
|
|
| `FORCE_COLOR` | Any non-empty value enables color even when output is not a terminal (ignored when color is disabled by `NO_COLOR` or `TERM`). |
|
|
|
|
Without these variables, color is enabled only when stdout is a terminal. `--ansi` forces color and `--no-ansi` turns it off whatever the environment says.
|
|
|
|
## Dependencies
|
|
|
|
- SummerCMS modules: `sunscreen` (scrubs error messages).
|
|
- Third-party: `github.com/spf13/cobra`, `github.com/spf13/pflag`, `golang.org/x/term`, `charm.land/lipgloss/v2` (with `lipgloss/v2/table`), `charm.land/bubbletea/v2`, `charm.land/bubbles/v2` (spinner, progress, key), `charm.land/huh/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`, `encoding/json`, `errors`, `fmt`, `io`, `os`, `runtime/debug`, `slices`, `strconv`, `strings`, `sync`, `time`, `unicode/utf8`.
|
|
|
|
## Testing
|
|
|
|
```sh
|
|
go test ./modules/bonfire/...
|
|
```
|
|
|
|
The tests use in-memory streams and need no external services.
|