tasks.md 6.2 KB

1. Project scaffolding

  • 1.1 Initialize Go module at repo root (go mod init github.com/kotoyuuko/zenmux-usage-cli, Go 1.22+)
  • 1.2 Create directory skeleton: cmd/zenmux-usage/, internal/api/, internal/config/, internal/render/, internal/api/testdata/, internal/config/testdata/
  • 1.3 Add dependencies via go get: github.com/fatih/color, gopkg.in/yaml.v3; commit go.mod/go.sum
  • 1.4 Add .gitignore covering zenmux-usage binary, dist/, config.yaml (but not config.example.yaml)
  • 1.5 Commit a config.example.yaml showing the two-account schema with placeholder keys
  • 1.6 Add a minimal README.md with install, config schema, env-var fallback, and example output sections

2. Config loader (internal/config)

  • 2.1 Define structs Config{Accounts []Account} and Account{Name, APIKey string} with yaml:"..." tags
  • 2.2 Implement DefaultPath() string honoring $XDG_CONFIG_HOME with fallback to ~/.config/zenmux-usage/config.yaml
  • 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
  • 2.4 Emit a stderr warning (not fatal) for unknown top-level or per-account keys
  • 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
  • 2.6 Implement Resolve(cfg *Config, flag ResolveFlags) ([]Account, error) encoding the precedence: --api-key--account filter → all accounts → env-var fallback → error
  • 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)

  • 3.1 Define response structs (Response, Data, Plan, QuotaWindow, QuotaMonthly) matching the documented JSON exactly
  • 3.2 Implement Client with configurable base URL, HTTP client, and timeout; default base URL https://zenmux.ai
  • 3.3 Implement FetchSubscriptionDetail(ctx, apiKey) (*Response, []byte, error) returning parsed struct, raw body (for --json passthrough), and error
  • 3.4 Map HTTP status codes to typed error values: ErrUnauthorized (401/403), ErrRateLimited (422), wrap timeouts as ErrTimeout
  • 3.5 Save example payload from the API docs to internal/api/testdata/sample.json
  • 3.6 Table-driven tests with httptest.Server: success, 401, 403, 422, 500, timeout, malformed JSON

4. Rendering (internal/render)

  • 4.1 Implement ProgressBar(percent float64, width int) string returning the / glyph string
  • 4.2 Implement BandColor(percent float64) returning green/yellow/red attribute based on the 60%/85% thresholds
  • 4.3 Implement RenderAccount(w io.Writer, name string, resp *Response, useColor bool) producing the header block + three quota rows + token USD summary
  • 4.4 Implement RenderAccountError(w io.Writer, name string, err error, useColor bool) printing the header and a single red error line
  • 4.5 Implement RenderAll(w io.Writer, results []AccountResult, useColor bool) that iterates results and inserts a blank line between account blocks
  • 4.6 Format USD values with $%.2f, flows preserving up to 2 decimal places; right-align numeric columns
  • 4.7 Render the monthly row with for used values (no used_* fields in API)
  • 4.8 TTY detection: disable colors when stdout is not a terminal, when --no-color is set, or when NO_COLOR env is present
  • 4.9 Snapshot tests with ANSI stripped: single account success, multi-account mixed (success + error), each color band

5. JSON output

  • 5.1 Implement RenderJSONSingle(w io.Writer, raw []byte) that writes the raw API body followed by a newline
  • 5.2 Implement RenderJSONMulti(w io.Writer, results []AccountResult) emitting [{account, success, data, error}, ...] in resolved order
  • 5.3 Ensure failed accounts in multi mode have data: null and a non-empty error string
  • 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)

  • 6.1 Parse flags with stdlib flag: --account, --config, --api-key, --json, --no-color, --timeout (default 10s), --version
  • 6.2 Invoke config.Resolve to produce []Account; handle exit codes 2/3/7 from its errors
  • 6.3 Emit the permissions warning via config.WarnIfLooseMode when a config file was actually loaded
  • 6.4 Iterate accounts sequentially, calling FetchSubscriptionDetail, collecting results (including per-account errors)
  • 6.5 Dispatch to JSON or human renderer based on --json and the single-vs-multi account count
  • 6.6 Compute the final exit code per design §9 (all-same-cause specific code, mixed → 1, all success → 0)
  • 6.7 Write all error messages to stderr; never write partial human output before a CLI-global error
  • 6.8 Stamp --version output with a version constant (ldflags-overridable for release builds)

7. Integration and packaging

  • 7.1 Add a Makefile (or justfile) with build, test, lint, run targets
  • 7.2 Verify go vet ./... and go test ./... pass cleanly
  • 7.3 Cross-compile sanity check for darwin/amd64, darwin/arm64, linux/amd64 via GOOS/GOARCH
  • 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 <name> to verify filtering; run zenmux-usage --json | jq '.[0].data.plan.tier'
  • 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

  • 8.1 Update README.md with a screenshot or ASCII sample showing a multi-account render
  • 8.2 Document the YAML config schema, default path, --config flag, and chmod 600 recommendation
  • 8.3 Document exit codes and the multi- vs single-account exit-code rule
  • 8.4 Document --json mode shape differences between single and multi account, with one jq example for each