diff --git a/src/auth/auth-commands.ts b/src/auth/auth-commands.ts index dcd248b3..7556094e 100644 --- a/src/auth/auth-commands.ts +++ b/src/auth/auth-commands.ts @@ -242,8 +242,8 @@ class AuthCommands { /** * Reset default profile - delegates to default-command.ts */ - async handleResetDefault(): Promise { - return handleResetDefault(this.getContext()); + async handleResetDefault(args: string[] = []): Promise { + return handleResetDefault(this.getContext(), args); } /** @@ -297,7 +297,7 @@ class AuthCommands { break; case 'reset-default': - await this.handleResetDefault(); + await this.handleResetDefault(commandArgs); break; case 'current': diff --git a/src/auth/commands/backup-command.ts b/src/auth/commands/backup-command.ts index e66068be..f6fe167d 100644 --- a/src/auth/commands/backup-command.ts +++ b/src/auth/commands/backup-command.ts @@ -11,7 +11,7 @@ import { } from '../resume-lane-diagnostics'; import { isAccountContextMetadata, resolveAccountContextPolicy } from '../account-context'; import { isProfileLocalSharedResourceMode } from '../shared-resource-policy'; -import { CommandContext, parseArgs } from './types'; +import { CommandContext, parseArgs, rejectUnsupportedAuthOptions } from './types'; interface BackupManifest { target: string; @@ -37,7 +37,11 @@ function copyDirectoryIfPresent(sourcePath: string, targetPath: string): boolean export async function handleBackup(ctx: CommandContext, args: string[]): Promise { await initUI(); - const { profileName, json } = parseArgs(args); + const parsed = parseArgs(args); + const { profileName, json } = parsed; + rejectUnsupportedAuthOptions(parsed, { + usage: 'ccs auth backup [--json]', + }); if (!profileName) { console.log(fail('Profile name is required')); diff --git a/src/auth/commands/create-command.ts b/src/auth/commands/create-command.ts index d407ace4..b0cd4aad 100644 --- a/src/auth/commands/create-command.ts +++ b/src/auth/commands/create-command.ts @@ -27,7 +27,7 @@ import { } from '../shared-resource-policy'; import { exitWithError } from '../../errors'; import { ExitCode } from '../../errors/exit-codes'; -import { CommandContext, parseArgs } from './types'; +import { CommandContext, parseArgs, rejectUnsupportedAuthOptions } from './types'; import { stripAmbientProviderCredentials } from './create-command-env'; import { isUnifiedMode } from '../../config/config-loader-facade'; @@ -40,29 +40,13 @@ function sanitizeProfileNameForInstance(name: string): string { */ export async function handleCreate(ctx: CommandContext, args: string[]): Promise { await initUI(); - const { - profileName, - force, - shareContext, - contextGroup, - deeperContinuity, - bare, - mode, - unknownFlags, - } = parseArgs(args); + const parsed = parseArgs(args); + const { profileName, force, shareContext, contextGroup, deeperContinuity, bare } = parsed; - const unsupportedOptions = [...(unknownFlags ?? []), ...(mode !== undefined ? ['--mode'] : [])]; - if (unsupportedOptions.length > 0) { - const unknownList = unsupportedOptions.map((flag) => `"${flag}"`).join(', '); - console.log(fail(`Unknown option(s): ${unknownList}`)); - console.log(''); - console.log( - `Usage: ${color('ccs auth create [--force] [--bare] [--share-context] [--context-group ] [--deeper-continuity]', 'command')}` - ); - console.log(`Help: ${color('ccs auth --help', 'command')}`); - console.log(''); - exitWithError(`Unknown option(s): ${unknownList}`, ExitCode.PROFILE_ERROR); - } + rejectUnsupportedAuthOptions(parsed, { + usage: + 'ccs auth create [--force] [--bare] [--share-context] [--context-group ] [--deeper-continuity]', + }); if (!profileName) { console.log(fail('Profile name is required')); diff --git a/src/auth/commands/default-command.ts b/src/auth/commands/default-command.ts index 2800f931..8836aee1 100644 --- a/src/auth/commands/default-command.ts +++ b/src/auth/commands/default-command.ts @@ -8,7 +8,7 @@ import { initUI, color, dim, ok, fail } from '../../utils/ui'; import { exitWithError } from '../../errors'; import { ExitCode } from '../../errors/exit-codes'; -import { CommandContext, parseArgs } from './types'; +import { CommandContext, parseArgs, rejectUnsupportedAuthOptions } from './types'; import { isUnifiedMode } from '../../config/config-loader-facade'; /** @@ -16,7 +16,11 @@ import { isUnifiedMode } from '../../config/config-loader-facade'; */ export async function handleDefault(ctx: CommandContext, args: string[]): Promise { await initUI(); - const { profileName } = parseArgs(args); + const parsed = parseArgs(args); + const { profileName } = parsed; + rejectUnsupportedAuthOptions(parsed, { + usage: 'ccs auth default ', + }); if (!profileName) { console.log(fail('Profile name is required')); @@ -48,8 +52,12 @@ export async function handleDefault(ctx: CommandContext, args: string[]): Promis /** * Handle the reset-default command (clear the custom default) */ -export async function handleResetDefault(ctx: CommandContext): Promise { +export async function handleResetDefault(ctx: CommandContext, args: string[] = []): Promise { await initUI(); + const parsed = parseArgs(args); + rejectUnsupportedAuthOptions(parsed, { + usage: 'ccs auth reset-default', + }); try { // Use unified or legacy based on config mode diff --git a/src/auth/commands/list-command.ts b/src/auth/commands/list-command.ts index 9172e90b..88646d0d 100644 --- a/src/auth/commands/list-command.ts +++ b/src/auth/commands/list-command.ts @@ -10,14 +10,24 @@ import { resolveAccountContextPolicy, formatAccountContextPolicy } from '../acco import { resolveSharedResourcePolicy } from '../shared-resource-policy'; import { exitWithError } from '../../errors'; import { ExitCode } from '../../errors/exit-codes'; -import { CommandContext, ListOutput, parseArgs, formatRelativeTime } from './types'; +import { + CommandContext, + ListOutput, + parseArgs, + formatRelativeTime, + rejectUnsupportedAuthOptions, +} from './types'; /** * Handle the list command */ export async function handleList(ctx: CommandContext, args: string[]): Promise { await initUI(); - const { verbose, json } = parseArgs(args); + const parsed = parseArgs(args); + const { verbose, json } = parsed; + rejectUnsupportedAuthOptions(parsed, { + usage: 'ccs auth list [--verbose] [--json]', + }); try { // Get profiles from both legacy (profiles.json) and unified config (config.yaml) diff --git a/src/auth/commands/remove-command.ts b/src/auth/commands/remove-command.ts index 9302d1b0..fed45098 100644 --- a/src/auth/commands/remove-command.ts +++ b/src/auth/commands/remove-command.ts @@ -11,7 +11,7 @@ import { InteractivePrompt } from '../../utils/prompt'; import { exitWithError } from '../../errors'; import { ExitCode } from '../../errors/exit-codes'; -import { CommandContext, parseArgs } from './types'; +import { CommandContext, parseArgs, rejectUnsupportedAuthOptions } from './types'; import { isUnifiedMode } from '../../config/config-loader-facade'; /** @@ -19,7 +19,11 @@ import { isUnifiedMode } from '../../config/config-loader-facade'; */ export async function handleRemove(ctx: CommandContext, args: string[]): Promise { await initUI(); - const { profileName, yes } = parseArgs(args); + const parsed = parseArgs(args); + const { profileName, yes } = parsed; + rejectUnsupportedAuthOptions(parsed, { + usage: 'ccs auth remove [--yes]', + }); if (!profileName) { console.log(fail('Profile name is required')); diff --git a/src/auth/commands/resources-command.ts b/src/auth/commands/resources-command.ts index a20998b8..f3269457 100644 --- a/src/auth/commands/resources-command.ts +++ b/src/auth/commands/resources-command.ts @@ -8,7 +8,7 @@ import { } from '../shared-resource-policy'; import { exitWithError } from '../../errors'; import { ExitCode } from '../../errors/exit-codes'; -import { CommandContext, parseArgs } from './types'; +import { CommandContext, parseArgs, rejectUnsupportedAuthOptions } from './types'; function formatMode(mode: SharedResourceMode): string { return mode === 'profile-local' ? 'profile-local' : 'shared'; @@ -22,7 +22,12 @@ function modeDescription(mode: SharedResourceMode): string { export async function handleResources(ctx: CommandContext, args: string[]): Promise { await initUI(); - const { profileName, mode, json } = parseArgs(args); + const parsed = parseArgs(args, { allowMode: true }); + const { profileName, mode, json } = parsed; + rejectUnsupportedAuthOptions(parsed, { + usage: 'ccs auth resources [--mode shared|profile-local] [--json]', + allowMode: true, + }); if (!profileName) { console.log(fail('Profile name is required')); diff --git a/src/auth/commands/show-command.ts b/src/auth/commands/show-command.ts index 212ef7a1..37602688 100644 --- a/src/auth/commands/show-command.ts +++ b/src/auth/commands/show-command.ts @@ -13,7 +13,7 @@ import { resolveConfiguredPlainCcsResumeLane } from '../resume-lane-diagnostics' import { resolveSharedResourcePolicy } from '../shared-resource-policy'; import { exitWithError } from '../../errors'; import { ExitCode } from '../../errors/exit-codes'; -import { CommandContext, ProfileOutput, parseArgs } from './types'; +import { CommandContext, ProfileOutput, parseArgs, rejectUnsupportedAuthOptions } from './types'; function formatHistorySummary(history: ReturnType): string { const scope = history.projects_shared ? 'shared projects' : 'profile-local projects'; @@ -26,7 +26,11 @@ function formatHistorySummary(history: ReturnType { await initUI(); - const { profileName, json } = parseArgs(args); + const parsed = parseArgs(args); + const { profileName, json } = parsed; + rejectUnsupportedAuthOptions(parsed, { + usage: 'ccs auth show [--json]', + }); if (!profileName) { console.log(fail('Profile name is required')); diff --git a/src/auth/commands/types.ts b/src/auth/commands/types.ts index 906f7135..f091b410 100644 --- a/src/auth/commands/types.ts +++ b/src/auth/commands/types.ts @@ -6,6 +6,9 @@ import ProfileRegistry from '../profile-registry'; import { InstanceManager } from '../../management/instance-manager'; +import { exitWithError } from '../../errors'; +import { ExitCode } from '../../errors/exit-codes'; +import { color, fail } from '../../utils/ui'; // Re-export for backward compatibility export { formatRelativeTime } from '../../utils/time'; @@ -83,10 +86,36 @@ export interface CommandContext { version: string; } +export function rejectUnsupportedAuthOptions( + parsed: Pick, + options: { usage: string; allowMode?: boolean } +): void { + const unsupportedOptions = [ + ...(parsed.unknownFlags ?? []), + ...(!options.allowMode && parsed.mode !== undefined ? ['--mode'] : []), + ]; + + if (unsupportedOptions.length === 0) { + return; + } + + const unknownList = unsupportedOptions.map((flag) => `"${flag}"`).join(', '); + console.log(fail(`Unknown option(s): ${unknownList}`)); + console.log(''); + console.log(`Usage: ${color(options.usage, 'command')}`); + console.log(`Help: ${color('ccs auth --help', 'command')}`); + console.log(''); + exitWithError(`Unknown option(s): ${unknownList}`, ExitCode.PROFILE_ERROR); +} + +interface ParseArgsOptions { + allowMode?: boolean; +} + /** * Parse command arguments from raw args array */ -export function parseArgs(args: string[]): AuthCommandArgs { +export function parseArgs(args: string[], options: ParseArgsOptions = {}): AuthCommandArgs { let profileName: string | undefined; let contextGroup: string | undefined; let mode: string | undefined; @@ -101,7 +130,10 @@ export function parseArgs(args: string[]): AuthCommandArgs { '--deeper-continuity', '--bare', ]); - const knownValueFlags = new Set(['--context-group', '--mode']); + const knownValueFlags = new Set(['--context-group']); + if (options.allowMode) { + knownValueFlags.add('--mode'); + } for (let i = 0; i < args.length; i++) { const arg = args[i]; @@ -118,7 +150,7 @@ export function parseArgs(args: string[]): AuthCommandArgs { continue; } - if (arg === '--mode') { + if (options.allowMode && arg === '--mode') { const next = args[i + 1]; if (!next || next.startsWith('-')) { mode = ''; @@ -135,7 +167,7 @@ export function parseArgs(args: string[]): AuthCommandArgs { continue; } - if (arg.startsWith('--mode=')) { + if (options.allowMode && arg.startsWith('--mode=')) { mode = arg.slice('--mode='.length); continue; } diff --git a/tests/unit/auth-command-args.test.ts b/tests/unit/auth-command-args.test.ts index 7673dee4..9badc1f2 100644 --- a/tests/unit/auth-command-args.test.ts +++ b/tests/unit/auth-command-args.test.ts @@ -64,19 +64,27 @@ describe('auth command args parsing', () => { }); it('parses shared resource mode value for resources command', () => { - const parsed = parseArgs(['work', '--mode', 'profile-local']); + const parsed = parseArgs(['work', '--mode', 'profile-local'], { allowMode: true }); expect(parsed.profileName).toBe('work'); expect(parsed.mode).toBe('profile-local'); }); it('parses inline shared resource mode value', () => { - const parsed = parseArgs(['work', '--mode=shared']); + const parsed = parseArgs(['work', '--mode=shared'], { allowMode: true }); expect(parsed.profileName).toBe('work'); expect(parsed.mode).toBe('shared'); }); + it('treats shared resource mode as unknown unless the command opts in', () => { + const parsed = parseArgs(['work', '--mode', 'shared']); + + expect(parsed.profileName).toBe('work'); + expect(parsed.mode).toBeUndefined(); + expect(parsed.unknownFlags).toEqual(['--mode']); + }); + it('tracks unknown flags and keeps positional profile intact', () => { const parsed = parseArgs(['--foo', 'bar', 'work']); diff --git a/tests/unit/auth-resources-command.test.ts b/tests/unit/auth-resources-command.test.ts index d76bc36f..88379cfe 100644 --- a/tests/unit/auth-resources-command.test.ts +++ b/tests/unit/auth-resources-command.test.ts @@ -6,6 +6,38 @@ import ProfileRegistry from '../../src/auth/profile-registry'; import InstanceManager from '../../src/management/instance-manager'; import { handleResources } from '../../src/auth/commands/resources-command'; import { handleCreate } from '../../src/auth/commands/create-command'; +import { handleList } from '../../src/auth/commands/list-command'; +import { handleShow } from '../../src/auth/commands/show-command'; +import { handleBackup } from '../../src/auth/commands/backup-command'; +import { handleRemove } from '../../src/auth/commands/remove-command'; +import { handleDefault, handleResetDefault } from '../../src/auth/commands/default-command'; + +async function expectProfileExit(action: () => Promise): Promise { + const originalExit = process.exit; + const originalLog = console.log; + const originalError = console.error; + const lines: string[] = []; + + process.exit = ((code?: number) => { + throw new Error(`process.exit(${code ?? 0})`); + }) as typeof process.exit; + console.log = (...args: unknown[]) => { + lines.push(args.map(String).join(' ')); + }; + console.error = (...args: unknown[]) => { + lines.push(args.map(String).join(' ')); + }; + + try { + await expect(action()).rejects.toThrow('process.exit(7)'); + } finally { + process.exit = originalExit; + console.log = originalLog; + console.error = originalError; + } + + return lines.join('\n'); +} describe('auth resources command', () => { let tempRoot = ''; @@ -143,38 +175,37 @@ describe('auth resources command', () => { it('rejects --mode on auth create instead of silently ignoring it', async () => { const registry = new ProfileRegistry(); const instanceMgr = new InstanceManager(); - const originalExit = process.exit; - const originalLog = console.log; - const originalError = console.error; - const lines: string[] = []; - process.exit = ((code?: number) => { - throw new Error(`process.exit(${code ?? 0})`); - }) as typeof process.exit; - console.log = (...args: unknown[]) => { - lines.push(args.map(String).join(' ')); - }; - console.error = (...args: unknown[]) => { - lines.push(args.map(String).join(' ')); - }; + const output = await expectProfileExit(() => + handleCreate( + { + registry, + instanceMgr, + version: 'test', + }, + ['work', '--mode', 'profile-local'] + ) + ); - try { - await expect( - handleCreate( - { - registry, - instanceMgr, - version: 'test', - }, - ['work', '--mode', 'profile-local'] - ) - ).rejects.toThrow('process.exit(7)'); - } finally { - process.exit = originalExit; - console.log = originalLog; - console.error = originalError; + expect(output).toContain('Unknown option(s): "--mode"'); + }); + + it('rejects --mode on non-resources auth commands instead of silently ignoring it', async () => { + const registry = new ProfileRegistry(); + const instanceMgr = new InstanceManager(); + const ctx = { registry, instanceMgr, version: 'test' }; + const cases: Array<[string, () => Promise]> = [ + ['list', () => handleList(ctx, ['--mode', 'shared'])], + ['show', () => handleShow(ctx, ['work', '--mode=shared'])], + ['backup', () => handleBackup(ctx, ['work', '--mode', 'profile-local'])], + ['remove', () => handleRemove(ctx, ['work', '--mode', 'shared'])], + ['default', () => handleDefault(ctx, ['work', '--mode', 'shared'])], + ['reset-default', () => handleResetDefault(ctx, ['--mode', 'shared'])], + ]; + + for (const [commandName, action] of cases) { + const output = await expectProfileExit(action); + expect(`${commandName}\n${output}`).toContain('Unknown option(s): "--mode"'); } - - expect(lines.join('\n')).toContain('Unknown option(s): "--mode"'); }); });