diff --git a/src/targets/codex-detector.ts b/src/targets/codex-detector.ts index 97cfac1a..e7070617 100644 --- a/src/targets/codex-detector.ts +++ b/src/targets/codex-detector.ts @@ -10,17 +10,26 @@ 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')); + const prioritized = shellCandidates.map((entry) => { + if (!/\.ps1$/i.test(entry)) { + return entry; } - prioritized.push(entry); - } - prioritized.push(...bareCandidates); - return [...new Set(prioritized)]; + for (const preferredExtension of ['.cmd', '.bat', '.exe']) { + const siblingCandidate = entry.replace(/\.ps1$/i, preferredExtension); + try { + if (fs.statSync(siblingCandidate).isFile()) { + return siblingCandidate; + } + } catch { + // Ignore missing sibling wrappers and keep the original PowerShell path. + } + } + + return entry; + }); + + return [...new Set([...prioritized, ...bareCandidates])]; } function runCodexProbe(codexPath: string, args: string[]): string | undefined { diff --git a/tests/unit/targets/codex-detector.test.ts b/tests/unit/targets/codex-detector.test.ts index bf4b0f4a..b0bb1e0d 100644 --- a/tests/unit/targets/codex-detector.test.ts +++ b/tests/unit/targets/codex-detector.test.ts @@ -73,7 +73,7 @@ describe('codex-detector', () => { execFileSyncSpy.mockRestore(); }); - it('prefers a sibling PowerShell wrapper over cmd when Windows PATH only exposes codex.cmd', () => { + it('keeps the cmd wrapper when Windows PATH exposes codex.cmd and a sibling ps1 also exists', () => { const fakeCmdCodex = path.join(tmpDir, 'codex.cmd'); const fakePsCodex = path.join(tmpDir, 'codex.ps1'); fs.writeFileSync(fakeCmdCodex, ''); @@ -82,7 +82,21 @@ describe('codex-detector', () => { const execSyncSpy = spyOn(childProcess, 'execSync').mockImplementation(() => `${fakeCmdCodex}\n`); - expect(detectCodexCli()).toBe(fakePsCodex); + expect(detectCodexCli()).toBe(fakeCmdCodex); + + execSyncSpy.mockRestore(); + }); + + it('replaces a Windows PowerShell wrapper with a sibling cmd shim when PATH returns codex.ps1', () => { + 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(() => `${fakePsCodex}\n`); + + expect(detectCodexCli()).toBe(fakeCmdCodex); execSyncSpy.mockRestore(); });