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 Flags []Flag Args []Arg Run func(ctx context.Context, in Input, out Output) error } // 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", "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) }