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
*/
async handleResetDefault(): Promise<void> {
return handleResetDefault(this.getContext());
async handleResetDefault(args: string[] = []): Promise<void> {
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':
+6 -2
View File
@@ -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<void> {
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) {
console.log(fail('Profile name is required'));
+7 -23
View File
@@ -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<void> {
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 <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);
}
rejectUnsupportedAuthOptions(parsed, {
usage:
'ccs auth create <profile> [--force] [--bare] [--share-context] [--context-group <name>] [--deeper-continuity]',
});
if (!profileName) {
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 { 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<void> {
await initUI();
const { profileName } = parseArgs(args);
const parsed = parseArgs(args);
const { profileName } = parsed;
rejectUnsupportedAuthOptions(parsed, {
usage: 'ccs auth default <profile>',
});
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<void> {
export async function handleResetDefault(ctx: CommandContext, args: string[] = []): Promise<void> {
await initUI();
const parsed = parseArgs(args);
rejectUnsupportedAuthOptions(parsed, {
usage: 'ccs auth reset-default',
});
try {
// 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 { 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<void> {
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)
+6 -2
View File
@@ -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<void> {
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) {
console.log(fail('Profile name is required'));
+7 -2
View File
@@ -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<void> {
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) {
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 { 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<typeof summarizeAccountHistory>): string {
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> {
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) {
console.log(fail('Profile name is required'));
+36 -4
View File
@@ -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<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
*/
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;
}
+10 -2
View File
@@ -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']);
+61 -30
View File
@@ -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<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', () => {
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<void>]> = [
['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"');
});
});