You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
internal/util: exit codes and the output.Writer #13
internal/util/exit and internal/util/output. All user-facing output goes through output.Writer, with pretty and json implementations selected by --output.
log/slog is a debug-only diagnostic channel on stderr, silent on a normal run, and never
used for user-facing reporting. Getting that boundary right here means no later package has to
think about it.
exit: the codes — 0 in sync, 1 error, and the outcome bits 2 drift detected and 4 applied with skipped repositories — as named constants with doc comments explaining each
output.Writer interface, plus pretty (lipgloss) and json (NDJSON) implementations
A table renderer for the pretty diff, and TTY detection so styling degrades cleanly
Added during review.Table is the only Writer method on stdout; Info reports
progress on stderr. Narration is not the product, and on stdout it puts an object with no
data keys into the NDJSON stream, so jq -r .group yields null for it. This diverges from specs-cli, deliberately: nothing here called Info yet, so there was no contract to break
Added during review. The outcome codes are disjoint bits that OR together, so a dry run
that finds drift and cannot reach a repository exits 6 rather than forcing a precedence
rule that throws half the answer away. exit.Error stays outside the bit space and stays
exclusive: a failed run cannot also report on a live state it never established. This is what
renumbered Skipped from 3 to 4 — free now, breaking after the first release
Added during review.exit.Err and exit.Of: the carrier that gets a code out of a
command, since RunE returns an error and nothing else. A nil Err field means silent —
exit 2 on a drifting dry run must not print an error line, because the drift was the
result. Unwrap keeps the sentinel visible to labelsync.KindOf. Pure internal/util/exit
with no Cobra dependency, so it lands here and unblocks internal/cmd: Cobra root, App, persistent flags, version #14 rather than the reverse
Added during review.Table takes typed rows — output.Table(w, rows, cols...) — rather
than (headers []string, rows [][]string). Parallel slices let a row disagree with its
headers with nothing to catch it, and forced every JSON value to be a string because the
pretty renderer needed one. Now the two audiences get different projections of the same value:
cells from the columns, JSON from marshalling the row, so a count stays a number and a column
can be formatted or computed without the record paying for it. JSONKey goes with the old
signature — with keys on the struct there is no heading left to normalise. Consumers groups: resolve and list group to repository membership #39 and cache clear and cache info #42 updated
Added during review.IsTTY takes any rather than io.Writer. The prune guard in Prune mode: report, MultiSelect, --prune=all, and the non-TTY guard #44
has to ask about stdin; the old signature let IsTTY(os.Stdin) compile only because *os.File happens to have a Write method, and read as though stdin were out of scope
slog wired to stderr, enabled only by --debug
Note the errcheck exclusion already configured in .golangci.yml: output is best-effort
because the Writer interface has no error channel
Tests
Golden files for both the pretty and JSON renderings. Assert the JSON writer emits one object per
line so the stream is parseable mid-run.
task lint and task test pass, every box above is ticked, and the change ships with its tests and documentation in the same PR (per AGENTS.md).
Implemented in #51. task lint, task test, and task md:check all pass.
Documentation landed as a new architecture page, Output & Exit Codes.
No README change: nothing here is reachable by a user until the command tree exists (#14), so the
exit codes and the --output contract belong in the README alongside the commands that produce
them.
The boxes marked added during review came out of writing that page: putting the reasoning next
to the code surfaced five decisions the original scope had left implicit. Every one is cheap now and
expensive later — Info has no callers, Table has no non-test callers, the exit numbers have not
shipped, and IsTTY has no call sites — which is why they were taken here rather than deferred.
Consequences elsewhere, all updated: #14 (the main handler and the Cobra Silence* wiring), #41 (the exit-code table, and the first place a run can be both drift and skipped), #37 (the
countdown gates on stderr, where it draws, not stdout), #44 (the prune guard gates on stdin), #39 and #42 (the two Table call sites).
Goal
internal/util/exitandinternal/util/output. All user-facing output goes throughoutput.Writer, withprettyandjsonimplementations selected by--output.log/slogis a debug-only diagnostic channel on stderr, silent on a normal run, and neverused for user-facing reporting. Getting that boundary right here means no later package has to
think about it.
Design reference
docs/design.md§ Output, § Exit codesScope
exit: the codes —0in sync,1error, and the outcome bits2drift detected and4applied with skipped repositories — as named constants with doc comments explaining eachoutput.Writerinterface, pluspretty(lipgloss) andjson(NDJSON) implementationsTableis the onlyWritermethod on stdout;Inforeportsprogress on stderr. Narration is not the product, and on stdout it puts an object with no
data keys into the NDJSON stream, so
jq -r .groupyieldsnullfor it. This diverges fromspecs-cli, deliberately: nothing here calledInfoyet, so there was no contract to breakthat finds drift and cannot reach a repository exits
6rather than forcing a precedencerule that throws half the answer away.
exit.Errorstays outside the bit space and staysexclusive: a failed run cannot also report on a live state it never established. This is what
renumbered
Skippedfrom3to4— free now, breaking after the first releaseexit.Errandexit.Of: the carrier that gets a code out of acommand, since
RunEreturns anerrorand nothing else. A nilErrfield means silent —exit
2on a drifting dry run must not print an error line, because the drift was theresult.
Unwrapkeeps the sentinel visible tolabelsync.KindOf. Pureinternal/util/exitwith no Cobra dependency, so it lands here and unblocks internal/cmd: Cobra root, App, persistent flags, version #14 rather than the reverse
Tabletakes typed rows —output.Table(w, rows, cols...)— ratherthan
(headers []string, rows [][]string). Parallel slices let a row disagree with itsheaders with nothing to catch it, and forced every JSON value to be a string because the
pretty renderer needed one. Now the two audiences get different projections of the same value:
cells from the columns, JSON from marshalling the row, so a count stays a number and a column
can be formatted or computed without the record paying for it.
JSONKeygoes with the oldsignature — with keys on the struct there is no heading left to normalise. Consumers groups: resolve and list group to repository membership #39 and
cache clear and cache info #42 updated
IsTTYtakesanyrather thanio.Writer. The prune guard in Prune mode: report, MultiSelect, --prune=all, and the non-TTY guard #44has to ask about stdin; the old signature let
IsTTY(os.Stdin)compile only because*os.Filehappens to have aWritemethod, and read as though stdin were out of scopeslogwired to stderr, enabled only by--debugerrcheckexclusion already configured in.golangci.yml: output is best-effortbecause the
Writerinterface has no error channelTests
Golden files for both the pretty and JSON renderings. Assert the JSON writer emits one object per
line so the stream is parseable mid-run.
Depends on
#12
Done when
task lintandtask testpass, every box above is ticked, and the change ships with its tests and documentation in the same PR (perAGENTS.md).Implemented in #51.
task lint,task test, andtask md:checkall pass.Documentation landed as a new architecture page,
Output & Exit Codes.
No README change: nothing here is reachable by a user until the command tree exists (#14), so the
exit codes and the
--outputcontract belong in the README alongside the commands that producethem.
The boxes marked added during review came out of writing that page: putting the reasoning next
to the code surfaced five decisions the original scope had left implicit. Every one is cheap now and
expensive later —
Infohas no callers,Tablehas no non-test callers, the exit numbers have notshipped, and
IsTTYhas no call sites — which is why they were taken here rather than deferred.Consequences elsewhere, all updated: #14 (the
mainhandler and the CobraSilence*wiring),#41 (the exit-code table, and the first place a run can be both drift and skipped), #37 (the
countdown gates on stderr, where it draws, not stdout), #44 (the prune guard gates on stdin),
#39 and #42 (the two
Tablecall sites).