diff --git a/packages/client/src/__tests__/source-repos.test.ts b/packages/client/src/__tests__/source-repos.test.ts index f2621be1c..73491dc20 100644 --- a/packages/client/src/__tests__/source-repos.test.ts +++ b/packages/client/src/__tests__/source-repos.test.ts @@ -43,6 +43,18 @@ describe("codexDesktopAppBinDirs", () => { expect(codexDesktopAppBinDirs("/Users/gandy", "darwin")).toEqual([ join("/Applications", "ChatGPT.app", "Contents", "Resources"), join("/Users/gandy", "Applications", "ChatGPT.app", "Contents", "Resources"), + join("/Applications", "ChatGPT.app", "Contents", "Resources", "codex-cli", "CodexCLI.app", "Contents", "MacOS"), + join( + "/Users/gandy", + "Applications", + "ChatGPT.app", + "Contents", + "Resources", + "codex-cli", + "CodexCLI.app", + "Contents", + "MacOS", + ), join("/Applications", "Codex.app", "Contents", "Resources"), join("/Users/gandy", "Applications", "Codex.app", "Contents", "Resources"), ]); diff --git a/packages/client/src/providers/codex/__tests__/binary.test.ts b/packages/client/src/providers/codex/__tests__/binary.test.ts index ea49a50be..19353ffca 100644 --- a/packages/client/src/providers/codex/__tests__/binary.test.ts +++ b/packages/client/src/providers/codex/__tests__/binary.test.ts @@ -2,6 +2,7 @@ import { chmodSync, mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync import { tmpdir } from "node:os"; import { delimiter, join } from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; +import { codexDesktopAppBinDirs } from "../../../runtime/install-locations.js"; import { CodexBinaryUnusableError, CodexBinaryVerifyTransientError, @@ -454,6 +455,43 @@ describe("codex binary resolution", () => { expect(findCodexExecutableOnPath({ PATH: "" }, deps)).toBe(legacyCodex); }); + it.each(["system", "user"])("discovers the nested ChatGPT Codex CLI in a %s installation", (installation) => { + tmp = mkdtempSync(join(tmpdir(), "ft-codex-nested-app-")); + const home = join(tmp, "home"); + const systemRoot = join(tmp, "system"); + const appRoot = installation === "system" ? systemRoot : home; + const binDir = join( + appRoot, + "Applications", + "ChatGPT.app", + "Contents", + "Resources", + "codex-cli", + "CodexCLI.app", + "Contents", + "MacOS", + ); + mkdirSync(binDir, { recursive: true }); + const executable = join(binDir, "codex"); + writeFileSync(executable, "#!/bin/sh\nexit 0\n"); + chmodSync(executable, 0o755); + + const desktopDirs = codexDesktopAppBinDirs(home, "darwin").map((dir) => + dir.startsWith("/Applications/") ? join(systemRoot, dir) : dir, + ); + expect( + findCodexExecutableOnPath( + { PATH: "", HOME: home }, + { + platform: "linux", + loginShellPathDirs: () => [], + wellKnownDirs: () => [], + desktopAppDirs: () => desktopDirs, + }, + ), + ).toBe(executable); + }); + it("prefers a login-shell PATH codex over the desktop app CLI", () => { tmp = mkdtempSync(join(tmpdir(), "ft-codex-login-vs-app-")); const loginDir = join(tmp, "login-bin"); diff --git a/packages/client/src/runtime/install-locations.ts b/packages/client/src/runtime/install-locations.ts index 9709eeff2..2b3bc13f4 100644 --- a/packages/client/src/runtime/install-locations.ts +++ b/packages/client/src/runtime/install-locations.ts @@ -176,6 +176,19 @@ export function codexDesktopAppBinDirs(home: string, platform: NodeJS.Platform = return [ join("/Applications", "ChatGPT.app", "Contents", "Resources"), join(home, "Applications", "ChatGPT.app", "Contents", "Resources"), + // Newer ChatGPT releases embed Codex in a nested app bundle. + join("/Applications", "ChatGPT.app", "Contents", "Resources", "codex-cli", "CodexCLI.app", "Contents", "MacOS"), + join( + home, + "Applications", + "ChatGPT.app", + "Contents", + "Resources", + "codex-cli", + "CodexCLI.app", + "Contents", + "MacOS", + ), join("/Applications", "Codex.app", "Contents", "Resources"), join(home, "Applications", "Codex.app", "Contents", "Resources"), ];