fix(bonfire): recognise cobra's unknown command error once in Execute (IN-03, quick-261028-rqd)

This commit is contained in:
Jakub Zych
2026-10-28 20:23:39 +01:00
parent 8476a65aa0
commit 39c59b8329
5 changed files with 75 additions and 13 deletions

View File

@@ -12,6 +12,7 @@ import (
"git.golem15.com/golem15/summercms/internal/argmask"
"git.golem15.com/golem15/summercms/modules/sunscreen"
"github.com/charmbracelet/colorprofile"
"github.com/spf13/cobra"
)
// UsageError marks a mistake in how a command was called: an unknown
@@ -40,13 +41,39 @@ func Usage(err error) error {
return &UsageError{Err: err}
}
// isUsage reports a UsageError in the chain or cobra's unknown command error.
// isUsage reports a UsageError in the chain. cobra's unknown command error
// counts only after Execute has converted it (unknownCommand).
func isUsage(err error) bool {
var ue *UsageError
if errors.As(err, &ue) {
return true
return errors.As(err, &ue)
}
// unknownCommandError is cobra's unknown command error once Execute has
// confirmed it came from cobra: Error keeps cobra's text, name and
// suggestions are read from it once.
type unknownCommandError struct {
msg string
name string
suggestions []string
}
func (e *unknownCommandError) Error() string { return e.msg }
// unknownCommand converts err into a UsageError around an
// unknownCommandError when it is cobra's unknown command error for args:
// root.Find on the same args fails at the root with the same message, so
// cobra rejected the command word and no command ran. Every other error,
// including a command's own error worded the same way, is returned as is.
func unknownCommand(root *cobra.Command, args []string, err error) error {
if err == nil || root == nil {
return err
}
return strings.HasPrefix(err.Error(), "unknown command")
cmd, _, ferr := root.Find(args)
if ferr == nil || cmd != root || ferr.Error() != err.Error() {
return err
}
name, suggestions := parseUnknownCommand(err.Error())
return &UsageError{Err: &unknownCommandError{msg: err.Error(), name: name, suggestions: suggestions}}
}
// HintError adds an explanation and a command to try to an error. The error
@@ -169,9 +196,10 @@ func WithExitCode(err error, code int) error {
// and the message, the explanation and the Try lines of a HintError ({bin}
// replaced by summer or bin/<name>, for the root named name), then "Run with
// -v for details." at verbosity 0 or the wrapped error chain under "Caused
// by:" at -v and above. cobra's unknown command error becomes `Command "x"
// is not defined.` with its suggestions as Try lines, and other usage errors
// suggest the command's help. Every message passes through sunscreen.Scrub,
// by:" at -v and above. An unknown command, as recognised by Execute,
// becomes `Command "x" is not defined.` with cobra's suggestions as Try
// lines, and other usage errors suggest the command's help. A raw cobra
// error passed in directly keeps cobra's text. Every message passes through sunscreen.Scrub,
// and every line through Sanitize.
// Errors wrapped by Reported print nothing.
func RenderError(w io.Writer, name string, err error, g Globals) {
@@ -189,12 +217,12 @@ func RenderError(w io.Writer, name string, err error, g Globals) {
if errors.As(err, &hint) {
explanation, try = hint.Explanation, hint.Try
}
if strings.HasPrefix(message, "unknown command") {
cmdName, suggestions := parseUnknownCommand(message)
message = `Command "` + cmdName + `" is not defined.`
var unknown *unknownCommandError
if errors.As(err, &unknown) {
message = `Command "` + unknown.name + `" is not defined.`
if try == "" {
var lines []string
for _, s := range suggestions {
for _, s := range unknown.suggestions {
lines = append(lines, "{bin} "+s)
}
if len(lines) == 0 {