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)"); +});