fix(auth): reject resource mode outside resources command

This commit is contained in:
Tam Nhu Tran
2026-05-08 23:29:03 -04:00
parent 64e1d1f815
commit bfe32257b6
11 changed files with 165 additions and 75 deletions
+3 -3
View File
@@ -242,8 +242,8 @@ class AuthCommands {
/** /**
* Reset default profile - delegates to default-command.ts * Reset default profile - delegates to default-command.ts
*/ */
async handleResetDefault(): Promise<void> { async handleResetDefault(args: string[] = []): Promise<void> {
return handleResetDefault(this.getContext()); return handleResetDefault(this.getContext(), args);
} }
/** /**
@@ -297,7 +297,7 @@ class AuthCommands {
break; break;
case 'reset-default': case 'reset-default':
await this.handleResetDefault(); await this.handleResetDefault(commandArgs);
break; break;
case 'current': case 'current':
+6 -2
View File
@@ -11,7 +11,7 @@ import {
} from '../resume-lane-diagnostics'; } from '../resume-lane-diagnostics';
import { isAccountContextMetadata, resolveAccountContextPolicy } from '../account-context'; import { isAccountContextMetadata, resolveAccountContextPolicy } from '../account-context';
import { isProfileLocalSharedResourceMode } from '../shared-resource-policy'; import { isProfileLocalSharedResourceMode } from '../shared-resource-policy';
import { CommandContext, parseArgs } from './types'; import { CommandContext, parseArgs, rejectUnsupportedAuthOptions } from './types';
interface BackupManifest { interface BackupManifest {
target: string; target: string;
@@ -37,7 +37,11 @@ function copyDirectoryIfPresent(sourcePath: string, targetPath: string): boolean
export async function handleBackup(ctx: CommandContext, args: string[]): Promise<void> { export async function handleBackup(ctx: CommandContext, args: string[]): Promise<void> {
await initUI(); await initUI();
const { profileName, json } = parseArgs(args); const parsed = parseArgs(args);
const { profileName, json } = parsed;
rejectUnsupportedAuthOptions(parsed, {
usage: 'ccs auth backup <profile|default> [--json]',
});
if (!profileName) { if (!profileName) {
console.log(fail('Profile name is required')); console.log(fail('Profile name is required'));
+7 -23
View File
@@ -27,7 +27,7 @@ import {
} from '../shared-resource-policy'; } from '../shared-resource-policy';
import { exitWithError } from '../../errors'; import { exitWithError } from '../../errors';
import { ExitCode } from '../../errors/exit-codes'; import { ExitCode } from '../../errors/exit-codes';
import { CommandContext, parseArgs } from './types'; import { CommandContext, parseArgs, rejectUnsupportedAuthOptions } from './types';
import { stripAmbientProviderCredentials } from './create-command-env'; import { stripAmbientProviderCredentials } from './create-command-env';
import { isUnifiedMode } from '../../config/config-loader-facade'; 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<void> { export async function handleCreate(ctx: CommandContext, args: string[]): Promise<void> {
await initUI(); await initUI();
const { const parsed = parseArgs(args);
profileName, const { profileName, force, shareContext, contextGroup, deeperContinuity, bare } = parsed;
force,
shareContext,
contextGroup,
deeperContinuity,
bare,
mode,
unknownFlags,
} = parseArgs(args);
const unsupportedOptions = [...(unknownFlags ?? []), ...(mode !== undefined ? ['--mode'] : [])]; rejectUnsupportedAuthOptions(parsed, {
if (unsupportedOptions.length > 0) { usage:
const unknownList = unsupportedOptions.map((flag) => `"${flag}"`).join(', '); 'ccs auth create <profile> [--force] [--bare] [--share-context] [--context-group <name>] [--deeper-continuity]',
console.log(fail(`Unknown option(s): ${unknownList}`)); });
console.log('');
console.log(
`Usage: ${color('ccs auth create <profile> [--force] [--bare] [--share-context] [--context-group <name>] [--deeper-continuity]', 'command')}`
);
console.log(`Help: ${color('ccs auth --help', 'command')}`);
console.log('');
exitWithError(`Unknown option(s): ${unknownList}`, ExitCode.PROFILE_ERROR);
}
if (!profileName) { if (!profileName) {
console.log(fail('Profile name is required')); console.log(fail('Profile name is required'));
+11 -3
View File
@@ -8,7 +8,7 @@ import { initUI, color, dim, ok, fail } from '../../utils/ui';
import { exitWithError } from '../../errors'; import { exitWithError } from '../../errors';
import { ExitCode } from '../../errors/exit-codes'; import { ExitCode } from '../../errors/exit-codes';
import { CommandContext, parseArgs } from './types'; import { CommandContext, parseArgs, rejectUnsupportedAuthOptions } from './types';
import { isUnifiedMode } from '../../config/config-loader-facade'; 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<void> { export async function handleDefault(ctx: CommandContext, args: string[]): Promise<void> {
await initUI(); await initUI();
const { profileName } = parseArgs(args); const parsed = parseArgs(args);
const { profileName } = parsed;
rejectUnsupportedAuthOptions(parsed, {
usage: 'ccs auth default <profile>',
});
if (!profileName) { if (!profileName) {
console.log(fail('Profile name is required')); 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) * Handle the reset-default command (clear the custom default)
*/ */
export async function handleResetDefault(ctx: CommandContext): Promise<void> { export async function handleResetDefault(ctx: CommandContext, args: string[] = []): Promise<void> {
await initUI(); await initUI();
const parsed = parseArgs(args);
rejectUnsupportedAuthOptions(parsed, {
usage: 'ccs auth reset-default',
});
try { try {
// Use unified or legacy based on config mode // Use unified or legacy based on config mode
+12 -2
View File
@@ -10,14 +10,24 @@ import { resolveAccountContextPolicy, formatAccountContextPolicy } from '../acco
import { resolveSharedResourcePolicy } from '../shared-resource-policy'; import { resolveSharedResourcePolicy } from '../shared-resource-policy';
import { exitWithError } from '../../errors'; import { exitWithError } from '../../errors';
import { ExitCode } from '../../errors/exit-codes'; 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 * Handle the list command
*/ */
export async function handleList(ctx: CommandContext, args: string[]): Promise<void> { export async function handleList(ctx: CommandContext, args: string[]): Promise<void> {
await initUI(); await initUI();
const { verbose, json } = parseArgs(args); const parsed = parseArgs(args);
const { verbose, json } = parsed;
rejectUnsupportedAuthOptions(parsed, {
usage: 'ccs auth list [--verbose] [--json]',
});
try { try {
// Get profiles from both legacy (profiles.json) and unified config (config.yaml) // Get profiles from both legacy (profiles.json) and unified config (config.yaml)
+6 -2
View File
@@ -11,7 +11,7 @@ import { InteractivePrompt } from '../../utils/prompt';
import { exitWithError } from '../../errors'; import { exitWithError } from '../../errors';
import { ExitCode } from '../../errors/exit-codes'; import { ExitCode } from '../../errors/exit-codes';
import { CommandContext, parseArgs } from './types'; import { CommandContext, parseArgs, rejectUnsupportedAuthOptions } from './types';
import { isUnifiedMode } from '../../config/config-loader-facade'; 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<void> { export async function handleRemove(ctx: CommandContext, args: string[]): Promise<void> {
await initUI(); await initUI();
const { profileName, yes } = parseArgs(args); const parsed = parseArgs(args);
const { profileName, yes } = parsed;
rejectUnsupportedAuthOptions(parsed, {
usage: 'ccs auth remove <profile> [--yes]',
});
if (!profileName) { if (!profileName) {
console.log(fail('Profile name is required')); console.log(fail('Profile name is required'));
+7 -2
View File
@@ -8,7 +8,7 @@ import {
} from '../shared-resource-policy'; } from '../shared-resource-policy';
import { exitWithError } from '../../errors'; import { exitWithError } from '../../errors';
import { ExitCode } from '../../errors/exit-codes'; import { ExitCode } from '../../errors/exit-codes';
import { CommandContext, parseArgs } from './types'; import { CommandContext, parseArgs, rejectUnsupportedAuthOptions } from './types';
function formatMode(mode: SharedResourceMode): string { function formatMode(mode: SharedResourceMode): string {
return mode === 'profile-local' ? 'profile-local' : 'shared'; 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<void> { export async function handleResources(ctx: CommandContext, args: string[]): Promise<void> {
await initUI(); 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 <profile> [--mode shared|profile-local] [--json]',
allowMode: true,
});
if (!profileName) { if (!profileName) {
console.log(fail('Profile name is required')); console.log(fail('Profile name is required'));
+6 -2
View File
@@ -13,7 +13,7 @@ import { resolveConfiguredPlainCcsResumeLane } from '../resume-lane-diagnostics'
import { resolveSharedResourcePolicy } from '../shared-resource-policy'; import { resolveSharedResourcePolicy } from '../shared-resource-policy';
import { exitWithError } from '../../errors'; import { exitWithError } from '../../errors';
import { ExitCode } from '../../errors/exit-codes'; import { ExitCode } from '../../errors/exit-codes';
import { CommandContext, ProfileOutput, parseArgs } from './types'; import { CommandContext, ProfileOutput, parseArgs, rejectUnsupportedAuthOptions } from './types';
function formatHistorySummary(history: ReturnType<typeof summarizeAccountHistory>): string { function formatHistorySummary(history: ReturnType<typeof summarizeAccountHistory>): string {
const scope = history.projects_shared ? 'shared projects' : 'profile-local projects'; const scope = history.projects_shared ? 'shared projects' : 'profile-local projects';
@@ -26,7 +26,11 @@ function formatHistorySummary(history: ReturnType<typeof summarizeAccountHistory
*/ */
export async function handleShow(ctx: CommandContext, args: string[]): Promise<void> { export async function handleShow(ctx: CommandContext, args: string[]): Promise<void> {
await initUI(); await initUI();
const { profileName, json } = parseArgs(args); const parsed = parseArgs(args);
const { profileName, json } = parsed;
rejectUnsupportedAuthOptions(parsed, {
usage: 'ccs auth show <profile> [--json]',
});
if (!profileName) { if (!profileName) {
console.log(fail('Profile name is required')); console.log(fail('Profile name is required'));
+36 -4
View File
@@ -6,6 +6,9 @@
import ProfileRegistry from '../profile-registry'; import ProfileRegistry from '../profile-registry';
import { InstanceManager } from '../../management/instance-manager'; 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 // Re-export for backward compatibility
export { formatRelativeTime } from '../../utils/time'; export { formatRelativeTime } from '../../utils/time';
@@ -83,10 +86,36 @@ export interface CommandContext {
version: string; version: string;
} }
export function rejectUnsupportedAuthOptions(
parsed: Pick<AuthCommandArgs, 'mode' | 'unknownFlags'>,
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 * 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 profileName: string | undefined;
let contextGroup: string | undefined; let contextGroup: string | undefined;
let mode: string | undefined; let mode: string | undefined;
@@ -101,7 +130,10 @@ export function parseArgs(args: string[]): AuthCommandArgs {
'--deeper-continuity', '--deeper-continuity',
'--bare', '--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++) { for (let i = 0; i < args.length; i++) {
const arg = args[i]; const arg = args[i];
@@ -118,7 +150,7 @@ export function parseArgs(args: string[]): AuthCommandArgs {
continue; continue;
} }
if (arg === '--mode') { if (options.allowMode && arg === '--mode') {
const next = args[i + 1]; const next = args[i + 1];
if (!next || next.startsWith('-')) { if (!next || next.startsWith('-')) {
mode = ''; mode = '';
@@ -135,7 +167,7 @@ export function parseArgs(args: string[]): AuthCommandArgs {
continue; continue;
} }
if (arg.startsWith('--mode=')) { if (options.allowMode && arg.startsWith('--mode=')) {
mode = arg.slice('--mode='.length); mode = arg.slice('--mode='.length);
continue; continue;
} }
+10 -2
View File
@@ -64,19 +64,27 @@ describe('auth command args parsing', () => {
}); });
it('parses shared resource mode value for resources command', () => { 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.profileName).toBe('work');
expect(parsed.mode).toBe('profile-local'); expect(parsed.mode).toBe('profile-local');
}); });
it('parses inline shared resource mode value', () => { 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.profileName).toBe('work');
expect(parsed.mode).toBe('shared'); 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', () => { it('tracks unknown flags and keeps positional profile intact', () => {
const parsed = parseArgs(['--foo', 'bar', 'work']); const parsed = parseArgs(['--foo', 'bar', 'work']);
+61 -30
View File
@@ -6,6 +6,38 @@ import ProfileRegistry from '../../src/auth/profile-registry';
import InstanceManager from '../../src/management/instance-manager'; import InstanceManager from '../../src/management/instance-manager';
import { handleResources } from '../../src/auth/commands/resources-command'; import { handleResources } from '../../src/auth/commands/resources-command';
import { handleCreate } from '../../src/auth/commands/create-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<void>): Promise<string> {
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', () => { describe('auth resources command', () => {
let tempRoot = ''; let tempRoot = '';
@@ -143,38 +175,37 @@ describe('auth resources command', () => {
it('rejects --mode on auth create instead of silently ignoring it', async () => { it('rejects --mode on auth create instead of silently ignoring it', async () => {
const registry = new ProfileRegistry(); const registry = new ProfileRegistry();
const instanceMgr = new InstanceManager(); const instanceMgr = new InstanceManager();
const originalExit = process.exit;
const originalLog = console.log;
const originalError = console.error;
const lines: string[] = [];
process.exit = ((code?: number) => { const output = await expectProfileExit(() =>
throw new Error(`process.exit(${code ?? 0})`); handleCreate(
}) as typeof process.exit; {
console.log = (...args: unknown[]) => { registry,
lines.push(args.map(String).join(' ')); instanceMgr,
}; version: 'test',
console.error = (...args: unknown[]) => { },
lines.push(args.map(String).join(' ')); ['work', '--mode', 'profile-local']
}; )
);
try { expect(output).toContain('Unknown option(s): "--mode"');
await expect( });
handleCreate(
{ it('rejects --mode on non-resources auth commands instead of silently ignoring it', async () => {
registry, const registry = new ProfileRegistry();
instanceMgr, const instanceMgr = new InstanceManager();
version: 'test', const ctx = { registry, instanceMgr, version: 'test' };
}, const cases: Array<[string, () => Promise<void>]> = [
['work', '--mode', 'profile-local'] ['list', () => handleList(ctx, ['--mode', 'shared'])],
) ['show', () => handleShow(ctx, ['work', '--mode=shared'])],
).rejects.toThrow('process.exit(7)'); ['backup', () => handleBackup(ctx, ['work', '--mode', 'profile-local'])],
} finally { ['remove', () => handleRemove(ctx, ['work', '--mode', 'shared'])],
process.exit = originalExit; ['default', () => handleDefault(ctx, ['work', '--mode', 'shared'])],
console.log = originalLog; ['reset-default', () => handleResetDefault(ctx, ['--mode', 'shared'])],
console.error = originalError; ];
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"');
}); });
}); });