Files
summercms/docs/console/writing-commands.md
Jakub Zych 14f6982c4d feat(bonfire): add Passthrough and Long command fields
- Command.Passthrough turns off cobra flag parsing and uses arbitrary
  args, so every argument after the command name, including --help, -h
  and --, reaches Run through Input.Args unchanged
- Command.Long sets cobra's Long text for the help command; without it
  the description is shown as before
- both fields are additive: commands without Passthrough parse flags and
  reject unknown ones exactly as before
- README and docs/console/writing-commands.md describe both fields
2026-10-08 15:49:39 +02:00

5.5 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 are not checked for duplicates, so keep your commands in your plugin's own namespace, such as blog:.

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.