Files
summercms/modules/bonfire/list.go
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

150 lines
4.1 KiB
Go

package bonfire
import (
"context"
"fmt"
"io"
"slices"
"strings"
"github.com/charmbracelet/colorprofile"
)
// builtinCommands describes the root's own commands, help and list. They are
// registered by NewRoot, so plugins cannot use their names.
func builtinCommands() []Command {
return []Command{
{
Name: "help",
Description: "Display help for a command",
Args: []Arg{{Name: "command_name", Description: "The command name"}},
},
{
Name: "list",
Description: "List commands",
Flags: []Flag{
{Name: "all", Description: "Show hidden commands too", Bare: true},
{Name: "format", Description: "The output format (txt or json)", Default: "txt"},
},
},
}
}
func builtinCommand(name string) Command {
for _, c := range builtinCommands() {
if c.Name == name {
return c
}
}
return Command{Name: name}
}
// usageName is how a binary is invoked in usage lines and examples: summer
// for the tool, bin/<name> for an application binary.
func usageName(name string) string {
if name == "summer" {
return name
}
return "bin/" + name
}
// lister renders the command list of one root.
type lister struct {
name string
commands []Command
version Version
out io.Writer
console *console
}
// command is the list command with its run function.
func (l *lister) command() Command {
c := builtinCommand("list")
c.Run = func(_ context.Context, in Input, _ Output) error {
all, _ := in.Flag("all")
format, _ := in.Flag("format")
switch format {
case "", "txt":
return l.render(l.console.globals, all == "true")
default:
return Usage(fmt.Errorf("list: unknown format %q (use txt or json)", format))
}
}
return c
}
// all returns the root's commands and its builtins.
func (l *lister) all() []Command {
return append(append([]Command(nil), l.commands...), builtinCommands()...)
}
// render writes the Artisan list: the version header, usage, the global
// options and the commands, top-level names first and then one block per
// namespace (the text before the first colon).
func (l *lister) render(g Globals, all bool) error {
p := DefaultPalette()
var top []Command
groups := map[string][]Command{}
width := 0
for _, c := range l.all() {
ns, _, namespaced := strings.Cut(c.Name, ":")
if namespaced {
groups[ns] = append(groups[ns], c)
} else {
top = append(top, c)
}
width = max(width, len(c.Name))
}
byName := func(a, b Command) int { return strings.Compare(a.Name, b.Name) }
slices.SortFunc(top, byName)
namespaces := make([]string, 0, len(groups))
for ns := range groups {
namespaces = append(namespaces, ns)
}
slices.Sort(namespaces)
var b strings.Builder
b.WriteString(p.Header.Render(l.version.Header(l.name)) + "\n\n")
b.WriteString(p.Header.Render("Usage:") + "\n")
b.WriteString(" " + usageName(l.name) + " <command> [options] [arguments]\n\n")
b.WriteString(p.Header.Render("Options:") + "\n")
writeGlobalOptions(&b, p)
b.WriteString("\n" + p.Header.Render("Available commands:") + "\n")
line := func(c Command) {
pad := strings.Repeat(" ", width-len(c.Name)+2)
b.WriteString(" " + p.Success.Render(c.Name) + pad + c.Description + "\n")
}
for _, c := range top {
line(c)
}
for _, ns := range namespaces {
b.WriteString(" " + p.Header.Render(ns) + "\n")
cmds := groups[ns]
slices.SortFunc(cmds, byName)
for _, c := range cmds {
line(c)
}
}
return writeStyled(l.out, g, b.String())
}
// writeGlobalOptions writes the global option lines, aligned.
func writeGlobalOptions(b *strings.Builder, p Palette) {
width := 0
for _, o := range globalOptions {
width = max(width, len(o.flags))
}
for _, o := range globalOptions {
pad := strings.Repeat(" ", width-len(o.flags)+2)
b.WriteString(" " + p.Success.Render(o.flags) + pad + o.description + "\n")
}
}
// writeStyled writes s to w through a colorprofile writer, so palette
// styles follow --ansi, --no-ansi, NO_COLOR and the terminal.
func writeStyled(w io.Writer, g Globals, s string) error {
cw := &colorprofile.Writer{Forward: w, Profile: DetectProfile(w, g.Color, nil)}
_, err := io.WriteString(cw, s)
return err
}