mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 06:16:37 +00:00
When CCS launches Claude with CLAUDE_CONFIG_DIR pointing to the isolated
instance path, agent team teammates spawned via tmux split-window don't
inherit it because tmux creates new panes from its own session environment.
This causes teammates to use ~/.claude/ instead of ~/.ccs/instances/{profile}/,
creating a split-brain in shared state (tasks, teams, mailbox).
Sets key CCS env vars in the tmux session environment via `tmux setenv` so
all new panes inherit the correct config directory.
129 lines
4.1 KiB
TypeScript
129 lines
4.1 KiB
TypeScript
/**
|
|
* Shell Executor Utilities
|
|
*
|
|
* Cross-platform shell execution utilities for CCS.
|
|
*/
|
|
|
|
import { spawn, ChildProcess, execSync } from 'child_process';
|
|
import { ErrorManager } from './error-manager';
|
|
import { getWebSearchHookEnv } from './websearch-manager';
|
|
|
|
/**
|
|
* Strip ANTHROPIC_* env vars from an environment object.
|
|
* Used for account/default profiles to prevent stale proxy config from
|
|
* interfering with native Claude API routing.
|
|
*/
|
|
export function stripAnthropicEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
|
const result: NodeJS.ProcessEnv = {};
|
|
for (const key of Object.keys(env)) {
|
|
if (!key.startsWith('ANTHROPIC_')) {
|
|
result[key] = env[key];
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* 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 by doubling them
|
|
// 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 exclamation marks (delayed expansion)
|
|
.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();
|
|
|
|
// For account/default profiles, strip ANTHROPIC_* from parent env to prevent
|
|
// stale proxy config (e.g., from prior CLIProxy sessions) from interfering
|
|
// with native Claude API routing. Settings-based profiles explicitly inject
|
|
// their own ANTHROPIC_* values, so they don't need this protection.
|
|
const profileType = envVars?.CCS_PROFILE_TYPE;
|
|
const baseEnv =
|
|
profileType === 'account' || profileType === 'default'
|
|
? stripAnthropicEnv(process.env)
|
|
: process.env;
|
|
|
|
// Prepare environment (merge with base env if envVars provided)
|
|
const env = envVars
|
|
? { ...baseEnv, ...envVars, ...webSearchEnv }
|
|
: { ...baseEnv, ...webSearchEnv };
|
|
|
|
// propagate key env vars to tmux session so agent team teammates
|
|
// (spawned via tmux split-window) inherit the correct config dir
|
|
if (process.env.TMUX && envVars) {
|
|
const tmuxPropagateVars = ['CLAUDE_CONFIG_DIR', 'CCS_PROFILE_TYPE', 'CCS_WEBSEARCH_SKIP'];
|
|
for (const key of tmuxPropagateVars) {
|
|
if (envVars[key]) {
|
|
try {
|
|
execSync(`tmux setenv ${key} ${JSON.stringify(envVars[key])}`, { stdio: 'ignore' });
|
|
} catch {
|
|
// tmux setenv can fail if not in a tmux session; safe to ignore
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
});
|
|
}
|