diff --git a/internal/cli/app.go b/internal/cli/app.go index b66cd89..44d1221 100644 --- a/internal/cli/app.go +++ b/internal/cli/app.go @@ -35,18 +35,16 @@ type AppOptions struct { Invokes []fx.Option } -// setupGlobals records the startup time and prints the startup banner. -// In --cron mode the banner is suppressed (LogOptions.Cron == true). +// setupGlobals records the startup time and, when an output-suppression +// flag is active, replaces the UI writer with a discarding one so no +// user-facing output is emitted. The startup banner itself is printed +// by the root command's PersistentPreRun (see maybePrintBanner). func setupGlobals(lc fx.Lifecycle, g *globals.Globals, v *vaultik.Vaultik, opts log.LogOptions) { lc.Append(fx.Hook{ OnStart: func(ctx context.Context) error { g.StartTime = time.Now().UTC() if opts.Cron || opts.Quiet { - // Replace UI writer with a discarding one so all - // user-facing output is suppressed. v.UI = ui.NewWithColor(io.Discard, false) - } else { - writeStartupBanner(v.UI, g.StartTime, g.ShortCommit()) } return nil }, diff --git a/internal/cli/root.go b/internal/cli/root.go index 872f1b2..7fc5a03 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" "strings" + "sync" "time" "github.com/adrg/xdg" @@ -13,6 +14,30 @@ import ( "sneak.berlin/go/vaultik/internal/ui" ) +// bannerOnce ensures the banner is printed at most once per process, +// even if multiple cobra hooks (PersistentPreRun, Help, Run) would +// otherwise each call maybePrintBanner. +var bannerOnce sync.Once + +// maybePrintBanner prints the application banner unless an output- +// suppression flag is active. Safe to call multiple times — it prints +// at most once per process. +func maybePrintBanner(cmd *cobra.Command) { + if rootFlags.Quiet { + return + } + if cronFlag := cmd.Flags().Lookup("cron"); cronFlag != nil && cronFlag.Value.String() == "true" { + return + } + bannerOnce.Do(func() { + short := globals.Commit + if len(short) > 12 { + short = short[:12] + } + writeStartupBanner(ui.New(os.Stdout), time.Now().UTC(), short) + }) +} + // RootFlags holds global flags that apply to all commands. // These flags are defined on the root command and inherited by all subcommands. type RootFlags struct { @@ -35,20 +60,25 @@ func NewRootCommand() *cobra.Command { public keys and uploads to S3-compatible storage. No private keys are needed on the source system.`, SilenceUsage: true, - // When invoked with no subcommand, print the banner then the - // usage/help. Cobra's default behavior (without a Run) just - // prints help, which skips the banner. + // Banner before every subcommand invocation that doesn't + // suppress output. fx setupGlobals will not print it again. + PersistentPreRun: func(cmd *cobra.Command, args []string) { + maybePrintBanner(cmd) + }, + // Bare 'vaultik' (no subcommand): banner + help. Run: func(cmd *cobra.Command, args []string) { - startTime := time.Now().UTC() - short := globals.Commit - if len(short) > 12 { - short = short[:12] - } - writeStartupBanner(ui.New(os.Stdout), startTime, short) + maybePrintBanner(cmd) _ = cmd.Help() }, } + // Help output (--help and group-level cmds) also gets the banner. + defaultHelp := cmd.HelpFunc() + cmd.SetHelpFunc(func(c *cobra.Command, args []string) { + maybePrintBanner(c) + defaultHelp(c, args) + }) + // Add global flags cmd.PersistentFlags().StringVar(&rootFlags.ConfigPath, "config", "", "Path to config file (default: $VAULTIK_CONFIG or platform config dir)") cmd.PersistentFlags().BoolVarP(&rootFlags.Verbose, "verbose", "v", false, "Enable verbose output")