mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-17 04:17:11 +00:00
fix(auth): reject resource mode outside resources command
This commit is contained in:
@@ -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':
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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']);
|
||||
|
||||
|
||||
@@ -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"');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user