Files
summercms/modules/bonfire/command.go
Jakub Zych fb32886775 feat(bonfire): Artisan help, examples, JSON listing and error hints
- help <command> and <command> --help print Description, Usage,
  Arguments, Options with defaults, Global options, Help and Examples
- Command.Examples and Command.Hidden (appended, additive); list --all
  shows hidden commands, list --format=json prints a bonfire.Listing
- CheckDocs reports commands lacking description, long help, examples or
  argument and option descriptions
- Error block with the ERROR badge, WithHint explanation and Try lines
  ({bin} replaced), cobra suggestions as Try, -v error chain, messages
  scrubbed by sunscreen; Reported errors print nothing and exit 1
2026-10-21 12:21:24 +02:00

173 lines
4.7 KiB
Go

package bonfire
import (
"context"
"errors"
"strings"
"github.com/spf13/cobra"
)
// ErrCommandName is returned when a plugin command is missing namespace:verb.
var ErrCommandName = errors.New("bonfire: plugin command name must be namespace:verb")
// Command is a console command value collected from plugins or the tool.
type Command struct {
Name string
Description string
// Long is optional extended help, shown under "Help:" by help
// <command> and <command> --help.
Long string
// Passthrough turns off flag parsing for the command: every argument
// after the command name, including --help, -h and --, reaches Run
// unchanged through Input.Args. Declared Flags are registered for the
// usage text only and are never parsed, so Input.Flag returns only
// their defaults. Declared Args shape the usage string but are not
// count-checked, so Input.Argument indexes the raw args. Passthrough
// suits wrapper commands that forward to another program.
Passthrough bool
Flags []Flag
Args []Arg
Run func(ctx context.Context, in Input, out Output) error
// Examples are shown under "Examples:" in the command's help and in
// the JSON listing, each as a "#" comment and the command line.
Examples []Example
// Hidden keeps a runnable command out of the list; list --all and the
// JSON listing still show it.
Hidden bool
}
// Example is one usage example of a command.
type Example struct {
// Command is the command line without the binary, for example
// "admin:token:create jane --json"; help prints it after summer or
// bin/<name>.
Command string
// Description is a short sentence shown as a "#" comment above it.
Description string
}
// Flag describes a string flag passed to a command.
type Flag struct {
Name string
Shorthand string
Description string
Default string
// Bare allows --flag without a value; cobra stores "true".
Bare bool
// Repeatable registers this flag as an ordered, multi-occurrence
// string flag (Cobra StringSlice) instead of a scalar string flag
// (08-CONTEXT.md D-19; 08-PATTERNS.md "bonfire repeatable-flag gap").
// Repeated `--name=a --name=b` preserves insertion order and is read
// back through Input.Flags, never Input.Flag. Existing scalar/bare
// flags are unaffected: Repeatable defaults to false.
Repeatable bool
}
// Arg describes a positional argument.
type Arg struct {
Name string
Description string
Required bool
}
// Input is the parsed argument and flag view handed to Command.Run.
type Input interface {
Args() []string
Argument(name string) (string, bool)
Flag(name string) (string, bool)
// Flags returns the ordered, repeated values of a Repeatable flag
// (D-19). It returns nil for a scalar/bare flag or an unset repeatable
// flag; callers must use Flag for scalar flags.
Flags(name string) []string
}
type cobraInput struct {
cmd *cobra.Command
spec Command
args []string
}
func (in cobraInput) Args() []string { return in.args }
func (in cobraInput) Argument(name string) (string, bool) {
for i, arg := range in.spec.Args {
if arg.Name == name {
if i < len(in.args) {
return in.args[i], true
}
return "", false
}
}
return "", false
}
// Flags returns the ordered values of a Repeatable (Cobra StringSlice)
// flag. It returns nil if the flag was never registered as repeatable
// (wrong pflag type) or the command has no flag set (08-CONTEXT.md D-19).
func (in cobraInput) Flags(name string) []string {
if in.cmd == nil || in.cmd.Flags() == nil {
return nil
}
vals, err := in.cmd.Flags().GetStringSlice(name)
if err != nil {
return nil
}
return vals
}
func (in cobraInput) Flag(name string) (string, bool) {
if in.cmd == nil || in.cmd.Flags() == nil {
return "", false
}
if !in.cmd.Flags().Changed(name) {
val, err := in.cmd.Flags().GetString(name)
if err != nil || val == "" {
return "", false
}
return val, true
}
val, err := in.cmd.Flags().GetString(name)
if err != nil {
return "", false
}
return val, true
}
func validCommandName(name string) bool {
switch name {
case "build", "dev", "new", "serve", "migrate":
return true
}
ns, verb, ok := strings.Cut(name, ":")
return ok && ns != "" && verb != "" && !strings.Contains(verb, " ")
}
func commandUsage(c Command) string {
parts := []string{c.Name}
for _, a := range c.Args {
if a.Required {
parts = append(parts, "<"+a.Name+">")
} else {
parts = append(parts, "["+a.Name+"]")
}
}
return strings.Join(parts, " ")
}
func positionalArgs(args []Arg) cobra.PositionalArgs {
required := 0
for _, a := range args {
if a.Required {
required++
}
}
if required == 0 {
return cobra.ArbitraryArgs
}
if required == len(args) {
return cobra.ExactArgs(required)
}
return cobra.MinimumNArgs(required)
}