mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-02 18:18:43 +00:00
fix: respect auto_update preference by disabling Claude auto-updater
This commit is contained in:
+8
-1
@@ -44,7 +44,12 @@ import { handleShellCompletionCommand } from './commands/shell-completion-comman
|
|||||||
import { handleUpdateCommand } from './commands/update-command';
|
import { handleUpdateCommand } from './commands/update-command';
|
||||||
|
|
||||||
// Import extracted utility functions
|
// Import extracted utility functions
|
||||||
import { execClaude, escapeShellArg, stripClaudeCodeEnv } from './utils/shell-executor';
|
import {
|
||||||
|
execClaude,
|
||||||
|
escapeShellArg,
|
||||||
|
stripClaudeCodeEnv,
|
||||||
|
getClaudeLaunchEnvOverrides,
|
||||||
|
} from './utils/shell-executor';
|
||||||
import { wireChildProcessSignals } from './utils/signal-forwarder';
|
import { wireChildProcessSignals } from './utils/signal-forwarder';
|
||||||
|
|
||||||
// Import target adapter system
|
// Import target adapter system
|
||||||
@@ -210,8 +215,10 @@ async function execClaudeWithProxy(
|
|||||||
const needsShell = isWindows && /\.(cmd|bat)$/i.test(claudeCli);
|
const needsShell = isWindows && /\.(cmd|bat)$/i.test(claudeCli);
|
||||||
const webSearchEnv = getWebSearchHookEnv();
|
const webSearchEnv = getWebSearchHookEnv();
|
||||||
const imageAnalysisEnv = getImageAnalysisHookEnv(profileName);
|
const imageAnalysisEnv = getImageAnalysisHookEnv(profileName);
|
||||||
|
const claudeLaunchEnv = getClaudeLaunchEnvOverrides();
|
||||||
const env = stripClaudeCodeEnv({
|
const env = stripClaudeCodeEnv({
|
||||||
...process.env,
|
...process.env,
|
||||||
|
...claudeLaunchEnv,
|
||||||
...envVars,
|
...envVars,
|
||||||
...webSearchEnv,
|
...webSearchEnv,
|
||||||
...imageAnalysisEnv,
|
...imageAnalysisEnv,
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { StreamBuffer, formatToolVerbose } from './executor/stream-parser';
|
|||||||
import { buildExecutionResult } from './executor/result-aggregator';
|
import { buildExecutionResult } from './executor/result-aggregator';
|
||||||
import { getCcsDir, getModelDisplayName } from '../utils/config-manager';
|
import { getCcsDir, getModelDisplayName } from '../utils/config-manager';
|
||||||
import { getProfileLookupCandidates } from '../utils/profile-compat';
|
import { getProfileLookupCandidates } from '../utils/profile-compat';
|
||||||
import { stripClaudeCodeEnv } from '../utils/shell-executor';
|
import { getClaudeLaunchEnvOverrides, stripClaudeCodeEnv } from '../utils/shell-executor';
|
||||||
|
|
||||||
// Re-export types for consumers
|
// Re-export types for consumers
|
||||||
export type { ExecutionOptions, ExecutionResult, StreamMessage } from './executor/types';
|
export type { ExecutionOptions, ExecutionResult, StreamMessage } from './executor/types';
|
||||||
@@ -210,7 +210,10 @@ export class HeadlessExecutor {
|
|||||||
|
|
||||||
// Strip Claude Code nested session guard env var to allow CCS delegation
|
// Strip Claude Code nested session guard env var to allow CCS delegation
|
||||||
// (Claude Code v2.1.39+ sets CLAUDECODE to detect nested sessions)
|
// (Claude Code v2.1.39+ sets CLAUDECODE to detect nested sessions)
|
||||||
const cleanEnv = stripClaudeCodeEnv(process.env);
|
const cleanEnv = stripClaudeCodeEnv({
|
||||||
|
...process.env,
|
||||||
|
...getClaudeLaunchEnvOverrides(),
|
||||||
|
});
|
||||||
|
|
||||||
const proc = spawn(claudeCli, args, {
|
const proc = spawn(claudeCli, args, {
|
||||||
cwd,
|
cwd,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { spawn, spawnSync, ChildProcess } from 'child_process';
|
|||||||
import { ErrorManager } from './error-manager';
|
import { ErrorManager } from './error-manager';
|
||||||
import { getWebSearchHookEnv } from './websearch-manager';
|
import { getWebSearchHookEnv } from './websearch-manager';
|
||||||
import { wireChildProcessSignals } from './signal-forwarder';
|
import { wireChildProcessSignals } from './signal-forwarder';
|
||||||
|
import { loadOrCreateUnifiedConfig } from '../config/unified-config-loader';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Strip ANTHROPIC_* env vars from an environment object.
|
* Strip ANTHROPIC_* env vars from an environment object.
|
||||||
@@ -40,6 +41,22 @@ export function stripClaudeCodeEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve CCS-managed environment overrides for Claude launch.
|
||||||
|
* - preferences.auto_update: false -> DISABLE_AUTOUPDATER=1
|
||||||
|
*/
|
||||||
|
export function getClaudeLaunchEnvOverrides(): NodeJS.ProcessEnv {
|
||||||
|
try {
|
||||||
|
const config = loadOrCreateUnifiedConfig();
|
||||||
|
if (config.preferences?.auto_update === false) {
|
||||||
|
return { DISABLE_AUTOUPDATER: '1' };
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Config read errors should never block Claude launch.
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Escape arguments for shell execution (cross-platform)
|
* Escape arguments for shell execution (cross-platform)
|
||||||
*
|
*
|
||||||
@@ -84,6 +101,7 @@ export function execClaude(
|
|||||||
|
|
||||||
// Get WebSearch hook config env vars
|
// Get WebSearch hook config env vars
|
||||||
const webSearchEnv = getWebSearchHookEnv();
|
const webSearchEnv = getWebSearchHookEnv();
|
||||||
|
const claudeLaunchEnv = getClaudeLaunchEnvOverrides();
|
||||||
|
|
||||||
// For account/default profiles, strip ANTHROPIC_* from parent env to prevent
|
// For account/default profiles, strip ANTHROPIC_* from parent env to prevent
|
||||||
// stale proxy config (e.g., from prior CLIProxy sessions) from interfering
|
// stale proxy config (e.g., from prior CLIProxy sessions) from interfering
|
||||||
@@ -97,8 +115,8 @@ export function execClaude(
|
|||||||
|
|
||||||
// Prepare environment (merge with base env if envVars provided)
|
// Prepare environment (merge with base env if envVars provided)
|
||||||
const mergedEnv = envVars
|
const mergedEnv = envVars
|
||||||
? { ...baseEnv, ...envVars, ...webSearchEnv }
|
? { ...baseEnv, ...claudeLaunchEnv, ...envVars, ...webSearchEnv }
|
||||||
: { ...baseEnv, ...webSearchEnv };
|
: { ...baseEnv, ...claudeLaunchEnv, ...webSearchEnv };
|
||||||
|
|
||||||
// Strip Claude Code nested session guard env var to allow CCS delegation
|
// Strip Claude Code nested session guard env var to allow CCS delegation
|
||||||
// (Claude Code v2.1.39+ sets CLAUDECODE to detect nested sessions)
|
// (Claude Code v2.1.39+ sets CLAUDECODE to detect nested sessions)
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, mock } from 'bun:test';
|
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, mock } from 'bun:test';
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import * as childProcess from 'child_process';
|
import * as childProcess from 'child_process';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as os from 'os';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
type SpawnCall = {
|
type SpawnCall = {
|
||||||
command: string;
|
command: string;
|
||||||
@@ -13,6 +16,8 @@ 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> = [];
|
||||||
let baselineSighupListeners: Array<(...args: unknown[]) => void> = [];
|
let baselineSighupListeners: Array<(...args: unknown[]) => void> = [];
|
||||||
|
let originalCcsHome: string | undefined;
|
||||||
|
let originalDisableAutoUpdater: string | undefined;
|
||||||
const realSpawn = childProcess.spawn.bind(childProcess);
|
const realSpawn = childProcess.spawn.bind(childProcess);
|
||||||
const realSpawnSync = childProcess.spawnSync.bind(childProcess);
|
const realSpawnSync = childProcess.spawnSync.bind(childProcess);
|
||||||
const realExecSync = childProcess.execSync.bind(childProcess);
|
const realExecSync = childProcess.execSync.bind(childProcess);
|
||||||
@@ -95,6 +100,18 @@ function registerChildProcessMock(): void {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function writeConfigWithAutoUpdatePreference(enabled: boolean): void {
|
||||||
|
const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-auto-update-pref-'));
|
||||||
|
process.env.CCS_HOME = tempHome;
|
||||||
|
const ccsDir = path.join(tempHome, '.ccs');
|
||||||
|
fs.mkdirSync(ccsDir, { recursive: true });
|
||||||
|
const yaml = `version: 8
|
||||||
|
preferences:
|
||||||
|
auto_update: ${enabled ? 'true' : 'false'}
|
||||||
|
`;
|
||||||
|
fs.writeFileSync(path.join(ccsDir, 'config.yaml'), yaml, 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
let execClaude: typeof import('../../../src/utils/shell-executor').execClaude;
|
let execClaude: typeof import('../../../src/utils/shell-executor').execClaude;
|
||||||
let stripClaudeCodeEnv: typeof import('../../../src/utils/shell-executor').stripClaudeCodeEnv;
|
let stripClaudeCodeEnv: typeof import('../../../src/utils/shell-executor').stripClaudeCodeEnv;
|
||||||
let HeadlessExecutor: typeof import('../../../src/delegation/headless-executor').HeadlessExecutor;
|
let HeadlessExecutor: typeof import('../../../src/delegation/headless-executor').HeadlessExecutor;
|
||||||
@@ -118,6 +135,9 @@ describe('CLAUDECODE environment stripping', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spawnCalls.length = 0;
|
spawnCalls.length = 0;
|
||||||
process.env.CCS_QUIET = '1';
|
process.env.CCS_QUIET = '1';
|
||||||
|
originalCcsHome = process.env.CCS_HOME;
|
||||||
|
originalDisableAutoUpdater = process.env.DISABLE_AUTOUPDATER;
|
||||||
|
delete process.env.DISABLE_AUTOUPDATER;
|
||||||
baselineSigintListeners = process.listeners('SIGINT');
|
baselineSigintListeners = process.listeners('SIGINT');
|
||||||
baselineSigtermListeners = process.listeners('SIGTERM');
|
baselineSigtermListeners = process.listeners('SIGTERM');
|
||||||
baselineSighupListeners = process.listeners('SIGHUP');
|
baselineSighupListeners = process.listeners('SIGHUP');
|
||||||
@@ -128,6 +148,13 @@ describe('CLAUDECODE environment stripping', () => {
|
|||||||
delete process.env.CLAUDECODE;
|
delete process.env.CLAUDECODE;
|
||||||
delete process.env.claudecode;
|
delete process.env.claudecode;
|
||||||
delete process.env.CCS_QUIET;
|
delete process.env.CCS_QUIET;
|
||||||
|
if (originalCcsHome !== undefined) process.env.CCS_HOME = originalCcsHome;
|
||||||
|
else delete process.env.CCS_HOME;
|
||||||
|
if (originalDisableAutoUpdater !== undefined) {
|
||||||
|
process.env.DISABLE_AUTOUPDATER = originalDisableAutoUpdater;
|
||||||
|
} else {
|
||||||
|
delete process.env.DISABLE_AUTOUPDATER;
|
||||||
|
}
|
||||||
|
|
||||||
for (const listener of process.listeners('SIGINT')) {
|
for (const listener of process.listeners('SIGINT')) {
|
||||||
if (!baselineSigintListeners.includes(listener)) {
|
if (!baselineSigintListeners.includes(listener)) {
|
||||||
@@ -197,7 +224,26 @@ describe('CLAUDECODE environment stripping', () => {
|
|||||||
expect(Object.keys(env).map((k) => k.toUpperCase())).not.toContain('CLAUDECODE');
|
expect(Object.keys(env).map((k) => k.toUpperCase())).not.toContain('CLAUDECODE');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('execClaude sets DISABLE_AUTOUPDATER=1 when preferences.auto_update is false', () => {
|
||||||
|
writeConfigWithAutoUpdatePreference(false);
|
||||||
|
execClaude('claude', ['--version'], { CCS_PROFILE_TYPE: 'default' });
|
||||||
|
|
||||||
|
expect(spawnCalls.length).toBeGreaterThan(0);
|
||||||
|
const env = spawnCalls[0].options?.env as NodeJS.ProcessEnv;
|
||||||
|
expect(env.DISABLE_AUTOUPDATER).toBe('1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('execClaude does not force DISABLE_AUTOUPDATER when preferences.auto_update is true', () => {
|
||||||
|
writeConfigWithAutoUpdatePreference(true);
|
||||||
|
execClaude('claude', ['--version'], { CCS_PROFILE_TYPE: 'default' });
|
||||||
|
|
||||||
|
expect(spawnCalls.length).toBeGreaterThan(0);
|
||||||
|
const env = spawnCalls[0].options?.env as NodeJS.ProcessEnv;
|
||||||
|
expect(env.DISABLE_AUTOUPDATER).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
it('headless executor spawn path strips CLAUDECODE before spawn', async () => {
|
it('headless executor spawn path strips CLAUDECODE before spawn', async () => {
|
||||||
|
writeConfigWithAutoUpdatePreference(false);
|
||||||
process.env.CLAUDECODE = 'nested';
|
process.env.CLAUDECODE = 'nested';
|
||||||
process.env.claudecode = 'nested-lower';
|
process.env.claudecode = 'nested-lower';
|
||||||
|
|
||||||
@@ -237,5 +283,6 @@ describe('CLAUDECODE environment stripping', () => {
|
|||||||
expect(spawnCalls.length).toBeGreaterThan(0);
|
expect(spawnCalls.length).toBeGreaterThan(0);
|
||||||
const env = spawnCalls[0].options?.env as NodeJS.ProcessEnv;
|
const env = spawnCalls[0].options?.env as NodeJS.ProcessEnv;
|
||||||
expect(Object.keys(env).map((k) => k.toUpperCase())).not.toContain('CLAUDECODE');
|
expect(Object.keys(env).map((k) => k.toUpperCase())).not.toContain('CLAUDECODE');
|
||||||
|
expect(env.DISABLE_AUTOUPDATER).toBe('1');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user