App reference

ADR-0011: A cobra CLI wrapping pkg/client as the Eagraí Clainne command-line client

Metadata

  • Status: Accepted
  • Date: 2026-07-25
  • Deciders: Eagraí Clainne Team
  • Converted from: RFC 005 (CLI Client), docs/rfcs/005-cli-client/ — now removed
  • Related: ADR-0007 (formerly RFC 001, Basic Authentication), ADR-0008 (formerly RFC 002, RBAC), ADR-0009 (formerly RFC 003, Service Authorization)

Context

The Eagraí Clainne API was reachable only through Connect RPC clients written in Go or through generic tools like grpcurl. That left no ergonomic path for administrative operations, testing and development workflows, scripting, CI/CD pipelines, or anyone without programming knowledge.

The project already had a high-level Go client library at pkg/client/ — type-safe wrappers for all four services (Users, Events, Items, Rewards), token management via SetAuthToken(), error wrapping with Connect codes, and ergonomic parameter types. Any CLI that built its own client layer would duplicate that tested code and let the two drift apart.

Decision

  1. The CLI is cmd/eagraiclainne, a cobra binary that wraps pkg/client/ directly. No parallel client layer: the CLI consumes the same library as programmatic callers, so behaviour and error handling stay consistent and fixes land in one place. When the user reference command needed a call the library lacked, the answer was a new Users.ReferenceUser wrapper in pkg/client — not a CLI-local RPC call. That command shipped on 2026-07-25 and completed the command set.

  2. Commands follow the resource/action structureeagraiclainne <resource> <action> [flags], kubectl/gh style — covering all four services (user, event, item, reward). No command aliases (e.g. get for read) during the private development phase: they add maintenance burden with no payoff while the only users are the team. Shorthand flags for common options (name, email, output, server) are kept.

  3. Configuration lives in the XDG config directory ($XDG_CONFIG_HOME/eagraiclainne, falling back to ~/.config/eagraiclainne), holding config.yaml (server URL, default output format) and the token file. Precedence is: command-line flags > environment variables (EAG_* prefix) > config file > built-in defaults.

  4. Token lifecycle is login/logout. eagraiclainne login authenticates and writes the JWT to a plaintext token file with 0600 permissions; subsequent commands read it and inject the auth header; eagraiclainne logout deletes it. The password is read via an interactive no-echo prompt by preference; a --password flag exists but is discouraged (shell history risk).

  5. Output and safety UX. Three output formats — json, yaml, table — selectable per command or via config. Destructive operations (deletes) require confirmation, skippable with --yes for scripting. Tables use the stdlib text/tabwriter rather than a third-party tablewriter dependency.

  6. Scope grew where the server did. List commands and shell completion were originally out of scope (the server had no List endpoints when the RFC was drafted); once server List endpoints landed, both shipped as part of the CLI.

Consequences

Positive

  • One client codepath. CLI and programmatic users exercise the same pkg/client wrappers, so the library stays honest — the CLI is a permanent consumer that catches regressions and forces gaps (like ReferenceUser) to be filled in the library rather than papered over locally.
  • Familiar, scriptable UX. The resource/action shape matches kubectl/gh muscle memory; json/yaml output plus --yes make the CLI usable in automation and CI without interactive plumbing.
  • Predictable configuration. XDG placement and the strict flag > env > file > default precedence mean behaviour is explainable and overridable at every level.
  • Minimal dependency surface. cobra and viper were already in go.mod; tabwriter is stdlib; the only meaningful additions were golang.org/x/term and YAML output support.

Negative / trade-offs

  • The token is stored in plaintext. 0600 permissions limit exposure to the owning user, but anything running as that user can read a credential that grants full account access. Accepted for now, mitigated by documenting the risk, providing logout, and keeping keychain/secret-service integration open as a future move.
  • Transport security is the operator's problem. The server URL is user-configurable, so TLS is a recommendation (with warnings on plain HTTP) rather than an enforced invariant.
  • No aliases means slightly more typing and a small re-decision to make if the CLI ever reaches users beyond the team.
  • The CLI must track the API by hand. Every new server endpoint needs a pkg/client wrapper and a command; there is no generation step keeping them in lockstep.

Alternatives considered

  • grpcurl with a shell wrapper. Rejected: poor UX, awkward flag passing, no type safety, not cross-platform.
  • REST API plus a curl/bash wrapper. Rejected: means maintaining both gRPC and REST surfaces, and Connect RPC already speaks HTTP/1.1 and HTTP/2.
  • Web-based admin panel instead of a CLI. Rejected: not scriptable; a web UI complements a CLI rather than replacing it, and one was planned separately.