From 8b1161bc73db069cb8f686e51be69c0c85490a78 Mon Sep 17 00:00:00 2001 From: NagyVikt Date: Mon, 4 May 2026 08:12:02 +0200 Subject: [PATCH] Show account plan type in codex-auth list Operators need the bare account inventory to distinguish Codex usage-based accounts from ChatGPT seat accounts without opening details for every snapshot. The list command now derives a friendly type label from existing planType metadata, while --details keeps the raw plan value for troubleshooting. Constraint: Reuse existing planType metadata; do not add new snapshot registry fields. Rejected: Add a new registry accountType field | would duplicate a value already derivable from planType and risk drift. Confidence: high Scope-risk: narrow Directive: Keep raw plan= in --details when changing friendly labels so unknown upstream plan strings stay debuggable. Tested: npm test (71/71); node --test dist/tests/account-plan-display.test.js; npm run build; git diff --check; openspec validate --specs Not-tested: Live codex-auth list against the user's real account directory was not run to avoid mutating local usage registry state. Co-authored-by: OmX --- README.md | 8 +-- .../colony-spec.md | 13 +++++ src/commands/list.ts | 7 +-- src/lib/accounts/plan-display.ts | 49 +++++++++++++++++++ src/tests/account-plan-display.test.ts | 24 +++++++++ 5 files changed, 94 insertions(+), 7 deletions(-) create mode 100644 openspec/changes/agent-codex-add-codex-auth-list-account-plan-labels-2026-05-04-08-05/colony-spec.md create mode 100644 src/lib/accounts/plan-display.ts create mode 100644 src/tests/account-plan-display.test.ts diff --git a/README.md b/README.md index 47af51f..25356fe 100644 --- a/README.md +++ b/README.md @@ -116,10 +116,10 @@ codex-auth use codex-auth list # bare list shows the saved account/snapshot name first, -# then the remaining 5h and weekly quota percentages +# then the account type and remaining 5h/weekly quota percentages # (the active row is marked with `*`) -# admin@compastor.com 5h=1% weekly=22% -# * recodee@portasmosonmagyarovar.hu 5h=99% weekly=44% +# admin@compastor.com type=ChatGPT seat (Business) 5h=1% weekly=22% +# * recodee@portasmosonmagyarovar.hu type=Usage based (Codex) 5h=99% weekly=44% # list accounts with mapping metadata (email/account/user/usage) codex-auth list --details @@ -170,7 +170,7 @@ codex-auth remove-login-hook - `codex-auth save [--force]` – Validates ``, ensures `auth.json` exists, then snapshots it to `~/.codex/accounts/.json`. By default, it blocks overwriting a name when the existing snapshot email differs from current auth. If `name` is omitted, it first tries reusing the active snapshot name when identity matches; otherwise it infers one from auth email. - `codex-auth login [name] [--device-auth] [--force]` – Runs `codex login` (optionally with device auth), waits for refreshed auth snapshot detection, then saves it. If `name` is omitted, it always infers one from auth email with unique-suffix handling for multi-workspace identities. - `codex-auth use [name]` – Accepts a name or launches an interactive selector with the current account pre-selected, writes `~/.codex/auth.json` as a regular file from the chosen snapshot, and records the active name. -- `codex-auth list [--details]` – Lists all saved snapshots alphabetically. In the default view, each row starts with the saved account/snapshot name, followed by `5h=` and `weekly=` remaining values, and the active row is marked with `*`. `--details` adds per-snapshot mapping metadata (email, account id, user id, and usage metadata) for easier session/account troubleshooting. +- `codex-auth list [--details]` – Lists all saved snapshots alphabetically. In the default view, each row starts with the saved account/snapshot name, followed by `type=`, `5h=`, and `weekly=` values, and the active row is marked with `*`. `type=` renders Codex usage-based plans as `Usage based (Codex)` and ChatGPT seat plans with their tier, such as Plus, Business, Pro, or Max. `--details` adds per-snapshot mapping metadata (email, account id, user id, raw plan, usage metadata, and friendly type) for easier session/account troubleshooting. - `codex-auth current` – Prints the active account name, or a friendly message if none is active. - `codex-auth self-update [--check] [--reinstall] [-y]` – Checks npm for newer release metadata. `--check` prints current/latest/status only. `--reinstall` forces reinstall even when already up to date. `-y` skips confirmation prompts. - `codex-auth remove [query|--all]` – Removes snapshots interactively or by selector. If the active account is removed, the best remaining account is activated automatically. diff --git a/openspec/changes/agent-codex-add-codex-auth-list-account-plan-labels-2026-05-04-08-05/colony-spec.md b/openspec/changes/agent-codex-add-codex-auth-list-account-plan-labels-2026-05-04-08-05/colony-spec.md new file mode 100644 index 0000000..2559ec3 --- /dev/null +++ b/openspec/changes/agent-codex-add-codex-auth-list-account-plan-labels-2026-05-04-08-05/colony-spec.md @@ -0,0 +1,13 @@ +# Add codex-auth list account type labels + +## Goal + +Show the account type in `codex-auth list` rows so operators can distinguish Codex usage-based accounts from ChatGPT seat plans. + +## Acceptance + +- Bare `codex-auth list` includes a `type=` field before quota percentages. +- Usage-based Codex plans render as `Usage based (Codex)`. +- ChatGPT seat plans render with tier labels for Plus, Business, Pro, and Max. +- `codex-auth list --details` includes the same friendly `type=` label while keeping raw `plan=` metadata. +- Focused TypeScript tests cover the formatter. diff --git a/src/commands/list.ts b/src/commands/list.ts index 3e11d36..3aa7c59 100644 --- a/src/commands/list.ts +++ b/src/commands/list.ts @@ -1,6 +1,7 @@ import { Flags } from "@oclif/core"; import prompts from "prompts"; import { BaseCommand } from "../lib/base-command"; +import { formatAccountType } from "../lib/accounts/plan-display"; import { fetchLatestNpmVersionCached, formatGlobalInstallCommand, @@ -17,7 +18,7 @@ export default class ListCommand extends BaseCommand { static flags = { details: Flags.boolean({ char: "d", - description: "Show per-account mapping metadata (email/account/user/usage)", + description: "Show per-account mapping metadata (email/account/user/type/usage)", default: false, }), } as const; @@ -38,7 +39,7 @@ export default class ListCommand extends BaseCommand { for (const account of accounts) { const mark = account.active ? "*" : " "; this.log( - `${mark} ${account.name} 5h=${this.formatRemaining(account.remaining5hPercent)} weekly=${this.formatRemaining(account.remainingWeeklyPercent)}`, + `${mark} ${account.name} type=${formatAccountType(account.planType)} 5h=${this.formatRemaining(account.remaining5hPercent)} weekly=${this.formatRemaining(account.remainingWeeklyPercent)}`, ); } return; @@ -57,7 +58,7 @@ export default class ListCommand extends BaseCommand { ` email=${account.email ?? "-"} account=${account.accountId ?? "-"} user=${account.userId ?? "-"}`, ); this.log( - ` plan=${account.planType ?? "-"} usage=${account.usageSource ?? "-"} 5h=${this.formatRemaining(account.remaining5hPercent)} weekly=${this.formatRemaining(account.remainingWeeklyPercent)} lastUsageAt=${account.lastUsageAt ?? "-"}`, + ` type=${formatAccountType(account.planType)} plan=${account.planType ?? "-"} usage=${account.usageSource ?? "-"} 5h=${this.formatRemaining(account.remaining5hPercent)} weekly=${this.formatRemaining(account.remainingWeeklyPercent)} lastUsageAt=${account.lastUsageAt ?? "-"}`, ); } }); diff --git a/src/lib/accounts/plan-display.ts b/src/lib/accounts/plan-display.ts new file mode 100644 index 0000000..9281d94 --- /dev/null +++ b/src/lib/accounts/plan-display.ts @@ -0,0 +1,49 @@ +const CHATGPT_PLAN_LABELS: Record = { + plus: "Plus", + business: "Business", + team: "Business", + pro: "Pro", + max: "Max", + enterprise: "Enterprise", + free: "Free", +}; + +const USAGE_BASED_PLAN_KEYS = new Set([ + "api", + "codex_usage_based", + "codexusagebased", + "metered", + "pay_as_you_go", + "payasyougo", + "usage", + "usage_based", + "usagebased", +]); + +function normalizePlanKey(planType: string): string { + return planType.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, ""); +} + +function titleCasePlanType(planType: string): string { + return planType + .trim() + .split(/[^a-zA-Z0-9]+/) + .filter(Boolean) + .map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1).toLowerCase()}`) + .join(" "); +} + +export function formatAccountType(planType: string | undefined): string { + if (!planType?.trim()) { + return "-"; + } + + const normalized = normalizePlanKey(planType); + const withoutChatGptPrefix = normalized.replace(/^chatgpt_/, ""); + if (USAGE_BASED_PLAN_KEYS.has(normalized) || USAGE_BASED_PLAN_KEYS.has(withoutChatGptPrefix)) { + return "Usage based (Codex)"; + } + + const chatGptPlanLabel = CHATGPT_PLAN_LABELS[withoutChatGptPrefix] ?? titleCasePlanType(planType); + return `ChatGPT seat (${chatGptPlanLabel})`; +} diff --git a/src/tests/account-plan-display.test.ts b/src/tests/account-plan-display.test.ts new file mode 100644 index 0000000..57d5a34 --- /dev/null +++ b/src/tests/account-plan-display.test.ts @@ -0,0 +1,24 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { formatAccountType } from "../lib/accounts/plan-display"; + +test("formatAccountType renders known ChatGPT seat tiers", () => { + assert.equal(formatAccountType("plus"), "ChatGPT seat (Plus)"); + assert.equal(formatAccountType("team"), "ChatGPT seat (Business)"); + assert.equal(formatAccountType("business"), "ChatGPT seat (Business)"); + assert.equal(formatAccountType("pro"), "ChatGPT seat (Pro)"); + assert.equal(formatAccountType("max"), "ChatGPT seat (Max)"); +}); + +test("formatAccountType renders Codex usage-based plans", () => { + assert.equal(formatAccountType("usage_based"), "Usage based (Codex)"); + assert.equal(formatAccountType("codex-usage-based"), "Usage based (Codex)"); + assert.equal(formatAccountType("pay_as_you_go"), "Usage based (Codex)"); +}); + +test("formatAccountType keeps unknown plan tiers visible", () => { + assert.equal(formatAccountType(undefined), "-"); + assert.equal(formatAccountType(""), "-"); + assert.equal(formatAccountType("enterprise_plus"), "ChatGPT seat (Enterprise Plus)"); +});