feat(01-03): add deterministic rich CLI output

- Inject bonfire.Output from stdin/stdout/stderr with stdlib widgets and x/term
- Degrade spinner, progress, table and prompts without a TTY or color
- Reject plugin command names that are not namespace:verb
This commit is contained in:
Jakub Zych
2026-09-16 13:54:44 +02:00
parent 9b1a25dc94
commit a6004e490f
16 changed files with 884 additions and 78 deletions

View File

@@ -2,12 +2,15 @@ package bonfire
import (
"context"
"fmt"
"io"
"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
@@ -39,13 +42,6 @@ type Input interface {
Flag(name string) (string, bool)
}
// Output is an injected writer used by commands instead of package-level stdout.
type Output interface {
io.Writer
Println(a ...any)
Printf(format string, a ...any)
}
type cobraInput struct {
cmd *cobra.Command
spec Command
@@ -84,27 +80,39 @@ func (in cobraInput) Flag(name string) (string, bool) {
return val, true
}
type writerOutput struct {
w io.Writer
func validCommandName(name string) bool {
switch name {
case "build", "dev":
return true
}
ns, verb, ok := strings.Cut(name, ":")
return ok && ns != "" && verb != "" && !strings.Contains(verb, " ")
}
func (o writerOutput) Write(p []byte) (int, error) {
if o.w == nil {
return 0, io.ErrClosedPipe
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 o.w.Write(p)
return strings.Join(parts, " ")
}
func (o writerOutput) Println(a ...any) {
if o.w == nil {
return
func positionalArgs(args []Arg) cobra.PositionalArgs {
required := 0
for _, a := range args {
if a.Required {
required++
}
}
fmt.Fprintln(o.w, a...)
}
func (o writerOutput) Printf(format string, a ...any) {
if o.w == nil {
return
if required == 0 {
return cobra.ArbitraryArgs
}
fmt.Fprintf(o.w, format, a...)
if required == len(args) {
return cobra.ExactArgs(required)
}
return cobra.MinimumNArgs(required)
}