mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-14 03:30:19 +00:00
🐛 fix(desktop): detect bundled Codex CLI from Codex.app on macOS
The OpenAI Codex desktop app ships the real `codex` CLI inside its `.app` bundle but does not symlink it onto PATH. Users who installed only the desktop app therefore failed PATH-based detection, so the heterogeneous agent never spawned Codex and silently produced no reply. Probe the bundled binary as a fallback candidate (after the PATH lookup, so a user's own install still wins), covering both system and per-user install locations. macOS only for now. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -182,6 +182,42 @@ describe('cliAgentDetectors', () => {
|
||||
expect(execFileMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('falls back to the bundled Codex.app binary when `codex` is not on PATH', async () => {
|
||||
// OpenAI's Codex desktop app ships the real CLI inside the .app but does
|
||||
// not symlink it onto PATH. A user with only the desktop app installed
|
||||
// has no `codex` on PATH, so detection must fall back to the bundle.
|
||||
const originalShell = process.env.SHELL;
|
||||
const originalPath = process.env.PATH;
|
||||
// Unset SHELL and pin an already-normalized PATH so `resolveCommandPath`
|
||||
// makes exactly one `which` attempt (no login-shell / normalized-PATH
|
||||
// fallback retry), leaving the bundle as the only viable candidate.
|
||||
delete process.env.SHELL;
|
||||
process.env.PATH = '/usr/bin:/bin';
|
||||
|
||||
try {
|
||||
// 1) `which codex` → not found
|
||||
callExecFileError(new Error('not found'));
|
||||
// 2) absolute bundle path resolves directly (no `which`); validate it
|
||||
callExecFile('codex-cli 0.138.0-alpha.7');
|
||||
|
||||
const { codexDetector } = await import('../cliAgentDetectors');
|
||||
const status = await codexDetector.detect();
|
||||
|
||||
expect(status.available).toBe(true);
|
||||
expect(status.path).toBe('/Applications/Codex.app/Contents/Resources/codex');
|
||||
expect(status.version).toBe('codex-cli 0.138.0-alpha.7');
|
||||
|
||||
// The validation call must target the bundled binary directly via execFile.
|
||||
expect(execMock).not.toHaveBeenCalled();
|
||||
const validateCall = execFileMock.mock.calls.at(-1)!;
|
||||
expect(validateCall[0]).toBe('/Applications/Codex.app/Contents/Resources/codex');
|
||||
expect(validateCall[1]).toEqual(['--version']);
|
||||
} finally {
|
||||
process.env.SHELL = originalShell;
|
||||
process.env.PATH = originalPath;
|
||||
}
|
||||
});
|
||||
|
||||
it('falls back to the login shell PATH for tools installed by shell setup', async () => {
|
||||
const originalPath = process.env.PATH;
|
||||
const originalShell = process.env.SHELL;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { exec, execFile } from 'node:child_process';
|
||||
import { platform } from 'node:os';
|
||||
import { homedir, platform } from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { promisify } from 'node:util';
|
||||
|
||||
@@ -258,12 +258,34 @@ export const claudeCodeDetector: IToolDetector = createValidatedDetector({
|
||||
validateKeywords: ['claude code'],
|
||||
});
|
||||
|
||||
/**
|
||||
* OpenAI's Codex desktop app bundles the real `codex` CLI inside its `.app`
|
||||
* but does not symlink it onto PATH. A user who installed only the desktop app
|
||||
* would therefore fail PATH-based detection, so we probe the bundled binary as
|
||||
* a fallback (tried after the PATH lookup, so a user's own install still wins).
|
||||
* Both the system (`/Applications`) and per-user (`~/Applications`) install
|
||||
* locations are covered. macOS only for now.
|
||||
*/
|
||||
const getCodexCandidates = (): string[] => {
|
||||
const candidates = ['codex'];
|
||||
|
||||
if (platform() === 'darwin') {
|
||||
const bundleRelativePath = 'Codex.app/Contents/Resources/codex';
|
||||
candidates.push(
|
||||
path.join('/Applications', bundleRelativePath),
|
||||
path.join(homedir(), 'Applications', bundleRelativePath),
|
||||
);
|
||||
}
|
||||
|
||||
return candidates;
|
||||
};
|
||||
|
||||
/**
|
||||
* OpenAI Codex CLI
|
||||
* @see https://github.com/openai/codex
|
||||
*/
|
||||
export const codexDetector: IToolDetector = createValidatedDetector({
|
||||
candidates: ['codex'],
|
||||
candidates: getCodexCandidates(),
|
||||
description: 'Codex - OpenAI agentic coding CLI',
|
||||
name: 'codex',
|
||||
priority: 2,
|
||||
|
||||
Reference in New Issue
Block a user