## 1. Project scaffolding - [x] 1.1 Initialize Go module at repo root (`go mod init github.com/kotoyuuko/zenmux-usage-cli`, Go 1.22+) - [x] 1.2 Create directory skeleton: `cmd/zenmux-usage/`, `internal/api/`, `internal/config/`, `internal/render/`, `internal/api/testdata/`, `internal/config/testdata/` - [x] 1.3 Add dependencies via `go get`: `github.com/fatih/color`, `gopkg.in/yaml.v3`; commit `go.mod`/`go.sum` - [x] 1.4 Add `.gitignore` covering `zenmux-usage` binary, `dist/`, `config.yaml` (but not `config.example.yaml`) - [x] 1.5 Commit a `config.example.yaml` showing the two-account schema with placeholder keys - [x] 1.6 Add a minimal `README.md` with install, config schema, env-var fallback, and example output sections ## 2. Config loader (`internal/config`) - [x] 2.1 Define structs `Config{Accounts []Account}` and `Account{Name, APIKey string}` with `yaml:"..."` tags - [x] 2.2 Implement `DefaultPath() string` honoring `$XDG_CONFIG_HOME` with fallback to `~/.config/zenmux-usage/config.yaml` - [x] 2.3 Implement `Load(path string) (*Config, error)` that parses YAML, validates required fields and unique names, and returns a typed `ErrParse` on schema/validation failure - [x] 2.4 Emit a stderr warning (not fatal) for unknown top-level or per-account keys - [x] 2.5 Implement `WarnIfLooseMode(path string, w io.Writer)` that checks POSIX mode bits and prints a one-line warning if `0044` bits are set; no-op on Windows - [x] 2.6 Implement `Resolve(cfg *Config, flag ResolveFlags) ([]Account, error)` encoding the precedence: `--api-key` → `--account` filter → all accounts → env-var fallback → error - [x] 2.7 Write tests with fixtures under `internal/config/testdata/`: valid two-account, duplicate names, missing `api_key`, empty accounts list, unknown field warning, env-var fallback path, `--account` miss ## 3. API client (`internal/api`) - [x] 3.1 Define response structs (`Response`, `Data`, `Plan`, `QuotaWindow`, `QuotaMonthly`) matching the documented JSON exactly - [x] 3.2 Implement `Client` with configurable base URL, HTTP client, and timeout; default base URL `https://zenmux.ai` - [x] 3.3 Implement `FetchSubscriptionDetail(ctx, apiKey) (*Response, []byte, error)` returning parsed struct, raw body (for `--json` passthrough), and error - [x] 3.4 Map HTTP status codes to typed error values: `ErrUnauthorized` (401/403), `ErrRateLimited` (422), wrap timeouts as `ErrTimeout` - [x] 3.5 Save example payload from the API docs to `internal/api/testdata/sample.json` - [x] 3.6 Table-driven tests with `httptest.Server`: success, 401, 403, 422, 500, timeout, malformed JSON ## 4. Rendering (`internal/render`) - [x] 4.1 Implement `ProgressBar(percent float64, width int) string` returning the `█`/`░` glyph string - [x] 4.2 Implement `BandColor(percent float64)` returning green/yellow/red attribute based on the 60%/85% thresholds - [x] 4.3 Implement `RenderAccount(w io.Writer, name string, resp *Response, useColor bool)` producing the header block + three quota rows + token USD summary - [x] 4.4 Implement `RenderAccountError(w io.Writer, name string, err error, useColor bool)` printing the header and a single red error line - [x] 4.5 Implement `RenderAll(w io.Writer, results []AccountResult, useColor bool)` that iterates results and inserts a blank line between account blocks - [x] 4.6 Format USD values with `$%.2f`, flows preserving up to 2 decimal places; right-align numeric columns - [x] 4.7 Render the monthly row with `—` for used values (no `used_*` fields in API) - [x] 4.8 TTY detection: disable colors when stdout is not a terminal, when `--no-color` is set, or when `NO_COLOR` env is present - [x] 4.9 Snapshot tests with ANSI stripped: single account success, multi-account mixed (success + error), each color band ## 5. JSON output - [x] 5.1 Implement `RenderJSONSingle(w io.Writer, raw []byte)` that writes the raw API body followed by a newline - [x] 5.2 Implement `RenderJSONMulti(w io.Writer, results []AccountResult)` emitting `[{account, success, data, error}, ...]` in resolved order - [x] 5.3 Ensure failed accounts in multi mode have `data: null` and a non-empty `error` string - [x] 5.4 Tests: single-account passthrough is byte-identical to input; multi-account array shape and ordering ## 6. CLI entrypoint (`cmd/zenmux-usage/main.go`) - [x] 6.1 Parse flags with stdlib `flag`: `--account`, `--config`, `--api-key`, `--json`, `--no-color`, `--timeout` (default `10s`), `--version` - [x] 6.2 Invoke `config.Resolve` to produce `[]Account`; handle exit codes 2/3/7 from its errors - [x] 6.3 Emit the permissions warning via `config.WarnIfLooseMode` when a config file was actually loaded - [x] 6.4 Iterate accounts sequentially, calling `FetchSubscriptionDetail`, collecting results (including per-account errors) - [x] 6.5 Dispatch to JSON or human renderer based on `--json` and the single-vs-multi account count - [x] 6.6 Compute the final exit code per design §9 (all-same-cause specific code, mixed → 1, all success → 0) - [x] 6.7 Write all error messages to stderr; never write partial human output before a CLI-global error - [x] 6.8 Stamp `--version` output with a `version` constant (ldflags-overridable for release builds) ## 7. Integration and packaging - [x] 7.1 Add a `Makefile` (or `justfile`) with `build`, `test`, `lint`, `run` targets - [x] 7.2 Verify `go vet ./...` and `go test ./...` pass cleanly - [x] 7.3 Cross-compile sanity check for `darwin/amd64`, `darwin/arm64`, `linux/amd64` via `GOOS`/`GOARCH` - [x] 7.4 Manual end-to-end: populate config with two real accounts, run `zenmux-usage`, confirm both blocks render and USD summaries match; run `zenmux-usage --account ` to verify filtering; run `zenmux-usage --json | jq '.[0].data.plan.tier'` - [x] 7.5 Verify each exit code manually: unknown `--account` (2), no config + no env (3), bad key single-account (4), invalid timeout single-account (6), malformed YAML (7), mixed outcomes (1) ## 8. Documentation - [x] 8.1 Update `README.md` with a screenshot or ASCII sample showing a multi-account render - [x] 8.2 Document the YAML config schema, default path, `--config` flag, and `chmod 600` recommendation - [x] 8.3 Document exit codes and the multi- vs single-account exit-code rule - [x] 8.4 Document `--json` mode shape differences between single and multi account, with one `jq` example for each