mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-30 12:22:19 +00:00
- add errors/ directory with error types, handler, cleanup registry - extract claude-spawner.ts from auth modules - update imports across auth, ccs, web-server Phase 2 & 4: Error handling consolidation
140 lines
3.6 KiB
TypeScript
140 lines
3.6 KiB
TypeScript
/**
|
|
* Claude Spawner Utilities
|
|
*
|
|
* Cross-platform Claude CLI spawn utilities for CCS.
|
|
* Handles Windows .cmd/.bat/.ps1 files properly.
|
|
*/
|
|
|
|
import { spawn, ChildProcess, SpawnOptions } from 'child_process';
|
|
import { escapeShellArg } from './shell-executor';
|
|
import { getClaudeCliInfo } from './claude-detector';
|
|
import { ErrorManager } from './error-manager';
|
|
|
|
export interface SpawnClaudeOptions {
|
|
/** Arguments to pass to Claude CLI */
|
|
args?: string[];
|
|
/** Environment variables to merge with process.env */
|
|
env?: NodeJS.ProcessEnv;
|
|
/** Working directory */
|
|
cwd?: string;
|
|
/** Stdio configuration (default: 'inherit') */
|
|
stdio?: SpawnOptions['stdio'];
|
|
}
|
|
|
|
export interface SpawnClaudeResult {
|
|
/** The spawned child process */
|
|
child: ChildProcess;
|
|
/** Path to the Claude CLI executable */
|
|
claudePath: string;
|
|
}
|
|
|
|
/**
|
|
* Spawn Claude CLI with cross-platform support.
|
|
*
|
|
* Handles Windows .cmd/.bat/.ps1 wrappers automatically.
|
|
* Returns the ChildProcess for custom event handling.
|
|
*
|
|
* @throws Error if Claude CLI is not found
|
|
*/
|
|
export function spawnClaude(options: SpawnClaudeOptions = {}): SpawnClaudeResult {
|
|
const claudeInfo = getClaudeCliInfo();
|
|
if (!claudeInfo) {
|
|
throw new Error('Claude CLI not found');
|
|
}
|
|
|
|
const { path: claudeCli, needsShell } = claudeInfo;
|
|
const { args = [], env, cwd, stdio = 'inherit' } = options;
|
|
|
|
// Merge environment
|
|
const mergedEnv = env ? { ...process.env, ...env } : process.env;
|
|
|
|
let child: ChildProcess;
|
|
if (needsShell) {
|
|
// Windows .cmd/.bat/.ps1: concatenate into string to avoid DEP0190 warning
|
|
const cmdString = [claudeCli, ...args].map(escapeShellArg).join(' ');
|
|
child = spawn(cmdString, {
|
|
stdio,
|
|
windowsHide: true,
|
|
shell: true,
|
|
env: mergedEnv,
|
|
cwd,
|
|
});
|
|
} else {
|
|
// Unix or Windows native: use array form (faster, no shell overhead)
|
|
child = spawn(claudeCli, args, {
|
|
stdio,
|
|
windowsHide: true,
|
|
env: mergedEnv,
|
|
cwd,
|
|
});
|
|
}
|
|
|
|
return { child, claudePath: claudeCli };
|
|
}
|
|
|
|
/**
|
|
* Spawn Claude CLI and wait for exit.
|
|
*
|
|
* Convenience function that returns a promise resolving to the exit code.
|
|
*/
|
|
export function spawnClaudeSync(options: SpawnClaudeOptions = {}): Promise<number> {
|
|
return new Promise((resolve, reject) => {
|
|
try {
|
|
const { child } = spawnClaude(options);
|
|
|
|
child.on('exit', (code) => {
|
|
resolve(code ?? 0);
|
|
});
|
|
|
|
child.on('error', (error) => {
|
|
reject(error);
|
|
});
|
|
} catch (error) {
|
|
reject(error);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Spawn Claude CLI with automatic exit handling.
|
|
*
|
|
* Exits the current process when Claude exits, preserving exit code/signal.
|
|
* Used for simple pass-through scenarios.
|
|
*/
|
|
export function execClaudeWithExitHandling(options: SpawnClaudeOptions = {}): void {
|
|
let result: SpawnClaudeResult;
|
|
|
|
try {
|
|
result = spawnClaude(options);
|
|
} catch {
|
|
// Claude not found - show error and exit
|
|
void ErrorManager.showClaudeNotFound();
|
|
process.exit(1);
|
|
}
|
|
|
|
const { child } = result;
|
|
|
|
child.on('exit', (code, signal) => {
|
|
if (signal) {
|
|
process.kill(process.pid, signal as NodeJS.Signals);
|
|
} else {
|
|
process.exit(code ?? 0);
|
|
}
|
|
});
|
|
|
|
child.on('error', async () => {
|
|
await ErrorManager.showClaudeNotFound();
|
|
process.exit(1);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Check if shell execution is needed for a given path.
|
|
*
|
|
* Returns true for Windows .cmd/.bat/.ps1 files.
|
|
*/
|
|
export function needsShellExecution(executablePath: string): boolean {
|
|
const isWindows = process.platform === 'win32';
|
|
return isWindows && /\.(cmd|bat|ps1)$/i.test(executablePath);
|
|
}
|