Skip to content

internal/cmd: Cobra root, App, persistent flags, version #14

Description

@Ilyes512

Goal

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.

Design reference

docs/design.md § CLI, § Package structure

Scope

  • root.go: the labelsync root command, wired from main.go via cmd.Execute()
  • app.go: an App struct carrying resolved config, the output writer, and the logger
  • Persistent flags: --config, --debug, --output pretty|json, --no-cache,
    --concurrency (default 8), --write-rate (default 70), --max-wait (default 15m)
  • 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.

The wiring this has to follow is written down in
Output & Exit Codes § Wiring it in Cobra,
including the main handler in full.

Depends on

#12, #13

Done when

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions