mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 10:16:49 +00:00
Additional hardening for Windows shell escaping: - Escape % as %% to prevent variable expansion - Escape ^ as ^^ to prevent escape sequence interpretation - Replace newlines/tabs with space to prevent parsing errors
88 lines
2.6 KiB
TypeScript
88 lines
2.6 KiB
TypeScript
/**
|
|
* Shell Executor Utilities
|
|
*
|
|
* Cross-platform shell execution utilities for CCS.
|
|
*/
|
|
|
|
import { spawn, ChildProcess } from 'child_process';
|
|
import { ErrorManager } from './error-manager';
|
|
import { getWebSearchHookEnv } from './websearch-manager';
|
|
|
|
/**
|
|
* Escape arguments for shell execution (cross-platform)
|
|
*
|
|
* IMPORTANT: On Windows, spawn({ shell: true }) uses cmd.exe by default,
|
|
* NOT PowerShell. cmd.exe does NOT recognize single quotes as string delimiters.
|
|
* We must use double quotes for cmd.exe compatibility.
|
|
*/
|
|
export function escapeShellArg(arg: string): string {
|
|
const isWindows = process.platform === 'win32';
|
|
|
|
if (isWindows) {
|
|
// cmd.exe: Use double quotes, escape inner double quotes with backslash
|
|
// cmd.exe interprets "" as escaped double quote inside quoted string
|
|
// Strip newlines/tabs that can break cmd.exe parsing
|
|
return (
|
|
'"' +
|
|
String(arg)
|
|
.replace(/[\r\n\t]/g, ' ') // Replace newlines/tabs with space
|
|
.replace(/%/g, '%%') // Escape percent signs
|
|
.replace(/\^/g, '^^') // Escape carets
|
|
.replace(/"/g, '""') + // Escape quotes
|
|
'"'
|
|
);
|
|
} else {
|
|
// Unix/macOS: Double quotes with escaped inner quotes
|
|
return '"' + String(arg).replace(/"/g, '\\"') + '"';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Execute Claude CLI with unified spawn logic
|
|
*/
|
|
export function execClaude(
|
|
claudeCli: string,
|
|
args: string[],
|
|
envVars: NodeJS.ProcessEnv | null = null
|
|
): void {
|
|
const isWindows = process.platform === 'win32';
|
|
const needsShell = isWindows && /\.(cmd|bat|ps1)$/i.test(claudeCli);
|
|
|
|
// Get WebSearch hook config env vars
|
|
const webSearchEnv = getWebSearchHookEnv();
|
|
|
|
// Prepare environment (merge with process.env if envVars provided)
|
|
const env = envVars
|
|
? { ...process.env, ...envVars, ...webSearchEnv }
|
|
: { ...process.env, ...webSearchEnv };
|
|
|
|
let child: ChildProcess;
|
|
if (needsShell) {
|
|
// When shell needed: concatenate into string to avoid DEP0190 warning
|
|
const cmdString = [claudeCli, ...args].map(escapeShellArg).join(' ');
|
|
child = spawn(cmdString, {
|
|
stdio: 'inherit',
|
|
windowsHide: true,
|
|
shell: true,
|
|
env,
|
|
});
|
|
} else {
|
|
// When no shell needed: use array form (faster, no shell overhead)
|
|
child = spawn(claudeCli, args, {
|
|
stdio: 'inherit',
|
|
windowsHide: true,
|
|
env,
|
|
});
|
|
}
|
|
|
|
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);
|
|
});
|
|
}
|