mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-02 12:19:35 +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;
|
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(() => {
|
afterEach(() => {
|
||||||
if (originalCcsHome !== undefined) {
|
if (originalCcsHome !== undefined) {
|
||||||
process.env.CCS_HOME = originalCcsHome;
|
process.env.CCS_HOME = originalCcsHome;
|
||||||
@@ -145,4 +154,69 @@ describe('execClaudeWithCLIProxy browser flag validation', () => {
|
|||||||
errorSpy.mockRestore();
|
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)
|
* - parseExecutorFlags() — flag extraction block (lines ~411-639 in original)
|
||||||
* - validateFlagCombinations() — cross-flag guard block (lines ~531-585)
|
* - 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.
|
* 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. */
|
/** Result of parsing CCS executor flags from args. */
|
||||||
export interface ParsedExecutorFlags {
|
export interface ParsedExecutorFlags {
|
||||||
|
parseFailed?: boolean;
|
||||||
forceAuth: boolean;
|
forceAuth: boolean;
|
||||||
pasteCallback: boolean;
|
pasteCallback: boolean;
|
||||||
portForward: boolean;
|
portForward: boolean;
|
||||||
@@ -181,7 +184,7 @@ export interface ParsedExecutorFlags {
|
|||||||
/**
|
/**
|
||||||
* Parse all CCS executor flags from args.
|
* 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
|
* Exits with process.exit(1) on conflicting flag combinations — identical to
|
||||||
* the original index.ts behavior.
|
* the original index.ts behavior.
|
||||||
*
|
*
|
||||||
@@ -247,7 +250,7 @@ export function parseExecutorFlags(
|
|||||||
console.error(fail('--kiro-auth-method requires a value'));
|
console.error(fail('--kiro-auth-method requires a value'));
|
||||||
console.error(' Supported values: aws, aws-authcode, google, github, idc');
|
console.error(' Supported values: aws, aws-authcode, google, github, idc');
|
||||||
process.exitCode = 1;
|
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({
|
return buildPartialFlags({
|
||||||
forceAuth,
|
forceAuth,
|
||||||
pasteCallback,
|
pasteCallback,
|
||||||
@@ -272,6 +275,7 @@ export function parseExecutorFlags(
|
|||||||
gitlabBaseUrl: undefined,
|
gitlabBaseUrl: undefined,
|
||||||
extendedContextOverride: undefined,
|
extendedContextOverride: undefined,
|
||||||
thinkingParse: parseThinkingOverride(args),
|
thinkingParse: parseThinkingOverride(args),
|
||||||
|
parseFailed: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const normalized = rawMethod.trim().toLowerCase();
|
const normalized = rawMethod.trim().toLowerCase();
|
||||||
@@ -303,6 +307,7 @@ export function parseExecutorFlags(
|
|||||||
gitlabBaseUrl: undefined,
|
gitlabBaseUrl: undefined,
|
||||||
extendedContextOverride: undefined,
|
extendedContextOverride: undefined,
|
||||||
thinkingParse: parseThinkingOverride(args),
|
thinkingParse: parseThinkingOverride(args),
|
||||||
|
parseFailed: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
kiroAuthMethod = normalizeKiroAuthMethod(normalized);
|
kiroAuthMethod = normalizeKiroAuthMethod(normalized);
|
||||||
@@ -339,6 +344,7 @@ export function parseExecutorFlags(
|
|||||||
gitlabBaseUrl: undefined,
|
gitlabBaseUrl: undefined,
|
||||||
extendedContextOverride: undefined,
|
extendedContextOverride: undefined,
|
||||||
thinkingParse: parseThinkingOverride(args),
|
thinkingParse: parseThinkingOverride(args),
|
||||||
|
parseFailed: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -373,6 +379,7 @@ export function parseExecutorFlags(
|
|||||||
gitlabBaseUrl: undefined,
|
gitlabBaseUrl: undefined,
|
||||||
extendedContextOverride: undefined,
|
extendedContextOverride: undefined,
|
||||||
thinkingParse: parseThinkingOverride(args),
|
thinkingParse: parseThinkingOverride(args),
|
||||||
|
parseFailed: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -408,6 +415,7 @@ export function parseExecutorFlags(
|
|||||||
gitlabBaseUrl: undefined,
|
gitlabBaseUrl: undefined,
|
||||||
extendedContextOverride: undefined,
|
extendedContextOverride: undefined,
|
||||||
thinkingParse: parseThinkingOverride(args),
|
thinkingParse: parseThinkingOverride(args),
|
||||||
|
parseFailed: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const normalized = rawFlow.trim().toLowerCase();
|
const normalized = rawFlow.trim().toLowerCase();
|
||||||
@@ -439,6 +447,7 @@ export function parseExecutorFlags(
|
|||||||
gitlabBaseUrl: undefined,
|
gitlabBaseUrl: undefined,
|
||||||
extendedContextOverride: undefined,
|
extendedContextOverride: undefined,
|
||||||
thinkingParse: parseThinkingOverride(args),
|
thinkingParse: parseThinkingOverride(args),
|
||||||
|
parseFailed: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
kiroIDCFlow = normalizeKiroIDCFlow(normalized);
|
kiroIDCFlow = normalizeKiroIDCFlow(normalized);
|
||||||
@@ -475,6 +484,7 @@ export function parseExecutorFlags(
|
|||||||
gitlabBaseUrl: undefined,
|
gitlabBaseUrl: undefined,
|
||||||
extendedContextOverride: undefined,
|
extendedContextOverride: undefined,
|
||||||
thinkingParse: parseThinkingOverride(args),
|
thinkingParse: parseThinkingOverride(args),
|
||||||
|
parseFailed: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -538,6 +548,7 @@ export function parseExecutorFlags(
|
|||||||
gitlabBaseUrl,
|
gitlabBaseUrl,
|
||||||
extendedContextOverride,
|
extendedContextOverride,
|
||||||
thinkingParse,
|
thinkingParse,
|
||||||
|
parseFailed: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -550,8 +561,8 @@ function buildPartialFlags(fields: ParsedExecutorFlags): ParsedExecutorFlags {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate flag combinations that are mutually exclusive or provider-scoped.
|
* Validate flag combinations that are mutually exclusive or provider-scoped.
|
||||||
* Calls process.exit(1) on any violation — identical to original index.ts.
|
* Sets process.exitCode=1 and returns false on any violation.
|
||||||
* Call AFTER parseExecutorFlags() and only if process.exitCode is still 0.
|
* Call AFTER parseExecutorFlags() and only if parseFailed is false.
|
||||||
*
|
*
|
||||||
* @param parsed Result of parseExecutorFlags()
|
* @param parsed Result of parseExecutorFlags()
|
||||||
* @param context Provider context (provider string + compositeProviders list)
|
* @param context Provider context (provider string + compositeProviders list)
|
||||||
@@ -561,7 +572,7 @@ export function validateFlagCombinations(
|
|||||||
parsed: ParsedExecutorFlags,
|
parsed: ParsedExecutorFlags,
|
||||||
context: { provider: string; compositeProviders: string[] },
|
context: { provider: string; compositeProviders: string[] },
|
||||||
args: string[]
|
args: string[]
|
||||||
): void {
|
): boolean {
|
||||||
const { provider, compositeProviders } = context;
|
const { provider, compositeProviders } = context;
|
||||||
const {
|
const {
|
||||||
kiroAuthMethod,
|
kiroAuthMethod,
|
||||||
@@ -575,7 +586,7 @@ export function validateFlagCombinations(
|
|||||||
if (kiroAuthMethod && provider !== 'kiro' && !compositeProviders.includes('kiro')) {
|
if (kiroAuthMethod && provider !== 'kiro' && !compositeProviders.includes('kiro')) {
|
||||||
console.error(fail('--kiro-auth-method is only valid for ccs kiro'));
|
console.error(fail('--kiro-auth-method is only valid for ccs kiro'));
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -589,7 +600,7 @@ export function validateFlagCombinations(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (kiroAuthMethod === 'idc' && !kiroIDCStartUrl) {
|
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'
|
' Example: ccs kiro --auth --kiro-auth-method idc --kiro-idc-start-url https://d-xxx.awsapps.com/start'
|
||||||
);
|
);
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -612,13 +623,15 @@ export function validateFlagCombinations(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((gitlabTokenLogin || gitlabBaseUrl) && provider !== 'gitlab') {
|
if ((gitlabTokenLogin || gitlabBaseUrl) && provider !== 'gitlab') {
|
||||||
const flagName = gitlabTokenLogin ? getGitLabTokenLoginFlagName(args) : '--gitlab-url';
|
const flagName = gitlabTokenLogin ? getGitLabTokenLoginFlagName(args) : '--gitlab-url';
|
||||||
console.error(fail(`${flagName} is only valid for ccs gitlab`));
|
console.error(fail(`${flagName} is only valid for ccs gitlab`));
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export interface BrowserLaunchSetupResult {
|
|||||||
export function resolveBrowserLaunchFlags(argsWithoutProxy: string[]): {
|
export function resolveBrowserLaunchFlags(argsWithoutProxy: string[]): {
|
||||||
browserLaunchOverride: BrowserLaunchOverride | undefined;
|
browserLaunchOverride: BrowserLaunchOverride | undefined;
|
||||||
argsWithoutBrowserFlags: string[];
|
argsWithoutBrowserFlags: string[];
|
||||||
|
parseFailed: boolean;
|
||||||
} {
|
} {
|
||||||
let browserLaunchOverride: BrowserLaunchOverride | undefined;
|
let browserLaunchOverride: BrowserLaunchOverride | undefined;
|
||||||
let argsWithoutBrowserFlags = argsWithoutProxy;
|
let argsWithoutBrowserFlags = argsWithoutProxy;
|
||||||
@@ -52,7 +53,7 @@ export function resolveBrowserLaunchFlags(argsWithoutProxy: string[]): {
|
|||||||
console.error(fail((error as Error).message));
|
console.error(fail((error as Error).message));
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
return { browserLaunchOverride: undefined, argsWithoutBrowserFlags };
|
return { browserLaunchOverride: undefined, argsWithoutBrowserFlags, parseFailed: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
const browserConfig = getBrowserConfig();
|
const browserConfig = getBrowserConfig();
|
||||||
@@ -72,7 +73,7 @@ export function resolveBrowserLaunchFlags(argsWithoutProxy: string[]): {
|
|||||||
console.error(warn(blockedBrowserOverrideWarning));
|
console.error(warn(blockedBrowserOverrideWarning));
|
||||||
}
|
}
|
||||||
|
|
||||||
return { browserLaunchOverride, argsWithoutBrowserFlags };
|
return { browserLaunchOverride, argsWithoutBrowserFlags, parseFailed: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -137,10 +137,12 @@ export async function execClaudeWithCLIProxy(
|
|||||||
log,
|
log,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { browserLaunchOverride, argsWithoutBrowserFlags } = resolveBrowserLaunchFlags(
|
const {
|
||||||
proxyResolution.argsWithoutProxy
|
browserLaunchOverride,
|
||||||
);
|
argsWithoutBrowserFlags,
|
||||||
if (process.exitCode === 1) return;
|
parseFailed: browserLaunchParseFailed,
|
||||||
|
} = resolveBrowserLaunchFlags(proxyResolution.argsWithoutProxy);
|
||||||
|
if (browserLaunchParseFailed) return;
|
||||||
|
|
||||||
const { proxyConfig, useRemoteProxy, localBackend, binaryPath, argsWithoutProxy } =
|
const { proxyConfig, useRemoteProxy, localBackend, binaryPath, argsWithoutProxy } =
|
||||||
await resolveExecutorProxy(proxyResolution, {
|
await resolveExecutorProxy(proxyResolution, {
|
||||||
@@ -168,11 +170,15 @@ export async function execClaudeWithCLIProxy(
|
|||||||
compositeProviders,
|
compositeProviders,
|
||||||
unifiedConfig,
|
unifiedConfig,
|
||||||
});
|
});
|
||||||
if (process.exitCode === 1) return;
|
if (parsedFlags.parseFailed) return;
|
||||||
|
|
||||||
// Validate cross-flag combinations (exits with code 1 on violation)
|
// Validate cross-flag combinations (reports failure without relying on ambient exitCode)
|
||||||
validateFlagCombinations(parsedFlags, { provider, compositeProviders }, argsWithoutProxy);
|
const flagCombinationsValid = validateFlagCombinations(
|
||||||
if (process.exitCode === 1) return;
|
parsedFlags,
|
||||||
|
{ provider, compositeProviders },
|
||||||
|
argsWithoutProxy
|
||||||
|
);
|
||||||
|
if (!flagCombinationsValid) return;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
forceConfig,
|
forceConfig,
|
||||||
|
|||||||
Reference in New Issue
Block a user