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
-
The CLI is
cmd/eagraiclainne, a cobra binary that wrapspkg/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 theuser referencecommand needed a call the library lacked, the answer was a newUsers.ReferenceUserwrapper inpkg/client— not a CLI-local RPC call. That command shipped on 2026-07-25 and completed the command set. -
Commands follow the resource/action structure —
eagraiclainne <resource> <action> [flags], kubectl/gh style — covering all four services (user, event, item, reward). No command aliases (e.g.getforread) 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. -
Configuration lives in the XDG config directory (
$XDG_CONFIG_HOME/eagraiclainne, falling back to~/.config/eagraiclainne), holdingconfig.yaml(server URL, default output format) and the token file. Precedence is: command-line flags > environment variables (EAG_*prefix) > config file > built-in defaults. -
Token lifecycle is login/logout.
eagraiclainne loginauthenticates and writes the JWT to a plaintext token file with0600permissions; subsequent commands read it and inject the auth header;eagraiclainne logoutdeletes it. The password is read via an interactive no-echo prompt by preference; a--passwordflag exists but is discouraged (shell history risk). -
Output and safety UX. Three output formats —
json,yaml,table— selectable per command or via config. Destructive operations (deletes) require confirmation, skippable with--yesfor scripting. Tables use the stdlibtext/tabwriterrather than a third-party tablewriter dependency. -
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/clientwrappers, so the library stays honest — the CLI is a permanent consumer that catches regressions and forces gaps (likeReferenceUser) to be filled in the library rather than papered over locally. - Familiar, scriptable UX. The resource/action shape matches kubectl/gh
muscle memory;
json/yamloutput plus--yesmake 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/termand YAML output support.
Negative / trade-offs
- The token is stored in plaintext.
0600permissions 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, providinglogout, 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/clientwrapper 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.