diff --git a/package.json b/package.json index 67ab369a..854a1d37 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,7 @@ "ui:build": "cd ui && bun run build", "ui:preview": "cd ui && bun run preview", "ui:validate": "cd ui && bun run validate", + "prepack": "bun run build:all", "prepare": "husky", "postinstall": "node scripts/postinstall.js" }, diff --git a/src/targets/codex-adapter.ts b/src/targets/codex-adapter.ts index d4491288..09a278d7 100644 --- a/src/targets/codex-adapter.ts +++ b/src/targets/codex-adapter.ts @@ -2,6 +2,7 @@ import { ChildProcess, spawn } from 'child_process'; import * as fs from 'fs'; import type { ProfileType } from '../types/profile'; import { runCleanup } from '../errors'; +import { expandPath } from '../utils/helpers'; import { wireChildProcessSignals } from '../utils/signal-forwarder'; import { escapeShellArg, stripAnthropicEnv, stripCodexSessionEnv } from '../utils/shell-executor'; import type { @@ -20,6 +21,7 @@ import { const CODEX_RUNTIME_PROVIDER_ID = 'ccs_runtime'; const CODEX_RUNTIME_ENV_KEY = 'CCS_CODEX_API_KEY'; const CODEX_REASONING_LEVELS = new Set(['minimal', 'low', 'medium', 'high', 'xhigh']); +const CODEX_INFO_FLAGS = new Set(['--help', '-h', '--version', '-v']); function formatTomlString(value: string): string { return JSON.stringify(value); @@ -83,6 +85,85 @@ function normalizeCodexReasoningOverride(value: string | number | undefined): st ); } +function isInformationalCodexInvocation(args: string[]): boolean { + if (args.length === 1) { + return CODEX_INFO_FLAGS.has(args[0] || ''); + } + + if (args.length === 2) { + return CODEX_INFO_FLAGS.has(args[1] || ''); + } + + return false; +} + +function normalizeExplicitCodexHomeEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv { + const rawCodexHome = env.CODEX_HOME; + if (rawCodexHome === undefined) { + return env; + } + + const trimmedCodexHome = rawCodexHome.trim(); + if (!trimmedCodexHome) { + const nextEnv = { ...env }; + delete nextEnv.CODEX_HOME; + return nextEnv; + } + + const normalizedCodexHome = expandPath(trimmedCodexHome); + if (normalizedCodexHome === rawCodexHome) { + return env; + } + + return { + ...env, + CODEX_HOME: normalizedCodexHome, + }; +} + +function prepareExplicitCodexHome( + env: NodeJS.ProcessEnv, + args: string[] +): { env: NodeJS.ProcessEnv; error?: string } { + const normalizedEnv = normalizeExplicitCodexHomeEnv(env); + const codexHome = normalizedEnv.CODEX_HOME; + if (!codexHome) { + return { env: normalizedEnv }; + } + + if (isInformationalCodexInvocation(args)) { + return { env: normalizedEnv }; + } + + try { + fs.mkdirSync(codexHome, { recursive: true }); + } catch (err) { + const error = err as NodeJS.ErrnoException; + if (error.code !== 'EEXIST') { + return { + env: normalizedEnv, + error: `[X] Unable to initialize CODEX_HOME (${error.code || 'unknown'}): ${codexHome}`, + }; + } + } + + try { + if (!fs.statSync(codexHome).isDirectory()) { + return { + env: normalizedEnv, + error: `[X] CODEX_HOME path is not a directory: ${codexHome}`, + }; + } + return { env: normalizedEnv }; + } catch (err) { + const error = err as NodeJS.ErrnoException; + return { + env: normalizedEnv, + error: `[X] Unable to access CODEX_HOME (${error.code || 'unknown'}): ${codexHome}`, + }; + } +} + export class CodexAdapter implements TargetAdapter { readonly type: TargetType = 'codex'; readonly displayName = 'Codex CLI'; @@ -207,6 +288,13 @@ export class CodexAdapter implements TargetAdapter { return exitWithCleanup(1); } + const codexHomePreparation = prepareExplicitCodexHome(env, args); + if (codexHomePreparation.error) { + console.error(codexHomePreparation.error); + return exitWithCleanup(1); + } + const launchEnv = codexHomePreparation.env; + const isWindows = process.platform === 'win32'; const isPowerShellScript = isWindows && /\.ps1$/i.test(codexPath); const needsShell = isWindows && /\.(cmd|bat)$/i.test(codexPath); @@ -216,7 +304,7 @@ export class CodexAdapter implements TargetAdapter { child = spawn( 'powershell.exe', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', codexPath, ...args], - { stdio: 'inherit', windowsHide: true, env } + { stdio: 'inherit', windowsHide: true, env: launchEnv } ); } else if (needsShell) { const cmdString = [codexPath, ...args].map(escapeShellArg).join(' '); @@ -224,10 +312,10 @@ export class CodexAdapter implements TargetAdapter { stdio: 'inherit', windowsHide: true, shell: true, - env, + env: launchEnv, }); } else { - child = spawn(codexPath, args, { stdio: 'inherit', windowsHide: true, env }); + child = spawn(codexPath, args, { stdio: 'inherit', windowsHide: true, env: launchEnv }); } wireChildProcessSignals(child, (err: NodeJS.ErrnoException) => { diff --git a/src/targets/codex-detector.ts b/src/targets/codex-detector.ts index faf52a35..97cfac1a 100644 --- a/src/targets/codex-detector.ts +++ b/src/targets/codex-detector.ts @@ -5,6 +5,23 @@ import { escapeShellArg } from '../utils/shell-executor'; import type { TargetBinaryInfo } from './target-adapter'; const CODEX_CONFIG_OVERRIDE_FEATURE = 'config-overrides'; +const CODEX_CONFIG_OVERRIDE_PROBE_ARGS = ['-c', 'model="gpt-5"', '--version']; + +function buildWindowsCodexCandidates(matches: string[]): string[] { + const shellCandidates = matches.filter((entry) => /\.(exe|cmd|bat|ps1)$/i.test(entry)); + const bareCandidates = matches.filter((entry) => !/\.(exe|cmd|bat|ps1)$/i.test(entry)); + const prioritized: string[] = []; + + for (const entry of shellCandidates) { + if (/\.(cmd|bat)$/i.test(entry)) { + prioritized.push(entry.replace(/\.(cmd|bat)$/i, '.ps1')); + } + prioritized.push(entry); + } + + prioritized.push(...bareCandidates); + return [...new Set(prioritized)]; +} function runCodexProbe(codexPath: string, args: string[]): string | undefined { const isWindows = process.platform === 'win32'; @@ -49,9 +66,25 @@ export function readCodexVersion(codexPath: string): string | undefined { return runCodexProbe(codexPath, ['--version'])?.trim(); } +function codexHelpAdvertisesConfigOverrides(helpText: string | undefined): boolean { + if (!helpText) { + return false; + } + + return /(^|\n)\s*-c,\s*--config\b/m.test(helpText) || helpText.includes('--config '); +} + +function codexSupportsConfigOverrideProbe(codexPath: string): boolean { + return !!runCodexProbe(codexPath, CODEX_CONFIG_OVERRIDE_PROBE_ARGS)?.trim(); +} + function detectCodexFeatures(codexPath: string): readonly string[] { + if (codexSupportsConfigOverrideProbe(codexPath)) { + return [CODEX_CONFIG_OVERRIDE_FEATURE]; + } + const helpText = runCodexProbe(codexPath, ['--help']); - return helpText?.includes('--config ') ? [CODEX_CONFIG_OVERRIDE_FEATURE] : []; + return codexHelpAdvertisesConfigOverrides(helpText) ? [CODEX_CONFIG_OVERRIDE_FEATURE] : []; } export function detectCodexCli(): string | null { @@ -95,12 +128,7 @@ export function detectCodexCli(): string | null { .map((entry) => entry.trim()) .filter(Boolean); - const candidates = isWindows - ? [ - ...matches.filter((entry) => /\.(exe|cmd|bat|ps1)$/i.test(entry)), - ...matches.filter((entry) => !/\.(exe|cmd|bat|ps1)$/i.test(entry)), - ] - : matches; + const candidates = isWindows ? buildWindowsCodexCandidates(matches) : matches; for (const candidate of candidates) { try { diff --git a/src/web-server/services/codex-dashboard-service.ts b/src/web-server/services/codex-dashboard-service.ts index ce95894f..725c0bc1 100644 --- a/src/web-server/services/codex-dashboard-service.ts +++ b/src/web-server/services/codex-dashboard-service.ts @@ -646,7 +646,7 @@ export function summarizeCodexMcpServers(value: unknown): CodexMcpServerDiagnost .sort((left, right) => left.name.localeCompare(right.name)); } -function getCodexSupportMatrix(): CodexSupportMatrixEntry[] { +function getCodexSupportMatrix(supportsManagedRouting: boolean): CodexSupportMatrixEntry[] { return [ { id: 'default', @@ -657,14 +657,18 @@ function getCodexSupportMatrix(): CodexSupportMatrixEntry[] { { id: 'cliproxy-provider-codex', label: 'cliproxy provider=codex', - supported: true, - notes: 'Routed through the CLIProxy Codex Responses bridge.', + supported: supportsManagedRouting, + notes: supportsManagedRouting + ? 'Routed through the CLIProxy Codex Responses bridge.' + : 'Requires a Codex build that exposes --config overrides.', }, { id: 'settings-with-bridge', label: 'settings with bridge metadata', - supported: true, - notes: 'Supported when the resolved API profile points at a Codex CLIProxy bridge.', + supported: supportsManagedRouting, + notes: supportsManagedRouting + ? 'Supported when the resolved API profile points at a Codex CLIProxy bridge.' + : 'Requires a Codex build that exposes --config overrides.', }, { id: 'cliproxy-composite', @@ -696,6 +700,7 @@ function getCodexSupportMatrix(): CodexSupportMatrixEntry[] { export async function getCodexDashboardDiagnostics(): Promise { const paths = resolveCodexConfigPaths(); const binaryInfo = getCodexBinaryInfo(); + const supportsConfigOverrides = !!binaryInfo && codexBinarySupportsConfigOverrides(binaryInfo); const docsReference = getCompatibleCliDocsReference('codex'); const fileProbe = await probeTomlObjectFile( paths.configPath, @@ -715,12 +720,12 @@ export async function getCodexDashboardDiagnostics(): Promise { 'dedicated ccsxp shortcut entrypoint should exist' ); assert(packageJson.scripts, 'package.json should have scripts field'); + assert.strictEqual( + packageJson.scripts.prepack, + 'bun run build:all', + 'prepack should rebuild packaged assets before npm pack/publish' + ); }); }); }); diff --git a/tests/unit/targets/codex-detector.test.ts b/tests/unit/targets/codex-detector.test.ts index c392bcf7..bf4b0f4a 100644 --- a/tests/unit/targets/codex-detector.test.ts +++ b/tests/unit/targets/codex-detector.test.ts @@ -72,4 +72,70 @@ describe('codex-detector', () => { execFileSyncSpy.mockRestore(); }); + + it('prefers a sibling PowerShell wrapper over cmd when Windows PATH only exposes codex.cmd', () => { + const fakeCmdCodex = path.join(tmpDir, 'codex.cmd'); + const fakePsCodex = path.join(tmpDir, 'codex.ps1'); + fs.writeFileSync(fakeCmdCodex, ''); + fs.writeFileSync(fakePsCodex, ''); + Object.defineProperty(process, 'platform', { value: 'win32' }); + + const execSyncSpy = spyOn(childProcess, 'execSync').mockImplementation(() => `${fakeCmdCodex}\n`); + + expect(detectCodexCli()).toBe(fakePsCodex); + + execSyncSpy.mockRestore(); + }); + + it('falls back to a direct -c probe when help text omits the config flag', () => { + const fakeCodex = path.join(tmpDir, 'codex'); + fs.writeFileSync(fakeCodex, ''); + process.env.CCS_CODEX_PATH = fakeCodex; + + const execFileSyncSpy = spyOn(childProcess, 'execFileSync').mockImplementation((command, args) => { + const joinedArgs = Array.isArray(args) ? args.join(' ') : ''; + + if (joinedArgs.includes('--help')) { + return 'Codex CLI\n'; + } + + if (joinedArgs.includes('-c') && joinedArgs.includes('--version')) { + return 'codex-cli 0.119.0-alpha.1'; + } + + return 'codex-cli 0.119.0-alpha.1'; + }); + + const info = getCodexBinaryInfo(); + + expect(info?.features).toContain('config-overrides'); + + execFileSyncSpy.mockRestore(); + }); + + it('still detects support from broader help text when the direct probe fails', () => { + const fakeCodex = path.join(tmpDir, 'codex'); + fs.writeFileSync(fakeCodex, ''); + process.env.CCS_CODEX_PATH = fakeCodex; + + const execFileSyncSpy = spyOn(childProcess, 'execFileSync').mockImplementation((command, args) => { + const joinedArgs = Array.isArray(args) ? args.join(' ') : ''; + + if (joinedArgs.includes('--help')) { + return 'Codex CLI\n -c, --config \n'; + } + + if (joinedArgs.includes('-c') && joinedArgs.includes('--version')) { + throw new Error('unsupported'); + } + + return 'codex-cli 0.119.0-alpha.1'; + }); + + const info = getCodexBinaryInfo(); + + expect(info?.features).toContain('config-overrides'); + + execFileSyncSpy.mockRestore(); + }); }); diff --git a/tests/unit/targets/codex-runtime-integration.test.ts b/tests/unit/targets/codex-runtime-integration.test.ts index 7e06621d..439e476d 100644 --- a/tests/unit/targets/codex-runtime-integration.test.ts +++ b/tests/unit/targets/codex-runtime-integration.test.ts @@ -126,7 +126,17 @@ if (envOut) { }) + '\\n' ); } -if (cliArgs.includes('--version') || cliArgs.includes('-v')) { +const configFlagIndex = cliArgs.findIndex((arg) => arg === '-c' || arg === '--config'); +if (process.env.CCS_TEST_CODEX_CONFIG_OVERRIDE_STATUS === 'unsupported' && configFlagIndex !== -1) { + process.stderr.write('codex: unknown option --config\\n'); + process.exit(1); +} +if ( + cliArgs.includes('--version') || + cliArgs.includes('-v') || + (configFlagIndex !== -1 && + (cliArgs[configFlagIndex + 2] === '--version' || cliArgs[configFlagIndex + 2] === '-v')) +) { process.stdout.write(process.env.CCS_TEST_CODEX_VERSION || 'codex-cli 0.118.0-alpha.3'); process.exit(0); } @@ -260,6 +270,120 @@ process.exit(0); ]); }); + it('creates an explicit CODEX_HOME directory before routed native Codex launches', () => { + if (process.platform === 'win32') return; + + const freshCodexHome = path.join(tmpHome, 'fresh-codex-home'); + const result = runCcs(['default', '--target', 'codex', '--effort', 'high', 'fix failing tests'], { + ...process.env, + CI: '1', + NO_COLOR: '1', + CCS_HOME: tmpHome, + CCS_CODEX_PATH: fakeCodexPath, + CCS_TEST_CODEX_ARGS_OUT: codexArgsLogPath, + CCS_TEST_CODEX_ENV_OUT: codexEnvLogPath, + CCS_TEST_CODEX_VERSION: 'codex-cli 9.9.9-test', + CODEX_HOME: freshCodexHome, + }); + + expect(result.status).toBe(0); + expect(fs.existsSync(freshCodexHome)).toBe(true); + expect(fs.statSync(freshCodexHome).isDirectory()).toBe(true); + expect(readLoggedCodexCalls(codexArgsLogPath)).toEqual([ + ['-c', 'model="gpt-5"', '--version'], + ['-c', 'model_reasoning_effort="high"', 'fix failing tests'], + ]); + const loggedEnv = readLoggedCodexEnv(codexEnvLogPath); + expect(loggedEnv).toHaveLength(2); + expect(loggedEnv.map((entry) => entry.CODEX_HOME)).toEqual([freshCodexHome, freshCodexHome]); + expect(loggedEnv[1]).toEqual({ + CODEX_HOME: freshCodexHome, + CODEX_CI: undefined, + CODEX_MANAGED_BY_BUN: undefined, + CODEX_THREAD_ID: undefined, + ANTHROPIC_BASE_URL: undefined, + }); + }); + + it('fails with a clean error when routed launches receive a file CODEX_HOME path', () => { + if (process.platform === 'win32') return; + + const invalidCodexHome = path.join(tmpHome, 'codex-home-file'); + fs.writeFileSync(invalidCodexHome, 'not-a-directory'); + + const result = runCcs( + ['default', '--target', 'codex', '--effort', 'high', 'fix failing tests'], + { + ...process.env, + CI: '1', + NO_COLOR: '1', + CCS_HOME: tmpHome, + CCS_CODEX_PATH: fakeCodexPath, + CCS_TEST_CODEX_ARGS_OUT: codexArgsLogPath, + CODEX_HOME: invalidCodexHome, + } + ); + + expect(result.status).toBe(1); + expect(result.stderr).toContain(`[X] CODEX_HOME path is not a directory: ${invalidCodexHome}`); + expect(readLoggedCodexCalls(codexArgsLogPath)).toEqual([['-c', 'model="gpt-5"', '--version']]); + }); + + it('keeps passthrough version launches aligned with native warning-only CODEX_HOME behavior', () => { + if (process.platform === 'win32') return; + + const readOnlyRoot = path.join(tmpHome, 'readonly-root'); + fs.mkdirSync(readOnlyRoot, { recursive: true }); + fs.chmodSync(readOnlyRoot, 0o555); + + try { + const result = runCodexAlias(['--version'], { + ...process.env, + CI: '1', + NO_COLOR: '1', + CCS_HOME: tmpHome, + CCS_CODEX_PATH: fakeCodexPath, + CCS_TEST_CODEX_ARGS_OUT: codexArgsLogPath, + CCS_TEST_CODEX_VERSION: 'codex-cli 9.9.9-test', + CODEX_HOME: path.join(readOnlyRoot, 'missing-codex-home'), + }); + + expect(result.status).toBe(0); + expect(result.stdout).toContain('codex-cli 9.9.9-test'); + expect(readLoggedCodexCalls(codexArgsLogPath)).toEqual([['--version']]); + } finally { + fs.chmodSync(readOnlyRoot, 0o755); + } + }); + + it('normalizes explicit CODEX_HOME before launching native Codex', () => { + if (process.platform === 'win32') return; + + const result = runCodexAlias(['--version'], { + ...process.env, + CI: '1', + NO_COLOR: '1', + HOME: tmpHome, + CCS_HOME: tmpHome, + CCS_CODEX_PATH: fakeCodexPath, + CCS_TEST_CODEX_ARGS_OUT: codexArgsLogPath, + CCS_TEST_CODEX_ENV_OUT: codexEnvLogPath, + CCS_TEST_CODEX_VERSION: 'codex-cli 9.9.9-test', + CODEX_HOME: '~/.codex-lit', + }); + + expect(result.status).toBe(0); + expect(readLoggedCodexEnv(codexEnvLogPath)).toEqual([ + { + CODEX_HOME: path.join(tmpHome, '.codex-lit'), + CODEX_CI: undefined, + CODEX_MANAGED_BY_BUN: undefined, + CODEX_THREAD_ID: undefined, + ANTHROPIC_BASE_URL: undefined, + }, + ]); + }); + it('keeps ccsxp pinned to native Codex even when a user passes another --target override', () => { if (process.platform === 'win32') return; @@ -324,6 +448,7 @@ process.exit(0); CCS_HOME: tmpHome, CCS_CODEX_PATH: fakeCodexPath, CCS_TEST_CODEX_ARGS_OUT: codexArgsLogPath, + CCS_TEST_CODEX_CONFIG_OVERRIDE_STATUS: 'unsupported', CCS_TEST_CODEX_VERSION: 'codex-cli 9.9.9-test', CCS_TEST_CODEX_HELP: ' -p, --profile \\n', } @@ -333,7 +458,31 @@ process.exit(0); expect(result.stderr).toContain('Codex CLI (codex-cli 9.9.9-test)'); expect(result.stderr).toContain('does not advertise --config overrides'); const calls = readLoggedCodexCalls(codexArgsLogPath); - expect(calls).toEqual([['--help'], ['--version']]); + expect(calls).toEqual([['-c', 'model="gpt-5"', '--version'], ['--help'], ['--version']]); + }); + + it('accepts native Codex reasoning overrides when the direct -c probe succeeds', () => { + if (process.platform === 'win32') return; + + const result = runCcs( + ['default', '--target', 'codex', '--effort', 'high', 'fix failing tests'], + { + ...process.env, + CI: '1', + NO_COLOR: '1', + CCS_HOME: tmpHome, + CCS_CODEX_PATH: fakeCodexPath, + CCS_TEST_CODEX_ARGS_OUT: codexArgsLogPath, + CCS_TEST_CODEX_VERSION: 'codex-cli 9.9.9-test', + CCS_TEST_CODEX_HELP: ' -p, --profile \\n', + } + ); + + expect(result.status).toBe(0); + expect(readLoggedCodexCalls(codexArgsLogPath)).toEqual([ + ['-c', 'model="gpt-5"', '--version'], + ['-c', 'model_reasoning_effort="high"', 'fix failing tests'], + ]); }); it('reports unsupported generic settings profiles before Codex install guidance', () => { diff --git a/tests/unit/targets/codex-settings-bridge-launch.test.ts b/tests/unit/targets/codex-settings-bridge-launch.test.ts index 814d4ce5..9e520ae0 100644 --- a/tests/unit/targets/codex-settings-bridge-launch.test.ts +++ b/tests/unit/targets/codex-settings-bridge-launch.test.ts @@ -68,6 +68,13 @@ describe('Codex settings bridge launch', () => { fs.writeFileSync( fakeCodexPath, `#!/bin/sh +if [ "$1" = "-c" ] || [ "$1" = "--config" ]; then + if [ "$3" = "--version" ] || [ "$3" = "-v" ]; then + echo "codex-cli 0.118.0-alpha.3" + exit 0 + fi +fi + if [ "$1" = "--version" ]; then echo "codex-cli 0.118.0-alpha.3" exit 0 diff --git a/tests/unit/web-server/codex-dashboard-service.test.ts b/tests/unit/web-server/codex-dashboard-service.test.ts index 2700622b..31c9ef65 100644 --- a/tests/unit/web-server/codex-dashboard-service.test.ts +++ b/tests/unit/web-server/codex-dashboard-service.test.ts @@ -20,14 +20,29 @@ const testRoot = path.join(os.tmpdir(), `ccs-codex-dashboard-test-${Date.now()}` const codexHome = path.join(testRoot, '.codex-home'); const codexStubPath = path.join(testRoot, 'codex'); -function writeCodexStub(options?: { helpText?: string; version?: string }) { +function writeCodexStub(options?: { + helpText?: string; + version?: string; + supportsConfigOverrides?: boolean; +}) { const helpText = options?.helpText ?? ' -c, --config \n -p, --profile \n'; const version = options?.version ?? 'codex-cli 0.118.0-alpha.3'; + const supportsConfigOverrides = options?.supportsConfigOverrides ?? true; fs.writeFileSync( codexStubPath, `#!/bin/sh +if [ "$1" = "-c" ] || [ "$1" = "--config" ]; then + if [ "${supportsConfigOverrides ? '1' : '0'}" != "1" ]; then + printf '%s\\n' 'codex: unknown option --config' >&2 + exit 1 + fi + if [ "$3" = "--version" ] || [ "$3" = "-v" ]; then + printf '%s\\n' "${version}" + exit 0 + fi +fi if [ "$1" = "--version" ]; then printf '%s\\n' "${version}" exit 0 @@ -272,7 +287,10 @@ requires_openai_auth = true }); it('warns when active profile is missing, config overrides are unavailable, or risky fields exist', async () => { - writeCodexStub({ helpText: ' -p, --profile \n' }); + writeCodexStub({ + helpText: ' -p, --profile \n', + supportsConfigOverrides: false, + }); fs.writeFileSync( path.join(codexHome, 'config.toml'), `profile = "missing-profile" @@ -300,6 +318,12 @@ bearer_token = "secret" expect(diagnostics.warnings.some((warning) => warning.includes('inline bearer_token'))).toBe( true ); + expect( + diagnostics.supportMatrix.find((entry) => entry.id === 'cliproxy-provider-codex')?.supported + ).toBe(false); + expect( + diagnostics.supportMatrix.find((entry) => entry.id === 'settings-with-bridge')?.supported + ).toBe(false); }); it('saves valid raw config content', async () => { diff --git a/ui/src/components/compatible-cli/codex-overview-tab.tsx b/ui/src/components/compatible-cli/codex-overview-tab.tsx index ee0bcda3..f74cd945 100644 --- a/ui/src/components/compatible-cli/codex-overview-tab.tsx +++ b/ui/src/components/compatible-cli/codex-overview-tab.tsx @@ -62,6 +62,7 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) { const inspectProfileCommand = diagnostics.config.activeProfile ? `codex --profile ${diagnostics.config.activeProfile}` : 'codex'; + const supportsManagedRouting = diagnostics.binary.supportsConfigOverrides; return ( @@ -142,29 +143,42 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) { -

- There are two supported paths. Use ccsxp if you want the built-in CCS - Codex provider shortcut. Use the saved recipe below if you want plain{' '} - codex or a personal alias like cxp to default to CLIProxy. -

-
-

Saved native Codex recipe

-
-                {CLIPROXY_NATIVE_CODEX_RECIPE}
-              
-
-
+ {supportsManagedRouting ? ( + <> +

+ There are two supported paths. Use ccsxp if you want the built-in CCS + Codex provider shortcut. Use the saved recipe below if you want plain{' '} + codex or a personal alias like cxp to default to + CLIProxy. +

+
+

Saved native Codex recipe

+
+                    {CLIPROXY_NATIVE_CODEX_RECIPE}
+                  
+
+
+

+ 1. Save a provider named cliproxy with the base URL and env key + above. +

+

+ 2. In Top-level settings, set Default provider{' '} + to cliproxy. +

+

+ 3. Export CLIPROXY_API_KEY in your shell before launching native + Codex. +

+
+ + ) : (

- 1. Save a provider named cliproxy with the base URL and env key above. + This Codex build can still use the native path, but CCS-backed Codex routing via{' '} + ccsxp or ccs codex --target codex stays unavailable until + the detected Codex binary exposes --config overrides.

-

- 2. In Top-level settings, set Default provider to{' '} - cliproxy. -

-

- 3. Export CLIPROXY_API_KEY in your shell before launching native Codex. -

-
+ )}
@@ -275,12 +289,16 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) { { label: 'CCS Codex shortcut', command: 'ccsxp "your prompt"', - description: 'Run the built-in CCS Codex provider on native Codex.', + description: supportsManagedRouting + ? 'Run the built-in CCS Codex provider on native Codex.' + : 'Requires a Codex build that exposes --config overrides.', }, { label: 'Explicit provider route', command: 'ccs codex --target codex "your prompt"', - description: 'Use the explicit built-in Codex provider route.', + description: supportsManagedRouting + ? 'Use the explicit built-in Codex provider route.' + : 'Requires a Codex build that exposes --config overrides.', }, { label: diagnostics.config.activeProfile @@ -312,9 +330,19 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {

CCS Codex provider / bridge

- Use ccsxp or ccs codex --target codex when you want the - built-in CCS Codex provider on native Codex. That path uses transient CCS-managed - overrides and is separate from the saved cliproxy recipe above. + {supportsManagedRouting ? ( + <> + Use ccsxp or ccs codex --target codex when you want + the built-in CCS Codex provider on native Codex. That path uses transient + CCS-managed overrides and is separate from the saved cliproxy{' '} + recipe above. + + ) : ( + <> + The CCS Codex provider route is currently unavailable because the detected Codex + build does not expose --config overrides. + + )}