refactor(bonfire): run the root without fang so bonfire's root help stays installed (IN-06, quick-261028-rqd)
This commit is contained in:
@@ -46,7 +46,7 @@ Use cases:
|
||||
- Hidden commands (`bonfire.Command.Hidden`) run but stay out of the list unless `list --all`.
|
||||
- JSON catalog: `list --format=json` prints every command, hidden ones flagged, with arguments, options, defaults and examples as a `bonfire.Listing` (`bonfire.Describe` builds it from a command slice).
|
||||
- Documentation coverage: `bonfire.CheckDocs` reports commands lacking a description, long help, examples, or argument and option descriptions.
|
||||
- Error block and exit codes: `bonfire.Execute` runs the root through fang and returns `bonfire.ExitOK`, `bonfire.ExitFailure`, or `bonfire.ExitUsage` for an unknown command or flag and a wrong number of arguments (`bonfire.UsageError`). Errors print on the error stream as a block (`bonfire.RenderError`): the ERROR badge and the message, the explanation and Try lines of `bonfire.WithHint` (`{bin}` becomes `bin/<name>` or `summer`), for an unknown command (recognised by `bonfire.Execute` from cobra's own error, never from the wording of a command's error) `Command "x" is not defined.` with cobra's "did you mean" suggestions as Try lines, and "Run with -v for details." or, at `-v`, the wrapped error chain; every message passes through `sunscreen.Scrub` and every line through `bonfire.Sanitize`, which strips control characters. Errors wrapped by `bonfire.Reported` print nothing. An error carrying a code through `bonfire.WithExitCode` exits with that code; summer uses it to exit with the code of an app binary it ran. `bonfire.Fail` prints errors that happen before the root exists. For an application binary (any root other than `summer`) that fails, other than with a usage error, while the working directory has no `config/` directory and a parent directory holds `summer.yaml`, both add the hint "This command runs from the application root." with `cd <relative path>; bin/<name> <args>` (only the message changes); the values of flags named like a password, secret, token or key are masked as `***` in that line, also a value that starts with `-`.
|
||||
- Error block and exit codes: `bonfire.Execute` runs the root (keeping its help function, which prints the command list) and returns `bonfire.ExitOK`, `bonfire.ExitFailure`, or `bonfire.ExitUsage` for an unknown command or flag and a wrong number of arguments (`bonfire.UsageError`). Errors print on the error stream as a block (`bonfire.RenderError`): the ERROR badge and the message, the explanation and Try lines of `bonfire.WithHint` (`{bin}` becomes `bin/<name>` or `summer`), for an unknown command (recognised by `bonfire.Execute` from cobra's own error, never from the wording of a command's error) `Command "x" is not defined.` with cobra's "did you mean" suggestions as Try lines, and "Run with -v for details." or, at `-v`, the wrapped error chain; every message passes through `sunscreen.Scrub` and every line through `bonfire.Sanitize`, which strips control characters. Errors wrapped by `bonfire.Reported` print nothing. An error carrying a code through `bonfire.WithExitCode` exits with that code; summer uses it to exit with the code of an app binary it ran. `bonfire.Fail` prints errors that happen before the root exists. For an application binary (any root other than `summer`) that fails, other than with a usage error, while the working directory has no `config/` directory and a parent directory holds `summer.yaml`, both add the hint "This command runs from the application root." with `cd <relative path>; bin/<name> <args>` (only the message changes); the values of flags named like a password, secret, token or key are masked as `***` in that line, also a value that starts with `-`.
|
||||
- Name checks at boot: two commands with the same name, or a command named `list` or `help`, make `bonfire.NewRoot` fail with `bonfire.ErrDuplicateCommand`; a command flag that reuses a global option's name or shorthand fails with `bonfire.ErrReservedFlag` instead of panicking.
|
||||
- Injectable streams (`bonfire.NewRootIO`, `bonfire.NewOutput`) so commands can be tested against buffers.
|
||||
- In-process calls: `bonfire.Call` runs a named command with arguments against any writer (Laravel `Artisan::call`), and `bonfire.Catalog` holds an application binary's final command list so code outside the Cobra root, such as the conga scheduler, can call any registered command.
|
||||
@@ -130,7 +130,7 @@ if catalog.Has("blog:import") {
|
||||
| `bonfire.Version` | Application and framework versions of a binary (`App`, `Framework`, `Local`). |
|
||||
| `bonfire.BuildVersion` | Reads the `bonfire.Version` from the binary's build information. |
|
||||
| `bonfire.Version.Header` | The list header: `SummerCMS acme v1.4.0 (framework v0.3.2)`. |
|
||||
| `bonfire.Execute` | Runs a root with arguments through fang and returns the exit code; prints the list when no command is given. |
|
||||
| `bonfire.Execute` | Runs a root with arguments and returns the exit code; prints the list when no command is given, and enables ANSI processing on Windows consoles. |
|
||||
| `bonfire.Fail` | Renders an error that happened before the root exists and returns `bonfire.ExitFailure`. |
|
||||
| `bonfire.ExitOK`, `bonfire.ExitFailure`, `bonfire.ExitUsage` | The exit codes 0, 1 and 2. |
|
||||
| `bonfire.RenderError` | Writes an error as the console error block. |
|
||||
@@ -192,7 +192,7 @@ Without these variables, color is enabled only when stdout is a terminal. `--ans
|
||||
## Dependencies
|
||||
|
||||
- SummerCMS modules: `sunscreen` (scrubs error messages); the framework-internal package `internal/argmask` (masks secret flag values in the Try line, shared with summer's delegate errors).
|
||||
- Third-party: `github.com/spf13/cobra`, `github.com/spf13/pflag`, `golang.org/x/term`, `charm.land/lipgloss/v2` (with `lipgloss/v2/table`), `charm.land/bubbletea/v2`, `charm.land/bubbles/v2` (spinner, progress, key), `charm.land/huh/v2`, `charm.land/fang/v2` and `github.com/charmbracelet/colorprofile` (decided in `.planning/notes/2026-10-21-console-charm-stack.md`).
|
||||
- Third-party: `github.com/spf13/cobra`, `github.com/spf13/pflag`, `golang.org/x/term`, `charm.land/lipgloss/v2` (with `lipgloss/v2/table`), `charm.land/bubbletea/v2`, `charm.land/bubbles/v2` (spinner, progress, key), `charm.land/huh/v2` and `github.com/charmbracelet/colorprofile` (decided in `.planning/notes/2026-10-21-console-charm-stack.md`).
|
||||
- Standard library: `bufio`, `context`, `encoding/json`, `errors`, `fmt`, `io`, `os`, `path/filepath`, `runtime/debug`, `slices`, `strconv`, `strings`, `sync`, `time`, `unicode/utf8`.
|
||||
|
||||
## Testing
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"charm.land/fang/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -27,8 +27,9 @@ const (
|
||||
// ExitFailure, ExitUsage for usage errors, or the code an error carries
|
||||
// through WithExitCode. With no command word in args (only global options,
|
||||
// -h or --help) it prints the command list. Errors are rendered on the
|
||||
// root's error stream by RenderError through fang's error handler; fang is
|
||||
// used without its version flag, man page and completion commands.
|
||||
// root's error stream by RenderError. Execute leaves the root's help
|
||||
// function alone, and on Windows enables ANSI processing on the output and
|
||||
// error streams when they are files (a no-op elsewhere).
|
||||
func Execute(ctx context.Context, root *cobra.Command, args []string) int {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
@@ -45,20 +46,24 @@ func Execute(ctx context.Context, root *cobra.Command, args []string) int {
|
||||
}
|
||||
resetFlags(root)
|
||||
root.SetArgs(args)
|
||||
// The settings fang applied; NewRootIO sets them already, a root built
|
||||
// elsewhere gets them here.
|
||||
root.SilenceUsage = true
|
||||
root.SilenceErrors = true
|
||||
root.CompletionOptions.DisableDefaultCmd = true
|
||||
for _, w := range []io.Writer{root.OutOrStdout(), root.ErrOrStderr()} {
|
||||
if f, ok := w.(*os.File); ok {
|
||||
lipgloss.EnableLegacyWindowsANSI(f)
|
||||
}
|
||||
}
|
||||
err := root.ExecuteContext(ctx)
|
||||
if err == nil {
|
||||
return ExitOK
|
||||
}
|
||||
err = unknownCommand(root, args, err)
|
||||
name := root.Name()
|
||||
err := fang.Execute(ctx, root,
|
||||
fang.WithoutVersion(),
|
||||
fang.WithoutManpage(),
|
||||
fang.WithoutCompletions(),
|
||||
fang.WithErrorHandler(func(w io.Writer, _ fang.Styles, err error) {
|
||||
// fang hands over a colorprofile writer built from the
|
||||
// environment only; RenderError unwraps it and applies
|
||||
// --ansi and --no-ansi itself.
|
||||
err = unknownCommand(root, args, err)
|
||||
RenderError(w, name, wrongDirectory(name, err, args, ""), g)
|
||||
}),
|
||||
)
|
||||
return exitCode(unknownCommand(root, args, err))
|
||||
RenderError(root.ErrOrStderr(), name, wrongDirectory(name, err, args, ""), g)
|
||||
return exitCode(err)
|
||||
}
|
||||
|
||||
// Fail renders err like Execute does and returns ExitFailure (ExitOK for a
|
||||
|
||||
@@ -103,3 +103,28 @@ func TestCallNoGlobalsHook(t *testing.T) {
|
||||
t.Error("a nil Catalog has commands")
|
||||
}
|
||||
}
|
||||
|
||||
// TestExecuteKeepsRootHelp (IN-06): after Execute the root's help function
|
||||
// is still bonfire's list renderer, not one Execute installed.
|
||||
func TestExecuteKeepsRootHelp(t *testing.T) {
|
||||
plainEnv(t)
|
||||
commands := []Command{{Name: "acme:ok", Description: "Succeed", Run: func(context.Context, Input, Output) error { return nil }}}
|
||||
var buf bytes.Buffer
|
||||
root, err := NewRootIO("acme", commands, strings.NewReader(""), &buf, &buf, WithVersion(goldenVersion))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if code := Execute(context.Background(), root, nil); code != ExitOK {
|
||||
t.Fatalf("Execute: exit %d\n%s", code, buf.String())
|
||||
}
|
||||
want := buf.String()
|
||||
buf.Reset()
|
||||
if code := Execute(context.Background(), root, []string{"list"}); code != ExitOK {
|
||||
t.Fatalf("Execute list: exit %d\n%s", code, buf.String())
|
||||
}
|
||||
buf.Reset()
|
||||
root.HelpFunc()(root, nil)
|
||||
if got := buf.String(); got != want {
|
||||
t.Errorf("root help after Execute:\n%s\nwant the command list:\n%s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user