Each module README now says where the module sits in the dependency graph, why it is its own module, and lists use cases for application developers.
136 lines
7.5 KiB
Markdown
136 lines
7.5 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 dependency-free base layer for console commands. `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: 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.
|
|
- 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.
|
|
- 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, 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.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.
|