feat(bonfire): global flags, Artisan list and exit codes

- Global options -v/-vv/-vvv, -q, --ansi/--no-ansi, -n and --env on every
  command; PeekGlobals reads them before config loads, OnGlobals hooks
  re-apply them after parsing (diary.ApplyGlobals in the generated main)
- Bare binary, --help and list print the Artisan list with the version
  header from BuildVersion and namespace blocks
- Execute runs the root through fang (no version, man page or completions)
  and returns 0, 1 or 2; Fail renders errors before the root exists
- NewRoot rejects duplicate command names (ErrDuplicateCommand) and flags
  that reuse a global option (ErrReservedFlag)
- Generated main and summer use PeekGlobals, compass.Open with --env,
  diary.FromGlobals and bonfire.Execute; docs checker knows the globals
This commit is contained in:
Jakub Zych
2026-10-21 12:13:23 +02:00
parent b7c07591cb
commit 8d4d7ce7a7
25 changed files with 1158 additions and 154 deletions

View File

@@ -10,7 +10,7 @@ Application logging modelled on Laravel's logging config: named channels (consol
`diary` owns the process logger of a SummerCMS application. `config/log.yaml` names a default channel, a global level and a map of channels, the way Laravel's `config/logging.php` does: a console channel that prints styled lines on a terminal, daily files with retention, and a stack that fans a record out to several channels. Code logs through named component loggers (`diary.For`), so every record carries the plugin or module it came from. Every channel is wrapped by `sunscreen.Wrap`, so credentials are redacted whichever channel a record reaches.
The application `main` that `summer build` generates calls `diary.Bootstrap` as its first statement, which installs a console channel before configuration is read, and `diary.Setup` right after `compass` loads `config/`.
The application `main` that `summer build` generates reads the console global options first (`bonfire.PeekGlobals`), calls `diary.Bootstrap` with them, which installs a console channel before configuration is read, and `diary.Setup` right after `compass` loads `config/`. Once the command line is parsed, `diary.ApplyGlobals` re-applies `-v`, `-q` and `--ansi`/`--no-ansi`.
## Role in the framework
@@ -33,6 +33,7 @@ Use cases:
- Daily files: the path `storage/logs/summer.log` gives files `storage/logs/summer-2026-10-21.log`, created mode 0640 in directories created 0750, opened on the first record, rotated at local midnight, and deleted after the retention in days (only files of that name pattern). A relative path resolves against the application root. A directory that cannot be written logs one warning from component `diary` on the console and disables the channel; it never fails the start-up.
- Redaction: every channel leaf is wrapped by `sunscreen.Wrap`.
- Runtime verbosity: `diary.SetVerbosity` lets info (1) or debug (2) through for every component and adds source locations on styled lines (3); quiet raises only the console channels to warn.
- Console global options: `diary.FromGlobals` turns `-v`/`-vv`/`-vvv`, `-q` and `--ansi`/`--no-ansi` into `diary.Options`, and `diary.ApplyGlobals` applies them to the running setup (the generated main registers it with `bonfire.OnGlobals`).
- The process default logger (`slog.Default`) and the standard `log` package log through the configured channels without a component.
## Usage
@@ -58,16 +59,20 @@ and writes warnings and errors as JSON to `storage/logs/summer-YYYY-MM-DD.log`.
The generated `main` installs it:
```go
func run(args []string, out io.Writer) error {
diary.Bootstrap(os.Stderr, diary.Options{})
cfg, err := compass.Load("config")
func run(args []string, out io.Writer) int {
g := bonfire.PeekGlobals(args)
diary.Bootstrap(os.Stderr, diary.FromGlobals(g))
cfg, err := compass.Open(compass.Options{Dir: "config", Env: g.Env})
if err != nil {
return err
return bonfire.Fail(os.Stderr, "acme", err, g)
}
if err := diary.Setup(cfg, diary.Options{}); err != nil {
return err
if err := diary.Setup(cfg, diary.FromGlobals(g)); err != nil {
return bonfire.Fail(os.Stderr, "acme", err, g)
}
// ...
root, err := bonfire.NewRoot("acme", commands, out,
bonfire.WithVersion(bonfire.BuildVersion()), bonfire.OnGlobals(diary.ApplyGlobals))
// ...
}
```
@@ -81,6 +86,8 @@ Tests that set diary up restore the previous process logger with `t.Cleanup(diar
| `diary.Bootstrap` | Installs a console channel on a writer before configuration is read and makes diary the process default logger. |
| `diary.Setup` | Parses `log.*` from a `compass` configuration, builds the channels and installs them; returns configuration errors. |
| `diary.SetVerbosity` | Changes verbosity and quiet at runtime. |
| `diary.FromGlobals` | Returns the `diary.Options` (verbosity, quiet, color mode) of the console global options in a `bonfire.Globals`. |
| `diary.ApplyGlobals` | Applies parsed `bonfire.Globals` to the running setup: verbosity and quiet, and the console color mode when it changed. |
| `diary.Verbosity` | Returns the current verbosity. |
| `diary.Reset` | Drops the setup, closes its files and restores the process default logger captured at the first setup. |
| `diary.Close` | Closes the open daily files; later records open them again. |

View File

@@ -70,6 +70,8 @@ type state struct {
verbosity int
quiet bool
channels *channels
conf Config
opts Options
}
// floor is the minimum level for records of component name: its
@@ -165,6 +167,8 @@ func install(conf Config, ch *channels, opts Options) {
verbosity: opts.Verbosity,
quiet: opts.Quiet,
channels: ch,
conf: conf,
opts: opts,
}
old := current.Swap(st)
slog.SetDefault(slog.New(newComponentHandler("")))
@@ -188,6 +192,7 @@ func SetVerbosity(v int, quiet bool) {
}
next := *st
next.verbosity, next.quiet = v, quiet
next.opts.Verbosity, next.opts.Quiet = v, quiet
for _, c := range st.channels.consoles {
c.apply(quiet)
}
@@ -195,6 +200,31 @@ func SetVerbosity(v int, quiet bool) {
current.Store(&next)
}
// FromGlobals returns the Options for the console global options: their
// verbosity, quiet switch and color mode. The generated main passes it to
// Bootstrap and Setup.
func FromGlobals(g bonfire.Globals) Options {
return Options{Verbosity: g.Verbosity, Quiet: g.Quiet, Color: g.Color}
}
// ApplyGlobals applies the parsed global options to the installed setup:
// the verbosity and quiet switch (SetVerbosity) and, when it differs from
// the one the channels were built with, the console color mode, which
// rebuilds the channels from the same configuration. The generated main
// registers it with bonfire.OnGlobals.
func ApplyGlobals(g bonfire.Globals) {
st := current.Load()
if st != nil && st.opts.Color != g.Color {
opts := st.opts
opts.Color, opts.Verbosity, opts.Quiet = g.Color, g.Verbosity, g.Quiet
if ch, err := buildChannels(st.conf, opts); err == nil {
install(st.conf, ch, opts)
return
}
}
SetVerbosity(g.Verbosity, g.Quiet)
}
// Verbosity returns the current verbosity (0 by default).
func Verbosity() int {
return int(verbosity.Load())