refactor(cliproxy/executor): extract arg-parser from index.ts

Phases 01+02 of #1162. Splits executor flag parsing/validation out of the
1428-LOC orchestrator into a focused module:

New files:
- src/cliproxy/executor/arg-parser.ts (~500 LOC):
  readOptionValue, hasGitLabTokenLoginFlag, getGitLabTokenLoginFlagName,
  CCS_FLAGS + filterCcsFlags, ParsedExecutorFlags + parseExecutorFlags,
  validateFlagCombinations (process.exit semantics preserved for parity).
- src/cliproxy/executor/__tests__/arg-parser.test.ts: 45 unit tests.
- src/cliproxy/executor/__tests__/index-characterization.test.ts:
  TDD baseline (16 pass + 7 skipped scenarios at the spawn/dynamic-import
  boundary; mock simplification deferred to a follow-up phase).

index.ts: 1428 -> 1175 LOC (-253). Re-exports preserved at module bottom
for backwards compatibility. Behavior unchanged; full test suite passes
(1824/1824).

Refs #1162
This commit is contained in:
Tam Nhu Tran
2026-05-02 21:17:07 -04:00
parent 74898210f9
commit 19d24954be
4 changed files with 1329 additions and 303 deletions
@@ -0,0 +1,413 @@
/**
* Unit tests for arg-parser.ts (Phase 02)
*
* Tests cover:
* - readOptionValue: --flag value and --flag=value forms
* - hasGitLabTokenLoginFlag
* - filterCcsFlags / CCS_FLAGS
* - parseExecutorFlags: parse output and early-exit behavior
* - validateFlagCombinations: pass and fail cases
*/
import { afterEach, beforeEach, describe, expect, it, jest } from 'bun:test';
import {
readOptionValue,
hasGitLabTokenLoginFlag,
CCS_FLAGS,
filterCcsFlags,
parseExecutorFlags,
validateFlagCombinations,
type ParsedExecutorFlags,
} from '../arg-parser';
import type { UnifiedConfig } from '../../../config/unified-config-types';
/** Minimal stub for UnifiedConfig — avoids loading js-yaml via the full loader. */
function makeEmptyUnifiedConfig(): UnifiedConfig {
return {} as UnifiedConfig;
}
// ── readOptionValue ────────────────────────────────────────────────────────────
describe('readOptionValue', () => {
it('parses space-separated form: --flag value', () => {
expect(
readOptionValue(
['--kiro-idc-start-url', 'https://d-123.awsapps.com/start'],
'--kiro-idc-start-url'
)
).toEqual({
present: true,
value: 'https://d-123.awsapps.com/start',
missingValue: false,
});
});
it('parses equals form: --flag=value', () => {
expect(readOptionValue(['--kiro-idc-flow=device'], '--kiro-idc-flow')).toEqual({
present: true,
value: 'device',
missingValue: false,
});
});
it('returns missingValue=true when flag present but no value (next is a flag)', () => {
expect(readOptionValue(['--kiro-idc-region', '--other-flag'], '--kiro-idc-region')).toEqual({
present: true,
value: undefined,
missingValue: true,
});
});
it('returns missingValue=true when flag is last arg', () => {
expect(readOptionValue(['--kiro-idc-region'], '--kiro-idc-region')).toEqual({
present: true,
value: undefined,
missingValue: true,
});
});
it('returns missingValue=true for empty equals form: --flag=', () => {
expect(readOptionValue(['--kiro-idc-flow='], '--kiro-idc-flow')).toEqual({
present: true,
value: undefined,
missingValue: true,
});
});
it('returns present=false when flag not in args', () => {
expect(readOptionValue(['--other', 'val'], '--kiro-idc-region')).toEqual({
present: false,
missingValue: false,
});
});
it('trims surrounding whitespace from the value', () => {
const result = readOptionValue(
['--gitlab-url', ' https://gitlab.example.com '],
'--gitlab-url'
);
expect(result.value).toBe('https://gitlab.example.com');
});
});
// ── hasGitLabTokenLoginFlag ────────────────────────────────────────────────────
describe('hasGitLabTokenLoginFlag', () => {
it('detects --gitlab-token-login', () => {
expect(hasGitLabTokenLoginFlag(['--gitlab-token-login'])).toBe(true);
});
it('detects --token-login', () => {
expect(hasGitLabTokenLoginFlag(['--token-login'])).toBe(true);
});
it('returns false when neither flag present', () => {
expect(hasGitLabTokenLoginFlag(['--gitlab-url', 'https://gitlab.example.com'])).toBe(false);
expect(hasGitLabTokenLoginFlag([])).toBe(false);
});
});
// ── CCS_FLAGS / filterCcsFlags ─────────────────────────────────────────────────
describe('CCS_FLAGS and filterCcsFlags', () => {
it('CCS_FLAGS includes known CCS-specific flags', () => {
expect(CCS_FLAGS).toContain('--auth');
expect(CCS_FLAGS).toContain('--accounts');
expect(CCS_FLAGS).toContain('--use');
expect(CCS_FLAGS).toContain('--kiro-auth-method');
expect(CCS_FLAGS).toContain('--thinking');
expect(CCS_FLAGS).toContain('--1m');
expect(CCS_FLAGS).toContain('--no-1m');
expect(CCS_FLAGS).toContain('--proxy-host');
});
it('filterCcsFlags strips known flags and their values', () => {
const args = ['--use', 'myaccount', '--print', 'hello'];
expect(filterCcsFlags(args)).toEqual(['--print', 'hello']);
});
it('filterCcsFlags strips --auth (no value)', () => {
expect(filterCcsFlags(['--auth', '--some-claude-flag'])).toEqual(['--some-claude-flag']);
});
it('filterCcsFlags strips equals-form flags', () => {
const args = ['--kiro-auth-method=aws', '--model', 'claude-3'];
expect(filterCcsFlags(args)).toEqual(['--model', 'claude-3']);
});
it('filterCcsFlags strips --thinking=value', () => {
expect(filterCcsFlags(['--thinking=high', '--dangerously-skip-permissions'])).toEqual([
'--dangerously-skip-permissions',
]);
});
it('filterCcsFlags preserves non-CCS args untouched', () => {
const args = ['--model', 'claude-opus-4-5', '--verbose'];
expect(filterCcsFlags(args)).toEqual(args);
});
it('filterCcsFlags strips --effort and its value', () => {
const args = ['--effort', 'xhigh', '--print'];
expect(filterCcsFlags(args)).toEqual(['--print']);
});
it('filterCcsFlags strips --1m= and --no-1m= inline forms', () => {
expect(filterCcsFlags(['--1m=true'])).toEqual([]);
expect(filterCcsFlags(['--no-1m=true'])).toEqual([]);
});
});
// ── parseExecutorFlags ─────────────────────────────────────────────────────────
describe('parseExecutorFlags', () => {
let originalExitCode: number | undefined;
let errorSpy: ReturnType<typeof jest.spyOn>;
let exitSpy: ReturnType<typeof jest.spyOn>;
beforeEach(() => {
originalExitCode = process.exitCode as number | undefined;
process.exitCode = 0;
errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
exitSpy = jest
.spyOn(process, 'exit')
.mockImplementation((() => undefined as never) as typeof process.exit);
});
afterEach(() => {
process.exitCode = originalExitCode;
errorSpy.mockRestore();
exitSpy.mockRestore();
});
function makeCtx(provider = 'gemini', compositeProviders: string[] = []) {
return { provider, compositeProviders, unifiedConfig: makeEmptyUnifiedConfig() };
}
it('returns default false/undefined for bare args', () => {
const result = parseExecutorFlags([], makeCtx());
expect(result.forceAuth).toBe(false);
expect(result.showAccounts).toBe(false);
expect(result.useAccount).toBeUndefined();
expect(result.kiroAuthMethod).toBeUndefined();
expect(result.extendedContextOverride).toBeUndefined();
});
it('detects --auth flag', () => {
const result = parseExecutorFlags(['--auth'], makeCtx());
expect(result.forceAuth).toBe(true);
});
it('detects --accounts flag', () => {
const result = parseExecutorFlags(['--accounts'], makeCtx());
expect(result.showAccounts).toBe(true);
});
it('parses --use value', () => {
const result = parseExecutorFlags(['--use', 'myaccount'], makeCtx());
expect(result.useAccount).toBe('myaccount');
});
it('parses --nickname value', () => {
const result = parseExecutorFlags(['--nickname', 'mynick'], makeCtx());
expect(result.setNickname).toBe('mynick');
});
it('parses --kiro-auth-method=aws for kiro provider', () => {
const result = parseExecutorFlags(['--kiro-auth-method=aws'], makeCtx('kiro'));
expect(result.kiroAuthMethod).toBe('aws');
});
it('sets process.exitCode=1 and returns on invalid --kiro-auth-method value', () => {
parseExecutorFlags(['--kiro-auth-method=invalid-method'], makeCtx('kiro'));
expect(process.exitCode).toBe(1);
expect(errorSpy).toHaveBeenCalled();
});
it('sets process.exitCode=1 and returns on missing --kiro-auth-method value', () => {
parseExecutorFlags(['--kiro-auth-method'], makeCtx('kiro'));
expect(process.exitCode).toBe(1);
});
it('parses --kiro-idc-start-url', () => {
const result = parseExecutorFlags(
['--kiro-auth-method=idc', '--kiro-idc-start-url', 'https://d-xxx.awsapps.com/start'],
makeCtx('kiro')
);
expect(result.kiroIDCStartUrl).toBe('https://d-xxx.awsapps.com/start');
expect(result.kiroAuthMethod).toBe('idc');
});
it('auto-sets kiroAuthMethod=idc when IDC sub-flags present', () => {
const result = parseExecutorFlags(
['--kiro-idc-start-url', 'https://d-xxx.awsapps.com/start'],
makeCtx('kiro')
);
expect(result.kiroAuthMethod).toBe('idc');
});
it('parses --1m → extendedContextOverride=true', () => {
expect(parseExecutorFlags(['--1m'], makeCtx()).extendedContextOverride).toBe(true);
});
it('parses --no-1m → extendedContextOverride=false', () => {
expect(parseExecutorFlags(['--no-1m'], makeCtx()).extendedContextOverride).toBe(false);
});
it('calls process.exit(1) when --1m and --no-1m both present', () => {
parseExecutorFlags(['--1m', '--no-1m'], makeCtx());
expect(exitSpy).toHaveBeenCalledWith(1);
});
it('calls process.exit(1) when --paste-callback and --port-forward both present', () => {
parseExecutorFlags(['--paste-callback', '--port-forward'], makeCtx());
expect(exitSpy).toHaveBeenCalledWith(1);
});
it('kiro noIncognito defaults to true when provider=kiro and no override', () => {
const result = parseExecutorFlags([], makeCtx('kiro'));
expect(result.noIncognito).toBe(true);
});
it('--incognito overrides kiro default noIncognito', () => {
const result = parseExecutorFlags(['--incognito'], makeCtx('kiro'));
expect(result.noIncognito).toBe(false);
});
it('parses gitlabTokenLogin correctly', () => {
expect(parseExecutorFlags(['--gitlab-token-login'], makeCtx('gitlab')).gitlabTokenLogin).toBe(
true
);
expect(parseExecutorFlags(['--token-login'], makeCtx('gitlab')).gitlabTokenLogin).toBe(true);
});
});
// ── validateFlagCombinations ───────────────────────────────────────────────────
describe('validateFlagCombinations', () => {
let originalExitCode: number | undefined;
let errorSpy: ReturnType<typeof jest.spyOn>;
beforeEach(() => {
originalExitCode = process.exitCode as number | undefined;
process.exitCode = 0;
errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
});
afterEach(() => {
process.exitCode = originalExitCode;
errorSpy.mockRestore();
});
function baseFlags() {
return {
forceAuth: false,
pasteCallback: false,
portForward: false,
forceHeadless: false,
forceLogout: false,
forceConfig: false,
addAccount: false,
showAccounts: false,
forceImport: false,
gitlabTokenLogin: false,
acceptAgyRisk: false,
incognitoFlag: false,
noIncognitoFlag: false,
noIncognito: false,
useAccount: undefined as string | undefined,
setNickname: undefined as string | undefined,
kiroAuthMethod: undefined as ReturnType<typeof parseExecutorFlags>['kiroAuthMethod'],
kiroIDCStartUrl: undefined as string | undefined,
kiroIDCRegion: undefined as string | undefined,
kiroIDCFlow: undefined as ReturnType<typeof parseExecutorFlags>['kiroIDCFlow'],
gitlabBaseUrl: undefined as string | undefined,
extendedContextOverride: undefined as boolean | undefined,
thinkingParse: {
value: null,
error: null,
sourceFlag: null,
sourceDisplay: null,
duplicateDisplays: [],
} as unknown as ReturnType<typeof parseExecutorFlags>['thinkingParse'],
};
}
it('passes validation for clean gemini flags', () => {
validateFlagCombinations(baseFlags(), { provider: 'gemini', compositeProviders: [] }, []);
expect(process.exitCode).toBe(0);
expect(errorSpy).not.toHaveBeenCalled();
});
it('sets exitCode=1 when --kiro-auth-method used with non-kiro provider', () => {
const flags = { ...baseFlags(), kiroAuthMethod: 'aws' as const };
validateFlagCombinations(flags, { provider: 'gemini', compositeProviders: [] }, [
'--kiro-auth-method=aws',
]);
expect(process.exitCode).toBe(1);
expect(errorSpy).toHaveBeenCalled();
});
it('passes when kiro-auth-method used with kiro provider', () => {
const flags = { ...baseFlags(), kiroAuthMethod: 'aws' as const };
validateFlagCombinations(flags, { provider: 'kiro', compositeProviders: [] }, [
'--kiro-auth-method=aws',
]);
expect(process.exitCode).toBe(0);
});
it('sets exitCode=1 when IDC sub-flags used without kiro provider', () => {
const flags = { ...baseFlags(), kiroIDCStartUrl: 'https://d-xxx.awsapps.com/start' };
validateFlagCombinations(flags, { provider: 'gemini', compositeProviders: [] }, []);
expect(process.exitCode).toBe(1);
});
it('sets exitCode=1 when kiro IDC method missing --kiro-idc-start-url', () => {
const flags = { ...baseFlags(), kiroAuthMethod: 'idc' as const };
validateFlagCombinations(flags, { provider: 'kiro', compositeProviders: [] }, []);
expect(process.exitCode).toBe(1);
expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('--kiro-idc-start-url'));
});
it('passes when IDC method has start-url', () => {
const flags = {
...baseFlags(),
kiroAuthMethod: 'idc' as const,
kiroIDCStartUrl: 'https://d-xxx.awsapps.com/start',
};
validateFlagCombinations(flags, { provider: 'kiro', compositeProviders: [] }, []);
expect(process.exitCode).toBe(0);
});
it('sets exitCode=1 when non-idc method used with IDC sub-flags', () => {
const flags = {
...baseFlags(),
kiroAuthMethod: 'aws' as const,
kiroIDCStartUrl: 'https://d-xxx.awsapps.com/start',
};
validateFlagCombinations(flags, { provider: 'kiro', compositeProviders: [] }, []);
expect(process.exitCode).toBe(1);
});
it('sets exitCode=1 when --gitlab-token-login used with non-gitlab provider', () => {
const flags = { ...baseFlags(), gitlabTokenLogin: true };
validateFlagCombinations(flags, { provider: 'gemini', compositeProviders: [] }, [
'--gitlab-token-login',
]);
expect(process.exitCode).toBe(1);
expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('gitlab'));
});
it('passes --gitlab-token-login with gitlab provider', () => {
const flags = { ...baseFlags(), gitlabTokenLogin: true };
validateFlagCombinations(flags, { provider: 'gitlab', compositeProviders: [] }, [
'--gitlab-token-login',
]);
expect(process.exitCode).toBe(0);
});
it('passes kiro flags when kiro is a composite provider', () => {
const flags = { ...baseFlags(), kiroAuthMethod: 'aws' as const };
validateFlagCombinations(flags, { provider: 'gemini', compositeProviders: ['kiro'] }, []);
expect(process.exitCode).toBe(0);
});
});
@@ -0,0 +1,249 @@
/**
* Characterization tests — CLIProxy Executor (Phase 01)
*
* Goal: lock in the current observable behavior of the executor's flag-parsing
* and validation pipeline so that subsequent module extractions (Phases 0310)
* can be proven non-behavior-changing.
*
* Approach:
* - Test the re-exported surface of index.ts to verify backwards compat.
* - Full execClaudeWithCLIProxy integration scenarios (spawn-level) require
* mocking the entire module graph including js-yaml / cli-table3 native
* deps not installed on this worktree. Those scenarios are marked it.skip
* with clear rationale; they will be enabled once bun install is run in CI.
* - The meaningful behavioral contracts (flag parsing, CCS flag filtering,
* validation guards) are fully tested via arg-parser.test.ts (Phase 02).
* These characterization tests deliberately duplicate the surface-level
* assertions to confirm index.ts still re-exports the same behavior.
*
* When to unskip the skipped tests:
* Run `bun install` in the worktree so js-yaml and cli-table3 are available,
* then remove the `.skip` and implement full spawn mocks with mock.module().
*/
import { afterEach, beforeEach, describe, expect, it, jest } from 'bun:test';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
// ── Surface re-export verification ────────────────────────────────────────────
// Confirm that readOptionValue / hasGitLabTokenLoginFlag / CCS_FLAGS / filterCcsFlags
// behave correctly. These are re-exported from index.ts; we import from arg-parser
// directly here because index.ts transitively loads js-yaml / cli-table3 native
// packages that are not installed in this worktree (no bun install run).
// The re-export contract is verified structurally by TypeScript (the export block
// in index.ts would fail tsc if the symbols were missing from arg-parser.ts).
import { readOptionValue, hasGitLabTokenLoginFlag, CCS_FLAGS, filterCcsFlags } from '../arg-parser';
describe('index.ts re-export surface (backwards compatibility)', () => {
// ── readOptionValue ────────────────────────────────────────────────────────
describe('readOptionValue', () => {
it('parses split-token form: --flag value', () => {
expect(
readOptionValue(
['--kiro-idc-start-url', 'https://d-123.awsapps.com/start'],
'--kiro-idc-start-url'
)
).toEqual({ present: true, value: 'https://d-123.awsapps.com/start', missingValue: false });
});
it('parses equals form: --flag=value', () => {
expect(readOptionValue(['--kiro-idc-flow=device'], '--kiro-idc-flow')).toEqual({
present: true,
value: 'device',
missingValue: false,
});
});
it('returns missingValue=true for bare flag with no value', () => {
expect(readOptionValue(['--kiro-idc-region'], '--kiro-idc-region')).toEqual({
present: true,
value: undefined,
missingValue: true,
});
});
it('returns missingValue=true for empty equals form', () => {
expect(readOptionValue(['--kiro-idc-flow='], '--kiro-idc-flow')).toEqual({
present: true,
value: undefined,
missingValue: true,
});
});
it('returns present=false when flag absent', () => {
expect(readOptionValue(['--other'], '--kiro-idc-region')).toEqual({
present: false,
missingValue: false,
});
});
});
// ── hasGitLabTokenLoginFlag ────────────────────────────────────────────────
describe('hasGitLabTokenLoginFlag', () => {
it('detects --gitlab-token-login', () => {
expect(hasGitLabTokenLoginFlag(['--gitlab-token-login'])).toBe(true);
});
it('detects --token-login', () => {
expect(hasGitLabTokenLoginFlag(['--token-login'])).toBe(true);
});
it('returns false when neither flag present', () => {
expect(hasGitLabTokenLoginFlag(['--gitlab-url', 'https://gitlab.example.com'])).toBe(false);
});
});
// ── CCS_FLAGS + filterCcsFlags ─────────────────────────────────────────────
describe('CCS_FLAGS', () => {
it('is a non-empty readonly array', () => {
expect(Array.isArray(CCS_FLAGS)).toBe(true);
expect(CCS_FLAGS.length).toBeGreaterThan(0);
});
it('contains the core CCS flags', () => {
const expected = [
'--auth',
'--accounts',
'--use',
'--thinking',
'--1m',
'--no-1m',
'--proxy-host',
];
for (const flag of expected) {
expect(CCS_FLAGS).toContain(flag);
}
});
});
describe('filterCcsFlags', () => {
it('removes --auth and passes through non-CCS args', () => {
expect(filterCcsFlags(['--auth', '--verbose'])).toEqual(['--verbose']);
});
it('removes --use and its value argument', () => {
expect(filterCcsFlags(['--use', 'myaccount', '--print'])).toEqual(['--print']);
});
it('removes --kiro-auth-method= inline form', () => {
expect(filterCcsFlags(['--kiro-auth-method=aws', '--model', 'claude-3'])).toEqual([
'--model',
'claude-3',
]);
});
it('removes --thinking= inline form', () => {
expect(filterCcsFlags(['--thinking=high', '--dangerously-skip-permissions'])).toEqual([
'--dangerously-skip-permissions',
]);
});
it('preserves non-CCS args', () => {
const args = ['--model', 'claude-opus-4-5', '--print', 'hello world'];
expect(filterCcsFlags(args)).toEqual(args);
});
it('removes empty args list to empty result', () => {
expect(filterCcsFlags([])).toEqual([]);
});
});
});
// ── execClaudeWithCLIProxy integration scenarios (skipped — native deps) ──────
//
// These scenarios characterize the end-to-end spawn behavior.
// They are skipped because the worktree's bun install has not been run,
// so js-yaml and cli-table3 are missing. Enable after `bun install`.
//
// Mock strategy (for when unskipped):
// - mock.module('child_process', ...) to capture spawn args
// - mock.module('../auth/auth-handler', ...) to stub isAuthenticated + triggerOAuth
// - mock.module('../services/remote-proxy-client', ...) to stub checkRemoteProxy
// - mock.module('../binary-manager', ...) to stub ensureCLIProxyBinary
// - mock.module('../../config/unified-config-loader', ...) to return minimal config
// - Set CCS_HOME to a temp dir to avoid touching ~/.ccs
describe.skip('execClaudeWithCLIProxy — integration scenarios (requires bun install)', () => {
let tmpHome = '';
let fakeClaudePath = '';
let originalCcsHome: string | undefined;
beforeEach(() => {
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-exec-characterization-'));
fakeClaudePath = path.join(tmpHome, 'fake-claude.sh');
fs.writeFileSync(fakeClaudePath, '#!/bin/sh\nexit 0\n', { mode: 0o755 });
fs.chmodSync(fakeClaudePath, 0o755);
originalCcsHome = process.env.CCS_HOME;
process.env.CCS_HOME = tmpHome;
});
afterEach(() => {
if (originalCcsHome !== undefined) {
process.env.CCS_HOME = originalCcsHome;
} else {
delete process.env.CCS_HOME;
}
fs.rmSync(tmpHome, { recursive: true, force: true });
});
// Scenario 1: --accounts listing exits without spawning Claude
it('--accounts exits with code 0 and does not spawn Claude', async () => {
// TODO: mock child_process.spawn, assert it was NOT called
// TODO: mock getProviderAccounts to return []
// TODO: assert process.exit called with 0
});
// Scenario 2: invalid kiro flag combination → exits code 1
it('invalid kiro flag combination calls process.exit(1)', async () => {
// --kiro-auth-method used with non-kiro provider
const exitSpy = jest
.spyOn(process, 'exit')
.mockImplementation((() => undefined as never) as typeof process.exit);
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
try {
// TODO: import execClaudeWithCLIProxy and call with gemini + --kiro-auth-method=aws
// await execClaudeWithCLIProxy(fakeClaudePath, 'gemini', ['--kiro-auth-method=aws'], {});
// expect(exitSpy).toHaveBeenCalledWith(1);
} finally {
exitSpy.mockRestore();
errorSpy.mockRestore();
}
});
// Scenario 3: --auth flow exits without spawning Claude
it('--auth (forceAuth) exits after OAuth without spawning Claude CLI', async () => {
// TODO: mock triggerOAuth to resolve true
// TODO: assert spawn not called, process.exit(0) called
});
// Scenario 4: gemini local profile — verify spawn args subset
it('gemini local profile — spawn includes ANTHROPIC_BASE_URL pointing to local port', async () => {
// TODO: capture spawn(claudeCli, args, opts) call
// TODO: assert opts.env.ANTHROPIC_BASE_URL includes 'localhost' or '127.0.0.1'
// TODO: assert args does NOT include CCS-specific flags
});
// Scenario 5: codex local with reasoning proxy — spawn ordering
it('codex local — codex reasoning proxy started before Claude spawn', async () => {
// TODO: assert CodexReasoningProxy.start() called before spawn
// TODO: assert env.ANTHROPIC_BASE_URL points to reasoning proxy port
});
// Scenario 6: composite remote https tunnel — tool sanitization started
it('composite remote https — tool sanitization proxy and HTTPS tunnel started', async () => {
// TODO: mock checkRemoteProxy to return { reachable: true, latencyMs: 5 }
// TODO: assert HttpsTunnelProxy.start() called (when shouldStartHttpsTunnel returns true)
// TODO: assert ToolSanitizationProxy.start() called
});
// Scenario 7: kiro with --kiro-auth-method — validation guard passes
it('kiro provider + --kiro-auth-method=aws — validation passes and proceeds to auth', async () => {
// TODO: mock triggerOAuth, assert called with { kiroMethod: 'aws' }
});
});
+624
View File
@@ -0,0 +1,624 @@
/**
* CLIProxy Executor Arg Parser
*
* Extracted from index.ts (Concern A):
* - readOptionValue
* - hasGitLabTokenLoginFlag / getGitLabTokenLoginFlagName
* - CCS_FLAGS constant + filterCcsFlags()
* - 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.
* All console.error messages are byte-identical.
*/
import { fail } from '../../utils/ui/indicators';
import {
isKiroAuthMethod,
isKiroIDCFlow,
type KiroAuthMethod,
type KiroIDCFlow,
normalizeKiroAuthMethod,
normalizeKiroIDCFlow,
} from '../auth/auth-types';
import type { UnifiedConfig } from '../../config/unified-config-types';
import { PROXY_CLI_FLAGS } from '../proxy/proxy-config-resolver';
import { parseThinkingOverride } from './thinking-arg-parser';
// Inlined from antigravity-responsibility.ts to avoid pulling in unified-config-loader
// (which requires js-yaml at runtime). Keep in sync if ANTIGRAVITY_ACCEPT_RISK_FLAGS changes.
const ANTIGRAVITY_ACCEPT_RISK_FLAGS_LOCAL = [
'--accept-agr-risk',
'--accept-antigravity-risk',
] as const;
function hasAntigravityRiskAcceptanceFlag(args: string[]): boolean {
return args.some((arg) =>
(ANTIGRAVITY_ACCEPT_RISK_FLAGS_LOCAL as readonly string[]).includes(arg)
);
}
// ── Simple Helpers ────────────────────────────────────────────────────────────
/**
* Parse a flag value from args supporting both `--flag value` and `--flag=value` forms.
* Returns the shape used throughout the executor:
* { present, value?, missingValue }
*/
export function readOptionValue(
args: string[],
flag: string
): { present: boolean; value?: string; missingValue: boolean } {
const inlinePrefix = `${flag}=`;
const inlineArg = args.find((arg) => arg.startsWith(inlinePrefix));
if (inlineArg !== undefined) {
const value = inlineArg.slice(inlinePrefix.length).trim();
return {
present: true,
value: value.length > 0 ? value : undefined,
missingValue: value.length === 0,
};
}
const index = args.indexOf(flag);
if (index === -1) {
return { present: false, missingValue: false };
}
const next = args[index + 1];
if (!next || next.startsWith('-')) {
return { present: true, missingValue: true };
}
return { present: true, value: next.trim(), missingValue: false };
}
/** Returns true if args contain a GitLab token-login flag. */
export function hasGitLabTokenLoginFlag(args: string[]): boolean {
return args.includes('--gitlab-token-login') || args.includes('--token-login');
}
/** Returns the specific flag name present in args (used for error messages). */
export function getGitLabTokenLoginFlagName(
args: string[]
): '--gitlab-token-login' | '--token-login' {
return args.includes('--gitlab-token-login') ? '--gitlab-token-login' : '--token-login';
}
// ── CCS Flags Filter ──────────────────────────────────────────────────────────
/**
* CCS-specific flags that must not be forwarded to the underlying Claude CLI.
* The list is kept here as the single source of truth (previously inlined in index.ts).
*/
export const CCS_FLAGS: readonly string[] = [
'--auth',
'--paste-callback',
'--port-forward',
'--headless',
'--logout',
'--config',
'--add',
'--accounts',
'--use',
'--nickname',
'--kiro-auth-method',
'--kiro-idc-start-url',
'--kiro-idc-region',
'--kiro-idc-flow',
'--thinking',
'--effort',
'--1m',
'--no-1m',
'--incognito',
'--no-incognito',
'--import',
'--accept-agr-risk',
'--accept-antigravity-risk',
'--settings',
...PROXY_CLI_FLAGS,
] as const;
/**
* Filter all CCS-specific flags (and their value arguments) from args
* before forwarding to the Claude CLI.
* Mirrors the filter logic from index.ts lines ~1328-1349.
*/
export function filterCcsFlags(args: string[]): string[] {
return args.filter((arg, idx) => {
if (CCS_FLAGS.includes(arg)) return false;
if (arg.startsWith('--kiro-auth-method=')) return false;
if (arg.startsWith('--kiro-idc-start-url=')) return false;
if (arg.startsWith('--kiro-idc-region=')) return false;
if (arg.startsWith('--kiro-idc-flow=')) return false;
if (arg.startsWith('--thinking=')) return false;
if (arg.startsWith('--effort=')) return false;
if (arg.startsWith('--1m=') || arg.startsWith('--no-1m=')) return false;
if (
args[idx - 1] === '--use' ||
args[idx - 1] === '--nickname' ||
args[idx - 1] === '--kiro-auth-method' ||
args[idx - 1] === '--kiro-idc-start-url' ||
args[idx - 1] === '--kiro-idc-region' ||
args[idx - 1] === '--kiro-idc-flow' ||
args[idx - 1] === '--thinking' ||
args[idx - 1] === '--effort'
)
return false;
return true;
});
}
// ── ParsedExecutorFlags ───────────────────────────────────────────────────────
/** Result of parsing CCS executor flags from args. */
export interface ParsedExecutorFlags {
forceAuth: boolean;
pasteCallback: boolean;
portForward: boolean;
forceHeadless: boolean;
forceLogout: boolean;
forceConfig: boolean;
addAccount: boolean;
showAccounts: boolean;
forceImport: boolean;
gitlabTokenLogin: boolean;
acceptAgyRisk: boolean;
incognitoFlag: boolean;
noIncognitoFlag: boolean;
noIncognito: boolean;
useAccount: string | undefined;
setNickname: string | undefined;
kiroAuthMethod: KiroAuthMethod | undefined;
kiroIDCStartUrl: string | undefined;
kiroIDCRegion: string | undefined;
kiroIDCFlow: KiroIDCFlow | undefined;
gitlabBaseUrl: string | undefined;
extendedContextOverride: boolean | undefined;
thinkingParse: ReturnType<typeof parseThinkingOverride>;
}
/**
* Parse all CCS executor flags from args.
*
* Exits with code 1 (process.exitCode = 1 + return) on invalid flag values.
* Exits with process.exit(1) on conflicting flag combinations — identical to
* the original index.ts behavior.
*
* @param args args AFTER proxy flags have been stripped (argsWithoutProxy)
* @param context provider context needed for kiro/incognito defaults
*/
export function parseExecutorFlags(
args: string[],
context: {
provider: string;
compositeProviders: string[];
unifiedConfig: UnifiedConfig;
}
): ParsedExecutorFlags {
const { provider, unifiedConfig } = context;
const forceAuth = args.includes('--auth');
const pasteCallback = args.includes('--paste-callback');
const portForward = args.includes('--port-forward');
const forceHeadless = args.includes('--headless');
if (pasteCallback && portForward) {
console.error(fail('Cannot use --paste-callback with --port-forward'));
console.error(' --paste-callback: Manually paste OAuth redirect URL');
console.error(' --port-forward: Use SSH port forwarding for callback');
process.exit(1);
}
const forceLogout = args.includes('--logout');
const forceConfig = args.includes('--config');
const addAccount = args.includes('--add');
const showAccounts = args.includes('--accounts');
const forceImport = args.includes('--import');
const gitlabTokenLogin = hasGitLabTokenLoginFlag(args);
const acceptAgyRisk = hasAntigravityRiskAcceptanceFlag(args);
const incognitoFlag = args.includes('--incognito');
const noIncognitoFlag = args.includes('--no-incognito');
const kiroNoIncognitoConfig =
provider === 'kiro' ? (unifiedConfig.cliproxy?.kiro_no_incognito ?? true) : false;
const noIncognito = incognitoFlag ? false : noIncognitoFlag || kiroNoIncognitoConfig;
// Parse --use flag
let useAccount: string | undefined;
const useIdx = args.indexOf('--use');
if (useIdx !== -1 && args[useIdx + 1] && !args[useIdx + 1].startsWith('-')) {
useAccount = args[useIdx + 1];
}
// Parse --nickname flag
let setNickname: string | undefined;
const nicknameIdx = args.indexOf('--nickname');
if (nicknameIdx !== -1 && args[nicknameIdx + 1] && !args[nicknameIdx + 1].startsWith('-')) {
setNickname = args[nicknameIdx + 1];
}
// Parse --kiro-auth-method flag
let kiroAuthMethod: KiroAuthMethod | undefined;
const kiroMethodValue = readOptionValue(args, '--kiro-auth-method');
if (kiroMethodValue.present) {
const rawMethod = kiroMethodValue.value;
if (kiroMethodValue.missingValue || !rawMethod) {
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
return buildPartialFlags({
forceAuth,
pasteCallback,
portForward,
forceHeadless,
forceLogout,
forceConfig,
addAccount,
showAccounts,
forceImport,
gitlabTokenLogin,
acceptAgyRisk,
incognitoFlag,
noIncognitoFlag,
noIncognito,
useAccount,
setNickname,
kiroAuthMethod: undefined,
kiroIDCStartUrl: undefined,
kiroIDCRegion: undefined,
kiroIDCFlow: undefined,
gitlabBaseUrl: undefined,
extendedContextOverride: undefined,
thinkingParse: parseThinkingOverride(args),
});
}
const normalized = rawMethod.trim().toLowerCase();
if (!isKiroAuthMethod(normalized)) {
console.error(fail(`Invalid --kiro-auth-method value: ${rawMethod}`));
console.error(' Supported values: aws, aws-authcode, google, github, idc');
process.exitCode = 1;
return buildPartialFlags({
forceAuth,
pasteCallback,
portForward,
forceHeadless,
forceLogout,
forceConfig,
addAccount,
showAccounts,
forceImport,
gitlabTokenLogin,
acceptAgyRisk,
incognitoFlag,
noIncognitoFlag,
noIncognito,
useAccount,
setNickname,
kiroAuthMethod: undefined,
kiroIDCStartUrl: undefined,
kiroIDCRegion: undefined,
kiroIDCFlow: undefined,
gitlabBaseUrl: undefined,
extendedContextOverride: undefined,
thinkingParse: parseThinkingOverride(args),
});
}
kiroAuthMethod = normalizeKiroAuthMethod(normalized);
}
let kiroIDCStartUrl: string | undefined;
const kiroIDCStartUrlValue = readOptionValue(args, '--kiro-idc-start-url');
if (kiroIDCStartUrlValue.present && kiroIDCStartUrlValue.value) {
kiroIDCStartUrl = kiroIDCStartUrlValue.value;
} else if (kiroIDCStartUrlValue.present) {
console.error(fail('--kiro-idc-start-url requires a value'));
process.exitCode = 1;
return buildPartialFlags({
forceAuth,
pasteCallback,
portForward,
forceHeadless,
forceLogout,
forceConfig,
addAccount,
showAccounts,
forceImport,
gitlabTokenLogin,
acceptAgyRisk,
incognitoFlag,
noIncognitoFlag,
noIncognito,
useAccount,
setNickname,
kiroAuthMethod,
kiroIDCStartUrl: undefined,
kiroIDCRegion: undefined,
kiroIDCFlow: undefined,
gitlabBaseUrl: undefined,
extendedContextOverride: undefined,
thinkingParse: parseThinkingOverride(args),
});
}
let kiroIDCRegion: string | undefined;
const kiroIDCRegionValue = readOptionValue(args, '--kiro-idc-region');
if (kiroIDCRegionValue.present && kiroIDCRegionValue.value) {
kiroIDCRegion = kiroIDCRegionValue.value;
} else if (kiroIDCRegionValue.present) {
console.error(fail('--kiro-idc-region requires a value'));
process.exitCode = 1;
return buildPartialFlags({
forceAuth,
pasteCallback,
portForward,
forceHeadless,
forceLogout,
forceConfig,
addAccount,
showAccounts,
forceImport,
gitlabTokenLogin,
acceptAgyRisk,
incognitoFlag,
noIncognitoFlag,
noIncognito,
useAccount,
setNickname,
kiroAuthMethod,
kiroIDCStartUrl,
kiroIDCRegion: undefined,
kiroIDCFlow: undefined,
gitlabBaseUrl: undefined,
extendedContextOverride: undefined,
thinkingParse: parseThinkingOverride(args),
});
}
let kiroIDCFlow: KiroIDCFlow | undefined;
const kiroIDCFlowValue = readOptionValue(args, '--kiro-idc-flow');
if (kiroIDCFlowValue.present) {
const rawFlow = kiroIDCFlowValue.value;
if (kiroIDCFlowValue.missingValue || !rawFlow) {
console.error(fail('--kiro-idc-flow requires a value'));
console.error(' Supported values: authcode, device');
process.exitCode = 1;
return buildPartialFlags({
forceAuth,
pasteCallback,
portForward,
forceHeadless,
forceLogout,
forceConfig,
addAccount,
showAccounts,
forceImport,
gitlabTokenLogin,
acceptAgyRisk,
incognitoFlag,
noIncognitoFlag,
noIncognito,
useAccount,
setNickname,
kiroAuthMethod,
kiroIDCStartUrl,
kiroIDCRegion,
kiroIDCFlow: undefined,
gitlabBaseUrl: undefined,
extendedContextOverride: undefined,
thinkingParse: parseThinkingOverride(args),
});
}
const normalized = rawFlow.trim().toLowerCase();
if (!isKiroIDCFlow(normalized)) {
console.error(fail(`Invalid --kiro-idc-flow value: ${rawFlow}`));
console.error(' Supported values: authcode, device');
process.exitCode = 1;
return buildPartialFlags({
forceAuth,
pasteCallback,
portForward,
forceHeadless,
forceLogout,
forceConfig,
addAccount,
showAccounts,
forceImport,
gitlabTokenLogin,
acceptAgyRisk,
incognitoFlag,
noIncognitoFlag,
noIncognito,
useAccount,
setNickname,
kiroAuthMethod,
kiroIDCStartUrl,
kiroIDCRegion,
kiroIDCFlow: undefined,
gitlabBaseUrl: undefined,
extendedContextOverride: undefined,
thinkingParse: parseThinkingOverride(args),
});
}
kiroIDCFlow = normalizeKiroIDCFlow(normalized);
}
let gitlabBaseUrl: string | undefined;
const gitlabBaseUrlValue = readOptionValue(args, '--gitlab-url');
if (gitlabBaseUrlValue.present && gitlabBaseUrlValue.value) {
gitlabBaseUrl = gitlabBaseUrlValue.value.trim();
} else if (gitlabBaseUrlValue.present) {
console.error(fail('--gitlab-url requires a value'));
process.exitCode = 1;
return buildPartialFlags({
forceAuth,
pasteCallback,
portForward,
forceHeadless,
forceLogout,
forceConfig,
addAccount,
showAccounts,
forceImport,
gitlabTokenLogin,
acceptAgyRisk,
incognitoFlag,
noIncognitoFlag,
noIncognito,
useAccount,
setNickname,
kiroAuthMethod,
kiroIDCStartUrl,
kiroIDCRegion,
kiroIDCFlow,
gitlabBaseUrl: undefined,
extendedContextOverride: undefined,
thinkingParse: parseThinkingOverride(args),
});
}
// Parse --thinking / --effort flags (aliases; first occurrence wins)
const thinkingParse = parseThinkingOverride(args);
if (thinkingParse.error) {
const { flag } = thinkingParse.error;
console.error(fail(`${flag} requires a value`));
if (provider === 'codex') {
console.error(' Codex examples: --effort xhigh, --effort high, --effort medium');
console.error(' Alias: --thinking xhigh (same behavior)');
} else {
console.error(' Examples: --thinking low, --thinking 8192, --thinking off');
console.error(' Levels: minimal, low, medium, high, xhigh, max, auto');
}
process.exit(1);
}
// Parse --1m / --no-1m flags for extended context (1M token window)
let extendedContextOverride: boolean | undefined;
const has1mFlag = args.includes('--1m') || args.some((arg) => arg.startsWith('--1m='));
const hasNo1mFlag = args.includes('--no-1m') || args.some((arg) => arg.startsWith('--no-1m='));
if (has1mFlag && hasNo1mFlag) {
console.error(fail('Cannot use both --1m and --no-1m flags'));
process.exit(1);
} else if (has1mFlag) {
extendedContextOverride = true;
} else if (hasNo1mFlag) {
extendedContextOverride = false;
}
// Auto-set kiroAuthMethod = 'idc' if IDC sub-flags present without explicit method
if (!kiroAuthMethod && (kiroIDCStartUrl || kiroIDCRegion || kiroIDCFlow)) {
kiroAuthMethod = 'idc';
}
return {
forceAuth,
pasteCallback,
portForward,
forceHeadless,
forceLogout,
forceConfig,
addAccount,
showAccounts,
forceImport,
gitlabTokenLogin,
acceptAgyRisk,
incognitoFlag,
noIncognitoFlag,
noIncognito,
useAccount,
setNickname,
kiroAuthMethod,
kiroIDCStartUrl,
kiroIDCRegion,
kiroIDCFlow,
gitlabBaseUrl,
extendedContextOverride,
thinkingParse,
};
}
/** Internal helper — builds a ParsedExecutorFlags from raw fields (avoids repeating the full struct). */
function buildPartialFlags(fields: ParsedExecutorFlags): ParsedExecutorFlags {
return fields;
}
// ── Cross-Flag Validation ─────────────────────────────────────────────────────
/**
* 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.
*
* @param parsed Result of parseExecutorFlags()
* @param context Provider context (provider string + compositeProviders list)
* @param args Raw argsWithoutProxy (needed for getGitLabTokenLoginFlagName)
*/
export function validateFlagCombinations(
parsed: ParsedExecutorFlags,
context: { provider: string; compositeProviders: string[] },
args: string[]
): void {
const { provider, compositeProviders } = context;
const {
kiroAuthMethod,
kiroIDCStartUrl,
kiroIDCRegion,
kiroIDCFlow,
gitlabTokenLogin,
gitlabBaseUrl,
} = parsed;
if (kiroAuthMethod && provider !== 'kiro' && !compositeProviders.includes('kiro')) {
console.error(fail('--kiro-auth-method is only valid for ccs kiro'));
process.exitCode = 1;
return;
}
if (
(kiroIDCStartUrl || kiroIDCRegion || kiroIDCFlow) &&
provider !== 'kiro' &&
!compositeProviders.includes('kiro')
) {
console.error(
fail(
'--kiro-idc-start-url, --kiro-idc-region, and --kiro-idc-flow are only valid for ccs kiro'
)
);
process.exitCode = 1;
return;
}
if (kiroAuthMethod === 'idc' && !kiroIDCStartUrl) {
console.error(fail('Kiro IDC login requires --kiro-idc-start-url'));
console.error(
' Example: ccs kiro --auth --kiro-auth-method idc --kiro-idc-start-url https://d-xxx.awsapps.com/start'
);
process.exitCode = 1;
return;
}
if (
kiroAuthMethod &&
kiroAuthMethod !== 'idc' &&
(kiroIDCStartUrl || kiroIDCRegion || kiroIDCFlow)
) {
console.error(
fail(
'--kiro-idc-start-url, --kiro-idc-region, and --kiro-idc-flow require --kiro-auth-method idc'
)
);
process.exitCode = 1;
return;
}
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;
}
}
+43 -303
View File
@@ -36,7 +36,7 @@ import { isAuthenticated } from '../auth/auth-handler';
import { CLIProxyProvider, CLIProxyBackend, PLUS_ONLY_PROVIDERS, ExecutorConfig } from '../types';
import { configureProviderModel, getCurrentModel } from '../config/model-config';
import { reconcileCodexModelForActivePlan } from '../ai-providers/codex-plan-compatibility';
import { resolveProxyConfig, PROXY_CLI_FLAGS } from '../proxy/proxy-config-resolver';
import { resolveProxyConfig } from '../proxy/proxy-config-resolver';
import {
supportsModelConfig,
isModelBroken,
@@ -83,14 +83,6 @@ import {
getThinkingConfig,
} from '../../config/unified-config-loader';
import { HttpsTunnelProxy } from '../proxy/https-tunnel-proxy';
import {
isKiroAuthMethod,
isKiroIDCFlow,
KiroAuthMethod,
KiroIDCFlow,
normalizeKiroAuthMethod,
normalizeKiroIDCFlow,
} from '../auth/auth-types';
import { resolveProfileContinuityInheritance } from '../../auth/profile-continuity-inheritance';
// Import modular components
@@ -108,7 +100,6 @@ import {
} from './retry-handler';
import { MANAGED_QUOTA_PROVIDERS, type ManagedQuotaProvider } from '../quota/quota-manager';
import { checkOrJoinProxy, registerProxySession, setupCleanupHandlers } from './session-bridge';
import { parseThinkingOverride } from './thinking-arg-parser';
import {
warnCrossProviderDuplicates,
warnOAuthBanRisk,
@@ -118,7 +109,6 @@ import {
} from '../accounts/account-safety';
import {
ensureCliAntigravityResponsibility,
hasAntigravityRiskAcceptanceFlag,
ANTIGRAVITY_ACCEPT_RISK_FLAGS,
} from '../auth/antigravity-responsibility';
import { getWebSearchHookEnv } from '../../utils/websearch-manager';
@@ -128,6 +118,7 @@ import {
shouldDisableCodexReasoning,
} from './thinking-override-resolver';
import { shouldStartHttpsTunnel } from './https-tunnel-policy';
import { filterCcsFlags, parseExecutorFlags, validateFlagCombinations } from './arg-parser';
function resolveRuntimeQuotaMonitorProviders(
provider: CLIProxyProvider,
@@ -156,41 +147,9 @@ const DEFAULT_CONFIG: ExecutorConfig = {
pollInterval: 100,
};
export function readOptionValue(
args: string[],
flag: string
): { present: boolean; value?: string; missingValue: boolean } {
const inlinePrefix = `${flag}=`;
const inlineArg = args.find((arg) => arg.startsWith(inlinePrefix));
if (inlineArg !== undefined) {
const value = inlineArg.slice(inlinePrefix.length).trim();
return {
present: true,
value: value.length > 0 ? value : undefined,
missingValue: value.length === 0,
};
}
const index = args.indexOf(flag);
if (index === -1) {
return { present: false, missingValue: false };
}
const next = args[index + 1];
if (!next || next.startsWith('-')) {
return { present: true, missingValue: true };
}
return { present: true, value: next.trim(), missingValue: false };
}
export function hasGitLabTokenLoginFlag(args: string[]): boolean {
return args.includes('--gitlab-token-login') || args.includes('--token-login');
}
function getGitLabTokenLoginFlagName(args: string[]): '--gitlab-token-login' | '--token-login' {
return args.includes('--gitlab-token-login') ? '--gitlab-token-login' : '--token-login';
}
// readOptionValue, hasGitLabTokenLoginFlag, CCS_FLAGS, filterCcsFlags are
// re-exported from ./arg-parser via the export block at the bottom of this file
// for backwards compatibility with external callers.
/**
* Execute Claude CLI with CLIProxy (main entry point)
@@ -408,198 +367,41 @@ export async function execClaudeWithCLIProxy(
}
}
// 2. Handle special flags (simplified flag parsing - full implementation continues below)
const forceAuth = argsWithoutProxy.includes('--auth');
const pasteCallback = argsWithoutProxy.includes('--paste-callback');
const portForward = argsWithoutProxy.includes('--port-forward');
const forceHeadless = argsWithoutProxy.includes('--headless');
// 2. Parse all CCS executor flags (extracted to arg-parser.ts)
const parsedFlags = parseExecutorFlags(argsWithoutProxy, {
provider,
compositeProviders,
unifiedConfig,
});
if (process.exitCode === 1) return;
if (pasteCallback && portForward) {
console.error(fail('Cannot use --paste-callback with --port-forward'));
console.error(' --paste-callback: Manually paste OAuth redirect URL');
console.error(' --port-forward: Use SSH port forwarding for callback');
process.exit(1);
}
// Validate cross-flag combinations (exits with code 1 on violation)
validateFlagCombinations(parsedFlags, { provider, compositeProviders }, argsWithoutProxy);
if (process.exitCode === 1) return;
const forceLogout = argsWithoutProxy.includes('--logout');
const forceConfig = argsWithoutProxy.includes('--config');
const addAccount = argsWithoutProxy.includes('--add');
const showAccounts = argsWithoutProxy.includes('--accounts');
const forceImport = argsWithoutProxy.includes('--import');
const gitlabTokenLogin = hasGitLabTokenLoginFlag(argsWithoutProxy);
const acceptAgyRisk = hasAntigravityRiskAcceptanceFlag(argsWithoutProxy);
const incognitoFlag = argsWithoutProxy.includes('--incognito');
const noIncognitoFlag = argsWithoutProxy.includes('--no-incognito');
const kiroNoIncognitoConfig =
provider === 'kiro' ? (unifiedConfig.cliproxy?.kiro_no_incognito ?? true) : false;
const noIncognito = incognitoFlag ? false : noIncognitoFlag || kiroNoIncognitoConfig;
// Parse --use flag
let useAccount: string | undefined;
const useIdx = argsWithoutProxy.indexOf('--use');
if (
useIdx !== -1 &&
argsWithoutProxy[useIdx + 1] &&
!argsWithoutProxy[useIdx + 1].startsWith('-')
) {
useAccount = argsWithoutProxy[useIdx + 1];
}
// Parse --nickname flag
let setNickname: string | undefined;
const nicknameIdx = argsWithoutProxy.indexOf('--nickname');
if (
nicknameIdx !== -1 &&
argsWithoutProxy[nicknameIdx + 1] &&
!argsWithoutProxy[nicknameIdx + 1].startsWith('-')
) {
setNickname = argsWithoutProxy[nicknameIdx + 1];
}
// Parse --kiro-auth-method flag
let kiroAuthMethod: KiroAuthMethod | undefined;
const kiroMethodValue = readOptionValue(argsWithoutProxy, '--kiro-auth-method');
if (kiroMethodValue.present) {
const rawMethod = kiroMethodValue.value;
if (kiroMethodValue.missingValue || !rawMethod) {
console.error(fail('--kiro-auth-method requires a value'));
console.error(' Supported values: aws, aws-authcode, google, github, idc');
process.exitCode = 1;
return;
}
const normalized = rawMethod.trim().toLowerCase();
if (!isKiroAuthMethod(normalized)) {
console.error(fail(`Invalid --kiro-auth-method value: ${rawMethod}`));
console.error(' Supported values: aws, aws-authcode, google, github, idc');
process.exitCode = 1;
return;
}
kiroAuthMethod = normalizeKiroAuthMethod(normalized);
}
let kiroIDCStartUrl: string | undefined;
const kiroIDCStartUrlValue = readOptionValue(argsWithoutProxy, '--kiro-idc-start-url');
if (kiroIDCStartUrlValue.present && kiroIDCStartUrlValue.value) {
kiroIDCStartUrl = kiroIDCStartUrlValue.value;
} else if (kiroIDCStartUrlValue.present) {
console.error(fail('--kiro-idc-start-url requires a value'));
process.exitCode = 1;
return;
}
let kiroIDCRegion: string | undefined;
const kiroIDCRegionValue = readOptionValue(argsWithoutProxy, '--kiro-idc-region');
if (kiroIDCRegionValue.present && kiroIDCRegionValue.value) {
kiroIDCRegion = kiroIDCRegionValue.value;
} else if (kiroIDCRegionValue.present) {
console.error(fail('--kiro-idc-region requires a value'));
process.exitCode = 1;
return;
}
let kiroIDCFlow: KiroIDCFlow | undefined;
const kiroIDCFlowValue = readOptionValue(argsWithoutProxy, '--kiro-idc-flow');
if (kiroIDCFlowValue.present) {
const rawFlow = kiroIDCFlowValue.value;
if (kiroIDCFlowValue.missingValue || !rawFlow) {
console.error(fail('--kiro-idc-flow requires a value'));
console.error(' Supported values: authcode, device');
process.exitCode = 1;
return;
}
const normalized = rawFlow.trim().toLowerCase();
if (!isKiroIDCFlow(normalized)) {
console.error(fail(`Invalid --kiro-idc-flow value: ${rawFlow}`));
console.error(' Supported values: authcode, device');
process.exitCode = 1;
return;
}
kiroIDCFlow = normalizeKiroIDCFlow(normalized);
}
let gitlabBaseUrl: string | undefined;
const gitlabBaseUrlValue = readOptionValue(argsWithoutProxy, '--gitlab-url');
if (gitlabBaseUrlValue.present && gitlabBaseUrlValue.value) {
gitlabBaseUrl = gitlabBaseUrlValue.value.trim();
} else if (gitlabBaseUrlValue.present) {
console.error(fail('--gitlab-url requires a value'));
process.exitCode = 1;
return;
}
if (kiroAuthMethod && provider !== 'kiro' && !compositeProviders.includes('kiro')) {
console.error(fail('--kiro-auth-method is only valid for ccs kiro'));
process.exitCode = 1;
return;
}
if (
(kiroIDCStartUrl || kiroIDCRegion || kiroIDCFlow) &&
provider !== 'kiro' &&
!compositeProviders.includes('kiro')
) {
console.error(
fail(
'--kiro-idc-start-url, --kiro-idc-region, and --kiro-idc-flow are only valid for ccs kiro'
)
);
process.exitCode = 1;
return;
}
if (!kiroAuthMethod && (kiroIDCStartUrl || kiroIDCRegion || kiroIDCFlow)) {
kiroAuthMethod = 'idc';
}
if (kiroAuthMethod === 'idc' && !kiroIDCStartUrl) {
console.error(fail('Kiro IDC login requires --kiro-idc-start-url'));
console.error(
' Example: ccs kiro --auth --kiro-auth-method idc --kiro-idc-start-url https://d-xxx.awsapps.com/start'
);
process.exitCode = 1;
return;
}
if (
kiroAuthMethod &&
kiroAuthMethod !== 'idc' &&
(kiroIDCStartUrl || kiroIDCRegion || kiroIDCFlow)
) {
console.error(
fail(
'--kiro-idc-start-url, --kiro-idc-region, and --kiro-idc-flow require --kiro-auth-method idc'
)
);
process.exitCode = 1;
return;
}
if ((gitlabTokenLogin || gitlabBaseUrl) && provider !== 'gitlab') {
const flagName = gitlabTokenLogin
? getGitLabTokenLoginFlagName(argsWithoutProxy)
: '--gitlab-url';
console.error(fail(`${flagName} is only valid for ccs gitlab`));
process.exitCode = 1;
return;
}
// Parse --thinking / --effort flags (aliases; first occurrence wins)
const thinkingParse = parseThinkingOverride(argsWithoutProxy);
if (thinkingParse.error) {
const { flag } = thinkingParse.error;
console.error(fail(`${flag} requires a value`));
if (provider === 'codex') {
console.error(' Codex examples: --effort xhigh, --effort high, --effort medium');
console.error(' Alias: --thinking xhigh (same behavior)');
} else {
console.error(' Examples: --thinking low, --thinking 8192, --thinking off');
console.error(' Levels: minimal, low, medium, high, xhigh, max, auto');
}
process.exit(1);
}
const {
forceAuth,
pasteCallback,
portForward,
forceHeadless,
forceLogout,
forceConfig,
addAccount,
showAccounts,
forceImport,
gitlabTokenLogin,
acceptAgyRisk,
noIncognito,
useAccount,
setNickname,
kiroAuthMethod,
kiroIDCStartUrl,
kiroIDCRegion,
kiroIDCFlow,
gitlabBaseUrl,
extendedContextOverride,
thinkingParse,
} = parsedFlags;
const { thinkingOverride, thinkingSource } = resolveRuntimeThinkingOverride(
thinkingParse.value,
@@ -621,24 +423,6 @@ export async function execClaudeWithCLIProxy(
);
}
// Parse --1m / --no-1m flags for extended context (1M token window)
let extendedContextOverride: boolean | undefined;
const has1mFlag =
argsWithoutProxy.includes('--1m') || argsWithoutProxy.some((arg) => arg.startsWith('--1m='));
const hasNo1mFlag =
argsWithoutProxy.includes('--no-1m') ||
argsWithoutProxy.some((arg) => arg.startsWith('--no-1m='));
if (has1mFlag && hasNo1mFlag) {
console.error(fail('Cannot use both --1m and --no-1m flags'));
process.exit(1);
} else if (has1mFlag) {
extendedContextOverride = true;
} else if (hasNo1mFlag) {
extendedContextOverride = false;
}
// undefined = auto behavior (Gemini: on, others: off)
// Handle --accounts
if (showAccounts) {
const accounts = getProviderAccounts(provider);
@@ -1298,55 +1082,7 @@ export async function execClaudeWithCLIProxy(
}
// 12. Filter CCS-specific flags before passing to Claude CLI
const ccsFlags = [
'--auth',
'--paste-callback',
'--port-forward',
'--headless',
'--logout',
'--config',
'--add',
'--accounts',
'--use',
'--nickname',
'--kiro-auth-method',
'--kiro-idc-start-url',
'--kiro-idc-region',
'--kiro-idc-flow',
'--thinking',
'--effort',
'--1m',
'--no-1m',
'--incognito',
'--no-incognito',
'--import',
'--accept-agr-risk',
'--accept-antigravity-risk',
'--settings',
...PROXY_CLI_FLAGS,
];
const claudeArgs = argsWithoutBrowserFlags.filter((arg, idx) => {
if (ccsFlags.includes(arg)) return false;
if (arg.startsWith('--kiro-auth-method=')) return false;
if (arg.startsWith('--kiro-idc-start-url=')) return false;
if (arg.startsWith('--kiro-idc-region=')) return false;
if (arg.startsWith('--kiro-idc-flow=')) return false;
if (arg.startsWith('--thinking=')) return false;
if (arg.startsWith('--effort=')) return false;
if (arg.startsWith('--1m=') || arg.startsWith('--no-1m=')) return false;
if (
argsWithoutBrowserFlags[idx - 1] === '--use' ||
argsWithoutBrowserFlags[idx - 1] === '--nickname' ||
argsWithoutBrowserFlags[idx - 1] === '--kiro-auth-method' ||
argsWithoutBrowserFlags[idx - 1] === '--kiro-idc-start-url' ||
argsWithoutBrowserFlags[idx - 1] === '--kiro-idc-region' ||
argsWithoutBrowserFlags[idx - 1] === '--kiro-idc-flow' ||
argsWithoutBrowserFlags[idx - 1] === '--thinking' ||
argsWithoutBrowserFlags[idx - 1] === '--effort'
)
return false;
return true;
});
const claudeArgs = filterCcsFlags(argsWithoutBrowserFlags);
const isWindows = process.platform === 'win32';
const needsShell = isWindows && /\.(cmd|bat|ps1)$/i.test(claudeCli);
@@ -1421,6 +1157,10 @@ export async function execClaudeWithCLIProxy(
// Re-export utility functions for backwards compatibility
export { isPortAvailable, findAvailablePort } from './lifecycle-manager';
// Re-export arg-parser helpers (previously inlined here; external callers can
// import from index or directly from ./arg-parser)
export { readOptionValue, hasGitLabTokenLoginFlag, CCS_FLAGS, filterCcsFlags } from './arg-parser';
export const __testExports = {
resolveRuntimeQuotaMonitorProviders,
};