mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 06:16:37 +00:00
fix(codex-auth): reject stray profile args
This commit is contained in:
@@ -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 ? '<invalid>' : '<unknown>')],
|
||||
['Plan', meta.plan_type ?? (authExists ? '<invalid>' : '<unknown>')],
|
||||
['Email', email ?? (authExists ? '<invalid>' : '<unknown>')],
|
||||
['Plan', plan ?? (authExists ? '<invalid>' : '<unknown>')],
|
||||
['Account ID', accountId ?? '-'],
|
||||
['Created', new Date(meta.created).toLocaleString()],
|
||||
['Last used', meta.last_used ? new Date(meta.last_used).toLocaleString() : 'never'],
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -168,17 +168,58 @@ describe('handleShowCodex — detail view', () => {
|
||||
expect(out).toContain('<unknown>');
|
||||
});
|
||||
|
||||
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 () => {
|
||||
|
||||
Reference in New Issue
Block a user