Skip to content

feat: add JSON API mode for account workflows#150

Draft
Loongphy wants to merge 1 commit into
mainfrom
agent/json-api-mode
Draft

feat: add JSON API mode for account workflows#150
Loongphy wants to merge 1 commit into
mainfrom
agent/json-api-mode

Conversation

@Loongphy

Copy link
Copy Markdown
Owner

Summary

  • Add structured JSON output for list, switch, and remove while keeping their human-facing CLI behavior aligned.
  • Return selector-resolution details and keep multi-account removal atomic when any selector is ambiguous or missing.
  • Expose display-ready plan and usage data, including refresh request/status/source metadata needed by the GUI.
  • Canonicalize Team/Business input as Business and migrate the released registry schema 3 format to the final schema 4 format.

Why

The GUI and CLI ship together and need one stable command contract. The CLI now owns plan normalization and workflow decisions, so JSON consumers receive final product semantics instead of reimplementing backend-value interpretation.

The registry schema remains at version 4. This change does not bump the application version or add a schema 5 migration.

Validation

  • zig fmt --check src tests
  • zig build test --summary failures
  • zig build run -- list
  • $autoreview: patch correct, no actionable findings (0.87 confidence)

@Loongphy

Copy link
Copy Markdown
Owner Author

@greptile review

@pkg-pr-new

pkg-pr-new Bot commented Jul 14, 2026

Copy link
Copy Markdown

Open in StackBlitz

@loongphy/codex-auth-darwin-arm64

npx https://pkg.pr.new/@loongphy/codex-auth-darwin-arm64@b4e7420

@loongphy/codex-auth-darwin-x64

npx https://pkg.pr.new/@loongphy/codex-auth-darwin-x64@b4e7420

@loongphy/codex-auth-linux-arm64

npx https://pkg.pr.new/@loongphy/codex-auth-linux-arm64@b4e7420

@loongphy/codex-auth-linux-x64

npx https://pkg.pr.new/@loongphy/codex-auth-linux-x64@b4e7420

@loongphy/codex-auth-win32-arm64

npx https://pkg.pr.new/@loongphy/codex-auth-win32-arm64@b4e7420

@loongphy/codex-auth-win32-x64

npx https://pkg.pr.new/@loongphy/codex-auth-win32-x64@b4e7420

@loongphy/codex-auth

npx https://pkg.pr.new/@loongphy/codex-auth@b4e7420

commit: b4e7420

@greptile-apps

greptile-apps Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a structured --json mode to the list, switch, and remove commands so a co-shipped GUI can consume stable, machine-readable output instead of re-implementing CLI screen-scraping. It also canonicalises Team/Business plan labels into a unified PlanType enum and migrates schema-3 stored plan values to schema-4 semantics via parseStoredPlanType.

  • JSON API (src/cli/json_output.zig, src/workflows/results.zig): Introduces typed ListResult, SwitchResult, and RemoveResult view types with full ownership semantics, serialised through a new writeListResult / writeSwitchResult / writeRemoveResult surface. Selector resolution for remove --json is made atomic — any ambiguous or missing selector causes the entire batch to be rejected before any mutation.
  • Plan normalisation (src/registry/parse.zig, src/registry/common.zig): Removes the .team variant, adds .go, and maps legacy backend strings (\"business\".enterprise, \"team\".business) through a schema-version-aware migration path.
  • Workspace account names (src/registry/account_ops.zig, src/workflows/account_names.zig): Renames isTeamAccount to isWorkspaceAccount and widens the match to include .edu in addition to .business and .enterprise.

Confidence Score: 4/5

Safe to merge; the JSON mode is additive, all CLI paths are preserved, and ownership/cleanup patterns across the new workflow helpers are consistent and correct.

The core logic — atomic selector resolution before any mutation, snapshot-before-remove ordering, schema-version-aware plan migration — is well-reasoned and handled correctly. The three style-level findings carry no runtime risk. The one logic note — .edu accounts newly entering the workspace account-name refresh scope — is worth confirming against whether that API endpoint supports edu plans.

src/registry/account_ops.zig (isWorkspaceAccount .edu expansion) and src/workflows/switch.zig (page_allocator in printJsonMutationError)

Important Files Changed

Filename Overview
src/workflows/results.zig New file defining view types and builder functions for structured JSON output; logic is correct but buildAccountViewForKey uses @constcast to work around a mutable-pointer API.
src/workflows/remove.zig Adds handleRemoveJson with atomic selector resolution; refactors selector resolution into resolveRemoveSelectors shared by both CLI and JSON paths.
src/workflows/switch.zig Adds handleSwitchQueryJson with proper error handling; printJsonMutationError uses std.heap.page_allocator directly, inconsistent with the rest of the codebase.
src/cli/json_output.zig New file providing write* helpers for all JSON API responses; schema_version constant (1) is separate from the registry schema version (4).
src/registry/parse.zig Replaces parsePlanType with normalizePlanType and parseStoredPlanType; correctly maps legacy backend values via schema-version-aware migration.
src/workflows/usage.zig Refactors local usage refresh path to emit detailed outcome metadata; adds LocalUsageRefreshResult enum replacing boolean return.
src/registry/account_ops.zig Renames isTeamAccount to isWorkspaceAccount and expands its match set to include .business, .enterprise, and .edu; all call sites updated.
src/workflows/list.zig Extracts computeListState helper, adds computeList for JSON callers; printJsonWorkflowError is a local copy of the same helper in switch.zig and remove.zig.
src/cli/commands/remove.zig Adds --json flag parsing with pre-scan for json_requested; selectors_transferred flag correctly replaces the old errdefer pattern.
src/cli/commands/switch.zig Adds --json flag and rejects it with previous-account target or picker target; error routing is correct.
src/registry/storage.zig Passes schema_version through to parseAccountRecord and parseUsage so legacy schema 3 data is migrated correctly.
src/workflows/root.zig Adds commandWantsJson helper and routes startup errors to JSON format when appropriate.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant GUI
    participant CLI as codex-auth CLI
    participant Workflow
    participant Registry

    GUI->>CLI: "codex-auth list --json"
    CLI->>Workflow: "handleList opts.json=true"
    Workflow->>Registry: loadRegistry + syncActiveAccountFromAuth
    Workflow->>Workflow: refreshForegroundUsage API or local
    Workflow->>Workflow: buildListResult AccountView array
    CLI-->>GUI: "JSON {schema_version, command, accounts, active_account_key}"

    GUI->>CLI: "codex-auth switch query --json"
    CLI->>Workflow: handleSwitchQueryJson
    Workflow->>Registry: resolveSwitchQueryLocally
    alt not_found
        CLI-->>GUI: "JSON error code account_not_found"
    else ambiguous
        CLI-->>GUI: "JSON error code ambiguous_query with candidates"
    else direct
        Workflow->>Registry: activateAccountByKey + saveRegistry
        Workflow->>Workflow: buildSwitchResult AccountView
        CLI-->>GUI: "JSON {schema_version, command:switch, switched_to}"
    end

    GUI->>CLI: "codex-auth remove selector --json"
    CLI->>Workflow: handleRemoveJson
    Workflow->>Workflow: resolveRemoveSelectors
    alt hasNotFound or hasAmbiguous
        Workflow->>Workflow: buildSelectorResolutionViews
        CLI-->>GUI: "JSON error code selector_resolution_failed with resolutions"
    else all resolved
        Workflow->>Workflow: buildAccountViewsForIndices snapshot before remove
        Workflow->>Registry: removeSelectedAccountsAndPersist
        CLI-->>GUI: "JSON {schema_version, command:remove, removed, new_active_account_key}"
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant GUI
    participant CLI as codex-auth CLI
    participant Workflow
    participant Registry

    GUI->>CLI: "codex-auth list --json"
    CLI->>Workflow: "handleList opts.json=true"
    Workflow->>Registry: loadRegistry + syncActiveAccountFromAuth
    Workflow->>Workflow: refreshForegroundUsage API or local
    Workflow->>Workflow: buildListResult AccountView array
    CLI-->>GUI: "JSON {schema_version, command, accounts, active_account_key}"

    GUI->>CLI: "codex-auth switch query --json"
    CLI->>Workflow: handleSwitchQueryJson
    Workflow->>Registry: resolveSwitchQueryLocally
    alt not_found
        CLI-->>GUI: "JSON error code account_not_found"
    else ambiguous
        CLI-->>GUI: "JSON error code ambiguous_query with candidates"
    else direct
        Workflow->>Registry: activateAccountByKey + saveRegistry
        Workflow->>Workflow: buildSwitchResult AccountView
        CLI-->>GUI: "JSON {schema_version, command:switch, switched_to}"
    end

    GUI->>CLI: "codex-auth remove selector --json"
    CLI->>Workflow: handleRemoveJson
    Workflow->>Workflow: resolveRemoveSelectors
    alt hasNotFound or hasAmbiguous
        Workflow->>Workflow: buildSelectorResolutionViews
        CLI-->>GUI: "JSON error code selector_resolution_failed with resolutions"
    else all resolved
        Workflow->>Workflow: buildAccountViewsForIndices snapshot before remove
        Workflow->>Registry: removeSelectedAccountsAndPersist
        CLI-->>GUI: "JSON {schema_version, command:remove, removed, new_active_account_key}"
    end
Loading

Comments Outside Diff (1)

  1. src/workflows/list.zig, line 139-155 (link)

    P2 printJsonWorkflowError duplicated across three files

    The same printJsonWorkflowError implementation appears in list.zig, remove.zig, and switch.zig. Extracting it to a shared location (e.g. src/cli/json_output.zig or a new src/workflows/json_errors.zig) would keep the three error-code strings ("curl_unavailable", "registry_error") in sync automatically if they ever need to change.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/workflows/list.zig
    Line: 139-155
    
    Comment:
    **`printJsonWorkflowError` duplicated across three files**
    
    The same `printJsonWorkflowError` implementation appears in `list.zig`, `remove.zig`, and `switch.zig`. Extracting it to a shared location (e.g. `src/cli/json_output.zig` or a new `src/workflows/json_errors.zig`) would keep the three error-code strings (`"curl_unavailable"`, `"registry_error"`) in sync automatically if they ever need to change.
    
    How can I resolve this? If you propose a fix, please make it concise.

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

    Fix in Codex

Fix All in Codex

Prompt To Fix All With AI
Fix the following 4 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 4
src/workflows/switch.zig:257-267
**`page_allocator` used instead of caller's allocator**

`printJsonMutationError` in `switch.zig` uses `std.heap.page_allocator` to format the error message, while the equivalent helper in `remove.zig` avoids allocation entirely by using a static string. Since `operation` is always the literal `"switch"`, the formatted string is effectively static — the simplest fix is to drop the `allocPrint` call entirely and inline the complete static message, matching the `remove.zig` style and eliminating the `page_allocator` dependency.

### Issue 2 of 4
src/workflows/results.zig:244-250
**`@constCast` to work around a mutable-pointer API**

`registry.findAccountIndexByAccountKey` is called with `@constCast(reg)` because its signature takes a mutable `*Registry`. If that function is ever extended to mutate the registry, the silent const-stripping here would be an unseen hazard. Consider adding a `*const` overload for `findAccountIndexByAccountKey`, or at minimum adding a brief comment explaining why the cast is safe here.

### Issue 3 of 4
src/workflows/list.zig:139-155
**`printJsonWorkflowError` duplicated across three files**

The same `printJsonWorkflowError` implementation appears in `list.zig`, `remove.zig`, and `switch.zig`. Extracting it to a shared location (e.g. `src/cli/json_output.zig` or a new `src/workflows/json_errors.zig`) would keep the three error-code strings (`"curl_unavailable"`, `"registry_error"`) in sync automatically if they ever need to change.

### Issue 4 of 4
src/registry/account_ops.zig:449-453
**`.edu` added to workspace account name refresh scope**

`isWorkspaceAccount` now returns `true` for `.edu` in addition to `.business` and `.enterprise`. The old `isTeamAccount` only matched `.team` (now `.business`). This means `.edu` accounts now participate in workspace account-name fetching and the `hasMissingAccountNameForUser` / `shouldFetchWorkspaceAccountNamesForUser` checks. If `.edu` accounts do not actually expose the account-names API, this could result in spurious fetch attempts or incorrect "missing name" warnings on every list.

Reviews (1): Last reviewed commit: "feat: add JSON API mode for account work..." | Re-trigger Greptile

Comment thread src/workflows/switch.zig
Comment on lines +257 to +267
fn printJsonMutationError(err: anyerror, operation: []const u8) anyerror {
if (err == error.OutOfMemory) return err;
const message = try std.fmt.allocPrint(
std.heap.page_allocator,
"the {s} operation could not be completed; stored state may have changed; run `list --json` before retrying",
.{operation},
);
defer std.heap.page_allocator.free(message);
try cli.json_output.printError("state_uncertain", message, null);
return error.StateUncertain;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 page_allocator used instead of caller's allocator

printJsonMutationError in switch.zig uses std.heap.page_allocator to format the error message, while the equivalent helper in remove.zig avoids allocation entirely by using a static string. Since operation is always the literal "switch", the formatted string is effectively static — the simplest fix is to drop the allocPrint call entirely and inline the complete static message, matching the remove.zig style and eliminating the page_allocator dependency.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/workflows/switch.zig
Line: 257-267

Comment:
**`page_allocator` used instead of caller's allocator**

`printJsonMutationError` in `switch.zig` uses `std.heap.page_allocator` to format the error message, while the equivalent helper in `remove.zig` avoids allocation entirely by using a static string. Since `operation` is always the literal `"switch"`, the formatted string is effectively static — the simplest fix is to drop the `allocPrint` call entirely and inline the complete static message, matching the `remove.zig` style and eliminating the `page_allocator` dependency.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Codex

Comment thread src/workflows/results.zig
Comment on lines +244 to +250
const account_idx = registry.findAccountIndexByAccountKey(@constCast(reg), account_key) orelse return error.AccountNotFound;
const selected = [_]usize{account_idx};
const views = try buildAccountViewsForIndices(allocator, reg, usage_state, &selected);
std.debug.assert(views.len == 1);
defer allocator.free(views);
return views[0];
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 @constCast to work around a mutable-pointer API

registry.findAccountIndexByAccountKey is called with @constCast(reg) because its signature takes a mutable *Registry. If that function is ever extended to mutate the registry, the silent const-stripping here would be an unseen hazard. Consider adding a *const overload for findAccountIndexByAccountKey, or at minimum adding a brief comment explaining why the cast is safe here.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/workflows/results.zig
Line: 244-250

Comment:
**`@constCast` to work around a mutable-pointer API**

`registry.findAccountIndexByAccountKey` is called with `@constCast(reg)` because its signature takes a mutable `*Registry`. If that function is ever extended to mutate the registry, the silent const-stripping here would be an unseen hazard. Consider adding a `*const` overload for `findAccountIndexByAccountKey`, or at minimum adding a brief comment explaining why the cast is safe here.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Codex

Comment thread src/registry/account_ops.zig
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