mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-05 18:16:28 +00:00
fix(persist): add auto-approve permission flags
This commit is contained in:
@@ -29,6 +29,9 @@ interface PersistCommandArgs {
|
|||||||
yes?: boolean;
|
yes?: boolean;
|
||||||
listBackups?: boolean;
|
listBackups?: boolean;
|
||||||
restore?: string | boolean;
|
restore?: string | boolean;
|
||||||
|
permissionMode?: PermissionMode;
|
||||||
|
dangerouslySkipPermissions?: boolean;
|
||||||
|
parseError?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ResolvedEnv {
|
interface ResolvedEnv {
|
||||||
@@ -37,6 +40,40 @@ interface ResolvedEnv {
|
|||||||
warning?: string;
|
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 */
|
/** Parse command line arguments */
|
||||||
function parseArgs(args: string[]): PersistCommandArgs {
|
function parseArgs(args: string[]): PersistCommandArgs {
|
||||||
const result: PersistCommandArgs = {
|
const result: PersistCommandArgs = {
|
||||||
@@ -49,7 +86,27 @@ function parseArgs(args: string[]): PersistCommandArgs {
|
|||||||
result.restore = restoreOption.missingValue ? true : restoreOption.value || true;
|
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('-')) {
|
if (!arg.startsWith('-')) {
|
||||||
result.profile = arg;
|
result.profile = arg;
|
||||||
break;
|
break;
|
||||||
@@ -415,6 +472,13 @@ async function showHelp(): Promise<void> {
|
|||||||
console.log('');
|
console.log('');
|
||||||
console.log(subheader('Options'));
|
console.log(subheader('Options'));
|
||||||
console.log(` ${color('--yes, -y', 'command')} Skip confirmation prompts (auto-backup)`);
|
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(` ${color('--help, -h', 'command')} Show this help message`);
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log(subheader('Backup Management'));
|
console.log(subheader('Backup Management'));
|
||||||
@@ -437,6 +501,12 @@ async function showHelp(): Promise<void> {
|
|||||||
console.log(` ${dim('# Persist with auto-confirmation')}`);
|
console.log(` ${dim('# Persist with auto-confirmation')}`);
|
||||||
console.log(` ${color('ccs persist gemini --yes', 'command')}`);
|
console.log(` ${color('ccs persist gemini --yes', 'command')}`);
|
||||||
console.log('');
|
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(` ${dim('# List all backups')}`);
|
||||||
console.log(` ${color('ccs persist --list-backups', 'command')}`);
|
console.log(` ${color('ccs persist --list-backups', 'command')}`);
|
||||||
console.log('');
|
console.log('');
|
||||||
@@ -474,6 +544,17 @@ export async function handlePersistCommand(args: string[]): Promise<void> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await initUI();
|
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) {
|
if (!parsedArgs.profile) {
|
||||||
console.log(fail('Profile name is required'));
|
console.log(fail('Profile name is required'));
|
||||||
console.log('');
|
console.log('');
|
||||||
@@ -529,6 +610,13 @@ export async function handlePersistCommand(args: string[]): Promise<void> {
|
|||||||
console.log(` ${color(paddedKey, 'command')} = ${displayValue}`);
|
console.log(` ${color(paddedKey, 'command')} = ${displayValue}`);
|
||||||
}
|
}
|
||||||
console.log('');
|
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
|
// Show warning if applicable
|
||||||
if (resolved.warning) {
|
if (resolved.warning) {
|
||||||
console.log(warn(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>;
|
existingEnv = rawEnv as Record<string, string>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const mergedSettings = {
|
const mergedSettings: Record<string, unknown> = {
|
||||||
...existingSettings,
|
...existingSettings,
|
||||||
env: {
|
env: {
|
||||||
...existingEnv,
|
...existingEnv,
|
||||||
...resolved.env,
|
...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
|
// Write merged settings
|
||||||
try {
|
try {
|
||||||
writeClaudeSettings(mergedSettings);
|
writeClaudeSettings(mergedSettings);
|
||||||
|
|||||||
@@ -20,11 +20,15 @@ describe('Persist Command', () => {
|
|||||||
* Simulates the argument parsing logic from persist-command.ts
|
* Simulates the argument parsing logic from persist-command.ts
|
||||||
*/
|
*/
|
||||||
function parseArgs(args) {
|
function parseArgs(args) {
|
||||||
|
const validPermissionModes = ['default', 'plan', 'acceptEdits', 'bypassPermissions'];
|
||||||
const result = {
|
const result = {
|
||||||
profile: undefined,
|
profile: undefined,
|
||||||
yes: false,
|
yes: false,
|
||||||
listBackups: false,
|
listBackups: false,
|
||||||
restore: undefined,
|
restore: undefined,
|
||||||
|
permissionMode: undefined,
|
||||||
|
dangerouslySkipPermissions: false,
|
||||||
|
parseError: undefined,
|
||||||
};
|
};
|
||||||
for (let i = 0; i < args.length; i++) {
|
for (let i = 0; i < args.length; i++) {
|
||||||
const arg = args[i];
|
const arg = args[i];
|
||||||
@@ -43,6 +47,27 @@ describe('Persist Command', () => {
|
|||||||
} else {
|
} else {
|
||||||
result.restore = true; // Use latest
|
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) {
|
} else if (!arg.startsWith('-') && !result.profile) {
|
||||||
result.profile = arg;
|
result.profile = arg;
|
||||||
}
|
}
|
||||||
@@ -117,6 +142,34 @@ describe('Persist Command', () => {
|
|||||||
assert.strictEqual(result.restore, '20260110_205324');
|
assert.strictEqual(result.restore, '20260110_205324');
|
||||||
assert.strictEqual(result.yes, true);
|
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
|
* Simulates the merge logic from persist-command.ts
|
||||||
*/
|
*/
|
||||||
function mergeSettings(existing, newEnv) {
|
function mergeSettings(existing, newEnv, permissionMode) {
|
||||||
const existingEnv = existing.env || {};
|
const existingEnv = existing.env || {};
|
||||||
return {
|
const merged = {
|
||||||
...existing,
|
...existing,
|
||||||
env: {
|
env: {
|
||||||
...existingEnv,
|
...existingEnv,
|
||||||
...newEnv,
|
...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', () => {
|
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_BASE_URL, 'http://test.com');
|
||||||
assert.strictEqual(result.env.ANTHROPIC_MODEL, 'test');
|
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