mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-02 08:19:59 +00:00
fix(proxy): clean up temp launch settings
This commit is contained in:
+4
-4
@@ -82,7 +82,7 @@ import { tryHandleRootCommand } from './commands/root-command-router';
|
|||||||
// Import extracted utility functions
|
// Import extracted utility functions
|
||||||
import { execClaude, stripAnthropicRoutingEnv, stripBrowserEnv } from './utils/shell-executor';
|
import { execClaude, stripAnthropicRoutingEnv, stripBrowserEnv } from './utils/shell-executor';
|
||||||
import { isDeprecatedGlmtProfileName, normalizeDeprecatedGlmtEnv } from './utils/glmt-deprecation';
|
import { isDeprecatedGlmtProfileName, normalizeDeprecatedGlmtEnv } from './utils/glmt-deprecation';
|
||||||
import { createOpenAICompatLaunchSettingsPath } from './utils/openai-compat-launch-settings';
|
import { createOpenAICompatLaunchSettings } from './utils/openai-compat-launch-settings';
|
||||||
import { maybeWarnAboutResumeLaneMismatch } from './auth/resume-lane-warning';
|
import { maybeWarnAboutResumeLaneMismatch } from './auth/resume-lane-warning';
|
||||||
import { createLogger } from './services/logging';
|
import { createLogger } from './services/logging';
|
||||||
import { buildCodexBrowserMcpOverrides } from './utils/browser-codex-overrides';
|
import { buildCodexBrowserMcpOverrides } from './utils/browser-codex-overrides';
|
||||||
@@ -1460,14 +1460,14 @@ async function main(): Promise<void> {
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
delete proxyEnv.ANTHROPIC_API_KEY;
|
delete proxyEnv.ANTHROPIC_API_KEY;
|
||||||
const launchSettingsPath = createOpenAICompatLaunchSettingsPath(
|
const launchSettings = createOpenAICompatLaunchSettings(
|
||||||
expandedSettingsPath,
|
expandedSettingsPath,
|
||||||
settings
|
settings
|
||||||
);
|
);
|
||||||
|
|
||||||
const launchArgs = [
|
const launchArgs = [
|
||||||
'--settings',
|
'--settings',
|
||||||
launchSettingsPath,
|
launchSettings.settingsPath,
|
||||||
...appendThirdPartyWebSearchToolArgs(browserArgs),
|
...appendThirdPartyWebSearchToolArgs(browserArgs),
|
||||||
];
|
];
|
||||||
const traceEnv = createWebSearchTraceContext({
|
const traceEnv = createWebSearchTraceContext({
|
||||||
@@ -1478,7 +1478,7 @@ async function main(): Promise<void> {
|
|||||||
settingsPath: expandedSettingsPath,
|
settingsPath: expandedSettingsPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
execClaude(claudeCli, launchArgs, { ...proxyEnv, ...traceEnv });
|
execClaude(claudeCli, launchArgs, { ...proxyEnv, ...traceEnv }, launchSettings.cleanup);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const launchArgs = [
|
const launchArgs = [
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import {
|
|||||||
stripAnthropicRoutingEnv,
|
stripAnthropicRoutingEnv,
|
||||||
stripClaudeCodeEnv,
|
stripClaudeCodeEnv,
|
||||||
} from '../utils/shell-executor';
|
} from '../utils/shell-executor';
|
||||||
import { createOpenAICompatLaunchSettingsPath } from '../utils/openai-compat-launch-settings';
|
import { createOpenAICompatLaunchSettings } from '../utils/openai-compat-launch-settings';
|
||||||
import { resolveProfileContinuityInheritance } from '../auth/profile-continuity-inheritance';
|
import { resolveProfileContinuityInheritance } from '../auth/profile-continuity-inheritance';
|
||||||
import {
|
import {
|
||||||
appendThirdPartyImageAnalysisToolArgs,
|
appendThirdPartyImageAnalysisToolArgs,
|
||||||
@@ -256,12 +256,12 @@ export class HeadlessExecutor {
|
|||||||
// Smart slash command detection and preservation
|
// Smart slash command detection and preservation
|
||||||
const processedPrompt = this._processSlashCommand(enhancedPrompt);
|
const processedPrompt = this._processSlashCommand(enhancedPrompt);
|
||||||
|
|
||||||
const launchSettingsPath = openAICompatProfile
|
const launchSettings = openAICompatProfile
|
||||||
? createOpenAICompatLaunchSettingsPath(settingsPath, settings)
|
? createOpenAICompatLaunchSettings(settingsPath, settings)
|
||||||
: settingsPath;
|
: { settingsPath, cleanup: () => {} };
|
||||||
|
|
||||||
// Prepare arguments
|
// Prepare arguments
|
||||||
const args: string[] = ['-p', processedPrompt, '--settings', launchSettingsPath];
|
const args: string[] = ['-p', processedPrompt, '--settings', launchSettings.settingsPath];
|
||||||
|
|
||||||
// Always use stream-json for real-time progress visibility
|
// Always use stream-json for real-time progress visibility
|
||||||
args.push('--output-format', 'stream-json', '--verbose');
|
args.push('--output-format', 'stream-json', '--verbose');
|
||||||
@@ -373,6 +373,7 @@ export class HeadlessExecutor {
|
|||||||
imageAnalysisEnv,
|
imageAnalysisEnv,
|
||||||
runtimeEnvVars,
|
runtimeEnvVars,
|
||||||
traceEnv,
|
traceEnv,
|
||||||
|
launchCleanup: launchSettings.cleanup,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -393,6 +394,7 @@ export class HeadlessExecutor {
|
|||||||
imageAnalysisEnv?: Record<string, string>;
|
imageAnalysisEnv?: Record<string, string>;
|
||||||
runtimeEnvVars?: NodeJS.ProcessEnv;
|
runtimeEnvVars?: NodeJS.ProcessEnv;
|
||||||
traceEnv?: Record<string, string>;
|
traceEnv?: Record<string, string>;
|
||||||
|
launchCleanup?: () => void;
|
||||||
}
|
}
|
||||||
): Promise<ExecutionResult> {
|
): Promise<ExecutionResult> {
|
||||||
const {
|
const {
|
||||||
@@ -406,6 +408,7 @@ export class HeadlessExecutor {
|
|||||||
imageAnalysisEnv = {},
|
imageAnalysisEnv = {},
|
||||||
runtimeEnvVars = {},
|
runtimeEnvVars = {},
|
||||||
traceEnv = {},
|
traceEnv = {},
|
||||||
|
launchCleanup = () => {},
|
||||||
} = ctx;
|
} = ctx;
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -443,6 +446,14 @@ export class HeadlessExecutor {
|
|||||||
let progressInterval: NodeJS.Timeout | undefined;
|
let progressInterval: NodeJS.Timeout | undefined;
|
||||||
const messages: StreamMessage[] = [];
|
const messages: StreamMessage[] = [];
|
||||||
let timedOut = false;
|
let timedOut = false;
|
||||||
|
let cleanedUp = false;
|
||||||
|
const cleanupLaunchArtifacts = () => {
|
||||||
|
if (cleanedUp) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cleanedUp = true;
|
||||||
|
launchCleanup();
|
||||||
|
};
|
||||||
|
|
||||||
// Setup signal handlers for cleanup
|
// Setup signal handlers for cleanup
|
||||||
const cleanupHandler = () => {
|
const cleanupHandler = () => {
|
||||||
@@ -501,6 +512,7 @@ export class HeadlessExecutor {
|
|||||||
|
|
||||||
// Handle completion
|
// Handle completion
|
||||||
proc.on('close', (exitCode: number | null) => {
|
proc.on('close', (exitCode: number | null) => {
|
||||||
|
cleanupLaunchArtifacts();
|
||||||
const duration = Date.now() - startTime;
|
const duration = Date.now() - startTime;
|
||||||
|
|
||||||
if (progressInterval) {
|
if (progressInterval) {
|
||||||
@@ -593,6 +605,7 @@ export class HeadlessExecutor {
|
|||||||
|
|
||||||
// Handle errors
|
// Handle errors
|
||||||
proc.on('error', (error: Error) => {
|
proc.on('error', (error: Error) => {
|
||||||
|
cleanupLaunchArtifacts();
|
||||||
if (progressInterval) clearInterval(progressInterval);
|
if (progressInterval) clearInterval(progressInterval);
|
||||||
reject(new Error(`Failed to execute Claude CLI: ${error.message}`));
|
reject(new Error(`Failed to execute Claude CLI: ${error.message}`));
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,10 +5,15 @@ import * as path from 'path';
|
|||||||
import type { Settings } from '../types/config';
|
import type { Settings } from '../types/config';
|
||||||
import { stripAnthropicRoutingEnv } from './shell-executor';
|
import { stripAnthropicRoutingEnv } from './shell-executor';
|
||||||
|
|
||||||
export function createOpenAICompatLaunchSettingsPath(
|
export interface OpenAICompatLaunchSettings {
|
||||||
|
settingsPath: string;
|
||||||
|
cleanup: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createOpenAICompatLaunchSettings(
|
||||||
settingsPath: string,
|
settingsPath: string,
|
||||||
settings: Settings
|
settings: Settings
|
||||||
): string {
|
): OpenAICompatLaunchSettings {
|
||||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-openai-compat-settings-'));
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-openai-compat-settings-'));
|
||||||
fs.chmodSync(tempDir, 0o700);
|
fs.chmodSync(tempDir, 0o700);
|
||||||
|
|
||||||
@@ -31,5 +36,17 @@ export function createOpenAICompatLaunchSettingsPath(
|
|||||||
mode: 0o600,
|
mode: 0o600,
|
||||||
});
|
});
|
||||||
|
|
||||||
return launchSettingsPath;
|
let cleanedUp = false;
|
||||||
|
const cleanup = (): void => {
|
||||||
|
if (cleanedUp) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cleanedUp = true;
|
||||||
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
settingsPath: launchSettingsPath,
|
||||||
|
cleanup,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -208,7 +208,8 @@ export function getWindowsEscapedCommandShell(): SpawnOptions['shell'] {
|
|||||||
export function execClaude(
|
export function execClaude(
|
||||||
claudeCli: string,
|
claudeCli: string,
|
||||||
args: string[],
|
args: string[],
|
||||||
envVars: NodeJS.ProcessEnv | null = null
|
envVars: NodeJS.ProcessEnv | null = null,
|
||||||
|
onExitCleanup?: () => void
|
||||||
): void {
|
): void {
|
||||||
const isWindows = process.platform === 'win32';
|
const isWindows = process.platform === 'win32';
|
||||||
const isPowerShellScript = isWindows && /\.ps1$/i.test(claudeCli);
|
const isPowerShellScript = isWindows && /\.ps1$/i.test(claudeCli);
|
||||||
@@ -286,6 +287,17 @@ export function execClaude(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let cleanedUp = false;
|
||||||
|
const runExitCleanup = (): void => {
|
||||||
|
if (cleanedUp) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cleanedUp = true;
|
||||||
|
onExitCleanup?.();
|
||||||
|
};
|
||||||
|
child.once('exit', runExitCleanup);
|
||||||
|
child.once('error', runExitCleanup);
|
||||||
|
|
||||||
wireChildProcessSignals(child, async (err: NodeJS.ErrnoException) => {
|
wireChildProcessSignals(child, async (err: NodeJS.ErrnoException) => {
|
||||||
if (err.code === 'EACCES') {
|
if (err.code === 'EACCES') {
|
||||||
console.error(`[X] Claude CLI is not executable: ${claudeCli}`);
|
console.error(`[X] Claude CLI is not executable: ${claudeCli}`);
|
||||||
|
|||||||
@@ -92,6 +92,8 @@ describe('settings profile browser launch', () => {
|
|||||||
let fakeClaudePath = '';
|
let fakeClaudePath = '';
|
||||||
let claudeArgsLogPath = '';
|
let claudeArgsLogPath = '';
|
||||||
let claudeEnvLogPath = '';
|
let claudeEnvLogPath = '';
|
||||||
|
let claudeSettingsPathLogPath = '';
|
||||||
|
let claudeSettingsSnapshotPath = '';
|
||||||
let browserProfileDir = '';
|
let browserProfileDir = '';
|
||||||
let devtoolsServer: ChildProcess | undefined;
|
let devtoolsServer: ChildProcess | undefined;
|
||||||
let baseEnv: NodeJS.ProcessEnv;
|
let baseEnv: NodeJS.ProcessEnv;
|
||||||
@@ -107,6 +109,8 @@ describe('settings profile browser launch', () => {
|
|||||||
fakeClaudePath = path.join(tmpHome, 'fake-claude.sh');
|
fakeClaudePath = path.join(tmpHome, 'fake-claude.sh');
|
||||||
claudeArgsLogPath = path.join(tmpHome, 'claude-args.txt');
|
claudeArgsLogPath = path.join(tmpHome, 'claude-args.txt');
|
||||||
claudeEnvLogPath = path.join(tmpHome, 'claude-env.txt');
|
claudeEnvLogPath = path.join(tmpHome, 'claude-env.txt');
|
||||||
|
claudeSettingsPathLogPath = path.join(tmpHome, 'claude-settings-path.txt');
|
||||||
|
claudeSettingsSnapshotPath = path.join(tmpHome, 'claude-settings-snapshot.json');
|
||||||
browserProfileDir = path.join(tmpHome, 'chrome-user-data');
|
browserProfileDir = path.join(tmpHome, 'chrome-user-data');
|
||||||
|
|
||||||
fs.mkdirSync(ccsDir, { recursive: true });
|
fs.mkdirSync(ccsDir, { recursive: true });
|
||||||
@@ -138,6 +142,19 @@ describe('settings profile browser launch', () => {
|
|||||||
fakeClaudePath,
|
fakeClaudePath,
|
||||||
`#!/bin/sh
|
`#!/bin/sh
|
||||||
printf "%s\n" "$@" > "${claudeArgsLogPath}"
|
printf "%s\n" "$@" > "${claudeArgsLogPath}"
|
||||||
|
settings_path=""
|
||||||
|
prev=""
|
||||||
|
for arg in "$@"; do
|
||||||
|
if [ "$prev" = "--settings" ]; then
|
||||||
|
settings_path="$arg"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
prev="$arg"
|
||||||
|
done
|
||||||
|
printf "%s" "$settings_path" > "${claudeSettingsPathLogPath}"
|
||||||
|
if [ -n "$settings_path" ] && [ -f "$settings_path" ]; then
|
||||||
|
cat "$settings_path" > "${claudeSettingsSnapshotPath}"
|
||||||
|
fi
|
||||||
{
|
{
|
||||||
printf "userDataDir=%s\n" "$CCS_BROWSER_USER_DATA_DIR"
|
printf "userDataDir=%s\n" "$CCS_BROWSER_USER_DATA_DIR"
|
||||||
printf "legacyProfileDir=%s\n" "$CCS_BROWSER_PROFILE_DIR"
|
printf "legacyProfileDir=%s\n" "$CCS_BROWSER_PROFILE_DIR"
|
||||||
@@ -308,7 +325,7 @@ exit 0
|
|||||||
expect(launchSettingsPath).not.toBe(settingsPath);
|
expect(launchSettingsPath).not.toBe(settingsPath);
|
||||||
|
|
||||||
const persistedLaunchSettings = JSON.parse(
|
const persistedLaunchSettings = JSON.parse(
|
||||||
fs.readFileSync(launchSettingsPath as string, 'utf8')
|
fs.readFileSync(claudeSettingsSnapshotPath, 'utf8')
|
||||||
) as {
|
) as {
|
||||||
env?: Record<string, string>;
|
env?: Record<string, string>;
|
||||||
hooks?: {
|
hooks?: {
|
||||||
@@ -333,6 +350,8 @@ exit 0
|
|||||||
expect(launchedEnv).not.toContain('anthropicBaseUrl=https://api.openai.com/v1');
|
expect(launchedEnv).not.toContain('anthropicBaseUrl=https://api.openai.com/v1');
|
||||||
expect(launchedEnv).toContain('anthropicModel=gpt-5.4');
|
expect(launchedEnv).toContain('anthropicModel=gpt-5.4');
|
||||||
expect(launchedEnv).toContain('maxOutputTokens=12345');
|
expect(launchedEnv).toContain('maxOutputTokens=12345');
|
||||||
|
expect(fs.readFileSync(claudeSettingsPathLogPath, 'utf8')).toBe(launchSettingsPath);
|
||||||
|
expect(fs.existsSync(launchSettingsPath as string)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not auto-enable browser reuse for settings-profile launches from env overrides alone', async () => {
|
it('does not auto-enable browser reuse for settings-profile launches from env overrides alone', async () => {
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ const STEERING_PROMPT_SNIPPET =
|
|||||||
'prefer the CCS MCP tool WebSearch instead of Bash/curl/http fetches';
|
'prefer the CCS MCP tool WebSearch instead of Bash/curl/http fetches';
|
||||||
const spawnCalls: SpawnCall[] = [];
|
const spawnCalls: SpawnCall[] = [];
|
||||||
const spawnSyncCalls: SpawnSyncCall[] = [];
|
const spawnSyncCalls: SpawnSyncCall[] = [];
|
||||||
|
const launchSettingsSnapshots: Array<{ path: string; content: string }> = [];
|
||||||
const originalPlatform = process.platform;
|
const originalPlatform = process.platform;
|
||||||
let baselineSigintListeners: Array<(...args: unknown[]) => void> = [];
|
let baselineSigintListeners: Array<(...args: unknown[]) => void> = [];
|
||||||
let baselineSigtermListeners: Array<(...args: unknown[]) => void> = [];
|
let baselineSigtermListeners: Array<(...args: unknown[]) => void> = [];
|
||||||
@@ -99,6 +100,16 @@ function registerChildProcessMock(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
spawnCalls.push({ command, args, options });
|
spawnCalls.push({ command, args, options });
|
||||||
|
const settingsIndex = args.indexOf('--settings');
|
||||||
|
if (settingsIndex >= 0) {
|
||||||
|
const settingsPath = args[settingsIndex + 1];
|
||||||
|
if (settingsPath && fs.existsSync(settingsPath)) {
|
||||||
|
launchSettingsSnapshots.push({
|
||||||
|
path: settingsPath,
|
||||||
|
content: fs.readFileSync(settingsPath, 'utf8'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const child = createMockChild();
|
const child = createMockChild();
|
||||||
setTimeout(() => child.emit('close', 0), 0);
|
setTimeout(() => child.emit('close', 0), 0);
|
||||||
@@ -193,6 +204,7 @@ describe('CLAUDECODE environment stripping', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spawnCalls.length = 0;
|
spawnCalls.length = 0;
|
||||||
spawnSyncCalls.length = 0;
|
spawnSyncCalls.length = 0;
|
||||||
|
launchSettingsSnapshots.length = 0;
|
||||||
process.env.CCS_QUIET = '1';
|
process.env.CCS_QUIET = '1';
|
||||||
|
|
||||||
// Save original env values for restoration in afterEach
|
// Save original env values for restoration in afterEach
|
||||||
@@ -695,8 +707,12 @@ describe('CLAUDECODE environment stripping', () => {
|
|||||||
const launchSettingsPath = args[settingsIndex + 1];
|
const launchSettingsPath = args[settingsIndex + 1];
|
||||||
expect(launchSettingsPath).toBeDefined();
|
expect(launchSettingsPath).toBeDefined();
|
||||||
expect(launchSettingsPath).not.toBe(path.join(ccsDir, 'bridge.settings.json'));
|
expect(launchSettingsPath).not.toBe(path.join(ccsDir, 'bridge.settings.json'));
|
||||||
|
const launchSettingsSnapshot = launchSettingsSnapshots.find(
|
||||||
|
(snapshot) => snapshot.path === launchSettingsPath
|
||||||
|
);
|
||||||
|
expect(launchSettingsSnapshot).toBeDefined();
|
||||||
|
|
||||||
const persistedLaunchSettings = JSON.parse(fs.readFileSync(launchSettingsPath, 'utf8')) as {
|
const persistedLaunchSettings = JSON.parse(launchSettingsSnapshot?.content || '{}') as {
|
||||||
env?: Record<string, string>;
|
env?: Record<string, string>;
|
||||||
hooks?: {
|
hooks?: {
|
||||||
PreToolUse?: Array<{
|
PreToolUse?: Array<{
|
||||||
@@ -713,6 +729,7 @@ describe('CLAUDECODE environment stripping', () => {
|
|||||||
expect(
|
expect(
|
||||||
persistedLaunchSettings.hooks?.PreToolUse?.[0]?.hooks?.[0]?.command
|
persistedLaunchSettings.hooks?.PreToolUse?.[0]?.hooks?.[0]?.command
|
||||||
).toBe('echo headless-bridge-hook');
|
).toBe('echo headless-bridge-hook');
|
||||||
|
expect(fs.existsSync(launchSettingsPath)).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('headless executor prepares image-analysis MCP and suppresses the legacy hook on healthy launches', async () => {
|
it('headless executor prepares image-analysis MCP and suppresses the legacy hook on healthy launches', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user