mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 02:11:28 +00:00
fix(persist): add auto-approve permission flags
This commit is contained in:
@@ -29,6 +29,9 @@ interface PersistCommandArgs {
|
||||
yes?: boolean;
|
||||
listBackups?: boolean;
|
||||
restore?: string | boolean;
|
||||
permissionMode?: PermissionMode;
|
||||
dangerouslySkipPermissions?: boolean;
|
||||
parseError?: string;
|
||||
}
|
||||
|
||||
interface ResolvedEnv {
|
||||
@@ -37,6 +40,40 @@ interface ResolvedEnv {
|
||||
warning?: string;
|
||||
}
|
||||
|
||||
const PERSIST_KNOWN_FLAGS = [
|
||||
'--yes',
|
||||
'-y',
|
||||
'--list-backups',
|
||||
'--restore',
|
||||
'--permission-mode',
|
||||
'--dangerously-skip-permissions',
|
||||
'--auto-approve',
|
||||
'--help',
|
||||
'-h',
|
||||
] as const;
|
||||
|
||||
const VALID_PERMISSION_MODES = ['default', 'plan', 'acceptEdits', 'bypassPermissions'] as const;
|
||||
|
||||
type PermissionMode = (typeof VALID_PERMISSION_MODES)[number];
|
||||
|
||||
function isPermissionMode(value: string): value is PermissionMode {
|
||||
return VALID_PERMISSION_MODES.includes(value as PermissionMode);
|
||||
}
|
||||
|
||||
function resolvePermissionMode(parsedArgs: PersistCommandArgs): PermissionMode | undefined {
|
||||
if (!parsedArgs.dangerouslySkipPermissions) {
|
||||
return parsedArgs.permissionMode;
|
||||
}
|
||||
|
||||
if (parsedArgs.permissionMode && parsedArgs.permissionMode !== 'bypassPermissions') {
|
||||
throw new Error(
|
||||
'--dangerously-skip-permissions conflicts with --permission-mode. Use bypassPermissions or remove one flag.'
|
||||
);
|
||||
}
|
||||
|
||||
return 'bypassPermissions';
|
||||
}
|
||||
|
||||
/** Parse command line arguments */
|
||||
function parseArgs(args: string[]): PersistCommandArgs {
|
||||
const result: PersistCommandArgs = {
|
||||
@@ -49,7 +86,27 @@ function parseArgs(args: string[]): PersistCommandArgs {
|
||||
result.restore = restoreOption.missingValue ? true : restoreOption.value || true;
|
||||
}
|
||||
|
||||
for (const arg of restoreOption.remainingArgs) {
|
||||
const permissionModeOption = extractOption(restoreOption.remainingArgs, ['--permission-mode'], {
|
||||
knownFlags: PERSIST_KNOWN_FLAGS,
|
||||
});
|
||||
if (permissionModeOption.found) {
|
||||
if (permissionModeOption.missingValue) {
|
||||
result.parseError = 'Missing value for --permission-mode';
|
||||
} else if (permissionModeOption.value) {
|
||||
if (!isPermissionMode(permissionModeOption.value)) {
|
||||
result.parseError = `Invalid --permission-mode "${permissionModeOption.value}". Valid modes: ${VALID_PERMISSION_MODES.join(', ')}`;
|
||||
} else {
|
||||
result.permissionMode = permissionModeOption.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.dangerouslySkipPermissions = hasAnyFlag(permissionModeOption.remainingArgs, [
|
||||
'--dangerously-skip-permissions',
|
||||
'--auto-approve',
|
||||
]);
|
||||
|
||||
for (const arg of permissionModeOption.remainingArgs) {
|
||||
if (!arg.startsWith('-')) {
|
||||
result.profile = arg;
|
||||
break;
|
||||
@@ -415,6 +472,13 @@ async function showHelp(): Promise<void> {
|
||||
console.log('');
|
||||
console.log(subheader('Options'));
|
||||
console.log(` ${color('--yes, -y', 'command')} Skip confirmation prompts (auto-backup)`);
|
||||
console.log(
|
||||
` ${color('--permission-mode <mode>', 'command')} Set default permission mode in settings.json`
|
||||
);
|
||||
console.log(
|
||||
` ${color('--dangerously-skip-permissions', 'command')} Persist auto-approve (bypassPermissions)`
|
||||
);
|
||||
console.log(` ${color('--auto-approve', 'command')} Alias for --dangerously-skip-permissions`);
|
||||
console.log(` ${color('--help, -h', 'command')} Show this help message`);
|
||||
console.log('');
|
||||
console.log(subheader('Backup Management'));
|
||||
@@ -437,6 +501,12 @@ async function showHelp(): Promise<void> {
|
||||
console.log(` ${dim('# Persist with auto-confirmation')}`);
|
||||
console.log(` ${color('ccs persist gemini --yes', 'command')}`);
|
||||
console.log('');
|
||||
console.log(` ${dim('# Persist with default permission mode')}`);
|
||||
console.log(` ${color('ccs persist glm --permission-mode acceptEdits', 'command')}`);
|
||||
console.log('');
|
||||
console.log(` ${dim('# Persist with auto-approve enabled')}`);
|
||||
console.log(` ${color('ccs persist codex --dangerously-skip-permissions', 'command')}`);
|
||||
console.log('');
|
||||
console.log(` ${dim('# List all backups')}`);
|
||||
console.log(` ${color('ccs persist --list-backups', 'command')}`);
|
||||
console.log('');
|
||||
@@ -474,6 +544,17 @@ export async function handlePersistCommand(args: string[]): Promise<void> {
|
||||
return;
|
||||
}
|
||||
await initUI();
|
||||
if (parsedArgs.parseError) {
|
||||
console.log(fail(parsedArgs.parseError));
|
||||
process.exit(1);
|
||||
}
|
||||
let resolvedPermissionMode: PermissionMode | undefined;
|
||||
try {
|
||||
resolvedPermissionMode = resolvePermissionMode(parsedArgs);
|
||||
} catch (error) {
|
||||
console.log(fail((error as Error).message));
|
||||
process.exit(1);
|
||||
}
|
||||
if (!parsedArgs.profile) {
|
||||
console.log(fail('Profile name is required'));
|
||||
console.log('');
|
||||
@@ -529,6 +610,13 @@ export async function handlePersistCommand(args: string[]): Promise<void> {
|
||||
console.log(` ${color(paddedKey, 'command')} = ${displayValue}`);
|
||||
}
|
||||
console.log('');
|
||||
if (resolvedPermissionMode) {
|
||||
console.log(`Default permission mode: ${color(resolvedPermissionMode, 'command')}`);
|
||||
if (resolvedPermissionMode === 'bypassPermissions') {
|
||||
console.log(warn('Auto-approve enabled: Claude will skip permission prompts by default.'));
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
// Show warning if applicable
|
||||
if (resolved.warning) {
|
||||
console.log(warn(resolved.warning));
|
||||
@@ -582,13 +670,30 @@ export async function handlePersistCommand(args: string[]): Promise<void> {
|
||||
existingEnv = rawEnv as Record<string, string>;
|
||||
}
|
||||
}
|
||||
const mergedSettings = {
|
||||
const mergedSettings: Record<string, unknown> = {
|
||||
...existingSettings,
|
||||
env: {
|
||||
...existingEnv,
|
||||
...resolved.env,
|
||||
},
|
||||
};
|
||||
if (resolvedPermissionMode) {
|
||||
const rawPermissions = existingSettings.permissions;
|
||||
let existingPermissions: Record<string, unknown> = {};
|
||||
if (rawPermissions !== undefined && rawPermissions !== null) {
|
||||
if (typeof rawPermissions !== 'object' || Array.isArray(rawPermissions)) {
|
||||
console.log(
|
||||
warn('Existing permissions in settings.json is not an object - it will be replaced')
|
||||
);
|
||||
} else {
|
||||
existingPermissions = rawPermissions as Record<string, unknown>;
|
||||
}
|
||||
}
|
||||
mergedSettings.permissions = {
|
||||
...existingPermissions,
|
||||
defaultMode: resolvedPermissionMode,
|
||||
};
|
||||
}
|
||||
// Write merged settings
|
||||
try {
|
||||
writeClaudeSettings(mergedSettings);
|
||||
|
||||
@@ -20,11 +20,15 @@ describe('Persist Command', () => {
|
||||
* Simulates the argument parsing logic from persist-command.ts
|
||||
*/
|
||||
function parseArgs(args) {
|
||||
const validPermissionModes = ['default', 'plan', 'acceptEdits', 'bypassPermissions'];
|
||||
const result = {
|
||||
profile: undefined,
|
||||
yes: false,
|
||||
listBackups: false,
|
||||
restore: undefined,
|
||||
permissionMode: undefined,
|
||||
dangerouslySkipPermissions: false,
|
||||
parseError: undefined,
|
||||
};
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
@@ -43,6 +47,27 @@ describe('Persist Command', () => {
|
||||
} else {
|
||||
result.restore = true; // Use latest
|
||||
}
|
||||
} else if (arg === '--permission-mode') {
|
||||
const nextArg = args[i + 1];
|
||||
if (!nextArg || nextArg.startsWith('-')) {
|
||||
result.parseError = 'Missing value for --permission-mode';
|
||||
} else if (!validPermissionModes.includes(nextArg)) {
|
||||
result.parseError = `Invalid --permission-mode "${nextArg}". Valid modes: ${validPermissionModes.join(', ')}`;
|
||||
} else {
|
||||
result.permissionMode = nextArg;
|
||||
i++; // Skip next arg
|
||||
}
|
||||
} else if (arg.startsWith('--permission-mode=')) {
|
||||
const mode = arg.split('=').slice(1).join('=');
|
||||
if (!mode.trim()) {
|
||||
result.parseError = 'Missing value for --permission-mode';
|
||||
} else if (!validPermissionModes.includes(mode)) {
|
||||
result.parseError = `Invalid --permission-mode "${mode}". Valid modes: ${validPermissionModes.join(', ')}`;
|
||||
} else {
|
||||
result.permissionMode = mode;
|
||||
}
|
||||
} else if (arg === '--dangerously-skip-permissions' || arg === '--auto-approve') {
|
||||
result.dangerouslySkipPermissions = true;
|
||||
} else if (!arg.startsWith('-') && !result.profile) {
|
||||
result.profile = arg;
|
||||
}
|
||||
@@ -117,6 +142,34 @@ describe('Persist Command', () => {
|
||||
assert.strictEqual(result.restore, '20260110_205324');
|
||||
assert.strictEqual(result.yes, true);
|
||||
});
|
||||
|
||||
it('parses --permission-mode with valid mode', () => {
|
||||
const result = parseArgs(['glm', '--permission-mode', 'acceptEdits']);
|
||||
assert.strictEqual(result.profile, 'glm');
|
||||
assert.strictEqual(result.permissionMode, 'acceptEdits');
|
||||
assert.strictEqual(result.parseError, undefined);
|
||||
});
|
||||
|
||||
it('parses --permission-mode=value syntax', () => {
|
||||
const result = parseArgs(['glm', '--permission-mode=bypassPermissions']);
|
||||
assert.strictEqual(result.permissionMode, 'bypassPermissions');
|
||||
assert.strictEqual(result.parseError, undefined);
|
||||
});
|
||||
|
||||
it('sets parseError for invalid --permission-mode', () => {
|
||||
const result = parseArgs(['glm', '--permission-mode', 'invalid']);
|
||||
assert.match(result.parseError, /Invalid --permission-mode/);
|
||||
});
|
||||
|
||||
it('parses --dangerously-skip-permissions flag', () => {
|
||||
const result = parseArgs(['glm', '--dangerously-skip-permissions']);
|
||||
assert.strictEqual(result.dangerouslySkipPermissions, true);
|
||||
});
|
||||
|
||||
it('parses --auto-approve as alias flag', () => {
|
||||
const result = parseArgs(['glm', '--auto-approve']);
|
||||
assert.strictEqual(result.dangerouslySkipPermissions, true);
|
||||
});
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
@@ -170,15 +223,28 @@ describe('Persist Command', () => {
|
||||
/**
|
||||
* Simulates the merge logic from persist-command.ts
|
||||
*/
|
||||
function mergeSettings(existing, newEnv) {
|
||||
function mergeSettings(existing, newEnv, permissionMode) {
|
||||
const existingEnv = existing.env || {};
|
||||
return {
|
||||
const merged = {
|
||||
...existing,
|
||||
env: {
|
||||
...existingEnv,
|
||||
...newEnv,
|
||||
},
|
||||
};
|
||||
|
||||
if (permissionMode) {
|
||||
const existingPermissions =
|
||||
existing.permissions && typeof existing.permissions === 'object'
|
||||
? existing.permissions
|
||||
: {};
|
||||
merged.permissions = {
|
||||
...existingPermissions,
|
||||
defaultMode: permissionMode,
|
||||
};
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
it('merges env vars into empty settings', () => {
|
||||
@@ -258,6 +324,30 @@ describe('Persist Command', () => {
|
||||
assert.strictEqual(result.env.ANTHROPIC_BASE_URL, 'http://test.com');
|
||||
assert.strictEqual(result.env.ANTHROPIC_MODEL, 'test');
|
||||
});
|
||||
|
||||
it('sets permissions.defaultMode when permission mode is provided', () => {
|
||||
const existing = {
|
||||
hooks: { PreToolUse: [] },
|
||||
};
|
||||
const result = mergeSettings(existing, { ANTHROPIC_MODEL: 'test' }, 'acceptEdits');
|
||||
|
||||
assert.strictEqual(result.permissions.defaultMode, 'acceptEdits');
|
||||
assert.deepStrictEqual(result.hooks, existing.hooks);
|
||||
});
|
||||
|
||||
it('preserves existing permissions allow/deny when setting defaultMode', () => {
|
||||
const existing = {
|
||||
permissions: {
|
||||
allow: ['Bash(ls:*)'],
|
||||
deny: ['Bash(rm:*)'],
|
||||
},
|
||||
};
|
||||
const result = mergeSettings(existing, { ANTHROPIC_MODEL: 'test' }, 'bypassPermissions');
|
||||
|
||||
assert.deepStrictEqual(result.permissions.allow, ['Bash(ls:*)']);
|
||||
assert.deepStrictEqual(result.permissions.deny, ['Bash(rm:*)']);
|
||||
assert.strictEqual(result.permissions.defaultMode, 'bypassPermissions');
|
||||
});
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
|
||||
Reference in New Issue
Block a user