From b006305ecd7cc519205f8c48af96ff7d5eb1ad5d Mon Sep 17 00:00:00 2001 From: Vincent Royer Date: Fri, 26 Jun 2026 10:56:07 -0700 Subject: [PATCH] Silence cobra usage/error dump on failure (#31) Cobra prints the full usage and flags block to stderr on any RunE or flag-parse error. That clutters logs and buries the real message, breaking the "data on stdout, errors on stderr" contract: a stray flag like `--version` produced a screenful of usage text. Set SilenceUsage and SilenceErrors on the root command and print a single-line "Error: " to stderr from Execute instead. stdout stays data-only; stderr carries one concise line. Adds a regression test asserting no usage/command block is emitted on a flag-parse error. Co-Authored-By: Claude Opus 4.8 --- cmd/root.go | 8 ++++++++ cmd/root_test.go | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 cmd/root_test.go diff --git a/cmd/root.go b/cmd/root.go index 090ce9e..fc2eb54 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,6 +1,7 @@ package cmd import ( + "fmt" "os" "github.com/spf13/cobra" @@ -15,10 +16,17 @@ fitdown-style markdown; pass --format json for the full structured row. LLM agents: run 'liftoff-export prime' for a one-screen orientation (I/O contract, subcommands, date flags, jq recipes).`, + // Keep the output contract clean: stdout is for data only. By default + // cobra dumps the full usage/flags block to stderr on any RunE or + // flag-parse error, which clutters logs and buries the actual message. + // Silence both and print a single-line error ourselves in Execute. (#31) + SilenceUsage: true, + SilenceErrors: true, } func Execute() { if err := rootCmd.Execute(); err != nil { + fmt.Fprintln(os.Stderr, "Error:", err) os.Exit(1) } } diff --git a/cmd/root_test.go b/cmd/root_test.go new file mode 100644 index 0000000..a71eb97 --- /dev/null +++ b/cmd/root_test.go @@ -0,0 +1,34 @@ +package cmd + +import ( + "bytes" + "strings" + "testing" +) + +// On a flag-parse error the root command must not dump cobra's usage/flags +// block: stdout stays data-only and stderr stays a single error line that +// Execute prints itself. (#31) +func TestRootCmd_NoUsageDumpOnError(t *testing.T) { + var out, errBuf bytes.Buffer + rootCmd.SetOut(&out) + rootCmd.SetErr(&errBuf) + rootCmd.SetArgs([]string{"--this-flag-does-not-exist"}) + t.Cleanup(func() { + rootCmd.SetArgs(nil) + rootCmd.SetOut(nil) + rootCmd.SetErr(nil) + }) + + err := rootCmd.Execute() + if err == nil { + t.Fatal("expected an error for an unknown flag, got nil") + } + combined := out.String() + errBuf.String() + if strings.Contains(combined, "Usage:") { + t.Errorf("usage block should be suppressed on error; got:\n%s", combined) + } + if strings.Contains(combined, "Available Commands:") { + t.Errorf("command list should be suppressed on error; got:\n%s", combined) + } +}