Files
summercms/modules/bonfire/README.md
Jakub Zych d9f939a1ea feat(11-02): run plugin schedules as River periodic jobs through bonfire.Call
- pact.HasSchedule with ScheduledCommand and Daily/DailyAt/Every cadences (no River import)
- bonfire.Call, Catalog and ErrUnknownCommand for in-process command runs
- conga Daily/Every wall-clock schedules in app.timezone, periodic jobs on every worker,
  scheduled queue (MaxAttempts 1, unique by args within the cadence period)
- scheduled worker runs only entries matching the compiled table; unregistered
  commands are skipped with a Warn log
- generated app main publishes bonfire.NewCatalog(commands); hello main regenerated
2026-09-29 19:47:51 +02:00

123 lines
6.3 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"`
## 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).
## 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`, `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: box-drawn tables (`bonfire.Output.Table`), a spinner around a function (`bonfire.Output.Spinner`) and a progress bar (`bonfire.Output.Progress`); both fall back to plain lines when output is not a terminal.
- Prompts: `bonfire.Output.Ask`, `bonfire.Output.Confirm`, `bonfire.Output.Choice` and `bonfire.Output.Secret`, which reads a hidden value on a terminal. Prompts return their defaults when input ends, and `bonfire.Output.Confirm` returns its default without asking when the session is not interactive.
- 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
})
},
}
root, err := bonfire.NewRoot("acme", []bonfire.Command{importPosts}, os.Stdout)
if err != nil {
os.Exit(1)
}
if err := root.Execute(); err != nil {
os.Exit(1)
}
}
```
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, flags, arguments and the `bonfire.Command.Run` function. |
| `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. |
| `bonfire.NewRootIO` | `bonfire.NewRoot` with injected stdin, stdout and stderr. |
| `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. |
## 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.
## Dependencies
- SummerCMS modules: none.
- Third-party: `github.com/spf13/cobra`, `golang.org/x/term`.
- Standard library: `bufio`, `context`, `errors`, `fmt`, `io`, `os`, `strconv`, `strings`, `sync`, `time`, `unicode/utf8`.
## Testing
```sh
go test ./modules/bonfire/...
```
The tests use in-memory streams and need no external services.