All checks were successful
check / check (push) Successful in 2m31s
The banner is printed to stdout before cobra parses, and bannerSuppressedInArgs recognised only --quiet, -q and --cron. So every --json document was preceded by two banner lines and a blank one, and `vaultik snapshot list --json | jq` failed. Passing opts.JSON as extraQuiet did not help: that calls UI.SetQuiet in an fx OnStart hook, long after Entry has printed. The raw-argv scan is extended rather than the banner moved after parsing. root.go documents that the banner must survive cobra rejecting its arguments and --help, and no single post-parse location covers those paths. The subcommand-versus-persistent distinction does not decide it: --cron is already in the suppression list and is itself subcommand-only, existing on snapshot create alone, so this adds another instance of an accepted imprecision rather than a new kind. The error directions are asymmetric - a false positive loses a decorative banner, a false negative corrupts a document - so the scan errs toward suppression, which is also why --json=false suppresses, exactly as --quiet=false already does. Four of the five --json commands now pipe into jq cleanly with no other flags: snapshot list, snapshot verify, snapshot remove, remote info. prune does not, because pruneLocalSnapshots writes three prose lines to stdout with no --json awareness. That reproduces identically before this change and -q never suppressed it either, since printlnStdout and stdoutf bypass v.UI entirely. Tracked as #108. Also fixed: TTYHandler's human-readable byte formatting did not survive grouping, because the key check compared against the bare attribute name and a grouped record presents it qualified. AGENTS.md policy 9 keyed the log format on stdout's TTY-ness, which #82 made false by moving the logger to stderr; it now names the log stream. Vaultik.Stderr keeps its field with the comment amended to say outright that nothing writes to it, and the dead listEnv.stderr is removed.
104 lines
3.2 KiB
Go
104 lines
3.2 KiB
Go
package cli
|
|
|
|
import (
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"sneak.berlin/go/vaultik/internal/globals"
|
|
"sneak.berlin/go/vaultik/internal/ui"
|
|
)
|
|
|
|
// shortCommitLen is the number of git commit hash characters shown in
|
|
// the startup banner.
|
|
const shortCommitLen = 12
|
|
|
|
// Entry is the main entry point for the CLI application.
|
|
// It prints the startup banner to stdout (unless a banner-suppressing
|
|
// flag is present in os.Args — see bannerSuppressedInArgs), executes the
|
|
// root cobra command, and routes any returned error through the
|
|
// ui.Writer so the user sees a properly formatted "🛑 ERROR:" line.
|
|
func Entry() {
|
|
emitStartupBanner(os.Args[1:], os.Stdout)
|
|
|
|
rootCmd := NewRootCommand()
|
|
rootCmd.SilenceErrors = true
|
|
|
|
err := rootCmd.Execute()
|
|
if err != nil {
|
|
ReportErrorf("%s", err.Error())
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// emitStartupBanner writes the startup banner to w unless args (the
|
|
// argument vector with the program name already stripped) contains a
|
|
// flag that suppresses it. Split out of Entry so that the decision — the
|
|
// only thing standing between a --json invocation and a parseable
|
|
// stdout — is reachable from a test without running the whole CLI.
|
|
func emitStartupBanner(args []string, w io.Writer) {
|
|
if bannerSuppressedInArgs(args) {
|
|
return
|
|
}
|
|
|
|
short := globals.Commit
|
|
if len(short) > shortCommitLen {
|
|
short = short[:shortCommitLen]
|
|
}
|
|
|
|
writeStartupBanner(ui.New(w), time.Now().UTC(), short)
|
|
}
|
|
|
|
// ReportErrorf emits a user-facing error to stderr in the standard
|
|
// 🛑 ERROR: format. Use it from goroutine error paths (where returning
|
|
// an error to cobra isn't an option) and anywhere else a CLI command
|
|
// must surface a failure outside the normal RunE return path.
|
|
func ReportErrorf(format string, args ...any) {
|
|
ui.New(os.Stderr).Errorf(format, args...)
|
|
}
|
|
|
|
// bannerSuppressedInArgs reports whether any of args is a flag that
|
|
// should suppress the startup banner (--quiet/-q/--cron/--json). Stops
|
|
// at the "--" argument terminator. Recognizes both long forms and short
|
|
// -q, including combined short flags like "-qv".
|
|
//
|
|
// This scans the raw argument vector because the banner is printed
|
|
// before cobra parses anything — deliberately, so that it still appears
|
|
// when cobra rejects the arguments and on --help. The consequence is
|
|
// that a flag is matched wherever it occurs in the vector, including
|
|
// positions where the command it belongs to would not accept it.
|
|
// --json is a subcommand flag rather than a persistent one, but so is
|
|
// --cron (it exists only on `snapshot create`), so this adds no new
|
|
// class of imprecision. The only cost of a false positive is a missing
|
|
// decorative banner; the cost of a false negative is a corrupt document
|
|
// on stdout, so the scan errs deliberately in that direction.
|
|
func bannerSuppressedInArgs(args []string) bool {
|
|
for _, a := range args {
|
|
if a == "--" {
|
|
return false
|
|
}
|
|
|
|
switch a {
|
|
case "--quiet", "-q", "--cron", "--json":
|
|
return true
|
|
}
|
|
|
|
if strings.HasPrefix(a, "--quiet=") ||
|
|
strings.HasPrefix(a, "--cron=") ||
|
|
strings.HasPrefix(a, "--json=") {
|
|
return true
|
|
}
|
|
// Combined short flags like -qv or -vq.
|
|
if len(a) > 1 && a[0] == '-' && a[1] != '-' {
|
|
for _, c := range a[1:] {
|
|
if c == 'q' {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|