Skip to content

feat: JSON errors and exit codes everywhere, and auth status --json - #44

Closed
alukach wants to merge 1 commit into
feat/status-commandfrom
feat/json-output
Closed

alukach wants to merge 1 commit into
feat/status-commandfrom
feat/json-output

Conversation

@alukach

@alukach alukach commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #40 (auth status). Retarget once it merges.

Why

Scripts and AI agents should never have to parse output laid out for people. This PR makes --json a contract across the CLI, writes the rules down for future commands, and gives auth status a --json form.

What

Rules: AGENTS.md, "JSON output" (which CLAUDE.md imports):

  • Where: every command that reports information takes --json. The exceptions already print for machines or are interactive: creds, exec, auth token, api, -i.
  • One document on stdout and nothing else there, with API field names and RFC 3339 times. Adding fields is fine; renaming or removing them is a breaking change.
  • Printing: through output::json.
  • Errors and exit codes: JSON errors on stderr, and one exit-code table for every command.

Errors and exit codes, for every command:

  • fail(e, json) prints a failure under --json as {"error": {"code", "message", "status"?, "field_errors"?}}, the API's own error shape, on stderr, keeping stdout empty.

  • It exits with the failure's kind:

    Code Meaning
    0 Success
    1 Any other failure, including a cancelled prompt
    2 Bad command-line usage (clap)
    3 Rejected input (400, 409, 422)
    4 Not signed in or not allowed (401, 403)
    5 Not found (404)
    130 Interrupted

    Without --json the message is printed as before.

  • How the API's details survive (src/failure.rs): commands pass errors around as plain text (String), which drops the API's code, status and field errors. failure::remember records them as an API error is turned into text, and classify finds them again in the text that reaches fail(), even when wrapped (for example by the saved-draft note). It's a deliberate bridge, marked with a ponytail: comment naming the proper fix: a typed error, which would mean rewriting about 80 function signatures across files under active change.

auth status --json: what auth status shows, as data: environment, logged_in, the login's state, account (account_id, name, profile_url), each token's times with an expired flag (the refresh token's issued_at), and the role's cached credentials. The Profile row and the refresh token's renewal time themselves are in #40.

Also: product print_json goes through output::json, and ProductCommand::wants_json() tells fail whether --json was asked for. The README documents auth status --json, and JSON output and exit codes.

Testing

  • cargo test and cargo test --features staging: 99 passed. New:
    • classifying API errors, including wrapped ones, and their exit codes;
    • the JSON error shape;
    • status_json.
  • cargo clippy --all-targets -- -D warnings and cargo fmt --check: clean.
  • Manual, with a file-cached session in a temporary HOME for a made-up environment (so no keychain entry was read), against a local stub API:
    • auth status --json returns the document above;
    • product view acct/nope --json leaves stdout empty, prints {"error":{"code":"not_found","message":"Product acct/nope not found","status":404}} on stderr, and exits 5. Without --json it prints the prose error, also exiting 5.

🤖 Generated with Claude Code

@claude

claude Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Claude finished @alukach's task in 18s —— View job


✅ No blocking issues — safe to merge.

I read the diff and didn't run the tests. I found no correctness or security problems.

Non-blocking notes:

  • src/failure.rs:47 — LAST is a thread_local!. It works today because the main future runs on the main thread and the only tokio::spawn (picker.rs:36, the Ctrl+C handler) never converts an ApiError. Any future spawn or spawn_blocking that turns an ApiError into a String would silently lose its code, status and field errors, and the exit code would fall back to 1. A static Mutex<Option<Remembered>> would remove that trap. The ponytail: comment already names the typed-error upgrade.
  • src/failure.rs:79 — classify matches with e.contains(&r.text), so a stale LAST from an earlier, recovered API error could mislabel an unrelated failure whose text happens to contain it. The text includes the code and status, so this is very unlikely.
  • Under --json, Ctrl+C via the picker.rs handler prints the prose "Interrupted." rather than a JSON error. -i is exempt from --json, so this is consistent with AGENTS.md.

Simplify (ponytail)

  • src/failure.rs:26-33 — Remembered duplicates Failure plus text. Store (String, Failure) in LAST and drop the second struct.
  • src/main.rs status_json — refresh_lifetime is passed as Option<(i64, &str)>, so run_status has to build it with Some((lifetime.0, lifetime.1.as_str())). Pass the borrowed tuple straight through.

💰 Estimated review cost: $0.15 · 0m18s · 6 turns

@alukach alukach changed the title feat: JSON errors and exit codes everywhere, auth status --json feat: JSON errors and exit codes everywhere, and auth status --json Oct 14, 2026
AGENTS.md gets the JSON-output rules: --json on every command that
reports information, one document on stdout, API field names, additive
changes only, output::json for printing, JSON errors on stderr, and one
table of exit codes.

fail(e, json) prints a failure as {"error": {code, message, status?,
field_errors?}} under --json and exits with its kind: 3 rejected input,
4 not signed in or not allowed, 5 not found, 130 interrupted, else 1.
failure.rs recovers the API's code, status and field errors from the
String errors commands return (a bridge until errors are typed).

auth status --json prints what auth status shows as data, with the
account's profile_url and the refresh token's issue time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@alukach

alukach commented Oct 14, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #45 (JSON errors, exit codes, output::json and the AGENTS.md rules) and #46 (auth status --json). The branch is kept.

@alukach alukach closed this Oct 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant