## Context Greenfield Go project. Target is a single static binary that a developer runs at the terminal and gets back a snapshot of their ZenMux subscription usage across one or more accounts. The only upstream dependency is the ZenMux Management API (`GET /api/v1/management/subscription/detail`), which returns three rolling quota windows (`quota_5_hour`, `quota_7_day`, `quota_monthly`) along with plan, rate, and account status metadata. The API is authenticated via a Management API key passed as `Authorization: Bearer `; each account has its own key. Accounts are configured via a YAML file; the CLI reads it once, fans out one HTTP request per account, and renders each account as a labeled block. There are no existing conventions in this repo yet — the design establishes them. ## Goals / Non-Goals **Goals:** - Single-command usage: `zenmux-usage` prints every configured account's three windows and USD value in under a second per account on a healthy network. - Readable at a glance: usage percentages are rendered as colored progress bars; absolute numbers (used/max flows, used/max USD) sit alongside; multiple accounts are clearly separated with a per-account header. - Scriptable: `--json` emits machine-readable output (array when multiple accounts, object when one) so it can be piped to `jq`. - Portable: pure-Go, cross-compiles to macOS/Linux/Windows with no runtime dependencies. - Clear errors: config-missing, auth failure, and rate-limit errors exit with distinct non-zero codes; a single failing account does NOT take down the whole run. - One config file, one command — no per-account flag juggling needed in the common case. **Non-Goals:** - Historical tracking, charting over time, or local caching — this is a point-in-time snapshot tool. - Managing API keys or editing the config file from within the CLI — read-only config consumer. - Watch/polling mode or TUI dashboard. (Could come later; not in this change.) - Supporting env-var-based per-account configuration (`ZENMUX_KEY_PERSONAL`, `ZENMUX_KEY_WORK`, ...). YAML file is the single source of truth for multi-account. The single env var is only a zero-config fallback. - Automatic encryption / keychain integration for the config file. Filesystem permissions are the user's responsibility in v1. ## Decisions ### 1. Language & module layout **Decision:** Go 1.22+; module path `github.com/kotoyuuko/zenmux-usage-cli` (placeholder — actual owner path can be swapped later). Layout: ``` cmd/zenmux-usage/main.go // entrypoint, flag parsing, exit codes, fan-out internal/api/client.go // HTTP client + typed response structs internal/api/client_test.go // table-driven tests against httptest.Server internal/config/config.go // YAML load + account resolution internal/config/config_test.go internal/render/render.go // progress bars, per-account block, multi-account layout internal/render/render_test.go config.example.yaml // committed example, no real keys ``` **Why:** `internal/` keeps packages unimportable outside this module — we're not publishing a library. `cmd/` is idiomatic for multi-binary repos. Config gets its own package so the CLI entrypoint stays thin. **Alternatives considered:** Flat package at root (simpler, but awkward to test rendering in isolation); `pkg/` (over-promises reusability). ### 2. Config file schema **Decision:** Single YAML file, default path `~/.config/zenmux-usage/config.yaml` (XDG-style). Schema: ```yaml accounts: - name: personal api_key: sk-zm-... - name: work api_key: sk-zm-... ``` - `accounts` (required, list, min length 1): each entry has `name` (required, unique, kebab- or snake-case) and `api_key` (required, non-empty string). - Unknown top-level keys are ignored with a one-line stderr warning (forward-compat). - No `default:` field in v1 — the default UX renders every account. Users can pin `--account` in a shell alias if they prefer one. **Why:** Flat list is the simplest structure that meets the requirement. A map (`accounts: {personal: {...}}`) would preserve uniqueness via YAML's own semantics but loses user-specified ordering, and ordering matters because the CLI renders accounts in file order. **Alternatives considered:** TOML (YAML is more common for this kind of user config and the yaml.v3 dep is tiny); map-of-accounts (loses order); per-account files in `~/.config/zenmux-usage/accounts.d/` (overkill). ### 3. Config discovery and precedence **Decision:** Resolution order for *what to fetch*: 1. `--api-key ` set → single ad-hoc account, name `cli`, no config file loaded. 2. `--config ` set → load that file (error if missing/invalid). 3. Default config at `~/.config/zenmux-usage/config.yaml` exists → load it. 4. No config file, `ZENMUX_MANAGEMENT_API_KEY` set → single ad-hoc account, name `env`. 5. None of the above → exit code 3 with a message pointing to the config path. If a config is loaded and `--account ` is set, filter to just that account (error if the name is not in the file). **Why:** Preserves a zero-config "set env var, run" path so users can try the binary before writing a config; the flag beats env beats config-file beats nothing in the usual CLI way. `--api-key` is explicitly highest precedence because it's a one-off override. ### 4. HTTP client **Decision:** `net/http` standard library with a 10-second default timeout, configurable via `--timeout`. Decode into typed structs using `encoding/json`. No third-party HTTP client. One request per account, fired **sequentially** in v1; 1–5 accounts is the realistic upper bound and sequential keeps logs and errors simple. **Why:** One endpoint, no retry/backoff complexity. Sequential is fast enough for ≤5 accounts (~1s each) and avoids interleaved error messages. **Alternatives considered:** `errgroup` with bounded parallelism — marginal wall-clock gain, much noisier failure modes; revisit if users report large account lists. ### 5. Terminal rendering **Decision:** Use `github.com/fatih/color` for ANSI color support with automatic no-TTY detection. Render progress bars by hand: a fixed width (default 30 chars) filled with `█` and empty with `░`, colorized by usage band (green < 60%, yellow 60–85%, red > 85%). For multiple accounts, separate blocks with a blank line and a bold header like `━━━ personal ━━━` so the eye can scan. **Why:** `fatih/color` handles `NO_COLOR`, Windows, and non-TTY. Hand-rolling the bar keeps us off the lipgloss/bubbletea dependency tree. **Alternatives considered:** - `charmbracelet/lipgloss` — overkill for fixed-width ASCII. - Pure stdlib ANSI codes — re-implementing NO_COLOR/non-TTY is annoying. ### 6. Flag parsing **Decision:** Standard `flag` package. Flags: `--account `, `--config `, `--api-key `, `--json`, `--no-color`, `--timeout ` (default `10s`), `--version`. **Why:** Matches Go conventions, one flag set, no cobra/urfave/cli dependency needed for ~7 flags. ### 7. Output format (human mode) **Decision:** For a single account, three sections, printed in this order: ``` ━━━ personal ━━━ ZenMux Subscription — Ultra plan ($200/mo) · healthy · $0.03283/flow 5 hour [██░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 7.15% 57.2 / 800 flows $1.88 / $26.26 7 day [██░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 6.73% 416.11 / 6182 flows $13.66 / $202.95 month [░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░] n/a — / 34560 flows — / $1134.33 Tokens consumed (estimated USD value): $13.66 Next reset: 5h → 2026-04-22 14:05 · 7d → 2026-04-28 00:00 ``` For multiple accounts, repeat the block with a blank line between accounts. A failed account still gets a header, but the body is replaced by a single red error line. **Trade-off:** The monthly window has `max_flows` and `max_value_usd` but no `used_*` fields per the spec. We render what we have and don't extrapolate. The "Tokens consumed" summary pulls from the 7-day `used_value_usd` since the monthly window lacks a used value. ### 8. JSON mode (`--json`) shape **Decision:** Array of objects, one per account, in config order: ```json [ {"account": "personal", "success": true, "data": {...}}, {"account": "work", "success": false, "error": "rate limited"} ] ``` When only a single account is resolved (via `--account`, `--api-key`, or env var), emit a single object with no wrapping array — matching the raw API shape so `jq .data.plan.tier` keeps working. **Why:** Preserves the single-account pipe ergonomics while giving multi-account users a clean iterable. Per-account errors go into the JSON (not stderr) so scripts don't have to interleave streams. ### 9. Exit codes **Decision:** - `0` — all fetched accounts succeeded - `1` — any other unexpected error, OR at least one account failed and the rest are uncertain - `2` — invalid arguments/flags - `3` — no config/key available, or config file not found when required - `4` — auth failure (401/403) — only when it's the sole failure mode across all accounts - `5` — rate limited (422) — same rule as above - `6` — network/timeout — same rule as above - `7` — config file parse error (malformed YAML or missing required fields) In multi-account mode, if accounts fail with mixed causes, the CLI uses exit code `1` and surfaces per-account error lines in the output. If all accounts fail with the *same* cause, the corresponding specific code is used. **Why:** Distinct codes help scripts branch. The "mixed failures → 1" rule keeps the mapping unambiguous. ### 10. Testing **Decision:** Use `httptest.NewServer` for the client; rendering tested by snapshotting string output with ANSI stripped. Config tested with YAML fixtures under `internal/config/testdata/`. Keep the example JSON payload from the API docs as a golden fixture in `internal/api/testdata/`. **Why:** No real network or filesystem dependencies in tests. Snapshot-style assertions catch formatting regressions cheaply. ## Risks / Trade-offs - **API schema drift** → If ZenMux adds/renames fields we miss them silently. *Mitigation:* treat unknown JSON fields as non-fatal (default `encoding/json` behavior), and log a one-line warning to stderr if `success: false`. - **Monthly `used_value_usd` absent** → Users may expect a monthly USD-burned figure. *Mitigation:* the "Tokens consumed USD" summary is explicitly scoped to the 7-day window; monthly row shows `—` rather than a faked computed value. - **Config contains plaintext keys** → Anyone with FS access can exfiltrate them. *Mitigation:* document `chmod 600`, include `.gitignore` entry, ship only `config.example.yaml`. On load, if file mode is world-readable on POSIX, print a stderr warning (not fatal). - **Sequential fetch latency** → N accounts × per-account latency. *Mitigation:* fine for the realistic N ≤ 5; parallel fan-out can be added later without a schema change. - **Terminal width** → Long numbers or narrow terminals break alignment. *Mitigation:* fixed 30-char bar, right-aligned numeric columns; no responsive layout for v1. - **Rate-limit (`422`)** → One account tripping rate limit could fail others if we share state. *Mitigation:* per-request HTTP client, no shared retry backoff, rate-limit is scoped to that account's result. - **Secret leakage via `--api-key` flag** → Visible in shell history and `ps`. *Mitigation:* prefer YAML config, document the flag as a one-off override only. - **YAML footguns** → Duplicate account names, typos in keys. *Mitigation:* validate on load: unique names required, required fields checked, unknown fields warned; fail fast with exit code 7. ## Migration Plan N/A — net-new binary, nothing to migrate. First release ships as `v0.1.0`. A `config.example.yaml` is committed to the repo to lower onboarding friction. ## Open Questions - Should the "Tokens consumed (estimated USD value)" line show the 7-day window (our current pick) or the 5-hour window? Defaulting to 7-day; revisit after first real-world use. - Should we support XDG's `$XDG_CONFIG_HOME` override in addition to the literal `~/.config/zenmux-usage/config.yaml` path? Leaning yes, trivial to implement — treat as a spec detail, not a separate decision. - Future: parallel fan-out for large account counts. Punt.