mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-25 03:55:27 +02:00
fix(daemon): detect Codex CLI under ChatGPT.app on macOS (#5250)
Detect the bundled Codex CLI under the relocated ChatGPT.app on macOS, while keeping the legacy Codex.app path so older installs still resolve. Closes #5205
This commit is contained in:
@@ -756,12 +756,22 @@ var defaultAgentCommandNames = []string{
|
||||
"pi", "cursor-agent", "copilot", "kimi", "kiro-cli", "codebuddy", "agy", "traecli",
|
||||
}
|
||||
|
||||
// codexDesktopAppBundlePaths returns candidate macOS app-bundle locations for
|
||||
// the bundled Codex CLI. OpenAI relocated the Desktop app from Codex.app to
|
||||
// ChatGPT.app (#5205). Candidates are ordered by install location first
|
||||
// (system /Applications before user ~/Applications); within each location the
|
||||
// new ChatGPT.app path is tried before the legacy Codex.app path, so updated
|
||||
// installs win while older installs still resolve.
|
||||
var codexDesktopAppBundlePaths = func() []string {
|
||||
paths := []string{
|
||||
"/Applications/ChatGPT.app/Contents/Resources/codex",
|
||||
"/Applications/Codex.app/Contents/Resources/codex",
|
||||
}
|
||||
if home, err := os.UserHomeDir(); err == nil {
|
||||
paths = append(paths, filepath.Join(home, "Applications", "Codex.app", "Contents", "Resources", "codex"))
|
||||
paths = append(paths,
|
||||
filepath.Join(home, "Applications", "ChatGPT.app", "Contents", "Resources", "codex"),
|
||||
filepath.Join(home, "Applications", "Codex.app", "Contents", "Resources", "codex"),
|
||||
)
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
@@ -660,6 +660,80 @@ func TestLoadConfig_UsesCodexDesktopAppBundleFallback(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Regression for #5205: after OpenAI moved the Desktop app to ChatGPT.app,
|
||||
// Multica must resolve the bundled CLI under ChatGPT.app (and prefer it over
|
||||
// the legacy Codex.app path when both exist).
|
||||
func TestLoadConfig_UsesChatGPTAppBundleCodexPath(t *testing.T) {
|
||||
pathDir := t.TempDir()
|
||||
fakeChatGPT := filepath.Join(pathDir, "ChatGPT.app", "Contents", "Resources", "codex")
|
||||
fakeLegacy := filepath.Join(pathDir, "Codex.app", "Contents", "Resources", "codex")
|
||||
for _, p := range []string{fakeChatGPT, fakeLegacy} {
|
||||
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
||||
t.Fatalf("mkdir: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(p, []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil {
|
||||
t.Fatalf("write fake CLI: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
oldBundlePaths := codexDesktopAppBundlePaths
|
||||
// Prefer ChatGPT first, matching production ordering.
|
||||
codexDesktopAppBundlePaths = func() []string { return []string{fakeChatGPT, fakeLegacy} }
|
||||
t.Cleanup(func() { codexDesktopAppBundlePaths = oldBundlePaths })
|
||||
|
||||
t.Setenv("PATH", t.TempDir())
|
||||
t.Setenv("SHELL", filepath.Join(t.TempDir(), "fish"))
|
||||
t.Setenv("MULTICA_DAEMON_ID", "11111111-1111-1111-1111-111111111111")
|
||||
pinNonCodexAgentsToMissingPaths(t)
|
||||
|
||||
cfg, err := LoadConfig(Overrides{
|
||||
ServerURL: "http://localhost:0",
|
||||
WorkspacesRoot: t.TempDir(),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("LoadConfig: %v", err)
|
||||
}
|
||||
got, ok := cfg.Agents["codex"]
|
||||
if !ok {
|
||||
t.Fatalf("expected codex agent from ChatGPT.app bundle, got %#v", cfg.Agents)
|
||||
}
|
||||
if got.Path != fakeChatGPT {
|
||||
t.Fatalf("codex path = %q, want ChatGPT.app path %q", got.Path, fakeChatGPT)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodexDesktopAppBundlePaths_IncludesChatGPTAndLegacy(t *testing.T) {
|
||||
paths := codexDesktopAppBundlePaths()
|
||||
var hasChatGPT, hasLegacy bool
|
||||
for _, p := range paths {
|
||||
if strings.Contains(p, "ChatGPT.app") && strings.HasSuffix(filepath.ToSlash(p), "Contents/Resources/codex") {
|
||||
hasChatGPT = true
|
||||
}
|
||||
if strings.Contains(p, "Codex.app") && strings.HasSuffix(filepath.ToSlash(p), "Contents/Resources/codex") {
|
||||
hasLegacy = true
|
||||
}
|
||||
}
|
||||
if !hasChatGPT {
|
||||
t.Fatalf("codexDesktopAppBundlePaths missing ChatGPT.app entry: %#v", paths)
|
||||
}
|
||||
if !hasLegacy {
|
||||
t.Fatalf("codexDesktopAppBundlePaths missing legacy Codex.app entry: %#v", paths)
|
||||
}
|
||||
// New path must be preferred (listed before legacy).
|
||||
chatgptIdx, legacyIdx := -1, -1
|
||||
for i, p := range paths {
|
||||
if chatgptIdx < 0 && strings.Contains(p, "ChatGPT.app") {
|
||||
chatgptIdx = i
|
||||
}
|
||||
if legacyIdx < 0 && strings.Contains(p, "Codex.app") {
|
||||
legacyIdx = i
|
||||
}
|
||||
}
|
||||
if chatgptIdx < 0 || legacyIdx < 0 || chatgptIdx > legacyIdx {
|
||||
t.Fatalf("expected ChatGPT.app before Codex.app, got indices chat=%d legacy=%d paths=%#v", chatgptIdx, legacyIdx, paths)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadConfig_CodexDesktopFallbackDoesNotOverrideExplicitPath(t *testing.T) {
|
||||
pathDir := t.TempDir()
|
||||
fakeCodex := filepath.Join(pathDir, "Codex.app", "Contents", "Resources", "codex")
|
||||
|
||||
Reference in New Issue
Block a user