mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 12:15:57 +00:00
fix(cliproxy): ignore stale executor exit code
This commit is contained in:
@@ -62,6 +62,15 @@ describe('execClaudeWithCLIProxy browser flag validation', () => {
|
||||
process.env.CCS_HOME = tmpHome;
|
||||
});
|
||||
|
||||
async function waitForFile(filePath: string): Promise<boolean> {
|
||||
const deadline = Date.now() + 2000;
|
||||
while (Date.now() < deadline) {
|
||||
if (fs.existsSync(filePath)) return true;
|
||||
await new Promise((resolve) => setTimeout(resolve, 25));
|
||||
}
|
||||
return fs.existsSync(filePath);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
if (originalCcsHome !== undefined) {
|
||||
process.env.CCS_HOME = originalCcsHome;
|
||||
@@ -145,4 +154,69 @@ describe('execClaudeWithCLIProxy browser flag validation', () => {
|
||||
errorSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it('does not treat a stale global exitCode as a current parse failure', async () => {
|
||||
const markerPath = path.join(tmpHome, 'fake-claude-launched');
|
||||
fs.writeFileSync(
|
||||
fakeClaudePath,
|
||||
`#!/bin/sh\nprintf launched > ${JSON.stringify(markerPath)}\nexit 0\n`,
|
||||
{ mode: 0o755 }
|
||||
);
|
||||
fs.chmodSync(fakeClaudePath, 0o755);
|
||||
|
||||
let requestCount = 0;
|
||||
const server = http.createServer((_req, res) => {
|
||||
requestCount += 1;
|
||||
res.writeHead(200, { 'content-type': 'application/json' });
|
||||
res.end('{"ok":true}');
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
server.listen(0, '127.0.0.1', resolve);
|
||||
});
|
||||
|
||||
const address = server.address();
|
||||
if (!address || typeof address === 'string') {
|
||||
server.close();
|
||||
throw new Error('Test server did not bind to a TCP port');
|
||||
}
|
||||
|
||||
const exitSpy = jest
|
||||
.spyOn(process, 'exit')
|
||||
.mockImplementation((() => undefined as never) as typeof process.exit);
|
||||
const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
|
||||
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
try {
|
||||
process.exitCode = 1;
|
||||
|
||||
await execClaudeWithCLIProxy(
|
||||
fakeClaudePath,
|
||||
'gemini',
|
||||
[
|
||||
'--proxy-host',
|
||||
'127.0.0.1',
|
||||
'--proxy-port',
|
||||
String(address.port),
|
||||
'--proxy-auth-token',
|
||||
'SECRET_TOKEN_FOR_VALIDATION',
|
||||
'--remote-only',
|
||||
'--print',
|
||||
'hello',
|
||||
],
|
||||
{}
|
||||
);
|
||||
|
||||
expect(await waitForFile(markerPath)).toBe(true);
|
||||
expect(requestCount).toBeGreaterThan(0);
|
||||
expect(exitSpy).toHaveBeenCalledWith(0);
|
||||
} finally {
|
||||
exitSpy.mockRestore();
|
||||
logSpy.mockRestore();
|
||||
errorSpy.mockRestore();
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.close((error) => (error ? reject(error) : resolve()));
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,7 +8,9 @@
|
||||
* - parseExecutorFlags() — flag extraction block (lines ~411-639 in original)
|
||||
* - validateFlagCombinations() — cross-flag guard block (lines ~531-585)
|
||||
*
|
||||
* IMPORTANT: process.exit semantics are kept identical to original index.ts.
|
||||
* IMPORTANT: process.exit semantics are kept identical to original index.ts,
|
||||
* with explicit parseFailed/validation return state for callers that must not
|
||||
* depend on ambient process.exitCode.
|
||||
* All console.error messages are byte-identical.
|
||||
*/
|
||||
|
||||
@@ -153,6 +155,7 @@ export function filterCcsFlags(args: string[]): string[] {
|
||||
|
||||
/** Result of parsing CCS executor flags from args. */
|
||||
export interface ParsedExecutorFlags {
|
||||
parseFailed?: boolean;
|
||||
forceAuth: boolean;
|
||||
pasteCallback: boolean;
|
||||
portForward: boolean;
|
||||
@@ -181,7 +184,7 @@ export interface ParsedExecutorFlags {
|
||||
/**
|
||||
* Parse all CCS executor flags from args.
|
||||
*
|
||||
* Exits with code 1 (process.exitCode = 1 + return) on invalid flag values.
|
||||
* Exits with code 1 (process.exitCode = 1 + parseFailed return) on invalid flag values.
|
||||
* Exits with process.exit(1) on conflicting flag combinations — identical to
|
||||
* the original index.ts behavior.
|
||||
*
|
||||
@@ -247,7 +250,7 @@ export function parseExecutorFlags(
|
||||
console.error(fail('--kiro-auth-method requires a value'));
|
||||
console.error(' Supported values: aws, aws-authcode, google, github, idc');
|
||||
process.exitCode = 1;
|
||||
// Caller must check process.exitCode = 1 and bail — matching original return behavior
|
||||
// Caller must check parseFailed and bail — matching original return behavior
|
||||
return buildPartialFlags({
|
||||
forceAuth,
|
||||
pasteCallback,
|
||||
@@ -272,6 +275,7 @@ export function parseExecutorFlags(
|
||||
gitlabBaseUrl: undefined,
|
||||
extendedContextOverride: undefined,
|
||||
thinkingParse: parseThinkingOverride(args),
|
||||
parseFailed: true,
|
||||
});
|
||||
}
|
||||
const normalized = rawMethod.trim().toLowerCase();
|
||||
@@ -303,6 +307,7 @@ export function parseExecutorFlags(
|
||||
gitlabBaseUrl: undefined,
|
||||
extendedContextOverride: undefined,
|
||||
thinkingParse: parseThinkingOverride(args),
|
||||
parseFailed: true,
|
||||
});
|
||||
}
|
||||
kiroAuthMethod = normalizeKiroAuthMethod(normalized);
|
||||
@@ -339,6 +344,7 @@ export function parseExecutorFlags(
|
||||
gitlabBaseUrl: undefined,
|
||||
extendedContextOverride: undefined,
|
||||
thinkingParse: parseThinkingOverride(args),
|
||||
parseFailed: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -373,6 +379,7 @@ export function parseExecutorFlags(
|
||||
gitlabBaseUrl: undefined,
|
||||
extendedContextOverride: undefined,
|
||||
thinkingParse: parseThinkingOverride(args),
|
||||
parseFailed: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -408,6 +415,7 @@ export function parseExecutorFlags(
|
||||
gitlabBaseUrl: undefined,
|
||||
extendedContextOverride: undefined,
|
||||
thinkingParse: parseThinkingOverride(args),
|
||||
parseFailed: true,
|
||||
});
|
||||
}
|
||||
const normalized = rawFlow.trim().toLowerCase();
|
||||
@@ -439,6 +447,7 @@ export function parseExecutorFlags(
|
||||
gitlabBaseUrl: undefined,
|
||||
extendedContextOverride: undefined,
|
||||
thinkingParse: parseThinkingOverride(args),
|
||||
parseFailed: true,
|
||||
});
|
||||
}
|
||||
kiroIDCFlow = normalizeKiroIDCFlow(normalized);
|
||||
@@ -475,6 +484,7 @@ export function parseExecutorFlags(
|
||||
gitlabBaseUrl: undefined,
|
||||
extendedContextOverride: undefined,
|
||||
thinkingParse: parseThinkingOverride(args),
|
||||
parseFailed: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -538,6 +548,7 @@ export function parseExecutorFlags(
|
||||
gitlabBaseUrl,
|
||||
extendedContextOverride,
|
||||
thinkingParse,
|
||||
parseFailed: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -550,8 +561,8 @@ function buildPartialFlags(fields: ParsedExecutorFlags): ParsedExecutorFlags {
|
||||
|
||||
/**
|
||||
* Validate flag combinations that are mutually exclusive or provider-scoped.
|
||||
* Calls process.exit(1) on any violation — identical to original index.ts.
|
||||
* Call AFTER parseExecutorFlags() and only if process.exitCode is still 0.
|
||||
* Sets process.exitCode=1 and returns false on any violation.
|
||||
* Call AFTER parseExecutorFlags() and only if parseFailed is false.
|
||||
*
|
||||
* @param parsed Result of parseExecutorFlags()
|
||||
* @param context Provider context (provider string + compositeProviders list)
|
||||
@@ -561,7 +572,7 @@ export function validateFlagCombinations(
|
||||
parsed: ParsedExecutorFlags,
|
||||
context: { provider: string; compositeProviders: string[] },
|
||||
args: string[]
|
||||
): void {
|
||||
): boolean {
|
||||
const { provider, compositeProviders } = context;
|
||||
const {
|
||||
kiroAuthMethod,
|
||||
@@ -575,7 +586,7 @@ export function validateFlagCombinations(
|
||||
if (kiroAuthMethod && provider !== 'kiro' && !compositeProviders.includes('kiro')) {
|
||||
console.error(fail('--kiro-auth-method is only valid for ccs kiro'));
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -589,7 +600,7 @@ export function validateFlagCombinations(
|
||||
)
|
||||
);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (kiroAuthMethod === 'idc' && !kiroIDCStartUrl) {
|
||||
@@ -598,7 +609,7 @@ export function validateFlagCombinations(
|
||||
' Example: ccs kiro --auth --kiro-auth-method idc --kiro-idc-start-url https://d-xxx.awsapps.com/start'
|
||||
);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -612,13 +623,15 @@ export function validateFlagCombinations(
|
||||
)
|
||||
);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((gitlabTokenLogin || gitlabBaseUrl) && provider !== 'gitlab') {
|
||||
const flagName = gitlabTokenLogin ? getGitLabTokenLoginFlagName(args) : '--gitlab-url';
|
||||
console.error(fail(`${flagName} is only valid for ccs gitlab`));
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ export interface BrowserLaunchSetupResult {
|
||||
export function resolveBrowserLaunchFlags(argsWithoutProxy: string[]): {
|
||||
browserLaunchOverride: BrowserLaunchOverride | undefined;
|
||||
argsWithoutBrowserFlags: string[];
|
||||
parseFailed: boolean;
|
||||
} {
|
||||
let browserLaunchOverride: BrowserLaunchOverride | undefined;
|
||||
let argsWithoutBrowserFlags = argsWithoutProxy;
|
||||
@@ -52,7 +53,7 @@ export function resolveBrowserLaunchFlags(argsWithoutProxy: string[]): {
|
||||
console.error(fail((error as Error).message));
|
||||
process.exitCode = 1;
|
||||
process.exit(1);
|
||||
return { browserLaunchOverride: undefined, argsWithoutBrowserFlags };
|
||||
return { browserLaunchOverride: undefined, argsWithoutBrowserFlags, parseFailed: true };
|
||||
}
|
||||
|
||||
const browserConfig = getBrowserConfig();
|
||||
@@ -72,7 +73,7 @@ export function resolveBrowserLaunchFlags(argsWithoutProxy: string[]): {
|
||||
console.error(warn(blockedBrowserOverrideWarning));
|
||||
}
|
||||
|
||||
return { browserLaunchOverride, argsWithoutBrowserFlags };
|
||||
return { browserLaunchOverride, argsWithoutBrowserFlags, parseFailed: false };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -137,10 +137,12 @@ export async function execClaudeWithCLIProxy(
|
||||
log,
|
||||
});
|
||||
|
||||
const { browserLaunchOverride, argsWithoutBrowserFlags } = resolveBrowserLaunchFlags(
|
||||
proxyResolution.argsWithoutProxy
|
||||
);
|
||||
if (process.exitCode === 1) return;
|
||||
const {
|
||||
browserLaunchOverride,
|
||||
argsWithoutBrowserFlags,
|
||||
parseFailed: browserLaunchParseFailed,
|
||||
} = resolveBrowserLaunchFlags(proxyResolution.argsWithoutProxy);
|
||||
if (browserLaunchParseFailed) return;
|
||||
|
||||
const { proxyConfig, useRemoteProxy, localBackend, binaryPath, argsWithoutProxy } =
|
||||
await resolveExecutorProxy(proxyResolution, {
|
||||
@@ -168,11 +170,15 @@ export async function execClaudeWithCLIProxy(
|
||||
compositeProviders,
|
||||
unifiedConfig,
|
||||
});
|
||||
if (process.exitCode === 1) return;
|
||||
if (parsedFlags.parseFailed) return;
|
||||
|
||||
// Validate cross-flag combinations (exits with code 1 on violation)
|
||||
validateFlagCombinations(parsedFlags, { provider, compositeProviders }, argsWithoutProxy);
|
||||
if (process.exitCode === 1) return;
|
||||
// Validate cross-flag combinations (reports failure without relying on ambient exitCode)
|
||||
const flagCombinationsValid = validateFlagCombinations(
|
||||
parsedFlags,
|
||||
{ provider, compositeProviders },
|
||||
argsWithoutProxy
|
||||
);
|
||||
if (!flagCombinationsValid) return;
|
||||
|
||||
const {
|
||||
forceConfig,
|
||||
|
||||
Reference in New Issue
Block a user