mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-02 16:19:27 +00:00
fix(codex): harden native runtime detection
This commit is contained in:
@@ -87,6 +87,7 @@
|
|||||||
"ui:build": "cd ui && bun run build",
|
"ui:build": "cd ui && bun run build",
|
||||||
"ui:preview": "cd ui && bun run preview",
|
"ui:preview": "cd ui && bun run preview",
|
||||||
"ui:validate": "cd ui && bun run validate",
|
"ui:validate": "cd ui && bun run validate",
|
||||||
|
"prepack": "bun run build:all",
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
"postinstall": "node scripts/postinstall.js"
|
"postinstall": "node scripts/postinstall.js"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { ChildProcess, spawn } from 'child_process';
|
|||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import type { ProfileType } from '../types/profile';
|
import type { ProfileType } from '../types/profile';
|
||||||
import { runCleanup } from '../errors';
|
import { runCleanup } from '../errors';
|
||||||
|
import { expandPath } from '../utils/helpers';
|
||||||
import { wireChildProcessSignals } from '../utils/signal-forwarder';
|
import { wireChildProcessSignals } from '../utils/signal-forwarder';
|
||||||
import { escapeShellArg, stripAnthropicEnv, stripCodexSessionEnv } from '../utils/shell-executor';
|
import { escapeShellArg, stripAnthropicEnv, stripCodexSessionEnv } from '../utils/shell-executor';
|
||||||
import type {
|
import type {
|
||||||
@@ -20,6 +21,7 @@ import {
|
|||||||
const CODEX_RUNTIME_PROVIDER_ID = 'ccs_runtime';
|
const CODEX_RUNTIME_PROVIDER_ID = 'ccs_runtime';
|
||||||
const CODEX_RUNTIME_ENV_KEY = 'CCS_CODEX_API_KEY';
|
const CODEX_RUNTIME_ENV_KEY = 'CCS_CODEX_API_KEY';
|
||||||
const CODEX_REASONING_LEVELS = new Set(['minimal', 'low', 'medium', 'high', 'xhigh']);
|
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 {
|
function formatTomlString(value: string): string {
|
||||||
return JSON.stringify(value);
|
return JSON.stringify(value);
|
||||||
@@ -83,10 +85,54 @@ function normalizeCodexReasoningOverride(value: string | number | undefined): st
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureExplicitCodexHomeDir(env: NodeJS.ProcessEnv): string | undefined {
|
function isInformationalCodexInvocation(args: string[]): boolean {
|
||||||
const codexHome = env.CODEX_HOME?.trim();
|
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) {
|
if (!codexHome) {
|
||||||
return undefined;
|
return { env: normalizedEnv };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isInformationalCodexInvocation(args)) {
|
||||||
|
return { env: normalizedEnv };
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -94,18 +140,27 @@ function ensureExplicitCodexHomeDir(env: NodeJS.ProcessEnv): string | undefined
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = err as NodeJS.ErrnoException;
|
const error = err as NodeJS.ErrnoException;
|
||||||
if (error.code !== 'EEXIST') {
|
if (error.code !== 'EEXIST') {
|
||||||
return `[X] Unable to initialize CODEX_HOME (${error.code || 'unknown'}): ${codexHome}`;
|
return {
|
||||||
|
env: normalizedEnv,
|
||||||
|
error: `[X] Unable to initialize CODEX_HOME (${error.code || 'unknown'}): ${codexHome}`,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!fs.statSync(codexHome).isDirectory()) {
|
if (!fs.statSync(codexHome).isDirectory()) {
|
||||||
return `[X] CODEX_HOME path is not a directory: ${codexHome}`;
|
return {
|
||||||
|
env: normalizedEnv,
|
||||||
|
error: `[X] CODEX_HOME path is not a directory: ${codexHome}`,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return undefined;
|
return { env: normalizedEnv };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const error = err as NodeJS.ErrnoException;
|
const error = err as NodeJS.ErrnoException;
|
||||||
return `[X] Unable to access CODEX_HOME (${error.code || 'unknown'}): ${codexHome}`;
|
return {
|
||||||
|
env: normalizedEnv,
|
||||||
|
error: `[X] Unable to access CODEX_HOME (${error.code || 'unknown'}): ${codexHome}`,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,11 +288,12 @@ export class CodexAdapter implements TargetAdapter {
|
|||||||
return exitWithCleanup(1);
|
return exitWithCleanup(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const codexHomeInitError = ensureExplicitCodexHomeDir(env);
|
const codexHomePreparation = prepareExplicitCodexHome(env, args);
|
||||||
if (codexHomeInitError) {
|
if (codexHomePreparation.error) {
|
||||||
console.error(codexHomeInitError);
|
console.error(codexHomePreparation.error);
|
||||||
return exitWithCleanup(1);
|
return exitWithCleanup(1);
|
||||||
}
|
}
|
||||||
|
const launchEnv = codexHomePreparation.env;
|
||||||
|
|
||||||
const isWindows = process.platform === 'win32';
|
const isWindows = process.platform === 'win32';
|
||||||
const isPowerShellScript = isWindows && /\.ps1$/i.test(codexPath);
|
const isPowerShellScript = isWindows && /\.ps1$/i.test(codexPath);
|
||||||
@@ -248,7 +304,7 @@ export class CodexAdapter implements TargetAdapter {
|
|||||||
child = spawn(
|
child = spawn(
|
||||||
'powershell.exe',
|
'powershell.exe',
|
||||||
['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', codexPath, ...args],
|
['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', codexPath, ...args],
|
||||||
{ stdio: 'inherit', windowsHide: true, env }
|
{ stdio: 'inherit', windowsHide: true, env: launchEnv }
|
||||||
);
|
);
|
||||||
} else if (needsShell) {
|
} else if (needsShell) {
|
||||||
const cmdString = [codexPath, ...args].map(escapeShellArg).join(' ');
|
const cmdString = [codexPath, ...args].map(escapeShellArg).join(' ');
|
||||||
@@ -256,10 +312,10 @@ export class CodexAdapter implements TargetAdapter {
|
|||||||
stdio: 'inherit',
|
stdio: 'inherit',
|
||||||
windowsHide: true,
|
windowsHide: true,
|
||||||
shell: true,
|
shell: true,
|
||||||
env,
|
env: launchEnv,
|
||||||
});
|
});
|
||||||
} else {
|
} 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) => {
|
wireChildProcessSignals(child, (err: NodeJS.ErrnoException) => {
|
||||||
|
|||||||
@@ -7,6 +7,22 @@ import type { TargetBinaryInfo } from './target-adapter';
|
|||||||
const CODEX_CONFIG_OVERRIDE_FEATURE = 'config-overrides';
|
const CODEX_CONFIG_OVERRIDE_FEATURE = 'config-overrides';
|
||||||
const CODEX_CONFIG_OVERRIDE_PROBE_ARGS = ['-c', 'model="gpt-5"', '--version'];
|
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 {
|
function runCodexProbe(codexPath: string, args: string[]): string | undefined {
|
||||||
const isWindows = process.platform === 'win32';
|
const isWindows = process.platform === 'win32';
|
||||||
const isPowerShellScript = isWindows && /\.ps1$/i.test(codexPath);
|
const isPowerShellScript = isWindows && /\.ps1$/i.test(codexPath);
|
||||||
@@ -112,12 +128,7 @@ export function detectCodexCli(): string | null {
|
|||||||
.map((entry) => entry.trim())
|
.map((entry) => entry.trim())
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
const candidates = isWindows
|
const candidates = isWindows ? buildWindowsCodexCandidates(matches) : matches;
|
||||||
? [
|
|
||||||
...matches.filter((entry) => /\.(exe|cmd|bat|ps1)$/i.test(entry)),
|
|
||||||
...matches.filter((entry) => !/\.(exe|cmd|bat|ps1)$/i.test(entry)),
|
|
||||||
]
|
|
||||||
: matches;
|
|
||||||
|
|
||||||
for (const candidate of candidates) {
|
for (const candidate of candidates) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -646,7 +646,7 @@ export function summarizeCodexMcpServers(value: unknown): CodexMcpServerDiagnost
|
|||||||
.sort((left, right) => left.name.localeCompare(right.name));
|
.sort((left, right) => left.name.localeCompare(right.name));
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCodexSupportMatrix(): CodexSupportMatrixEntry[] {
|
function getCodexSupportMatrix(supportsManagedRouting: boolean): CodexSupportMatrixEntry[] {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
id: 'default',
|
id: 'default',
|
||||||
@@ -657,14 +657,18 @@ function getCodexSupportMatrix(): CodexSupportMatrixEntry[] {
|
|||||||
{
|
{
|
||||||
id: 'cliproxy-provider-codex',
|
id: 'cliproxy-provider-codex',
|
||||||
label: 'cliproxy provider=codex',
|
label: 'cliproxy provider=codex',
|
||||||
supported: true,
|
supported: supportsManagedRouting,
|
||||||
notes: 'Routed through the CLIProxy Codex Responses bridge.',
|
notes: supportsManagedRouting
|
||||||
|
? 'Routed through the CLIProxy Codex Responses bridge.'
|
||||||
|
: 'Requires a Codex build that exposes --config overrides.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'settings-with-bridge',
|
id: 'settings-with-bridge',
|
||||||
label: 'settings with bridge metadata',
|
label: 'settings with bridge metadata',
|
||||||
supported: true,
|
supported: supportsManagedRouting,
|
||||||
notes: 'Supported when the resolved API profile points at a Codex CLIProxy bridge.',
|
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',
|
id: 'cliproxy-composite',
|
||||||
@@ -696,6 +700,7 @@ function getCodexSupportMatrix(): CodexSupportMatrixEntry[] {
|
|||||||
export async function getCodexDashboardDiagnostics(): Promise<CodexDashboardDiagnostics> {
|
export async function getCodexDashboardDiagnostics(): Promise<CodexDashboardDiagnostics> {
|
||||||
const paths = resolveCodexConfigPaths();
|
const paths = resolveCodexConfigPaths();
|
||||||
const binaryInfo = getCodexBinaryInfo();
|
const binaryInfo = getCodexBinaryInfo();
|
||||||
|
const supportsConfigOverrides = !!binaryInfo && codexBinarySupportsConfigOverrides(binaryInfo);
|
||||||
const docsReference = getCompatibleCliDocsReference('codex');
|
const docsReference = getCompatibleCliDocsReference('codex');
|
||||||
const fileProbe = await probeTomlObjectFile(
|
const fileProbe = await probeTomlObjectFile(
|
||||||
paths.configPath,
|
paths.configPath,
|
||||||
@@ -715,12 +720,12 @@ export async function getCodexDashboardDiagnostics(): Promise<CodexDashboardDiag
|
|||||||
const features = summarizeCodexFeatureFlags(config?.features);
|
const features = summarizeCodexFeatureFlags(config?.features);
|
||||||
const projectTrust = summarizeCodexProjectTrust(config?.projects);
|
const projectTrust = summarizeCodexProjectTrust(config?.projects);
|
||||||
const mcpServers = summarizeCodexMcpServers(config?.mcp_servers);
|
const mcpServers = summarizeCodexMcpServers(config?.mcp_servers);
|
||||||
const supportMatrix = getCodexSupportMatrix();
|
const supportMatrix = getCodexSupportMatrix(supportsConfigOverrides);
|
||||||
|
|
||||||
const warnings: string[] = [];
|
const warnings: string[] = [];
|
||||||
if (!binaryInfo) {
|
if (!binaryInfo) {
|
||||||
warnings.push('Codex binary is not detected in PATH or CCS_CODEX_PATH.');
|
warnings.push('Codex binary is not detected in PATH or CCS_CODEX_PATH.');
|
||||||
} else if (!codexBinarySupportsConfigOverrides(binaryInfo)) {
|
} else if (!supportsConfigOverrides) {
|
||||||
warnings.push(
|
warnings.push(
|
||||||
'This Codex build does not expose --config overrides required for CCS-backed Codex routing.'
|
'This Codex build does not expose --config overrides required for CCS-backed Codex routing.'
|
||||||
);
|
);
|
||||||
@@ -766,7 +771,7 @@ export async function getCodexDashboardDiagnostics(): Promise<CodexDashboardDiag
|
|||||||
source: process.env.CCS_CODEX_PATH ? 'CCS_CODEX_PATH' : binaryInfo ? 'PATH' : 'missing',
|
source: process.env.CCS_CODEX_PATH ? 'CCS_CODEX_PATH' : binaryInfo ? 'PATH' : 'missing',
|
||||||
version: binaryInfo?.version ?? null,
|
version: binaryInfo?.version ?? null,
|
||||||
overridePath: process.env.CCS_CODEX_PATH || null,
|
overridePath: process.env.CCS_CODEX_PATH || null,
|
||||||
supportsConfigOverrides: codexBinarySupportsConfigOverrides(binaryInfo),
|
supportsConfigOverrides,
|
||||||
},
|
},
|
||||||
file: fileProbe.diagnostics,
|
file: fileProbe.diagnostics,
|
||||||
workspacePath: process.cwd(),
|
workspacePath: process.cwd(),
|
||||||
|
|||||||
@@ -175,6 +175,11 @@ describe('cross-platform', () => {
|
|||||||
'dedicated ccsxp shortcut entrypoint should exist'
|
'dedicated ccsxp shortcut entrypoint should exist'
|
||||||
);
|
);
|
||||||
assert(packageJson.scripts, 'package.json should have scripts field');
|
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'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -73,6 +73,20 @@ describe('codex-detector', () => {
|
|||||||
execFileSyncSpy.mockRestore();
|
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', () => {
|
it('falls back to a direct -c probe when help text omits the config flag', () => {
|
||||||
const fakeCodex = path.join(tmpDir, 'codex');
|
const fakeCodex = path.join(tmpDir, 'codex');
|
||||||
fs.writeFileSync(fakeCodex, '');
|
fs.writeFileSync(fakeCodex, '');
|
||||||
|
|||||||
@@ -270,11 +270,11 @@ process.exit(0);
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('creates an explicit CODEX_HOME directory before launching native Codex', () => {
|
it('creates an explicit CODEX_HOME directory before routed native Codex launches', () => {
|
||||||
if (process.platform === 'win32') return;
|
if (process.platform === 'win32') return;
|
||||||
|
|
||||||
const freshCodexHome = path.join(tmpHome, 'fresh-codex-home');
|
const freshCodexHome = path.join(tmpHome, 'fresh-codex-home');
|
||||||
const result = runCcsxpAlias(['--version'], {
|
const result = runCcs(['default', '--target', 'codex', '--effort', 'high', 'fix failing tests'], {
|
||||||
...process.env,
|
...process.env,
|
||||||
CI: '1',
|
CI: '1',
|
||||||
NO_COLOR: '1',
|
NO_COLOR: '1',
|
||||||
@@ -289,25 +289,31 @@ process.exit(0);
|
|||||||
expect(result.status).toBe(0);
|
expect(result.status).toBe(0);
|
||||||
expect(fs.existsSync(freshCodexHome)).toBe(true);
|
expect(fs.existsSync(freshCodexHome)).toBe(true);
|
||||||
expect(fs.statSync(freshCodexHome).isDirectory()).toBe(true);
|
expect(fs.statSync(freshCodexHome).isDirectory()).toBe(true);
|
||||||
expect(readLoggedCodexCalls(codexArgsLogPath)).toEqual([['--version']]);
|
expect(readLoggedCodexCalls(codexArgsLogPath)).toEqual([
|
||||||
expect(readLoggedCodexEnv(codexEnvLogPath)).toEqual([
|
['-c', 'model="gpt-5"', '--version'],
|
||||||
{
|
['-c', 'model_reasoning_effort="high"', 'fix failing tests'],
|
||||||
CODEX_HOME: freshCodexHome,
|
|
||||||
CODEX_CI: undefined,
|
|
||||||
CODEX_MANAGED_BY_BUN: undefined,
|
|
||||||
CODEX_THREAD_ID: undefined,
|
|
||||||
ANTHROPIC_BASE_URL: undefined,
|
|
||||||
},
|
|
||||||
]);
|
]);
|
||||||
|
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 CODEX_HOME points to a file', () => {
|
it('fails with a clean error when routed launches receive a file CODEX_HOME path', () => {
|
||||||
if (process.platform === 'win32') return;
|
if (process.platform === 'win32') return;
|
||||||
|
|
||||||
const invalidCodexHome = path.join(tmpHome, 'codex-home-file');
|
const invalidCodexHome = path.join(tmpHome, 'codex-home-file');
|
||||||
fs.writeFileSync(invalidCodexHome, 'not-a-directory');
|
fs.writeFileSync(invalidCodexHome, 'not-a-directory');
|
||||||
|
|
||||||
const result = runCcsxpAlias(['--version'], {
|
const result = runCcs(
|
||||||
|
['default', '--target', 'codex', '--effort', 'high', 'fix failing tests'],
|
||||||
|
{
|
||||||
...process.env,
|
...process.env,
|
||||||
CI: '1',
|
CI: '1',
|
||||||
NO_COLOR: '1',
|
NO_COLOR: '1',
|
||||||
@@ -315,11 +321,67 @@ process.exit(0);
|
|||||||
CCS_CODEX_PATH: fakeCodexPath,
|
CCS_CODEX_PATH: fakeCodexPath,
|
||||||
CCS_TEST_CODEX_ARGS_OUT: codexArgsLogPath,
|
CCS_TEST_CODEX_ARGS_OUT: codexArgsLogPath,
|
||||||
CODEX_HOME: invalidCodexHome,
|
CODEX_HOME: invalidCodexHome,
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
expect(result.status).toBe(1);
|
expect(result.status).toBe(1);
|
||||||
expect(result.stderr).toContain(`[X] CODEX_HOME path is not a directory: ${invalidCodexHome}`);
|
expect(result.stderr).toContain(`[X] CODEX_HOME path is not a directory: ${invalidCodexHome}`);
|
||||||
expect(readLoggedCodexCalls(codexArgsLogPath)).toEqual([]);
|
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', () => {
|
it('keeps ccsxp pinned to native Codex even when a user passes another --target override', () => {
|
||||||
|
|||||||
@@ -318,6 +318,12 @@ bearer_token = "secret"
|
|||||||
expect(diagnostics.warnings.some((warning) => warning.includes('inline bearer_token'))).toBe(
|
expect(diagnostics.warnings.some((warning) => warning.includes('inline bearer_token'))).toBe(
|
||||||
true
|
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 () => {
|
it('saves valid raw config content', async () => {
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
|
|||||||
const inspectProfileCommand = diagnostics.config.activeProfile
|
const inspectProfileCommand = diagnostics.config.activeProfile
|
||||||
? `codex --profile ${diagnostics.config.activeProfile}`
|
? `codex --profile ${diagnostics.config.activeProfile}`
|
||||||
: 'codex';
|
: 'codex';
|
||||||
|
const supportsManagedRouting = diagnostics.binary.supportsConfigOverrides;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ScrollArea className="h-full">
|
<ScrollArea className="h-full">
|
||||||
@@ -142,29 +143,42 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
|
|||||||
</CardTitle>
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-3 text-sm text-muted-foreground">
|
<CardContent className="space-y-3 text-sm text-muted-foreground">
|
||||||
<p>
|
{supportsManagedRouting ? (
|
||||||
There are two supported paths. Use <code>ccsxp</code> if you want the built-in CCS
|
<>
|
||||||
Codex provider shortcut. Use the saved recipe below if you want plain{' '}
|
<p>
|
||||||
<code>codex</code> or a personal alias like <code>cxp</code> to default to CLIProxy.
|
There are two supported paths. Use <code>ccsxp</code> if you want the built-in CCS
|
||||||
</p>
|
Codex provider shortcut. Use the saved recipe below if you want plain{' '}
|
||||||
<div className="rounded-md border bg-muted/20 p-3">
|
<code>codex</code> or a personal alias like <code>cxp</code> to default to
|
||||||
<p className="font-medium text-foreground">Saved native Codex recipe</p>
|
CLIProxy.
|
||||||
<pre className="mt-2 overflow-x-auto rounded-md bg-background p-3 text-xs text-foreground">
|
</p>
|
||||||
{CLIPROXY_NATIVE_CODEX_RECIPE}
|
<div className="rounded-md border bg-muted/20 p-3">
|
||||||
</pre>
|
<p className="font-medium text-foreground">Saved native Codex recipe</p>
|
||||||
</div>
|
<pre className="mt-2 overflow-x-auto rounded-md bg-background p-3 text-xs text-foreground">
|
||||||
<div className="space-y-1">
|
{CLIPROXY_NATIVE_CODEX_RECIPE}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p>
|
||||||
|
1. Save a provider named <code>cliproxy</code> with the base URL and env key
|
||||||
|
above.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
2. In <strong>Top-level settings</strong>, set <strong>Default provider</strong>{' '}
|
||||||
|
to <code>cliproxy</code>.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
3. Export <code>CLIPROXY_API_KEY</code> in your shell before launching native
|
||||||
|
Codex.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
<p>
|
<p>
|
||||||
1. Save a provider named <code>cliproxy</code> with the base URL and env key above.
|
This Codex build can still use the native path, but CCS-backed Codex routing via{' '}
|
||||||
|
<code>ccsxp</code> or <code>ccs codex --target codex</code> stays unavailable until
|
||||||
|
the detected Codex binary exposes <code>--config</code> overrides.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
)}
|
||||||
2. In <strong>Top-level settings</strong>, set <strong>Default provider</strong> to{' '}
|
|
||||||
<code>cliproxy</code>.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
3. Export <code>CLIPROXY_API_KEY</code> in your shell before launching native Codex.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
@@ -275,12 +289,16 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
|
|||||||
{
|
{
|
||||||
label: 'CCS Codex shortcut',
|
label: 'CCS Codex shortcut',
|
||||||
command: 'ccsxp "your prompt"',
|
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',
|
label: 'Explicit provider route',
|
||||||
command: 'ccs codex --target codex "your prompt"',
|
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
|
label: diagnostics.config.activeProfile
|
||||||
@@ -312,9 +330,19 @@ export function CodexOverviewTab({ diagnostics }: CodexOverviewTabProps) {
|
|||||||
<div className="rounded-md border p-3 text-sm">
|
<div className="rounded-md border p-3 text-sm">
|
||||||
<p className="font-medium">CCS Codex provider / bridge</p>
|
<p className="font-medium">CCS Codex provider / bridge</p>
|
||||||
<p className="mt-1 text-muted-foreground">
|
<p className="mt-1 text-muted-foreground">
|
||||||
Use <code>ccsxp</code> or <code>ccs codex --target codex</code> when you want the
|
{supportsManagedRouting ? (
|
||||||
built-in CCS Codex provider on native Codex. That path uses transient CCS-managed
|
<>
|
||||||
overrides and is separate from the saved <code>cliproxy</code> recipe above.
|
Use <code>ccsxp</code> or <code>ccs codex --target codex</code> 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 <code>cliproxy</code>{' '}
|
||||||
|
recipe above.
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
The CCS Codex provider route is currently unavailable because the detected Codex
|
||||||
|
build does not expose <code>--config</code> overrides.
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
Reference in New Issue
Block a user