Files
summercms/modules/bonfire/README.md
Jakub Zych c38dc1fe48 feat(surf): serve startup summary, shutdown report and http request log
- 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
2026-10-21 12:59:07 +02:00

18 KiB

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

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:

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 ": 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

go test ./modules/bonfire/...

The tests use in-memory streams and need no external services.