diff --git a/src/codex-auth/commands/show-detail-view.ts b/src/codex-auth/commands/show-detail-view.ts index 077af14a..9ae5114f 100644 --- a/src/codex-auth/commands/show-detail-view.ts +++ b/src/codex-auth/commands/show-detail-view.ts @@ -69,6 +69,8 @@ export function showProfileDetail( if (isActive) states.push('active'); const stateStr = states.join(','); const accountId = meta.account_id ?? identity.account_id ?? null; + const email = meta.email ?? identity.email ?? null; + const plan = meta.plan_type ?? identity.plan_type ?? null; if (json) { const out: CodexProfileOutput = { @@ -77,8 +79,8 @@ export function showProfileDetail( is_active: isActive, created: meta.created, last_used: meta.last_used ?? null, - email: identity.email ?? null, - plan: meta.plan_type ?? null, + email, + plan, account_id: accountId, profile_dir: profileDir, auth_json_exists: authExists, @@ -98,8 +100,8 @@ export function showProfileDetail( ['Profile dir', profileDir], ['config.toml', configTarget ? `-> ${configTarget} (symlink)` : '(not linked)'], ['auth.json', authState], - ['Email', identity.email ?? (authExists ? '' : '')], - ['Plan', meta.plan_type ?? (authExists ? '' : '')], + ['Email', email ?? (authExists ? '' : '')], + ['Plan', plan ?? (authExists ? '' : '')], ['Account ID', accountId ?? '-'], ['Created', new Date(meta.created).toLocaleString()], ['Last used', meta.last_used ? new Date(meta.last_used).toLocaleString() : 'never'], diff --git a/src/codex-auth/commands/types.ts b/src/codex-auth/commands/types.ts index 04eb5829..523ba2fe 100644 --- a/src/codex-auth/commands/types.ts +++ b/src/codex-auth/commands/types.ts @@ -28,6 +28,7 @@ export interface CodexAuthArgs { shell?: string; unknownFlags?: string[]; seenOptions?: string[]; + extraPositionals?: string[]; } // ── Profile output shape (JSON mode) ───────────────────────────────────────── @@ -86,6 +87,9 @@ export function parseArgs(args: string[]): CodexAuthArgs { if (positional.length > 0) { result.profileName = positional[0]; } + if (positional.length > 1) { + result.extraPositionals = positional.slice(1); + } return result; } @@ -108,10 +112,17 @@ export function rejectUnsupportedOptions( if (seen.has('--json') && !allowed.json) unsupported.add('--json'); if (seen.has('--force') && !allowed.force) unsupported.add('--force'); if (seen.has('--shell') && !allowed.shell) unsupported.add('--shell'); + const extraPositionals = parsed.extraPositionals ?? []; - if (unsupported.size > 0) { + if (unsupported.size > 0 || extraPositionals.length > 0) { const flags = [...unsupported].join(', '); process.stderr.write(`Usage: ${color(usage, 'command')}\n`); - exitWithError(`Unknown options: ${flags}`, ExitCode.GENERAL_ERROR); + const details = [ + flags ? `Unknown options: ${flags}` : null, + extraPositionals.length > 0 + ? `Unexpected arguments: ${extraPositionals.map((arg) => `"${arg}"`).join(', ')}` + : null, + ].filter(Boolean); + exitWithError(details.join('; '), ExitCode.GENERAL_ERROR); } } diff --git a/tests/unit/codex-auth/commands/remove-command.test.ts b/tests/unit/codex-auth/commands/remove-command.test.ts index e52f753a..f612686d 100644 --- a/tests/unit/codex-auth/commands/remove-command.test.ts +++ b/tests/unit/codex-auth/commands/remove-command.test.ts @@ -146,6 +146,42 @@ describe('handleRemoveCodex — only profile', () => { // ── confirmation prompt ─────────────────────────────────────────────────────── describe('handleRemoveCodex — confirmation', () => { + it('rejects extra positional arguments before deleting anything', async () => { + const { handleRemoveCodex } = await import( + '../../../../src/codex-auth/commands/remove-command' + ); + const ctx = await makeCtx('work'); + + let exitCode = -1; + const err: string[] = []; + const origExit = process.exit; + const origError = console.error; + const origWrite = process.stderr.write.bind(process.stderr); + process.exit = (code?: number) => { + exitCode = code ?? 0; + throw new Error('exit'); + }; + console.error = (...a: unknown[]) => err.push(a.join(' ')); + process.stderr.write = (chunk: string | Uint8Array) => { + err.push(String(chunk)); + return true; + }; + + try { + await handleRemoveCodex(ctx, ['work', 'accidental', '--yes']); + } catch { + /* process.exit */ + } finally { + process.exit = origExit; + console.error = origError; + process.stderr.write = origWrite; + } + + expect(exitCode).toBeGreaterThan(0); + expect(ctx.registry.hasProfile('work')).toBe(true); + expect(err.join('')).toContain('Unexpected arguments: "accidental"'); + }); + it('cancels when user declines confirmation', async () => { await mockConfirmNo(); const { handleRemoveCodex } = await import( diff --git a/tests/unit/codex-auth/commands/show-command.test.ts b/tests/unit/codex-auth/commands/show-command.test.ts index bd557629..ac56b946 100644 --- a/tests/unit/codex-auth/commands/show-command.test.ts +++ b/tests/unit/codex-auth/commands/show-command.test.ts @@ -168,17 +168,58 @@ describe('handleShowCodex — detail view', () => { expect(out).toContain(''); }); - it('detail JSON includes account_id from registry metadata when auth.json is missing', async () => { + it('detail JSON includes cached registry identity when auth.json is missing', async () => { const { handleShowCodex } = await import('../../../../src/codex-auth/commands/show-command'); const ctx = await makeCtx('registrydetail'); ctx.registry.updateProfile('registrydetail', { account_id: 'acct-from-registry-detail', + email: 'detail@example.com', + plan_type: 'team', }); const out = await captureStdout(() => handleShowCodex(ctx, ['registrydetail', '--json'])); - const parsed = JSON.parse(out) as { account_id: string | null }; + const parsed = JSON.parse(out) as { + account_id: string | null; + email: string | null; + plan: string | null; + }; expect(parsed.account_id).toBe('acct-from-registry-detail'); + expect(parsed.email).toBe('detail@example.com'); + expect(parsed.plan).toBe('team'); + }); + + it('rejects extra positional arguments instead of ignoring them', async () => { + const { handleShowCodex } = await import('../../../../src/codex-auth/commands/show-command'); + const ctx = await makeCtx('myprofile'); + + let exitCode = -1; + const err: string[] = []; + const origExit = process.exit; + const origError = console.error; + const origWrite = process.stderr.write.bind(process.stderr); + process.exit = (code?: number) => { + exitCode = code ?? 0; + throw new Error('exit'); + }; + console.error = (...a: unknown[]) => err.push(a.join(' ')); + process.stderr.write = (chunk: string | Uint8Array) => { + err.push(String(chunk)); + return true; + }; + + try { + await handleShowCodex(ctx, ['myprofile', 'extra']); + } catch { + /* process.exit */ + } finally { + process.exit = origExit; + console.error = origError; + process.stderr.write = origWrite; + } + + expect(exitCode).toBeGreaterThan(0); + expect(err.join('')).toContain('Unexpected arguments: "extra"'); }); it('does not crash with malformed auth.json', async () => {