--- title: Writing commands description: Add console commands to a plugin with bonfire.Command values, arguments, flags, styled output and prompts, and run commands in-process. section: console order: 40 --- # Writing commands A plugin adds console commands to the application binary the way a WinterCMS plugin calls `registerConsoleCommand`. In SummerCMS a command is a plain `bonfire.Command` value, and the plugin returns its commands from `pact.HasCommands`. `summer make:command acme.blog Publish` generates a starting point in `console/publish.go`. ## Defining a command A `bonfire.Command` has a name, a description, its positional arguments and flags, and a run function: - The name is in `namespace:verb` form, such as `blog:publish`. Plugin commands must use this form; only a few framework commands have bare names. - Each `bonfire.Arg` is a positional argument with a name, a description and a `Required` marker. The usage line shows required arguments as `` and optional ones as `[name]`. - Each `bonfire.Flag` is a string flag. Set `bonfire.Flag.Bare` for a switch such as `--dry-run` that stores `true` when given alone, and `bonfire.Flag.Repeatable` for a flag that can be given several times. - `bonfire.Command.Run` receives the context, a `bonfire.Input` and a `bonfire.Output`. - `bonfire.Command.Long` is optional extended help, shown under "Help:" by `help ` and ` --help`. `bonfire.Command.Examples` and `bonfire.Command.Hidden` are described below. Set `bonfire.Command.Passthrough` for a command that forwards to another program: flag parsing is turned off, so every argument after the command name, including `--help`, `-h` and `--`, reaches `bonfire.Input.Args` unchanged. Declared flags then appear in the usage text only, and declared arguments are not count-checked. Read arguments with `bonfire.Input.Argument`, scalar and bare flags with `bonfire.Input.Flag`, and repeatable flags with `bonfire.Input.Flags`, which returns the values in the order given: ```go src=modules/bonfire/example_test.go#ExampleCatalog publish := bonfire.Command{ Name: "blog:publish", Description: "Publish a post", Args: []bonfire.Arg{{Name: "slug", Description: "Post slug", Required: true}}, Flags: []bonfire.Flag{ {Name: "dry-run", Description: "Report without writing", Bare: true}, {Name: "tag", Description: "Tag to add (repeatable)", Repeatable: true}, }, Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error { slug, _ := in.Argument("slug") dryRun, _ := in.Flag("dry-run") out.Printf("publish %s, tags %v, dry run %s\n", slug, in.Flags("tag"), dryRun) return nil }, } // The generated main publishes a catalog of every command on the app. catalog := bonfire.NewCatalog([]bonfire.Command{publish}) args := []string{"hello-world", "--tag", "news", "--tag", "go", "--dry-run"} if err := catalog.Call(context.Background(), "blog:publish", args, os.Stdout); err != nil { fmt.Println(err) } // Output: publish hello-world, tags [news go], dry run true ``` ## Registering commands Return the commands from the plugin's `Commands` method, which implements `pact.HasCommands`. The generated `main` appends every plugin's commands after the framework's runtime commands. Command names must be unique: when two commands share a name, or a command is named `list` or `help`, `bonfire.NewRoot` fails with `bonfire.ErrDuplicateCommand` naming the command, and the binary does not start. Keep your commands in your plugin's own namespace, such as `blog:`. The global options are reserved as well: a flag named `verbose`, `quiet`, `ansi`, `no-ansi`, `no-interaction`, `env` or `help`, or with the shorthand `v`, `q`, `n` or `h`, makes `bonfire.NewRoot` fail with `bonfire.ErrReservedFlag`. A scaffolded plugin's `Commands` method returns the generated list of everything in `console/`, so commands created with `summer make:command` are registered without editing `plugin.go`. ## Output `bonfire.Output` is the console your command writes to. Besides `bonfire.Output.Printf` and `bonfire.Output.Println`, it provides: - status lines: `bonfire.Output.Info`, `bonfire.Output.Success`, `bonfire.Output.Warning` and `bonfire.Output.Error` (the last one writes to the error stream); - widgets: `bonfire.Output.Table`, `bonfire.Output.Spinner` around a function and `bonfire.Output.Progress` for a progress bar. On a terminal they are SSU's live views on the charm stack: a spinner, a gradient bar and boxed tables; - prompts: `bonfire.Output.Ask`, `bonfire.Output.Confirm`, `bonfire.Output.Choice` and `bonfire.Output.Secret`. On an interactive terminal they are huh fields and an arrow-key selector; otherwise they read lines from stdin, so a password can be piped in, and return their defaults when input ends, so a command run from cron or a script never hangs. How much of this appears depends on where the command runs and on the global options: | 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 | printed, tables tab-separated | read a line from stdin; `Confirm` returns its default | | `-q` | none | info, success and warning lines hidden; errors kept | printed | 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 | Print the data your command exists to produce (a token, a key, JSON) with `bonfire.Output.Println` or `bonfire.Output.Printf`, never as a status line: `-q` keeps that output and drops only the decoration around it. `bonfire.IsQuiet`, `bonfire.VerbosityOf` and `bonfire.GlobalsOf` tell a command which options it runs under. Before a destructive step, ask with `bonfire.ConfirmProceed`. It asks only on an interactive terminal; off a terminal, with `-n` or `-q`, and when the scheduler calls the command, it returns true without asking, so deploys over SSH never wait for an answer. For a loop over many items, `bonfire.Process` runs a function per item and shows SSU's process view on a terminal, one row per item with its state, or prints one `✓ item` or `✗ item: error` line per item elsewhere; it returns the failures joined. Inside `bonfire.Output.Progress`, `bonfire.Current` names the item being worked on and `bonfire.Failed` records a failed one. Log lines and output printed while a live view runs appear above the view (`bonfire.PrintAbove`). Return an error from `Run` to fail the command. The binary prints it as an error block on stderr and exits with status 1; see [Errors and hints](#errors-and-hints). ## Examples and hidden commands `bonfire.Command.Examples` lists usage examples. Each `bonfire.Example` holds the command line without the binary and a short description; help prints the description as a `#` comment and the line after `bin/` (or `summer` for the tool's own commands), so one example serves both. `bonfire.Command.Hidden` keeps a command that still runs out of the list; `list --all` and `list --format=json` show it: ```go src=modules/bonfire/example_test.go#ExampleExample export := bonfire.Command{ Name: "blog:export", Description: "Export published posts as JSON", Long: "Writes every published post, newest first, to a JSON file.", Flags: []bonfire.Flag{ {Name: "path", Description: "Output file", Default: "posts.json"}, }, Examples: []bonfire.Example{ {Command: "blog:export --path=backup.json", Description: "Export to another file"}, }, Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error { return nil }, } // Hidden commands run, but list shows them only with --all. cleanup := bonfire.Command{ Name: "blog:cleanup", Description: "Remove orphaned uploads", Hidden: true, } root, err := bonfire.NewRootIO("acme", []bonfire.Command{export, cleanup}, strings.NewReader(""), os.Stdout, os.Stdout) if err != nil { fmt.Println(err) return } bonfire.Execute(context.Background(), root, []string{"help", "blog:export"}) // Output: // Description: // Export published posts as JSON // // Usage: // blog:export [options] // // Options: // --path=PATH Output file [default: "posts.json"] // // Global options: // -h, --help Display help for the given command // -q, --quiet Do not output any message // -v|vv|vvv Increase verbosity // --ansi|no-ansi Force (or disable) ANSI output // -n, --no-interaction Do not ask any interactive question // --env The environment the command runs under // // Help: // Writes every published post, newest first, to a JSON file. // // Examples: // # Export to another file // bin/acme blog:export --path=backup.json ``` `list --format=json` prints every command with its arguments, options (with defaults) and examples as a `bonfire.Listing`, for documentation checks, scripts and agents. `bonfire.Describe` builds the same listing from a command slice. ### What a finished command has The framework's own commands follow these rules, and so should a plugin's: - **Description:** one line in the imperative, sentence case, without a trailing period: "Publish a blog post by its slug". - **Long help:** what the command does, when to run it (by hand, from cron, from the scheduler), what each flag changes, and its exit codes. - **Two or three examples:** the common call first, then the flags people reach for. - **A result line:** a one-shot command ends with a line that says what it did, such as `Published hello-world` or `Imported 120 posts (3 skipped)`, and returns an error when it failed, so the exit code is 1. - **Data on stdout:** what a script reads (a token, a key, `--json` output) goes through `Println` or `Printf`, which `-q` does not hide; information and success lines go through `Info` and `Success`, which it does. - **Flags with a meaning people know:** `--json` for output scripts read, `--pretend` or `--dry-run` for a run that changes nothing, `--force` to skip a confirmation. A destructive step asks through `bonfire.ConfirmProceed`, which never asks off an interactive terminal; never give an existing flag a new meaning. - **A named logger:** log through `diary.For("")` from a package variable, not `slog.Default`, so `log.components` can raise or silence the plugin. See [Logging](../services/logging.md). ## Errors and hints When `Run` returns an error, the binary prints a block on stderr: the ERROR badge and the message, then "Run with -v for details.". With `-v` the block lists the wrapped errors under "Caused by:" instead. Messages pass through `sunscreen.Scrub`, so a token inside an error text is not printed. Wrap an error with `bonfire.WithHint` to explain it and to say what to run next. `{bin}` in the Try text becomes `bin/` or `summer`, so the hint names the right program: ```go src=modules/bonfire/example_test.go#ExampleWithHint publish := bonfire.Command{ Name: "blog:publish", Description: "Publish a draft post", Args: []bonfire.Arg{{Name: "slug", Description: "Post slug", Required: true}}, Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error { slug, _ := in.Argument("slug") return bonfire.WithHint(fmt.Errorf("blog: no draft named %q", slug), "Only drafts can be published; published posts keep their slug.", "{bin} blog:drafts") }, } root, err := bonfire.NewRootIO("acme", []bonfire.Command{publish}, strings.NewReader(""), os.Stdout, os.Stdout) if err != nil { fmt.Println(err) return } code := bonfire.Execute(context.Background(), root, []string{"blog:publish", "hello-world"}) fmt.Println("exit code:", code) // Output: // ERROR blog: no draft named "hello-world" // // Only drafts can be published; published posts keep their slug. // // Try: // bin/acme blog:drafts // // Run with -v for details. // // exit code: 1 ``` A command that has already printed its problems, such as a checker that lists every finding, returns `bonfire.Reported(err)`: the binary exits 1 and prints nothing more. A mistake in how the command was called, such as an unknown flag or a missing argument, exits 2 and the block suggests `help `; wrap your own usage checks with `bonfire.Usage` to get the same exit code. An application binary also explains the most common mistake: running it from the wrong directory. When a command fails, the working directory has no `config/` directory and a parent directory holds `summer.yaml`, the block says "This command runs from the application root." and suggests the same command from there: ```text ERROR lagoon: database.dsn is empty (set SUMMER_DATABASE__DSN) This command runs from the application root. Try: cd ../..; bin/acme migrate ``` Only the message changes: the binary still reads its configuration from `config/` in the working directory. `summer` is not affected, because its commands find the application root themselves. ## Documentation coverage `bonfire.CheckDocs` reports each command that lacks a description, long help, an example, or a description of one of its arguments or options. It only reports; the framework's own test fails on any problem, and a plugin repository can call it from a test and log the problems: ```go src=modules/bonfire/example_test.go#ExampleCheckDocs commands := []bonfire.Command{ { Name: "blog:publish", Description: "Publish a draft post", Long: "Publishes the draft with the given slug and notifies subscribers.", Args: []bonfire.Arg{{Name: "slug", Description: "Post slug", Required: true}}, Examples: []bonfire.Example{{Command: "blog:publish hello-world"}}, }, { Name: "blog:reindex", Description: "Rebuild the search index", Flags: []bonfire.Flag{{Name: "batch", Default: "500"}}, }, } for _, problem := range bonfire.CheckDocs(commands) { fmt.Println(problem) } // Output: blog:reindex: missing long help, examples, option --batch description ``` ## Calling commands in-process `bonfire.Call` runs one command of a slice by name with its arguments and writes the output to any writer, like `Artisan::call` in Laravel. Tests use it to exercise a command without building a binary: ```go src=modules/bonfire/example_test.go#ExampleCall commands := []bonfire.Command{{ Name: "acme:greet", Description: "Greet someone by name", Args: []bonfire.Arg{{Name: "name", Required: true}}, Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error { name, _ := in.Argument("name") out.Printf("Hello, %s\n", name) return nil }, }} if err := bonfire.Call(context.Background(), commands, "acme:greet", []string{"blog"}, os.Stdout); err != nil { fmt.Println(err) } // Output: Hello, blog ``` The generated `main` also publishes the application's complete command list as a `bonfire.Catalog` on the container. Code outside the command line, such as the scheduler, looks it up and calls commands through `bonfire.Catalog.Call`, and checks for one with `bonfire.Catalog.Has`. ## Running your command After `summer build`, your command is part of the binary: ```sh ./bin/hello greeter:hello ``` That command comes from the greeter plugin of `examples/hello`, whose `Commands` method returns one `bonfire.Command`.