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

126 lines
3.1 KiB
Go

package bonfire
import (
"strconv"
"strings"
)
// helpUsage is the usage line of a command in help and the JSON listing:
// the name, its arguments and [options]; passthrough commands end with
// [--] <args...> because everything after the name reaches them unparsed.
func helpUsage(c Command) string {
u := commandUsage(c) + " [options]"
if c.Passthrough {
u += " [--] <args...>"
}
return u
}
// optionLabel is the left column of a flag: "-x, --name=NAME" for value
// flags, "--name" for bare flags, indented four spaces without shorthand.
func optionLabel(f Flag) string {
label := " "
if f.Shorthand != "" {
label = "-" + f.Shorthand + ", "
}
label += "--" + f.Name
if !f.Bare {
label += "=" + strings.ToUpper(strings.ReplaceAll(f.Name, "-", "_"))
}
return label
}
// optionDetail is a flag's description followed by its default and the
// repeatable marker.
func optionDetail(f Flag) string {
parts := []string{}
if f.Description != "" {
parts = append(parts, f.Description)
}
if f.Default != "" && !f.Bare {
if _, err := strconv.Atoi(f.Default); err == nil {
parts = append(parts, "[default: "+f.Default+"]")
} else {
parts = append(parts, "[default: "+strconv.Quote(f.Default)+"]")
}
}
if f.Repeatable {
parts = append(parts, "(multiple values allowed)")
}
return strings.Join(parts, " ")
}
// renderHelp writes a command's help in the Artisan layout: Description,
// Usage, Arguments, Options with defaults, Global options, Help (the long
// text) and Examples, each section present only when it has content.
func (st *rootState) renderHelp(c Command, g Globals) error {
p := DefaultPalette()
var b strings.Builder
section := func(title string) {
if b.Len() > 0 {
b.WriteString("\n")
}
b.WriteString(p.Header.Render(title+":") + "\n")
}
if c.Description != "" {
section("Description")
b.WriteString(" " + c.Description + "\n")
}
section("Usage")
b.WriteString(" " + helpUsage(c) + "\n")
width := 0
for _, a := range c.Args {
width = max(width, len(a.Name))
}
for _, f := range c.Flags {
width = max(width, len(optionLabel(f)))
}
row := func(label, detail string) {
line := " " + p.Success.Render(label)
if detail != "" {
line += strings.Repeat(" ", width-len(label)+2) + detail
}
b.WriteString(line + "\n")
}
if len(c.Args) > 0 {
section("Arguments")
for _, a := range c.Args {
row(a.Name, a.Description)
}
}
if len(c.Flags) > 0 {
section("Options")
for _, f := range c.Flags {
row(optionLabel(f), optionDetail(f))
}
}
section("Global options")
writeGlobalOptions(&b, p)
if long := strings.TrimSpace(c.Long); long != "" {
section("Help")
for _, line := range strings.Split(long, "\n") {
if strings.TrimSpace(line) == "" {
b.WriteString("\n")
continue
}
b.WriteString(" " + line + "\n")
}
}
if len(c.Examples) > 0 {
section("Examples")
bin := usageName(st.name)
for i, e := range c.Examples {
if i > 0 {
b.WriteString("\n")
}
if e.Description != "" {
b.WriteString(" " + p.Muted.Render("# "+e.Description) + "\n")
}
b.WriteString(" " + bin + " " + e.Command + "\n")
}
}
return writeStyled(st.out, g, b.String())
}