Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,5 +58,5 @@ JSON output, and several new account management commands.

Added per-command documentation under [docs/commands/](./docs/commands/README.md), plus
[docs/json-api.md](./docs/json-api.md), [docs/api.md](./docs/api.md),
[docs/schema-migration.md](./docs/schema-migration.md), and
[docs/schema.md](./docs/schema.md), and
[docs/table-layout.md](./docs/table-layout.md).
1 change: 1 addition & 0 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ pub fn build(b: *std.Build) void {
"tests/registry_test.zig",
"tests/session_test.zig",
"tests/time_relative_test.zig",
"tests/time_timestamp_test.zig",
"tests/table_layout_test.zig",
"tests/tui_display_test.zig",
"tests/tui_session_test.zig",
Expand Down
29 changes: 18 additions & 11 deletions docs/commands/list.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ codex-auth list --json
- Syncs the current `auth.json` into the registry before rendering when the current auth file is parseable.
- Shows selectable row numbers using the same ordering as `switch` and `remove`.
- Groups rows by email when the same email owns multiple account snapshots.
- Non-live output shows `ACCOUNT`, `PLAN`, `CREDITS`, `RESET CREDITS`, `5H`, `WEEKLY`, and `LAST ACTIVITY`.
- Non-live output shows `ACCOUNT`, `PLAN`, `PLAN EXPIRE`, `CREDITS`, `RESET CREDITS`, `5H`, `WEEKLY`, `AUTH EXPIRE`, and `LAST ACTIVITY`.
The deferred `LAST ACTIVITY`, `PLAN EXPIRE`, and `AUTH EXPIRE` columns appear only when the terminal is wide enough.

## Refresh Modes

Expand All @@ -27,15 +28,17 @@ codex-auth list --json
- `--skip-api` forbids remote API calls for this command.
- `--live` keeps refreshing the terminal view and requires a TTY.
- In live tables, `*` before the number marks the active account and `!` marks a refresh error.
- Live account details show the access-token expiry and, per account, the
available reset-credit count with the earliest card expiry. The
`ACCESS EXPIRE` and `RESET CREDITS` columns render in local time; the
available reset credits are also expandable per account in the live view.
- List output shows the access-token expiry and plan expiry. Live output also
shows the per-account available reset-credit count with the earliest card
expiry. The `AUTH EXPIRE`, `PLAN EXPIRE`, and `RESET CREDITS` columns render
in local time; available reset credits are also expandable in the live view.
- Access-token expiry is shown in local time and is not the account or subscription
expiry; refreshing credentials can extend it. Plan expiry is shown as unavailable
because the current data sources do not provide it. Server card lists may be
capped; compare `available_count` with the JSON `credits` length before treating
the list as exhaustive. After a refresh failure, details may be cached.
expiry; refreshing credentials can extend it. `PLAN EXPIRE` comes from the
`chatgpt_subscription_active_until` claim in the stored `id_token`. Missing or
already-expired values render as `-`, so a stale snapshot does not look current.
Server card lists may be capped; compare `available_count` with the JSON
`credits` length before treating the list as exhaustive. After a refresh failure,
details may be cached.
- `--json` emits one machine-readable JSON document and cannot be combined with `--live`.

When local-only refresh is active, only the active account can be updated from local rollout files. Non-active rows use the stored registry snapshot.
Expand All @@ -47,8 +50,12 @@ When local-only refresh is active, only the active account can be updated from l
- Grouped rows keep the shared email in the header; child rows with both alias and account name render as `alias(account name)`.
- Usage cells show remaining percent and reset time when that data is known.
- In non-live output, `CREDITS` shows the integer part of the current `credits.balance` value.
- `RESET CREDITS` remains a separate field that shows the stored reset-credit count when remote usage refresh provides it.
- JSON output includes the returned reset-credit details.
- `PLAN EXPIRE` renders the local date from the stored `id_token` subscription claim.
Missing or past timestamps render as `-` in both plain and live tables.
- `RESET CREDITS` remains a separate field that shows the stored reset-credit count when remote usage refresh provides it. A zero count renders as `0`; a non-zero count with no known expiry renders as `N (-)`.
- JSON output includes the raw `plan_expires_at` timestamp and the returned reset-credit details.
The terminal column hides past plan-expiry timestamps, but the JSON value remains the
stored timestamp so callers can detect stale snapshots.
- Remote refresh failures can render row overlays such as `401`, `403`, `TimedOut`, or `MissingAuth`.
- `LAST ACTIVITY` is based on the last stored usage update time.
- `--json` returns accounts in the same display order and includes the same row numbers shown by the table.
Expand Down
145 changes: 145 additions & 0 deletions docs/dynamic-columns.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
# Dynamic Column Layout

All table views use one responsive allocator in `src/cli/table_layout.zig`.
The allocator separates a column's semantic identity from its position on
screen, so each view can keep its own visual order while sharing the same width
policy and hide order.

## Model

Each view supplies:

- `columns`: the visual order and content width of its supported columns;
- `first_order`, `grow_order`, `deferred_order`: the shared priority lists;
- `prefix_width`: width of the row prefix (`#`, checkbox, index);
- `separator_width`: width of the separator between visible columns.

A column with content width `0` is unsupported or hidden by that view. Hidden
columns are skipped completely, including their header, cells, and separators.
Their width and separator budget is redistributed to the remaining columns.

## Visual Order

The order of each view is fixed. Unsupported columns are simply omitted.

| View | Visual order |
|---|---|
| `list` | `ACCOUNT`, `PLAN`, `PLAN EXPIRE`, `CREDITS`, `RESET CREDITS`, `5H`, `WEEKLY`, `AUTH EXPIRE`, `LAST ACTIVITY` |
| `list --live` | `#*`, `ACCOUNT`, `PLAN`, `PLAN EXPIRE`, `RESET CREDITS`, `5H`, `WEEKLY`, `AUTH EXPIRE`, `LAST` |
| `switch`, `remove` | `ACCOUNT`, `PLAN`, `5H`, `WEEKLY`, `LAST` |

## Shared Priority

Every view uses the same ordered lists. A view with an unsupported column
(content width `0`) skips that entry without changing the order.

### Reserve order (highest to lowest)

```text
#* -> ACCOUNT (10) -> 5H (4) -> WEEKLY (6) -> PLAN (4)
-> RESET CREDITS (2) -> CREDITS (2)
```

`PLAN EXPIRE`, `AUTH EXPIRE`, and `LAST` are not in this list; they are deferred.

### Grow order

```text
5H -> WEEKLY -> PLAN -> RESET CREDITS -> CREDITS
```

### Deferred order

```text
AUTH EXPIRE (10) -> PLAN EXPIRE (10) -> LAST (4)
```

Deferred columns are considered only after every normal column has grown. Each
still needs its target width and separator to fit, so deferred columns never
become one-column dot cells.

`ACCOUNT` is expanded last, after the deferred pass, using only leftover width.
A long account label therefore cannot hide `AUTH EXPIRE`, `PLAN EXPIRE`, or
`LAST ACTIVITY`.

## Shared Hide Order

From wide to narrow, columns disappear in the reverse priority order. The
complete shared order is:

```text
LAST
-> PLAN EXPIRE
-> AUTH EXPIRE
-> CREDITS
-> RESET CREDITS
-> PLAN
-> WEEKLY
-> 5H
-> ACCOUNT
-> #*
```

Unsupported columns are already absent from a view:

- plain `list` has no `#*`: `LAST -> PLAN EXPIRE -> AUTH EXPIRE -> CREDITS -> RESET CREDITS -> PLAN -> WEEKLY -> 5H -> ACCOUNT`.
- `list --live` has no `CREDITS`: `LAST -> PLAN EXPIRE -> AUTH EXPIRE -> RESET CREDITS -> PLAN -> WEEKLY -> 5H -> ACCOUNT -> #*`.
- `switch` / `remove` support none of the credits, auth-expiry, or plan-expiry columns: `LAST -> PLAN -> WEEKLY -> 5H -> ACCOUNT`.

`LAST` is the absolute lowest-priority column and hides first. `CREDITS` sits
just above the deferred expiry fields; `RESET CREDITS`, which carries the
actionable reset count, stays above it.

## Allocation Phases

1. **Reserve targets** in `first_order`. A column is admitted only when its
target width and separator fit. `ACCOUNT` may use a partial width down to
2 columns.
2. **Grow** visible columns in `grow_order` toward their content width.
3. **Admit deferred columns** in `deferred_order`, only when their target
width and separator still fit after every normal column has grown.

Separators are charged only when a column becomes visible. A line never exceeds
the requested terminal width.

## Target Widths

| Column | Target |
|---|---:|
| `#*` | index width (at least `#*`) |
| `ACCOUNT` | 10 (minimum 2) |
| `PLAN` | 4 |
| `PLAN EXPIRE` | 10, deferred |
| `CREDITS` | 2 |
| `RESET CREDITS` | 2 |
| `5H` | 4 |
| `WEEKLY` | 6 |
| `LAST` | 4, deferred |
| `AUTH EXPIRE` | 10, deferred |

## Headers

Wide headers use their full label. Narrow columns use compact labels:

| Column | Compact labels |
|---|---|
| `CREDITS` | `CR` |
| `RESET CREDITS` | `RESET`, then `RC` |
| `AUTH EXPIRE` | `AUTH EXP`, then `AUTH` |
| `PLAN EXPIRE` | `PLAN EXP`, then `PLAN` |

## Adding a Column

1. Add a `ColumnId` and its default target width.
2. Add it to the visual order of each view that supports it. Unsupported views
pass content width `0`.
3. Add it to the shared `first_order`, `grow_order`, or `deferred_order`
according to its priority.
4. Add width-aware cell formatting and a compact header when needed.
5. Add a width-sweep test for hidden/visible behavior, line length, and any
value that must stay complete (for example `100%`).

## Related

- Account-label truncation: [table-layout.md](table-layout.md)
- `list` column contents: [commands/list.md](commands/list.md)
13 changes: 9 additions & 4 deletions docs/implement.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ This document is the implementation index for `codex-auth`. Command-specific beh
- Command behavior: [docs/commands/README.md](./commands/README.md)
- API refresh and endpoint rules: [docs/api.md](./api.md)
- File permissions: [docs/permissions.md](./permissions.md)
- Schema migration: [docs/schema-migration.md](./schema-migration.md)
- Registry schema and compatibility: [docs/schema.md](./schema.md)
- Test organization: [docs/tests.md](./tests.md)
- Release and CI: [docs/release.md](./release.md)

Expand All @@ -31,14 +31,15 @@ Managed files:

## Registry Compatibility

- `registry.json.schema_version` is the on-disk migration gate.
- `registry.json.schema_version` is the on-disk compatibility gate.
- `version = 2` registries using `active_email` and email-keyed snapshots are migrated to the current schema.
- Current-layout files that still use the top-level `version = 3` key are rewritten to the current schema.
- Loading a supported older schema performs the migration in memory and rewrites `registry.json` in the current format.
- Loading a newer `schema_version` is rejected with `UnsupportedRegistryVersion`.
- Additive optional derived/cache fields stay within the current schema when missing data is safe and recoverable; see [docs/schema.md](./schema.md).
- Saving always rewrites `registry.json` into the current schema field set.

See [docs/schema-migration.md](./schema-migration.md) for versioning policy and migration rules.
See [docs/schema.md](./schema.md) for the compatibility policy, data classification, and migration rules.

## Account Identity

Expand Down Expand Up @@ -76,6 +77,8 @@ If `OPENAI_API_KEY` is present, the account is treated as API-key auth. Otherwis
- `tokens.id_token`
- a ChatGPT account context from `tokens.account_id`, JWT `https://api.openai.com/auth.chatgpt_account_id`, or JWT `https://api.openai.com/auth.organizations[].id`
- JWT user identity from `chatgpt_user_id` or `user_id`
- Optional plan expiry from JWT `https://api.openai.com/auth.chatgpt_subscription_active_until`
(ISO-8601 string or epoch seconds/milliseconds)

If required identity fields are missing or mismatched, import/login fails. Existing-registry foreground sync skips unsyncable auth files and continues with registry state already on disk.

Expand All @@ -86,7 +89,7 @@ Foreground account commands sync `auth.json` before their main work when the cur
The sync flow is:

1. Read `~/.codex/auth.json`.
2. Parse email, plan, auth mode, `chatgpt_user_id`, and `chatgpt_account_id`.
2. Parse email, plan, plan expiry, auth mode, `chatgpt_user_id`, and `chatgpt_account_id`.
3. Match by `record_key`.
4. Update the matching account and active key, or create a new account record when no match exists.
5. Rewrite the managed account snapshot when contents changed.
Expand Down Expand Up @@ -124,6 +127,8 @@ When API usage refresh is disabled, local usage refresh reads Codex rollout file
- Alias labels take precedence for child rows.
- Duplicate workspace-style plan labels may use stable numbered labels.
- `PLAN` comes from the auth claim when available, then falls back to usage snapshot plan data.
- `PLAN EXPIRE` comes from the stored `id_token` subscription claim. Missing or already-expired
timestamps render as `-`; the column is omitted on narrow terminals.
- Usage cells show remaining percentage plus reset time when reset data is known.
- `LAST ACTIVITY` is rendered as relative time from `last_usage_at`.

Expand Down
14 changes: 10 additions & 4 deletions docs/json-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ shortcut and is rejected when combined with `--json`.
"plan": "business",
"auth_mode": "chatgpt",
"access_token_expires_at": null,
"plan_expires_at": null,
"active": true,
"created_at": 1730000000,
"last_used_at": 1730001000,
Expand Down Expand Up @@ -83,17 +84,22 @@ their `id`, `status`, `expires_at`, and `title`. The backend may cap the
returned list, so treat it as non-exhaustive. All expiry timestamps in the
terminal views render in local time.

The API currently does not expose a verifiable subscription-plan expiration
timestamp, so the JSON contract intentionally does not label token expiry as
plan expiry.
`plan_expires_at` is the subscription-period end parsed from the stored
`id_token` claim `https://api.openai.com/auth.chatgpt_subscription_active_until`
and normalized to Unix seconds. It is `null` when the claim is absent, for
example for API-key or free-plan accounts. The value is a snapshot: it changes
only when the stored auth token is refreshed. The `PLAN EXPIRE` terminal column
hides timestamps that are not in the future and renders `-`; JSON keeps the raw
timestamp so callers can detect stale data.

`account_key` is stable and should be used for switch/remove calls. `number` is
an ephemeral display selector valid only for the ordering returned by the
current invocation. Empty aliases and account names are `null`.

`access_token_expires_at` is the Unix timestamp when the stored access token expires.
It describes credential lifetime only; it is not a subscription or account
entitlement expiration timestamp.
entitlement expiration timestamp. Use `plan_expires_at` for the subscription
period end.

`plan` is already normalized by the CLI. Important mappings are:

Expand Down
Loading
Loading