From 53c0a65a43500895202a0509f0e8535789d25274 Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Wed, 28 Oct 2026 20:26:20 +0100 Subject: [PATCH] refactor(bonfire): run the root without fang so bonfire's root help stays installed (IN-06, quick-261028-rqd) --- go.mod | 6 ------ go.sum | 12 ------------ modules/bonfire/README.md | 6 +++--- modules/bonfire/execute.go | 37 ++++++++++++++++++++---------------- modules/bonfire/root_test.go | 25 ++++++++++++++++++++++++ scripts/check-phase19.8.sh | 4 ++-- 6 files changed, 51 insertions(+), 39 deletions(-) diff --git a/go.mod b/go.mod index 4bb18a04..fb5040a3 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,6 @@ ignore ./admin/node_modules require ( charm.land/bubbles/v2 v2.2.1 charm.land/bubbletea/v2 v2.1.0 - charm.land/fang/v2 v2.0.1 charm.land/huh/v2 v2.0.3 charm.land/lipgloss/v2 v2.0.6 github.com/alecthomas/chroma/v2 v2.27.0 @@ -82,7 +81,6 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/charmbracelet/harmonica v0.2.0 // indirect github.com/charmbracelet/ultraviolet v0.0.0-20261008173134-6b8d4baf91b4 // indirect - github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444 // indirect github.com/charmbracelet/x/exp/ordered v0.1.0 // indirect github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect github.com/charmbracelet/x/term v0.2.2 // indirect @@ -140,10 +138,6 @@ require ( github.com/moby/sys/userns v0.1.0 // indirect github.com/moby/term v0.5.2 // indirect github.com/muesli/cancelreader v0.2.2 // indirect - github.com/muesli/mango v0.1.0 // indirect - github.com/muesli/mango-cobra v1.2.0 // indirect - github.com/muesli/mango-pflag v0.1.0 // indirect - github.com/muesli/roff v0.1.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect diff --git a/go.sum b/go.sum index e4557185..47b9d5ba 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,6 @@ charm.land/bubbles/v2 v2.2.1 h1:Fq1+qm5hV6GkvzLQDhCBpXXE5tLgvh1PRriCLwSvIQU= charm.land/bubbles/v2 v2.2.1/go.mod h1:wdMgn+sje1KNXdwFizIWjbf328fIUBxqEmJ/vYPo8yc= charm.land/bubbletea/v2 v2.1.0 h1:A9fj9h5LFrvQhQHFNggp8K8rvY6CUfgn9dMbb6p2HIw= charm.land/bubbletea/v2 v2.1.0/go.mod h1:xWHeyxIX9/wiFU2kgPW04ojOZyTzrWf+8lGjUCaftiU= -charm.land/fang/v2 v2.0.1 h1:zQCM8JQJ1JnQX/66B5jlCYBUxL2as5JXQZ2KJ6EL0mY= -charm.land/fang/v2 v2.0.1/go.mod h1:S1GmkpcvK+OB5w9caywUnJcsMew45Ot8FXqoz8ALrII= charm.land/huh/v2 v2.0.3 h1:2cJsMqEPwSywGHvdlKsJyQKPtSJLVnFKyFbsYZTlLkU= charm.land/huh/v2 v2.0.3/go.mod h1:93eEveeeqn47MwiC3tf+2atZ2l7Is88rAtmZNZ8x9Wc= charm.land/lipgloss/v2 v2.0.6 h1:EaGKeuA8FvF+v2BT5VmZd2LoYLaMZJXA5n34th8nCIQ= @@ -116,8 +114,6 @@ github.com/charmbracelet/x/conpty v0.1.1 h1:s1bUxjoi7EpqiXysVtC+a8RrvPPNcNvAjfi4 github.com/charmbracelet/x/conpty v0.1.1/go.mod h1:OmtR77VODEFbiTzGE9G1XiRJAga6011PIm4u5fTNZpk= github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA= github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0= -github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444 h1:IJDiTgVE56gkAGfq0lBEloWgkXMk4hl/bmuPoicI4R0= -github.com/charmbracelet/x/exp/charmtone v0.0.0-20250603201427-c31516f43444/go.mod h1:T9jr8CzFpjhFVHjNjKwbAD7KwBNyFnj2pntAO7F2zw0= github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA= github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I= github.com/charmbracelet/x/exp/ordered v0.1.0 h1:55/qLwjIh0gL0Vni+QAWk7T/qRVP6sBf+2agPBgnOFE= @@ -309,14 +305,6 @@ github.com/modelcontextprotocol/go-sdk v1.8.0 h1:KIvahhYqwtbeniWVPs3TcXEA7b8jEtw github.com/modelcontextprotocol/go-sdk v1.8.0/go.mod h1:dL7u98E/zjJTGzEq+j30jQ8K2k1mb6LeAH4inEcSGts= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= -github.com/muesli/mango v0.1.0 h1:DZQK45d2gGbql1arsYA4vfg4d7I9Hfx5rX/GCmzsAvI= -github.com/muesli/mango v0.1.0/go.mod h1:5XFpbC8jY5UUv89YQciiXNlbi+iJgt29VDC5xbzrLL4= -github.com/muesli/mango-cobra v1.2.0 h1:DQvjzAM0PMZr85Iv9LIMaYISpTOliMEg+uMFtNbYvWg= -github.com/muesli/mango-cobra v1.2.0/go.mod h1:vMJL54QytZAJhCT13LPVDfkvCUJ5/4jNUKF/8NC2UjA= -github.com/muesli/mango-pflag v0.1.0 h1:UADqbYgpUyRoBja3g6LUL+3LErjpsOwaC9ywvBWe7Sg= -github.com/muesli/mango-pflag v0.1.0/go.mod h1:YEQomTxaCUp8PrbhFh10UfbhbQrM/xJ4i2PB8VTLLW0= -github.com/muesli/roff v0.1.0 h1:YD0lalCotmYuF5HhZliKWlIx7IEhiXeSfq7hNjFqGF8= -github.com/muesli/roff v0.1.0/go.mod h1:pjAHQM9hdUUwm/krAfrLGgJkXJ+YuhtsfZ42kieB2Ig= github.com/nicksnyder/go-i18n/v2 v2.6.1 h1:JDEJraFsQE17Dut9HFDHzCoAWGEQJom5s0TRd17NIEQ= github.com/nicksnyder/go-i18n/v2 v2.6.1/go.mod h1:Vee0/9RD3Quc/NmwEjzzD7VTZ+Ir7QbXocrkhOzmUKA= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= diff --git a/modules/bonfire/README.md b/modules/bonfire/README.md index 18992c38..ef89b4ef 100644 --- a/modules/bonfire/README.md +++ b/modules/bonfire/README.md @@ -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/` 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 ; bin/ ` (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/` 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 ; bin/ ` (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 diff --git a/modules/bonfire/execute.go b/modules/bonfire/execute.go index 24ee8462..d8a5cbeb 100644 --- a/modules/bonfire/execute.go +++ b/modules/bonfire/execute.go @@ -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 diff --git a/modules/bonfire/root_test.go b/modules/bonfire/root_test.go index 3c11aab5..8cf896e6 100644 --- a/modules/bonfire/root_test.go +++ b/modules/bonfire/root_test.go @@ -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) + } +} diff --git a/scripts/check-phase19.8.sh b/scripts/check-phase19.8.sh index 2a9dcd68..dad95f60 100755 --- a/scripts/check-phase19.8.sh +++ b/scripts/check-phase19.8.sh @@ -131,8 +131,8 @@ BONFIRE_TESTS=( TestProcessViewStripsControl TestTableStripsControl # quick 261022-4z8 (re-review fixes): IN-15, WR-09. TestNestedProcessBuildsNoProgram TestPrintAboveAfterCancel - # quick 261028-rqd (info findings): IN-03. - TestPluginErrorWordedUnknownCommand + # quick 261028-rqd (info findings): IN-03, IN-06. + TestPluginErrorWordedUnknownCommand TestExecuteKeepsRootHelp ) DOCSITE_TESTS=( # Plan 04: the docs checker reads past the global options.