From fec9728088b2337058e94665e32b9fa387fc40bf Mon Sep 17 00:00:00 2001 From: bbsngg Date: Tue, 28 Apr 2026 14:18:04 -0400 Subject: [PATCH] Fix Codex CLI version drift in shell --- package-lock.json | 188 +++++------------------- package.json | 4 +- server/index.js | 15 +- server/routes/cli-auth.js | 7 +- server/utils/__tests__/codexCli.test.js | 19 +++ server/utils/cliResolution.js | 17 ++- server/utils/codexCli.js | 70 +++++++++ 7 files changed, 150 insertions(+), 170 deletions(-) create mode 100644 server/utils/__tests__/codexCli.test.js create mode 100644 server/utils/codexCli.js diff --git a/package-lock.json b/package-lock.json index 02579727..30fe1924 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,8 +22,8 @@ "@iarna/toml": "^2.2.5", "@modelcontextprotocol/sdk": "^1.29.0", "@octokit/rest": "^22.0.0", - "@openai/codex": "^0.104.0", - "@openai/codex-sdk": "^0.101.0", + "@openai/codex": "^0.125.0", + "@openai/codex-sdk": "^0.125.0", "@replit/codemirror-minimap": "^0.5.2", "@tailwindcss/typography": "^0.5.16", "@uiw/react-codemirror": "^4.23.13", @@ -3963,9 +3963,9 @@ } }, "node_modules/@openai/codex": { - "version": "0.104.0", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.104.0.tgz", - "integrity": "sha512-pPa2VGHozwjPsPOYAEXcH7nNt1QH7AZR8zV8jYx6BFi1LJlmJkan2rvIS4MYbPdi2O6cd5kWfPCAHE0fEV2ifA==", + "version": "0.125.0", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.125.0.tgz", + "integrity": "sha512-GiE9wlgL95u/5BRirY5d3EaRLU1tu7Y1R09R8lCHHVmcQdSmhS809FdPDWH3gIYHS7ZriAPqXwJ3aLA0WKl40Q==", "license": "Apache-2.0", "bin": { "codex": "bin/codex.js" @@ -3974,19 +3974,19 @@ "node": ">=16" }, "optionalDependencies": { - "@openai/codex-darwin-arm64": "npm:@openai/codex@0.104.0-darwin-arm64", - "@openai/codex-darwin-x64": "npm:@openai/codex@0.104.0-darwin-x64", - "@openai/codex-linux-arm64": "npm:@openai/codex@0.104.0-linux-arm64", - "@openai/codex-linux-x64": "npm:@openai/codex@0.104.0-linux-x64", - "@openai/codex-win32-arm64": "npm:@openai/codex@0.104.0-win32-arm64", - "@openai/codex-win32-x64": "npm:@openai/codex@0.104.0-win32-x64" + "@openai/codex-darwin-arm64": "npm:@openai/codex@0.125.0-darwin-arm64", + "@openai/codex-darwin-x64": "npm:@openai/codex@0.125.0-darwin-x64", + "@openai/codex-linux-arm64": "npm:@openai/codex@0.125.0-linux-arm64", + "@openai/codex-linux-x64": "npm:@openai/codex@0.125.0-linux-x64", + "@openai/codex-win32-arm64": "npm:@openai/codex@0.125.0-win32-arm64", + "@openai/codex-win32-x64": "npm:@openai/codex@0.125.0-win32-x64" } }, "node_modules/@openai/codex-darwin-arm64": { "name": "@openai/codex", - "version": "0.104.0-darwin-arm64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.104.0-darwin-arm64.tgz", - "integrity": "sha512-Y+lifRKAgNSBcaIM5UXXYnGWAJrPORPXABZBCxxiwwB8/XzZRDwp3K+X5i7dT0GfKScGFXuul6sJ2sVSPL4w4A==", + "version": "0.125.0-darwin-arm64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.125.0-darwin-arm64.tgz", + "integrity": "sha512-Gn2fHiSO0XgyHp1OSd5DWUTm66Bv9UEuipW5pVEj1E+hWZCOrdqnYttllKFWtRGj5yiKefNX3JIxONgh/ZwlOQ==", "cpu": [ "arm64" ], @@ -4001,9 +4001,9 @@ }, "node_modules/@openai/codex-darwin-x64": { "name": "@openai/codex", - "version": "0.104.0-darwin-x64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.104.0-darwin-x64.tgz", - "integrity": "sha512-TwQ9zj0XbSrtCxFWKnnSQfmWmKhNMx1rSpSaSrLNSFVohxRwOWUZ2GBciO6jCLEiJvswR6nTMy1mA0n7MyVJiw==", + "version": "0.125.0-darwin-x64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.125.0-darwin-x64.tgz", + "integrity": "sha512-TZ5Lek2X/UXTI9LXFxzarvQaJeuTrqVh4POc7soO/8RclVnCxADnCf15sivxLd5eiFW4t0myGoeVoM4lciRiRg==", "cpu": [ "x64" ], @@ -4018,9 +4018,9 @@ }, "node_modules/@openai/codex-linux-arm64": { "name": "@openai/codex", - "version": "0.104.0-linux-arm64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.104.0-linux-arm64.tgz", - "integrity": "sha512-3oBBjMaCnhGfijsklOzVqG0LH/IFWoDnRJkvFl1utMI+GJECUr37uL/KsSFTuC2kIjham6U57dAK6xQnQxqxPQ==", + "version": "0.125.0-linux-arm64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.125.0-linux-arm64.tgz", + "integrity": "sha512-pPnJoJD6rZ2Iin0zNt/up36bO2/EOp2B+1/rPHu/lSq3PJbT3Fmnfut2kJy5LylXb7bGA2XQbtqOogZzIbnlkA==", "cpu": [ "arm64" ], @@ -4035,9 +4035,9 @@ }, "node_modules/@openai/codex-linux-x64": { "name": "@openai/codex", - "version": "0.104.0-linux-x64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.104.0-linux-x64.tgz", - "integrity": "sha512-vhYaWsEwZmxZbeu5u9/k3VO1F4aTMYaTCebRgdzux7bfeDw2nms1SAcP+AkfCStqVSz26yaPGbwcUMqaknW4gQ==", + "version": "0.125.0-linux-x64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.125.0-linux-x64.tgz", + "integrity": "sha512-K2NTTEeBpz/G+N2x17UGWfauRt3So+ir4f+U/60l5PPnYEJB/w3YZrlXo2G9og8Dm9BqtoBAjoPV74sRv9tWWQ==", "cpu": [ "x64" ], @@ -4051,144 +4051,22 @@ } }, "node_modules/@openai/codex-sdk": { - "version": "0.101.0", - "resolved": "https://registry.npmjs.org/@openai/codex-sdk/-/codex-sdk-0.101.0.tgz", - "integrity": "sha512-Lrar2pDvGUX64itSbMNKuNBzxh72UwKokY4TPuXJRURwGX0qyDi80n7DiVivC40BwFsQWNs6behSo/9Mr6PoLw==", + "version": "0.125.0", + "resolved": "https://registry.npmjs.org/@openai/codex-sdk/-/codex-sdk-0.125.0.tgz", + "integrity": "sha512-1xCIHdSbQVF880nJ2aVWdPIsWZbSpKODwuP9y/gvtChDYhYfYEW0DKp2H8ZlctkzIjlzS/WzYmP6ZZPHIvs2Dg==", "license": "Apache-2.0", "dependencies": { - "@openai/codex": "0.101.0" + "@openai/codex": "0.125.0" }, "engines": { "node": ">=18" } }, - "node_modules/@openai/codex-sdk/node_modules/@openai/codex": { - "version": "0.101.0", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.101.0.tgz", - "integrity": "sha512-H874q5K5I3chrT588BaddMr7GNvRYypc8C1MKWytNUF2PgxWMko2g/2DgKbt5OdajZKMsWdbsPywu34KQGf5Qw==", - "license": "Apache-2.0", - "bin": { - "codex": "bin/codex.js" - }, - "engines": { - "node": ">=16" - }, - "optionalDependencies": { - "@openai/codex-darwin-arm64": "npm:@openai/codex@0.101.0-darwin-arm64", - "@openai/codex-darwin-x64": "npm:@openai/codex@0.101.0-darwin-x64", - "@openai/codex-linux-arm64": "npm:@openai/codex@0.101.0-linux-arm64", - "@openai/codex-linux-x64": "npm:@openai/codex@0.101.0-linux-x64", - "@openai/codex-win32-arm64": "npm:@openai/codex@0.101.0-win32-arm64", - "@openai/codex-win32-x64": "npm:@openai/codex@0.101.0-win32-x64" - } - }, - "node_modules/@openai/codex-sdk/node_modules/@openai/codex-darwin-arm64": { - "name": "@openai/codex", - "version": "0.101.0-darwin-arm64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.101.0-darwin-arm64.tgz", - "integrity": "sha512-unk4rTRQQ9o0w2Upu35IsJHpoZHJ+tU/myn6LNhUjcP9FrjLnEcAQJ6WIMtdTYVPja1PGhFSO0DNxV79GMvehw==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/@openai/codex-sdk/node_modules/@openai/codex-darwin-x64": { - "name": "@openai/codex", - "version": "0.101.0-darwin-x64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.101.0-darwin-x64.tgz", - "integrity": "sha512-+KFi1IapCQGd3vLQp2lI4xI3hu2QffDZYt7Fhfw6NxEFOKhHnTamRtQ5yI8jYQcYF+pQfYF2fyiuXLM1lITLQw==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/@openai/codex-sdk/node_modules/@openai/codex-linux-arm64": { - "name": "@openai/codex", - "version": "0.101.0-linux-arm64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.101.0-linux-arm64.tgz", - "integrity": "sha512-RkDnQeq7M6ZBtD+8i+I5ewjjOf02BcJq6r1kN4RBewfAQBsz6B73Ns3OrI2bHVRsuPtAf8Cf1S4xg/eFZT2Omg==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/@openai/codex-sdk/node_modules/@openai/codex-linux-x64": { - "name": "@openai/codex", - "version": "0.101.0-linux-x64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.101.0-linux-x64.tgz", - "integrity": "sha512-SJeEdQ4ReEU3nvtceZ1uY3me6oWoB3djr3GnZmAUCEUuYEWD1kRGprAyJB1N0B+8zhSv0SU2e9sX5t3aCV4AwQ==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/@openai/codex-sdk/node_modules/@openai/codex-win32-arm64": { - "name": "@openai/codex", - "version": "0.101.0-win32-arm64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.101.0-win32-arm64.tgz", - "integrity": "sha512-WQ8QsychjHyvlr+vCSTMbd2/yrBIZre5tRuM79eZi973BJz0CSEiFsNSGg5fvpnJuiHHawZ/8HWeir7nlatamQ==", - "cpu": [ - "arm64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=16" - } - }, - "node_modules/@openai/codex-sdk/node_modules/@openai/codex-win32-x64": { - "name": "@openai/codex", - "version": "0.101.0-win32-x64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.101.0-win32-x64.tgz", - "integrity": "sha512-H+7h9x0fYrJRUZZHCA62Dzb/CS5Scl1sUw1aamfmHJzzorX+uTFOgGsibzqFpHTd6nRM4q8//fCdSxe5wUpOQQ==", - "cpu": [ - "x64" - ], - "license": "Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=16" - } - }, "node_modules/@openai/codex-win32-arm64": { "name": "@openai/codex", - "version": "0.104.0-win32-arm64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.104.0-win32-arm64.tgz", - "integrity": "sha512-2ypuM6yWcjAtq7DmEgFBsmtw7rWLcoy6Cxaq+Hn8dZfEdijASyc59AzyWhWLKYLuOxcprFn/oQitElrpPD9JOA==", + "version": "0.125.0-win32-arm64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.125.0-win32-arm64.tgz", + "integrity": "sha512-zxoUakw9oIHIFrAyk400XkkLBJFA6nOym0NDq6sQ/jhdcYraKqNSRCII2nsBwZHk+/4zgUvuk52iuutgysY/rQ==", "cpu": [ "arm64" ], @@ -4203,9 +4081,9 @@ }, "node_modules/@openai/codex-win32-x64": { "name": "@openai/codex", - "version": "0.104.0-win32-x64", - "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.104.0-win32-x64.tgz", - "integrity": "sha512-awyNLtfbTbj+2JzgsAIm+KFrxeAmxe/Fuqw/ZwBj8ljtO7SQWTT3kxDbf7iuA7E7IErGlQw/plgFgq/LJdsacg==", + "version": "0.125.0-win32-x64", + "resolved": "https://registry.npmjs.org/@openai/codex/-/codex-0.125.0-win32-x64.tgz", + "integrity": "sha512-ofpOK+OWH5QFuUZ9pTM0d/PcXUXiIP5z5DpRcE9MlucJoyOl4Zy4Nu3NcuHF4YzCkZMQb6x3j0tjDEPHKqNQzw==", "cpu": [ "x64" ], diff --git a/package.json b/package.json index bd863c26..a5a05d9b 100644 --- a/package.json +++ b/package.json @@ -78,8 +78,8 @@ "@iarna/toml": "^2.2.5", "@modelcontextprotocol/sdk": "^1.29.0", "@octokit/rest": "^22.0.0", - "@openai/codex": "^0.104.0", - "@openai/codex-sdk": "^0.101.0", + "@openai/codex": "^0.125.0", + "@openai/codex-sdk": "^0.125.0", "@replit/codemirror-minimap": "^0.5.2", "@tailwindcss/typography": "^0.5.16", "@uiw/react-codemirror": "^4.23.13", diff --git a/server/index.js b/server/index.js index 94da5f95..dde7392d 100755 --- a/server/index.js +++ b/server/index.js @@ -78,6 +78,7 @@ import { validateApiKey, authenticateToken, authenticateWebSocket } from './midd import { IS_PLATFORM } from './constants/config.js'; import { enqueueTelemetryEvent } from './telemetry.js'; import { resolveCursorCliCommand, isCursorLoginCommand, isGeminiLoginCommand, normalizeCursorLoginCommand } from './utils/cursorCommand.js'; +import { buildCodexCliEnv, codexCommandForShell } from './utils/codexCli.js'; import { getGeminiApiKeyForUser, withGeminiApiKeyEnv } from './utils/geminiApiKey.js'; import { DEFAULT_BACKEND_PORT, @@ -2031,18 +2032,18 @@ function handleShellConnection(ws) { } } } else if (provider === 'codex') { - // Use codex command + const codexCommand = codexCommandForShell(process.env, os.platform()); if (os.platform() === 'win32') { if (hasSession && sessionId) { - shellCommand = `Set-Location -Path "${projectPath}"; codex resume ${sessionId}; if ($LASTEXITCODE -ne 0) { codex }`; + shellCommand = `Set-Location -Path "${projectPath}"; ${codexCommand} resume ${sessionId}; if ($LASTEXITCODE -ne 0) { ${codexCommand} }`; } else { - shellCommand = `Set-Location -Path "${projectPath}"; codex`; + shellCommand = `Set-Location -Path "${projectPath}"; ${codexCommand}`; } } else { if (hasSession && sessionId) { - shellCommand = `cd "${projectPath}" && codex resume ${sessionId} || codex`; + shellCommand = `cd "${projectPath}" && ${codexCommand} resume ${sessionId} || ${codexCommand}`; } else { - shellCommand = `cd "${projectPath}" && codex`; + shellCommand = `cd "${projectPath}" && ${codexCommand}`; } } } else if (provider === 'gemini') { @@ -2112,7 +2113,9 @@ function handleShellConnection(ws) { cols: termCols, rows: termRows, cwd: spawnCwd, - env: buildEmbeddedShellEnv(process.env) + env: provider === 'codex' + ? buildEmbeddedShellEnv(buildCodexCliEnv(process.env)) + : buildEmbeddedShellEnv(process.env) }); console.log('🟢 Shell process started with PTY, PID:', shellProcess.pid); diff --git a/server/routes/cli-auth.js b/server/routes/cli-auth.js index fe5025b1..9846c2cc 100644 --- a/server/routes/cli-auth.js +++ b/server/routes/cli-auth.js @@ -6,6 +6,7 @@ import os from 'os'; import fetch from 'node-fetch'; import { resolveCursorCliCommand } from '../utils/cursorCommand.js'; import { resolveAvailableCliCommand } from '../utils/cliResolution.js'; +import { buildCodexCliEnv, getCodexCliCommand } from '../utils/codexCli.js'; import { DEFAULT_OLLAMA_URL, detectGPUs, @@ -840,7 +841,8 @@ function checkCursorStatus() { // 2. OPENAI_API_KEY from server environment variable // 3. OPENAI_API_KEY from ~/.codex/auth.json async function checkCodexCredentials() { - let cliCommand = process.env.CODEX_CLI_PATH || 'codex'; + const codexCliEnv = buildCodexCliEnv(process.env); + let cliCommand = getCodexCliCommand(process.env); try { if (isCliMockedMissing('codex')) { return { @@ -856,7 +858,8 @@ async function checkCodexCredentials() { const resolvedCliCommand = await resolveAvailableCliCommand({ envVarName: 'CODEX_CLI_PATH', defaultCommands: ['codex'], - appendWindowsSuffixes: true + appendWindowsSuffixes: true, + env: codexCliEnv }); cliCommand = resolvedCliCommand || cliCommand; diff --git a/server/utils/__tests__/codexCli.test.js b/server/utils/__tests__/codexCli.test.js new file mode 100644 index 00000000..3c909588 --- /dev/null +++ b/server/utils/__tests__/codexCli.test.js @@ -0,0 +1,19 @@ +import { describe, expect, it } from 'vitest'; +import path from 'path'; +import { buildCodexCliEnv, codexCommandForShell } from '../codexCli.js'; + +describe('codexCli', () => { + it('removes Dr. Claw local node_modules/.bin from Codex CLI PATH probes', () => { + const localBin = path.join(process.cwd(), 'node_modules', '.bin'); + const externalBin = path.join(path.sep, 'usr', 'local', 'bin'); + const env = buildCodexCliEnv({ + PATH: [localBin, externalBin].join(path.delimiter), + }); + + expect(env.PATH.split(path.delimiter)).toEqual([externalBin]); + }); + + it('uses CODEX_CLI_PATH as the shell command when configured', () => { + expect(codexCommandForShell({ CODEX_CLI_PATH: '/opt/homebrew/bin/codex' }, 'darwin')).toBe("'/opt/homebrew/bin/codex'"); + }); +}); diff --git a/server/utils/cliResolution.js b/server/utils/cliResolution.js index e015baa9..c19138dc 100644 --- a/server/utils/cliResolution.js +++ b/server/utils/cliResolution.js @@ -11,6 +11,7 @@ function isCommandNotFoundExitCode(code) { * @param {string} options.envVarName Environment variable name containing an override command. * @param {string[]} [options.legacyEnvVarNames=[]] Older env var names checked after envVarName (migration). * @param {string[]} options.defaultCommands Fallback command names in preference order. + * @param {object} [options.env=process.env] Environment used for env var overrides. * @param {string} [options.platform=process.platform] Runtime platform, used for Windows suffix handling. * @param {boolean} [options.appendWindowsSuffixes=false] Whether to append .cmd/.exe candidates on Windows. * @returns {string[]} Unique command candidates in probe order. @@ -19,12 +20,13 @@ function getCliCommandCandidates({ envVarName, legacyEnvVarNames = [], defaultCommands, + env = process.env, platform = process.platform, appendWindowsSuffixes = false }) { let envCommand = ''; for (const key of [envVarName, ...legacyEnvVarNames].filter(Boolean)) { - const s = String(process.env[key] || '').trim(); + const s = String(env[key] || '').trim(); if (s) { envCommand = s; break; @@ -67,6 +69,7 @@ function isCommandAvailable(command, args = ['--help'], platform = process.platf const result = spawnSync(command, args, { stdio: 'ignore', + env: process.env, shell: platform === 'win32' }); @@ -81,9 +84,10 @@ function isCommandAvailable(command, args = ['--help'], platform = process.platf * @param {Object} [options] * @param {string} [options.platform=process.platform] Runtime platform. * @param {number} [options.timeoutMs=3000] Max probe duration. + * @param {object} [options.env=process.env] Environment used for command probes. * @returns {Promise} True when command can be spawned. */ -function checkCommandAvailable(command, args = ['--help'], { platform = process.platform, timeoutMs = 3000 } = {}) { +function checkCommandAvailable(command, args = ['--help'], { platform = process.platform, timeoutMs = 3000, env = process.env } = {}) { return new Promise((resolve) => { let completed = false; @@ -91,7 +95,7 @@ function checkCommandAvailable(command, args = ['--help'], { platform = process. try { childProcess = spawn(command, args, { stdio: 'ignore', - env: process.env, + env, shell: platform === 'win32' }); } catch { @@ -144,8 +148,9 @@ function checkCommandAvailable(command, args = ['--help'], { platform = process. * @param {string[]} options.defaultCommands Fallback command names in preference order. * @param {string[]} [options.args=['--help']] Probe arguments. * @param {string} [options.platform=process.platform] Runtime platform. + * @param {object} [options.env=process.env] Environment used for command probes. * @param {boolean} [options.appendWindowsSuffixes=false] Whether to append .cmd/.exe candidates on Windows. - * @param {(command: string, args: string[], options: {platform: string}) => Promise} [options.probe=checkCommandAvailable] Async probe function. + * @param {(command: string, args: string[], options: {platform: string, env: object}) => Promise} [options.probe=checkCommandAvailable] Async probe function. * @returns {Promise} First available command, or null. */ async function resolveAvailableCliCommand({ @@ -154,6 +159,7 @@ async function resolveAvailableCliCommand({ defaultCommands, args = ['--help'], platform = process.platform, + env = process.env, appendWindowsSuffixes = false, probe = (command, probeArgs, probeOptions) => checkCommandAvailable(command, probeArgs, probeOptions) }) { @@ -161,12 +167,13 @@ async function resolveAvailableCliCommand({ envVarName, legacyEnvVarNames, defaultCommands, + env, platform, appendWindowsSuffixes }); for (const candidate of candidates) { - if (await probe(candidate, args, { platform })) { + if (await probe(candidate, args, { platform, env })) { return candidate; } } diff --git a/server/utils/codexCli.js b/server/utils/codexCli.js new file mode 100644 index 00000000..e03120d2 --- /dev/null +++ b/server/utils/codexCli.js @@ -0,0 +1,70 @@ +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const APP_ROOT = path.resolve(__dirname, '..', '..'); + +function getPathEnvKey(env = process.env) { + if (process.platform !== 'win32') return 'PATH'; + return Object.keys(env).find((key) => key.toLowerCase() === 'path') || 'Path'; +} + +function normalizePathForCompare(value) { + const resolved = path.resolve(value); + return process.platform === 'win32' ? resolved.toLowerCase() : resolved; +} + +function getLocalNodeBinPaths() { + return [ + path.join(APP_ROOT, 'node_modules', '.bin'), + path.join(process.cwd(), 'node_modules', '.bin'), + ]; +} + +function stripLocalNodeBinFromPath(pathValue = '') { + if (!pathValue) return pathValue; + + const blocked = new Set(getLocalNodeBinPaths().map(normalizePathForCompare)); + return pathValue + .split(path.delimiter) + .filter((entry) => { + if (!entry) return false; + return !blocked.has(normalizePathForCompare(entry)); + }) + .join(path.delimiter); +} + +function buildCodexCliEnv(baseEnv = process.env) { + const env = { ...baseEnv }; + const pathKey = getPathEnvKey(env); + env[pathKey] = stripLocalNodeBinFromPath(env[pathKey] || ''); + return env; +} + +function getCodexCliCommand(env = process.env) { + return String(env.CODEX_CLI_PATH || '').trim() || 'codex'; +} + +function quotePosix(value) { + return `'${String(value).replace(/'/g, `'\\''`)}'`; +} + +function quotePowerShell(value) { + return `'${String(value).replace(/'/g, "''")}'`; +} + +function codexCommandForShell(env = process.env, platform = process.platform) { + const command = getCodexCliCommand(env); + if (command === 'codex') return command; + + return platform === 'win32' + ? `& ${quotePowerShell(command)}` + : quotePosix(command); +} + +export { + buildCodexCliEnv, + codexCommandForShell, + getCodexCliCommand, +};