Files
summercms/docs/console/writing-commands.md
Jakub Zych 8d4d7ce7a7 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
2026-10-21 12:13:23 +02:00

5.9 KiB

title, description, section, order
title description section order
Writing commands Add console commands to a plugin with bonfire.Command values, arguments, flags, styled output and prompts, and run commands in-process. console 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 <name> 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 by the help command; without it the description is shown. 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:

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, which fall back to plain lines when the output is not a terminal;
  • prompts: bonfire.Output.Ask, bonfire.Output.Confirm, bonfire.Output.Choice and bonfire.Output.Secret. Prompts return their defaults when input ends, so a command run from cron or a script never hangs.

Return an error from Run to fail the command. The binary prints it and exits with status 1.

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:

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:

./bin/hello greeter:hello

That command comes from the greeter plugin of examples/hello, whose Commands method returns one bonfire.Command.