Entry writes the banner to stdout before cobra parses anything, and the
scan that decides whether to write it knew --quiet, -q and --cron but
not --json. Every --json document therefore arrived behind two lines of
prose and a blank line, and `vaultik snapshot list --json | jq` failed.
Passing opts.JSON as extraQuiet could not help: that reaches UI.SetQuiet
through an fx OnStart hook, long after the banner is already written.
With the logger moved to stderr in #82, this was the last writer that
could put something on stdout the caller did not ask for.
The design question the issue raised is answered in favour of extending
the raw-argv scan rather than moving the banner after parsing. The
banner is printed first deliberately, so that it still appears when
cobra rejects the arguments and on --help; after parsing there is no
single place that covers those paths, so "after parsing" means either
reimplementing the banner in several handlers or losing it exactly where
a human most wants to know which build just ran. The objection to the
scan is that --json is a subcommand flag matched anywhere in the vector,
but --cron is already in the list and is also a subcommand flag: it
exists only on `snapshot create`. So this adds another instance of an
imprecision the code already accepts, not a new kind of one. The two
error directions are not symmetric either — a false positive loses a
decorative banner, a false negative corrupts a document — so the scan
errs toward suppression, and --json=false suppresses it exactly as
--quiet=false already does.
Three tests at the CLI layer, where internal/vaultik's existing guard
cannot reach. TestEntryJSONStdoutIsExactlyOneDocument runs Entry itself
over the process's real stdout descriptor, through cobra and the fx
graph to the document, and asserts the capture decodes as one JSON value
with nothing after it; it is hermetic because file:// storage needs no
credentials and `snapshot list` treats a destination store with no
metadata/ as an empty list rather than a failure. A second covers the
argument vectors of all five --json commands plus the pre-subcommand and
--json=true forms. A third asserts the banner is still printed without a
suppressing flag, so the first cannot be satisfied by deleting it.
AGENTS.md policy 9 still keyed the structured-log format on stdout's
TTY-ness after #82 moved that decision to stderr; it now names the log
stream. A rules file that misdescribes the code misleads exactly the
readers who trust it most.
Two smaller findings from the same review. bytesAttrKey's human-readable
byte formatting stopped applying under an open group, because the key
reaching the comparison is group-qualified: "bytes" logged under a group
arrives as "transfer.bytes" and fell back to a bare number. The match is
now made on the final dot-separated segment, tested both grouped and
ungrouped. And listEnv.stderr in snapshot_list_test.go, assigned but
never read since those tests began capturing the process's stderr, is
removed. Vaultik.Stderr is kept — nothing writes to it today, which its
comment now says outright rather than leaving the next reader to hunt
for a writer that does not exist.
`prune --json` still does not survive jq, for an unrelated reason found
while verifying this: pruneLocalSnapshots writes three lines of prose to
stdout with no --json awareness, on main and after this change alike,
and -q never suppressed them either. Filed as #108 rather than fixed
here, being a different writer on a different code path.