From 968681f261f6d0f00168347f4bdc203a151ef542 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Sat, 2 May 2026 21:38:38 -0400 Subject: [PATCH] refactor(cliproxy/executor): extract proxy-resolver from index.ts Phase 03 of #1162. Splits proxy + binary resolution out of the orchestrator into a focused module: - src/cliproxy/executor/proxy-resolver.ts (196 LOC): ResolvedProxy + ResolveExecutorProxyContext interfaces, resolveExecutorProxy function. Encapsulates proxy config resolution, port mutation, remote reachability check, fallback prompt, local backend selection, and binary acquisition. - src/cliproxy/executor/__tests__/proxy-resolver.test.ts: 10 unit tests covering local/remote/fallback paths. index.ts: 1168 -> 1045 LOC (-123). Removes 9 now-unused imports. Browser flag handling intentionally left in index.ts for Phase 04. Behavior unchanged; full suite passes 1824/1824. Refs #1162 --- .../executor/__tests__/proxy-resolver.test.ts | 257 ++++++++++++++++++ src/cliproxy/executor/index.ts | 147 +--------- src/cliproxy/executor/proxy-resolver.ts | 196 +++++++++++++ 3 files changed, 465 insertions(+), 135 deletions(-) create mode 100644 src/cliproxy/executor/__tests__/proxy-resolver.test.ts create mode 100644 src/cliproxy/executor/proxy-resolver.ts diff --git a/src/cliproxy/executor/__tests__/proxy-resolver.test.ts b/src/cliproxy/executor/__tests__/proxy-resolver.test.ts new file mode 100644 index 00000000..86af7137 --- /dev/null +++ b/src/cliproxy/executor/__tests__/proxy-resolver.test.ts @@ -0,0 +1,257 @@ +/** + * Unit tests for proxy-resolver.ts (Phase 03 extraction) + * + * Tests cover the proxy resolution + remote reachability + binary acquisition + * logic extracted from executor/index.ts. + */ + +import { beforeEach, describe, expect, it, jest } from 'bun:test'; +import type { ResolveExecutorProxyContext } from '../proxy-resolver'; +import type { ExecutorConfig } from '../../types'; +import type { UnifiedConfig } from '../../../config/schemas/unified-config'; + +// ── Module mocks ────────────────────────────────────────────────────────────── + +const mockEnsureCLIProxyBinary = jest.fn().mockResolvedValue('/usr/local/bin/cliproxy'); +const mockGetConfiguredBackend = jest.fn().mockReturnValue('original'); +const mockGetPlusBackendUnavailableMessage = jest.fn().mockReturnValue('Plus backend unavailable'); + +jest.mock('../../binary-manager', () => ({ + ensureCLIProxyBinary: mockEnsureCLIProxyBinary, + getConfiguredBackend: mockGetConfiguredBackend, + getPlusBackendUnavailableMessage: mockGetPlusBackendUnavailableMessage, +})); + +const mockCheckRemoteProxy = jest.fn(); +jest.mock('../../services/remote-proxy-client', () => ({ + checkRemoteProxy: mockCheckRemoteProxy, +})); + +jest.mock('../retry-handler', () => ({ + isNetworkError: jest.fn().mockReturnValue(false), + handleNetworkError: jest.fn(), +})); + +const mockResolveProxyConfig = jest.fn(); +jest.mock('../../proxy/proxy-config-resolver', () => ({ + resolveProxyConfig: mockResolveProxyConfig, +})); + +jest.mock('../../config/config-generator', () => ({ + CLIPROXY_DEFAULT_PORT: 8317, + validatePort: jest.fn((port: number | undefined) => port ?? 8317), +})); + +// ── Import after mocks ──────────────────────────────────────────────────────── + +const { resolveExecutorProxy } = await import('../proxy-resolver'); + +// ── Helpers ─────────────────────────────────────────────────────────────────── + +function makeMinimalUnifiedConfig(): UnifiedConfig { + return { + cliproxy_server: undefined, + } as unknown as UnifiedConfig; +} + +function makeBaseCfg(): ExecutorConfig { + return { + port: 8317, + timeout: 5000, + verbose: false, + pollInterval: 100, + }; +} + +function makeContext( + overrides: Partial = {} +): ResolveExecutorProxyContext { + return { + unifiedConfig: makeMinimalUnifiedConfig(), + allProviders: ['gemini'], + verbose: false, + cfg: makeBaseCfg(), + log: jest.fn(), + ...overrides, + }; +} + +/** Mock resolveProxyConfig to return a local-mode config */ +function mockLocalProxyConfig(remainingArgs: string[] = []): void { + mockResolveProxyConfig.mockReturnValue({ + config: { + mode: 'local', + port: 8317, + protocol: 'http', + fallbackEnabled: false, + autoStartLocal: false, + remoteOnly: false, + forceLocal: true, + }, + remainingArgs, + }); +} + +/** Mock resolveProxyConfig to return a remote-mode config */ +function mockRemoteProxyConfig(remainingArgs: string[] = []): void { + mockResolveProxyConfig.mockReturnValue({ + config: { + mode: 'remote', + host: '192.168.1.100', + port: 8317, + protocol: 'http', + fallbackEnabled: false, + autoStartLocal: false, + remoteOnly: false, + forceLocal: false, + }, + remainingArgs, + }); +} + +// ── Tests ───────────────────────────────────────────────────────────────────── + +beforeEach(() => { + jest.clearAllMocks(); + mockEnsureCLIProxyBinary.mockResolvedValue('/usr/local/bin/cliproxy'); + mockGetConfiguredBackend.mockReturnValue('original'); +}); + +describe('resolveExecutorProxy — local mode', () => { + it('returns useRemoteProxy=false and correct binary for local mode', async () => { + mockLocalProxyConfig(['--verbose']); + const result = await resolveExecutorProxy(['--verbose'], makeContext()); + + expect(result.useRemoteProxy).toBe(false); + expect(result.localBackend).toBe('original'); + expect(result.binaryPath).toBe('/usr/local/bin/cliproxy'); + expect(result.argsWithoutProxy).toEqual(['--verbose']); + }); + + it('strips proxy flags and passes remainingArgs through', async () => { + mockLocalProxyConfig(['clean-arg']); + const result = await resolveExecutorProxy(['--local-proxy', 'clean-arg'], makeContext()); + + expect(result.argsWithoutProxy).toEqual(['clean-arg']); + expect(result.useRemoteProxy).toBe(false); + }); + + it('does not call checkRemoteProxy in local mode', async () => { + mockLocalProxyConfig(); + await resolveExecutorProxy([], makeContext()); + + expect(mockCheckRemoteProxy).not.toHaveBeenCalled(); + }); +}); + +describe('resolveExecutorProxy — remote mode reachable', () => { + it('returns useRemoteProxy=true when remote proxy is reachable', async () => { + mockRemoteProxyConfig(); + mockCheckRemoteProxy.mockResolvedValue({ reachable: true, latencyMs: 12, error: undefined }); + + const result = await resolveExecutorProxy([], makeContext()); + + expect(result.useRemoteProxy).toBe(true); + }); + + it('skips binary acquisition when remote proxy is reachable', async () => { + mockRemoteProxyConfig(); + mockCheckRemoteProxy.mockResolvedValue({ reachable: true, latencyMs: 5, error: undefined }); + + const result = await resolveExecutorProxy([], makeContext()); + + expect(result.binaryPath).toBeUndefined(); + expect(mockEnsureCLIProxyBinary).not.toHaveBeenCalled(); + }); +}); + +describe('resolveExecutorProxy — remote mode unreachable', () => { + it('throws expected message when remoteOnly=true and remote is unreachable', async () => { + mockResolveProxyConfig.mockReturnValue({ + config: { + mode: 'remote', + host: '192.168.1.100', + port: 8317, + protocol: 'http', + fallbackEnabled: false, + autoStartLocal: false, + remoteOnly: true, + forceLocal: false, + }, + remainingArgs: [], + }); + mockCheckRemoteProxy.mockResolvedValue({ reachable: false, error: 'Connection refused' }); + + await expect(resolveExecutorProxy([], makeContext())).rejects.toThrow( + 'Remote proxy unreachable and --remote-only specified' + ); + }); + + it('throws when fallback disabled and remote is unreachable', async () => { + mockResolveProxyConfig.mockReturnValue({ + config: { + mode: 'remote', + host: '192.168.1.100', + port: 8317, + protocol: 'http', + fallbackEnabled: false, + autoStartLocal: false, + remoteOnly: false, + forceLocal: false, + }, + remainingArgs: [], + }); + mockCheckRemoteProxy.mockResolvedValue({ reachable: false, error: 'Timeout' }); + + await expect(resolveExecutorProxy([], makeContext())).rejects.toThrow( + 'Remote proxy unreachable and fallback disabled' + ); + }); + + it('falls back to local and acquires binary when autoStartLocal=true', async () => { + mockResolveProxyConfig.mockReturnValue({ + config: { + mode: 'remote', + host: '192.168.1.100', + port: 8317, + protocol: 'http', + fallbackEnabled: true, + autoStartLocal: true, + remoteOnly: false, + forceLocal: false, + }, + remainingArgs: [], + }); + mockCheckRemoteProxy.mockResolvedValue({ reachable: false, error: 'Timeout' }); + mockEnsureCLIProxyBinary.mockResolvedValue('/usr/local/bin/cliproxy'); + + const result = await resolveExecutorProxy([], makeContext()); + + expect(result.useRemoteProxy).toBe(false); + expect(result.binaryPath).toBe('/usr/local/bin/cliproxy'); + expect(mockEnsureCLIProxyBinary).toHaveBeenCalled(); + }); +}); + +describe('resolveExecutorProxy — proxyConfig propagated in result', () => { + it('returns the resolved proxyConfig object', async () => { + mockLocalProxyConfig(); + + const result = await resolveExecutorProxy([], makeContext()); + + expect(result.proxyConfig).toBeDefined(); + expect(result.proxyConfig.mode).toBe('local'); + expect(result.proxyConfig.port).toBe(8317); + }); + + it('returns mutated cfg with validated port', async () => { + mockLocalProxyConfig(); + const ctx = makeContext(); + + const result = await resolveExecutorProxy([], ctx); + + // cfg is mutated in place and also returned + expect(result.cfg).toBe(ctx.cfg); + expect(result.cfg.port).toBe(8317); + }); +}); diff --git a/src/cliproxy/executor/index.ts b/src/cliproxy/executor/index.ts index 4d77d6ee..074fea99 100644 --- a/src/cliproxy/executor/index.ts +++ b/src/cliproxy/executor/index.ts @@ -14,29 +14,20 @@ import { spawn, ChildProcess } from 'child_process'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; -import { ProgressIndicator } from '../../utils/progress-indicator'; import { ok, fail, info, warn } from '../../utils/ui'; import { getCcsDir } from '../../utils/config-manager'; import { escapeShellArg, getWindowsEscapedCommandShell } from '../../utils/shell-executor'; -import { - ensureCLIProxyBinary, - getConfiguredBackend, - getPlusBackendUnavailableMessage, -} from '../binary-manager'; import { generateConfig, getProviderConfig, ensureProviderSettings, getProviderSettingsPath, CLIPROXY_DEFAULT_PORT, - validatePort, } from '../config/config-generator'; -import { checkRemoteProxy } from '../services/remote-proxy-client'; import { isAuthenticated } from '../auth/auth-handler'; -import { CLIProxyProvider, CLIProxyBackend, PLUS_ONLY_PROVIDERS, ExecutorConfig } from '../types'; +import { CLIProxyProvider, ExecutorConfig } from '../types'; import { configureProviderModel, getCurrentModel } from '../config/model-config'; import { reconcileCodexModelForActivePlan } from '../ai-providers/codex-plan-compatibility'; -import { resolveProxyConfig } from '../proxy/proxy-config-resolver'; import { supportsModelConfig, isModelBroken, @@ -92,12 +83,7 @@ import { logEnvironment, resolveCliproxyImageAnalysisEnv, } from './env-resolver'; -import { - isNetworkError, - handleNetworkError, - handleTokenExpiration, - handleQuotaCheck, -} from './retry-handler'; +import { handleTokenExpiration, handleQuotaCheck } from './retry-handler'; import { MANAGED_QUOTA_PROVIDERS, type ManagedQuotaProvider } from '../quota/quota-manager'; import { checkOrJoinProxy, registerProxySession, setupCleanupHandlers } from './session-bridge'; import { @@ -119,6 +105,7 @@ import { } from './thinking-override-resolver'; import { shouldStartHttpsTunnel } from './https-tunnel-policy'; import { filterCcsFlags, parseExecutorFlags, validateFlagCombinations } from './arg-parser'; +import { resolveExecutorProxy } from './proxy-resolver'; function resolveRuntimeQuotaMonitorProviders( provider: CLIProxyProvider, @@ -197,26 +184,15 @@ export async function execClaudeWithCLIProxy( // Collect all providers to validate (default + composite tiers) const allProviders = [provider, ...compositeProviders]; - const cliproxyServerConfig = unifiedConfig.cliproxy_server; - const { config: proxyConfig, remainingArgs: argsWithoutProxy } = resolveProxyConfig(args, { - remote: cliproxyServerConfig?.remote - ? { - enabled: cliproxyServerConfig.remote.enabled, - host: cliproxyServerConfig.remote.host, - port: cliproxyServerConfig.remote.port, - protocol: cliproxyServerConfig.remote.protocol, - auth_token: cliproxyServerConfig.remote.auth_token, - management_key: cliproxyServerConfig.remote.management_key, - timeout: cliproxyServerConfig.remote.timeout, - } - : undefined, - local: cliproxyServerConfig?.local - ? { - port: cliproxyServerConfig.local.port, - auto_start: cliproxyServerConfig.local.auto_start, - } - : undefined, - }); + const { proxyConfig, useRemoteProxy, localBackend, binaryPath, argsWithoutProxy } = + await resolveExecutorProxy(args, { + unifiedConfig, + allProviders, + verbose, + cfg, + log, + }); + let browserLaunchOverride: BrowserLaunchOverride | undefined; let argsWithoutBrowserFlags = argsWithoutProxy; try { @@ -245,21 +221,6 @@ export async function execClaudeWithCLIProxy( console.error(warn(blockedBrowserOverrideWarning)); } - // Port resolution and validation - if (cfg.port && cfg.port !== CLIPROXY_DEFAULT_PORT) { - if (proxyConfig.port !== CLIPROXY_DEFAULT_PORT) { - cfg.port = proxyConfig.port; - } - } else if (proxyConfig.port !== CLIPROXY_DEFAULT_PORT) { - cfg.port = proxyConfig.port; - } - cfg.port = validatePort(cfg.port); - - log(`Proxy mode: ${proxyConfig.mode}`); - if (proxyConfig.mode === 'remote') { - log(`Remote host: ${proxyConfig.host}:${proxyConfig.port} (${proxyConfig.protocol})`); - } - // Setup first-class CCS WebSearch runtime ensureWebSearchMcpOrThrow(); const imageAnalysisMcpReady = ensureImageAnalysisMcpOrThrow(); @@ -280,93 +241,9 @@ export async function execClaudeWithCLIProxy( log(`Provider: ${providerConfig.displayName}`); warnOAuthBanRisk(provider); - // Check remote proxy if configured - let useRemoteProxy = false; - let localBackend: CLIProxyBackend = 'original'; - if (proxyConfig.mode === 'remote' && proxyConfig.host) { - const status = await checkRemoteProxy({ - host: proxyConfig.host, - port: proxyConfig.port, - protocol: proxyConfig.protocol, - authToken: proxyConfig.authToken, - timeout: proxyConfig.timeout ?? 2000, - allowSelfSigned: proxyConfig.allowSelfSigned ?? false, - }); - - if (status.reachable) { - useRemoteProxy = true; - console.log( - ok( - `Connected to remote proxy at ${proxyConfig.host}:${proxyConfig.port} (${status.latencyMs}ms)` - ) - ); - } else { - console.error(warn(`Remote proxy unreachable: ${status.error}`)); - - if (proxyConfig.remoteOnly) { - throw new Error('Remote proxy unreachable and --remote-only specified'); - } - - if (proxyConfig.fallbackEnabled) { - if (proxyConfig.autoStartLocal) { - console.log(info('Falling back to local proxy...')); - } else { - if (process.stdin.isTTY) { - const readline = await import('readline'); - const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); - const answer = await new Promise((resolve) => { - rl.question('Start local proxy instead? [Y/n] ', resolve); - }); - rl.close(); - if (answer.toLowerCase() === 'n') { - throw new Error('Remote proxy unreachable and user declined fallback'); - } - } - console.log(info('Starting local proxy...')); - } - } else { - throw new Error('Remote proxy unreachable and fallback disabled'); - } - } - } - - if (!useRemoteProxy) { - localBackend = getConfiguredBackend({ notifyOnPlus: true }); - - for (const p of allProviders) { - if (localBackend === 'original' && PLUS_ONLY_PROVIDERS.includes(p as CLIProxyProvider)) { - console.error(''); - console.error(fail(getPlusBackendUnavailableMessage(p))); - console.error(''); - throw new Error(`Provider ${p} requires local CLIProxy Plus backend`); - } - } - } - // Variables for local proxy mode - let binaryPath: string | undefined; let sessionId: string | undefined; - // 1. Ensure binary exists (downloads if needed) - SKIP for remote mode - if (!useRemoteProxy) { - const spinner = new ProgressIndicator('Preparing CLIProxy'); - spinner.start(); - - try { - binaryPath = await ensureCLIProxyBinary(verbose, { skipAutoUpdate: true }); - spinner.succeed('CLIProxy binary ready'); - } catch (error) { - spinner.fail('Failed to prepare CLIProxy'); - const err = error as Error; - - if (isNetworkError(err)) { - handleNetworkError(err); - } - - throw error; - } - } - // 2. Parse all CCS executor flags (extracted to arg-parser.ts) const parsedFlags = parseExecutorFlags(argsWithoutProxy, { provider, diff --git a/src/cliproxy/executor/proxy-resolver.ts b/src/cliproxy/executor/proxy-resolver.ts new file mode 100644 index 00000000..eae39abb --- /dev/null +++ b/src/cliproxy/executor/proxy-resolver.ts @@ -0,0 +1,196 @@ +/** + * Proxy Resolver — Concern D + * + * Handles proxy configuration resolution, remote proxy reachability check, + * local-backend selection, and CLIProxy binary acquisition. + * + * Extracted from executor/index.ts to isolate the proxy-resolution concern. + * All log messages, error messages, and exit semantics are byte-identical to + * the original implementation. + */ + +import { ProgressIndicator } from '../../utils/progress-indicator'; +import { ok, fail, info, warn } from '../../utils/ui'; +import { + ensureCLIProxyBinary, + getConfiguredBackend, + getPlusBackendUnavailableMessage, +} from '../binary-manager'; +import { checkRemoteProxy } from '../services/remote-proxy-client'; +import { CLIProxyProvider, CLIProxyBackend, PLUS_ONLY_PROVIDERS, ExecutorConfig } from '../types'; +import { resolveProxyConfig } from '../proxy/proxy-config-resolver'; +import { CLIPROXY_DEFAULT_PORT, validatePort } from '../config/config-generator'; +import type { ResolvedProxyConfig } from '../types'; +import type { UnifiedConfig } from '../../config/schemas/unified-config'; +import { isNetworkError, handleNetworkError } from './retry-handler'; + +/** Result returned from resolveExecutorProxy */ +export interface ResolvedProxy { + /** Resolved proxy config after merging CLI > ENV > config.yaml > defaults */ + proxyConfig: ResolvedProxyConfig; + /** Whether to use the remote proxy (vs spawning a local one) */ + useRemoteProxy: boolean; + /** Which local backend binary to use ('original' | 'plus') */ + localBackend: CLIProxyBackend; + /** Absolute path to CLIProxy binary; undefined when useRemoteProxy=true */ + binaryPath: string | undefined; + /** Args after proxy-related flags are stripped out */ + argsWithoutProxy: string[]; + /** Mutated executor config (port resolved and validated) */ + cfg: ExecutorConfig; +} + +/** Dependencies injected by the orchestrator */ +export interface ResolveExecutorProxyContext { + unifiedConfig: UnifiedConfig; + allProviders: CLIProxyProvider[]; + verbose: boolean; + cfg: ExecutorConfig; + log: (msg: string) => void; +} + +/** + * Resolves proxy configuration, checks remote reachability, selects the local + * backend, and ensures the CLIProxy binary is present when running locally. + * + * Mutates `context.cfg.port` in-place (same as original orchestrator behaviour). + */ +export async function resolveExecutorProxy( + args: string[], + context: ResolveExecutorProxyContext +): Promise { + const { unifiedConfig, allProviders, verbose: _verbose, cfg, log } = context; + + // Resolve proxy config from CLI flags > ENV > config.yaml > defaults + const cliproxyServerConfig = unifiedConfig.cliproxy_server; + const { config: proxyConfig, remainingArgs: argsWithoutProxy } = resolveProxyConfig(args, { + remote: cliproxyServerConfig?.remote + ? { + enabled: cliproxyServerConfig.remote.enabled, + host: cliproxyServerConfig.remote.host, + port: cliproxyServerConfig.remote.port, + protocol: cliproxyServerConfig.remote.protocol, + auth_token: cliproxyServerConfig.remote.auth_token, + management_key: cliproxyServerConfig.remote.management_key, + timeout: cliproxyServerConfig.remote.timeout, + } + : undefined, + local: cliproxyServerConfig?.local + ? { + port: cliproxyServerConfig.local.port, + auto_start: cliproxyServerConfig.local.auto_start, + } + : undefined, + }); + + // Port resolution and validation (mutates cfg in-place) + if (cfg.port && cfg.port !== CLIPROXY_DEFAULT_PORT) { + if (proxyConfig.port !== CLIPROXY_DEFAULT_PORT) { + cfg.port = proxyConfig.port; + } + } else if (proxyConfig.port !== CLIPROXY_DEFAULT_PORT) { + cfg.port = proxyConfig.port; + } + cfg.port = validatePort(cfg.port); + + log(`Proxy mode: ${proxyConfig.mode}`); + if (proxyConfig.mode === 'remote') { + log(`Remote host: ${proxyConfig.host}:${proxyConfig.port} (${proxyConfig.protocol})`); + } + + // Check remote proxy reachability + let useRemoteProxy = false; + let localBackend: CLIProxyBackend = 'original'; + + if (proxyConfig.mode === 'remote' && proxyConfig.host) { + const status = await checkRemoteProxy({ + host: proxyConfig.host, + port: proxyConfig.port, + protocol: proxyConfig.protocol, + authToken: proxyConfig.authToken, + timeout: proxyConfig.timeout ?? 2000, + allowSelfSigned: proxyConfig.allowSelfSigned ?? false, + }); + + if (status.reachable) { + useRemoteProxy = true; + console.log( + ok( + `Connected to remote proxy at ${proxyConfig.host}:${proxyConfig.port} (${status.latencyMs}ms)` + ) + ); + } else { + console.error(warn(`Remote proxy unreachable: ${status.error}`)); + + if (proxyConfig.remoteOnly) { + throw new Error('Remote proxy unreachable and --remote-only specified'); + } + + if (proxyConfig.fallbackEnabled) { + if (proxyConfig.autoStartLocal) { + console.log(info('Falling back to local proxy...')); + } else { + if (process.stdin.isTTY) { + const readline = await import('readline'); + const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); + const answer = await new Promise((resolve) => { + rl.question('Start local proxy instead? [Y/n] ', resolve); + }); + rl.close(); + if (answer.toLowerCase() === 'n') { + throw new Error('Remote proxy unreachable and user declined fallback'); + } + } + console.log(info('Starting local proxy...')); + } + } else { + throw new Error('Remote proxy unreachable and fallback disabled'); + } + } + } + + // Local backend selection (only when not using remote proxy) + if (!useRemoteProxy) { + localBackend = getConfiguredBackend({ notifyOnPlus: true }); + + for (const p of allProviders) { + if (localBackend === 'original' && PLUS_ONLY_PROVIDERS.includes(p as CLIProxyProvider)) { + console.error(''); + console.error(fail(getPlusBackendUnavailableMessage(p))); + console.error(''); + throw new Error(`Provider ${p} requires local CLIProxy Plus backend`); + } + } + } + + // Binary acquisition — skipped when using remote proxy + let binaryPath: string | undefined; + + if (!useRemoteProxy) { + const spinner = new ProgressIndicator('Preparing CLIProxy'); + spinner.start(); + + try { + binaryPath = await ensureCLIProxyBinary(_verbose, { skipAutoUpdate: true }); + spinner.succeed('CLIProxy binary ready'); + } catch (error) { + spinner.fail('Failed to prepare CLIProxy'); + const err = error as Error; + + if (isNetworkError(err)) { + handleNetworkError(err); + } + + throw error; + } + } + + return { + proxyConfig, + useRemoteProxy, + localBackend, + binaryPath, + argsWithoutProxy, + cfg, + }; +}