Files
ccs/src/delegation/delegation-handler.ts
T
Tam Nhu Tran 4f6e61739c refactor(config): adopt config-loader-facade across the codebase
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
2026-05-03 01:42:53 -04:00

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);
}
}
}