mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-11 12:09:03 +00:00
Issue #1161. Sweeps 127 files to import from src/config/config-loader-facade.ts instead of unified-config-loader or utils/config-manager directly. WRITE callers (32 files): replaced raw saveUnifiedConfig / mutateUnifiedConfig / updateUnifiedConfig calls with the facade's cache-coherent wrappers saveConfig / mutateConfig / updateConfig. This fixes a latent stale-cache window where direct writes through the underlying loader bypassed the facade's memoization. READ callers (95 files): mechanical import-path migration only — function names unchanged because the facade re-exports them. No behavior change. Also updated: - tests/unit/utils/browser/browser-setup.test.ts (DI interface rename) - src/management/checks/image-analysis-check.ts (dynamic import rename) - src/web-server/health-service.ts (dynamic require rename) - src/ccs.ts (path prefix fix from sweep script) After sweep: zero raw write callers remain outside src/config/. Direct imports of config-manager remain only for symbols not in the facade (getConfigPath, getCcsDirSource, etc). Behavior unchanged; full suite passes 1824/1824. Out of scope: switching loadOrCreateUnifiedConfig() callers to getCachedConfig() — needs per-callsite cache-safety analysis. Tracked as follow-up. Refs #1161
343 lines
10 KiB
JavaScript
343 lines
10 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { HeadlessExecutor } from './headless-executor';
|
|
import { SessionManager } from './session-manager';
|
|
import { ResultFormatter } from './result-formatter';
|
|
import { DelegationValidator } from '../utils/delegation-validator';
|
|
import { SettingsParser } from './settings-parser';
|
|
import { fail, warn } from '../utils/ui';
|
|
import { getCcsDir } from '../config/config-loader-facade';
|
|
|
|
const PROFILE_FLAGS_WITH_VALUE = new Set(['-p', '--prompt', '--effort']);
|
|
|
|
/**
|
|
* Parse and validate a string flag value
|
|
* @returns value if valid, undefined if invalid/missing
|
|
*/
|
|
function parseStringFlag(
|
|
args: string[],
|
|
flagName: string,
|
|
options?: { allowDashPrefix?: boolean }
|
|
): string | undefined {
|
|
const index = args.indexOf(flagName);
|
|
if (index === -1 || index >= args.length - 1) return undefined;
|
|
|
|
const value = args[index + 1];
|
|
|
|
// Reject dash-prefixed values (likely another flag)
|
|
if (!options?.allowDashPrefix && value.startsWith('-')) {
|
|
console.error(warn(`${flagName} value "${value}" looks like a flag. Ignoring.`));
|
|
return undefined;
|
|
}
|
|
|
|
// Reject empty/whitespace-only
|
|
if (!value.trim()) {
|
|
console.error(warn(`${flagName} value is empty. Ignoring.`));
|
|
return undefined;
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
interface ParsedArgs {
|
|
profile: string;
|
|
prompt: string;
|
|
options: {
|
|
cwd: string;
|
|
outputFormat: string;
|
|
permissionMode: string;
|
|
timeout?: number;
|
|
resumeSession?: boolean;
|
|
sessionId?: string;
|
|
// Claude Code CLI passthrough flags (explicit)
|
|
maxTurns?: number;
|
|
fallbackModel?: string;
|
|
agents?: string;
|
|
betas?: string;
|
|
extraArgs?: string[]; // Catch-all for new/unknown flags
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Delegation command handler
|
|
* Routes -p flag commands to HeadlessExecutor with enhanced features
|
|
*/
|
|
export class DelegationHandler {
|
|
/**
|
|
* Route delegation command
|
|
* @param args - Full args array from ccs.js
|
|
*/
|
|
async route(args: string[]): Promise<void> {
|
|
try {
|
|
// 1. Parse args into { profile, prompt, options }
|
|
const parsed = this._parseArgs(args);
|
|
|
|
// 2. Detect special profiles (glm:continue, kimi:continue)
|
|
if (parsed.profile.includes(':continue')) {
|
|
return await this._handleContinue(parsed);
|
|
}
|
|
|
|
// 3. Validate profile
|
|
this._validateProfile(parsed.profile);
|
|
|
|
// 4. Execute via HeadlessExecutor
|
|
const result = await HeadlessExecutor.execute(parsed.profile, parsed.prompt, parsed.options);
|
|
|
|
// 5. Format and display results
|
|
const formatted = await ResultFormatter.format(result);
|
|
console.log(formatted);
|
|
|
|
// 6. Exit with proper code
|
|
process.exit(result.exitCode || 0);
|
|
} catch (error) {
|
|
console.error(fail(`Delegation error: ${(error as Error).message}`));
|
|
if (process.env.CCS_DEBUG) {
|
|
console.error((error as Error).stack);
|
|
}
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handle continue command (resume last session)
|
|
* @param parsed - Parsed args
|
|
*/
|
|
async _handleContinue(parsed: ParsedArgs): Promise<void> {
|
|
const baseProfile = parsed.profile.replace(':continue', '');
|
|
|
|
// Get last session from SessionManager
|
|
const sessionMgr = new SessionManager();
|
|
const lastSession = sessionMgr.getLastSession(baseProfile);
|
|
|
|
if (!lastSession) {
|
|
console.error(fail(`No previous session found for ${baseProfile}`));
|
|
console.error(` Start a new session first with: ccs ${baseProfile} -p "task"`);
|
|
process.exit(1);
|
|
}
|
|
|
|
// Execute with resume flag
|
|
const result = await HeadlessExecutor.execute(baseProfile, parsed.prompt, {
|
|
...parsed.options,
|
|
resumeSession: true,
|
|
sessionId: lastSession.sessionId,
|
|
});
|
|
|
|
const formatted = await ResultFormatter.format(result);
|
|
console.log(formatted);
|
|
|
|
process.exit(result.exitCode || 0);
|
|
}
|
|
|
|
/**
|
|
* Parse args into structured format
|
|
* @param args - Raw args
|
|
* @returns { profile, prompt, options }
|
|
*/
|
|
_parseArgs(args: string[]): ParsedArgs {
|
|
// Extract profile (first non-flag arg or 'default')
|
|
const profile = this._extractProfile(args);
|
|
|
|
// Extract prompt from -p or --prompt
|
|
const prompt = this._extractPrompt(args);
|
|
|
|
// Extract options (--timeout, --permission-mode, etc.)
|
|
const options = this._extractOptions(args);
|
|
|
|
return { profile, prompt, options };
|
|
}
|
|
|
|
/**
|
|
* Extract profile from args (first non-flag arg)
|
|
* @param args - Args array
|
|
* @returns profile name
|
|
*/
|
|
_extractProfile(args: string[]): string {
|
|
// Find first arg that doesn't start with '-' and isn't -p value
|
|
let skipNext = false;
|
|
for (let i = 0; i < args.length; i++) {
|
|
if (skipNext) {
|
|
skipNext = false;
|
|
continue;
|
|
}
|
|
|
|
if (PROFILE_FLAGS_WITH_VALUE.has(args[i])) {
|
|
skipNext = true;
|
|
continue;
|
|
}
|
|
|
|
if (args[i].startsWith('--effort=')) continue;
|
|
|
|
if (!args[i].startsWith('-')) {
|
|
return args[i];
|
|
}
|
|
}
|
|
|
|
// No profile specified, return empty string (will error in validation)
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Extract prompt from -p flag
|
|
* @param args - Args array
|
|
* @returns prompt text
|
|
*/
|
|
_extractPrompt(args: string[]): string {
|
|
const pIndex = args.indexOf('-p');
|
|
const promptIndex = args.indexOf('--prompt');
|
|
|
|
const index = pIndex !== -1 ? pIndex : promptIndex;
|
|
|
|
if (index === -1 || index === args.length - 1) {
|
|
console.error(fail('Missing prompt after -p flag'));
|
|
console.error(' Usage: ccs glm -p "task description"');
|
|
process.exit(1);
|
|
}
|
|
|
|
return args[index + 1];
|
|
}
|
|
|
|
/**
|
|
* Extract options from remaining args
|
|
* @param args - Args array
|
|
* @returns options for HeadlessExecutor
|
|
*/
|
|
_extractOptions(args: string[]): ParsedArgs['options'] {
|
|
const cwd = process.cwd();
|
|
|
|
// Read default permission mode from .claude/settings.local.json
|
|
// Falls back to 'acceptEdits' if file doesn't exist
|
|
const defaultPermissionMode = SettingsParser.parseDefaultPermissionMode(cwd);
|
|
|
|
const options: ParsedArgs['options'] = {
|
|
cwd,
|
|
outputFormat: 'stream-json',
|
|
permissionMode: defaultPermissionMode,
|
|
};
|
|
|
|
// Parse permission-mode (CLI flag overrides settings file)
|
|
const permModeIndex = args.indexOf('--permission-mode');
|
|
if (permModeIndex !== -1 && permModeIndex < args.length - 1) {
|
|
options.permissionMode = args[permModeIndex + 1];
|
|
}
|
|
|
|
// Parse timeout (validated: positive integer, max 10 minutes)
|
|
const timeoutIndex = args.indexOf('--timeout');
|
|
if (timeoutIndex !== -1 && timeoutIndex < args.length - 1) {
|
|
const rawVal = args[timeoutIndex + 1];
|
|
const val = parseInt(rawVal, 10);
|
|
if (!isNaN(val) && val > 0 && val <= 600000) {
|
|
options.timeout = val;
|
|
} else if (isNaN(val)) {
|
|
console.error(warn(`--timeout "${rawVal}" is not a number. Using default.`));
|
|
} else if (val <= 0) {
|
|
console.error(warn(`--timeout ${val} must be positive. Using default.`));
|
|
} else if (val > 600000) {
|
|
console.error(warn(`--timeout ${val} exceeds max (600000ms). Using default.`));
|
|
}
|
|
}
|
|
|
|
// Parse --max-turns (limit agentic turns, max 100)
|
|
const maxTurnsIndex = args.indexOf('--max-turns');
|
|
if (maxTurnsIndex !== -1 && maxTurnsIndex < args.length - 1) {
|
|
const rawVal = args[maxTurnsIndex + 1];
|
|
const val = parseInt(rawVal, 10);
|
|
if (!isNaN(val) && val > 0 && val <= 100) {
|
|
options.maxTurns = val;
|
|
} else if (isNaN(val)) {
|
|
console.error(warn(`--max-turns "${rawVal}" is not a number. Ignoring.`));
|
|
} else if (val <= 0) {
|
|
console.error(warn(`--max-turns ${val} must be positive. Ignoring.`));
|
|
} else if (val > 100) {
|
|
console.error(warn(`--max-turns ${val} exceeds max (100). Using 100.`));
|
|
options.maxTurns = 100;
|
|
}
|
|
}
|
|
|
|
// Parse --fallback-model (auto-fallback on overload)
|
|
options.fallbackModel = parseStringFlag(args, '--fallback-model');
|
|
|
|
// Parse --agents (dynamic subagent JSON)
|
|
const agentsValue = parseStringFlag(args, '--agents');
|
|
if (agentsValue) {
|
|
// Validate JSON structure
|
|
try {
|
|
JSON.parse(agentsValue);
|
|
options.agents = agentsValue;
|
|
} catch {
|
|
console.error(warn('--agents must be valid JSON. Ignoring.'));
|
|
}
|
|
}
|
|
|
|
// Parse --betas (experimental features)
|
|
options.betas = parseStringFlag(args, '--betas');
|
|
|
|
// Collect extra args to pass through to Claude CLI
|
|
// CCS-handled flags with values (skip these and their values):
|
|
const ccsFlagsWithValue = new Set([
|
|
'-p',
|
|
'--prompt',
|
|
'--timeout',
|
|
'--permission-mode',
|
|
'--max-turns',
|
|
'--fallback-model',
|
|
'--agents',
|
|
'--betas',
|
|
]);
|
|
const extraArgs: string[] = [];
|
|
const profile = this._extractProfile(args);
|
|
|
|
for (let i = 0; i < args.length; i++) {
|
|
const arg = args[i];
|
|
|
|
// Skip profile name (non-flag first arg)
|
|
if (arg === profile && !arg.startsWith('-')) continue;
|
|
|
|
// Skip CCS-handled flags and their values
|
|
if (ccsFlagsWithValue.has(arg)) {
|
|
i++; // Skip next arg (the value)
|
|
continue;
|
|
}
|
|
|
|
// Collect flags and their values as passthrough
|
|
if (arg.startsWith('-')) {
|
|
extraArgs.push(arg);
|
|
// If next arg exists and doesn't start with '-', it's likely a value
|
|
if (i + 1 < args.length && !args[i + 1].startsWith('-')) {
|
|
extraArgs.push(args[i + 1]);
|
|
i++; // Skip the value we just added
|
|
}
|
|
}
|
|
}
|
|
|
|
if (extraArgs.length > 0) {
|
|
options.extraArgs = extraArgs;
|
|
}
|
|
|
|
return options;
|
|
}
|
|
|
|
/**
|
|
* Validate profile exists and is configured
|
|
* @param profile - Profile name
|
|
*/
|
|
_validateProfile(profile: string): void {
|
|
if (!profile) {
|
|
console.error(fail('No profile specified'));
|
|
console.error(' Usage: ccs <profile> -p "task"');
|
|
console.error(' Examples: ccs glm -p "task", ccs km -p "task"');
|
|
process.exit(1);
|
|
}
|
|
|
|
// Use DelegationValidator to check profile
|
|
const validation = DelegationValidator.validate(profile);
|
|
if (!validation.valid) {
|
|
console.error(fail(`Profile '${profile}' is not configured for delegation`));
|
|
console.error(` ${validation.error}`);
|
|
console.error('');
|
|
console.error(' Run: ccs doctor');
|
|
console.error(` Or configure: ${getCcsDir()}/${profile}.settings.json`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
}
|