mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 12:15:57 +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');
|
if (isActive) states.push('active');
|
||||||
const stateStr = states.join(',');
|
const stateStr = states.join(',');
|
||||||
const accountId = meta.account_id ?? identity.account_id ?? null;
|
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) {
|
if (json) {
|
||||||
const out: CodexProfileOutput = {
|
const out: CodexProfileOutput = {
|
||||||
@@ -77,8 +79,8 @@ export function showProfileDetail(
|
|||||||
is_active: isActive,
|
is_active: isActive,
|
||||||
created: meta.created,
|
created: meta.created,
|
||||||
last_used: meta.last_used ?? null,
|
last_used: meta.last_used ?? null,
|
||||||
email: identity.email ?? null,
|
email,
|
||||||
plan: meta.plan_type ?? null,
|
plan,
|
||||||
account_id: accountId,
|
account_id: accountId,
|
||||||
profile_dir: profileDir,
|
profile_dir: profileDir,
|
||||||
auth_json_exists: authExists,
|
auth_json_exists: authExists,
|
||||||
@@ -98,8 +100,8 @@ export function showProfileDetail(
|
|||||||
['Profile dir', profileDir],
|
['Profile dir', profileDir],
|
||||||
['config.toml', configTarget ? `-> ${configTarget} (symlink)` : '(not linked)'],
|
['config.toml', configTarget ? `-> ${configTarget} (symlink)` : '(not linked)'],
|
||||||
['auth.json', authState],
|
['auth.json', authState],
|
||||||
['Email', identity.email ?? (authExists ? '<invalid>' : '<unknown>')],
|
['Email', email ?? (authExists ? '<invalid>' : '<unknown>')],
|
||||||
['Plan', meta.plan_type ?? (authExists ? '<invalid>' : '<unknown>')],
|
['Plan', plan ?? (authExists ? '<invalid>' : '<unknown>')],
|
||||||
['Account ID', accountId ?? '-'],
|
['Account ID', accountId ?? '-'],
|
||||||
['Created', new Date(meta.created).toLocaleString()],
|
['Created', new Date(meta.created).toLocaleString()],
|
||||||
['Last used', meta.last_used ? new Date(meta.last_used).toLocaleString() : 'never'],
|
['Last used', meta.last_used ? new Date(meta.last_used).toLocaleString() : 'never'],
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export interface CodexAuthArgs {
|
|||||||
shell?: string;
|
shell?: string;
|
||||||
unknownFlags?: string[];
|
unknownFlags?: string[];
|
||||||
seenOptions?: string[];
|
seenOptions?: string[];
|
||||||
|
extraPositionals?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Profile output shape (JSON mode) ─────────────────────────────────────────
|
// ── Profile output shape (JSON mode) ─────────────────────────────────────────
|
||||||
@@ -86,6 +87,9 @@ export function parseArgs(args: string[]): CodexAuthArgs {
|
|||||||
if (positional.length > 0) {
|
if (positional.length > 0) {
|
||||||
result.profileName = positional[0];
|
result.profileName = positional[0];
|
||||||
}
|
}
|
||||||
|
if (positional.length > 1) {
|
||||||
|
result.extraPositionals = positional.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -108,10 +112,17 @@ export function rejectUnsupportedOptions(
|
|||||||
if (seen.has('--json') && !allowed.json) unsupported.add('--json');
|
if (seen.has('--json') && !allowed.json) unsupported.add('--json');
|
||||||
if (seen.has('--force') && !allowed.force) unsupported.add('--force');
|
if (seen.has('--force') && !allowed.force) unsupported.add('--force');
|
||||||
if (seen.has('--shell') && !allowed.shell) unsupported.add('--shell');
|
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(', ');
|
const flags = [...unsupported].join(', ');
|
||||||
process.stderr.write(`Usage: ${color(usage, 'command')}\n`);
|
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 ───────────────────────────────────────────────────────
|
// ── confirmation prompt ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
describe('handleRemoveCodex — confirmation', () => {
|
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 () => {
|
it('cancels when user declines confirmation', async () => {
|
||||||
await mockConfirmNo();
|
await mockConfirmNo();
|
||||||
const { handleRemoveCodex } = await import(
|
const { handleRemoveCodex } = await import(
|
||||||
|
|||||||
@@ -168,17 +168,58 @@ describe('handleShowCodex — detail view', () => {
|
|||||||
expect(out).toContain('<unknown>');
|
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 { handleShowCodex } = await import('../../../../src/codex-auth/commands/show-command');
|
||||||
const ctx = await makeCtx('registrydetail');
|
const ctx = await makeCtx('registrydetail');
|
||||||
ctx.registry.updateProfile('registrydetail', {
|
ctx.registry.updateProfile('registrydetail', {
|
||||||
account_id: 'acct-from-registry-detail',
|
account_id: 'acct-from-registry-detail',
|
||||||
|
email: 'detail@example.com',
|
||||||
|
plan_type: 'team',
|
||||||
});
|
});
|
||||||
|
|
||||||
const out = await captureStdout(() => handleShowCodex(ctx, ['registrydetail', '--json']));
|
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.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 () => {
|
it('does not crash with malformed auth.json', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user