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/cmd: Cobra root, App, persistent flags, version #14
The command tree's skeleton: root.go, app.go, version.go. Subsequent command issues add
leaves to this tree rather than inventing their own wiring.
.goreleaser.yml and the Dockerfile already inject -X github.com/specsnl/labelsync/internal/cmd.Version — the Version variable has to live in
this package with exactly that name, or every build ships as dev.
version.go with --dont-prettify, reading the linker-injected Version
Errors returned from commands map to exit codes via exit.Of; the single os.Exit lives in main and nowhere else, because it skips deferred cleanup and would leak temp files,
unreleased locks, and unflushed writers from inside a command
main prints the failure through app.Out.WriteErr — not fmt.Fprintln(os.Stderr, err),
which is what would drop error_kind from a JSON run's final line — and prints nothing
for a silent *exit.Err (a nil Err field, meaning the non-zero code reports an outcome
rather than a failure)
SilenceUsage and SilenceErrors both set on the root command: without the first, every
runtime failure appends the full usage block; without the second, Cobra prints its own bare Error: line in addition to main's, and its copy carries no error_kind
Writers and the logger are constructed from cmd.OutOrStdout() / cmd.ErrOrStderr(), never
the NewDefault* constructors — those hardcode os.Stdout / os.Stderr and are how output
silently escapes a test's buffers
Added during implementation.output.Writer gained WriteResult, the product-level line
for a command whose whole answer is one value. version is that command: Info would put it
on stderr where $(labelsync version --dont-prettify) cannot see it, and a one-row bordered
table would reach stdout by pretending the value is something it is not. The output page
already said this case would get a new method rather than moving Info back to stdout, so
this follows that decision rather than making a new one
Tests
Flag defaults and parsing; --output=json selects the JSON writer; an error wrapping a sentinel
produces the right exit code and the right error_kind in JSON output. Capture both streams with cmd.SetOut / cmd.SetErr — if a test cannot see the output, the wiring is wrong, which is the
whole reason the accessors are mandatory.
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 #52. task lint, task test, and task md:check all pass. Every box is ticked;
nothing left out.
Two things decided while implementing, both explained in the PR:
main's error handling is a report() function rather than four inline lines, because os.Exit
cannot be tested and everything above it can. The silence guard follows the carrier's Err field,
not its Code: a carrier holding a real failure still prints even when its code is
outcome-shaped.
Invalid flag values (--output yaml, a zero --concurrency) return plain errors rather than new
sentinels. The sentinels describe how a run can fail once it is under way; a value rejected before
any work starts is a usage error, and error_kind is a public contract not worth adding to by
reflex.
Docs: the output page's Cobra section now describes code rather than a plan, plus a new section for WriteResult; the overview page gained a "How the tree is wired" section, and its exit-code table
said Skipped was 3 — corrected to 4. The README was written out from its one-line stub with
the global flags, version, the output contract, and the exit codes that #13 deferred to it.
Goal
The command tree's skeleton:
root.go,app.go,version.go. Subsequent command issues addleaves to this tree rather than inventing their own wiring.
.goreleaser.ymland theDockerfilealready inject-X github.com/specsnl/labelsync/internal/cmd.Version— theVersionvariable has to live inthis package with exactly that name, or every build ships as
dev.Design reference
docs/design.md§ CLI, § Package structureScope
root.go: thelabelsyncroot command, wired frommain.goviacmd.Execute()app.go: anAppstruct carrying resolved config, the output writer, and the logger--config,--debug,--output pretty|json,--no-cache,--concurrency(default 8),--write-rate(default 70),--max-wait(default 15m)version.gowith--dont-prettify, reading the linker-injectedVersionexit.Of; the singleos.Exitlives inmainand nowhere else, because it skips deferred cleanup and would leak temp files,unreleased locks, and unflushed writers from inside a command
mainprints the failure throughapp.Out.WriteErr— notfmt.Fprintln(os.Stderr, err),which is what would drop
error_kindfrom a JSON run's final line — and prints nothingfor a silent
*exit.Err(a nilErrfield, meaning the non-zero code reports an outcomerather than a failure)
SilenceUsageandSilenceErrorsboth set on the root command: without the first, everyruntime failure appends the full usage block; without the second, Cobra prints its own bare
Error:line in addition tomain's, and its copy carries noerror_kindcmd.OutOrStdout()/cmd.ErrOrStderr(), neverthe
NewDefault*constructors — those hardcodeos.Stdout/os.Stderrand are how outputsilently escapes a test's buffers
output.WritergainedWriteResult, the product-level linefor a command whose whole answer is one value.
versionis that command:Infowould put iton stderr where
$(labelsync version --dont-prettify)cannot see it, and a one-row borderedtable would reach stdout by pretending the value is something it is not. The output page
already said this case would get a new method rather than moving
Infoback to stdout, sothis follows that decision rather than making a new one
Tests
Flag defaults and parsing;
--output=jsonselects the JSON writer; an error wrapping a sentinelproduces the right exit code and the right
error_kindin JSON output. Capture both streams withcmd.SetOut/cmd.SetErr— if a test cannot see the output, the wiring is wrong, which is thewhole reason the accessors are mandatory.
The wiring this has to follow is written down in
Output & Exit Codes § Wiring it in Cobra,
including the
mainhandler in full.Depends on
#12, #13
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 #52.
task lint,task test, andtask md:checkall pass. Every box is ticked;nothing left out.
Two things decided while implementing, both explained in the PR:
main's error handling is areport()function rather than four inline lines, becauseos.Exitcannot be tested and everything above it can. The silence guard follows the carrier's
Errfield,not its
Code: a carrier holding a real failure still prints even when its code isoutcome-shaped.
--output yaml, a zero--concurrency) return plain errors rather than newsentinels. The sentinels describe how a run can fail once it is under way; a value rejected before
any work starts is a usage error, and
error_kindis a public contract not worth adding to byreflex.
Docs: the output page's Cobra section now describes code rather than a plan, plus a new section for
WriteResult; the overview page gained a "How the tree is wired" section, and its exit-code tablesaid
Skippedwas3— corrected to4. The README was written out from its one-line stub withthe global flags,
version, the output contract, and the exit codes that #13 deferred to it.