Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,14 @@ go install github.com/SilkageNet/codex-switch/cmd/codex-switch@latest
Release archives for macOS, Linux, and Windows are published on GitHub.

Adding accounts and querying live usage require an official Codex CLI that this
process can launch. On Windows, install the
process can launch. On macOS, `codex-switch` checks `PATH` first and then the
official Codex and ChatGPT application bundles in `/Applications` and
`~/Applications`, including the nested CLI layout used by current ChatGPT.app
releases. On Windows, install the
[standalone Codex CLI](https://learn.chatgpt.com/docs/codex/cli); the executable
bundled inside the Codex desktop app's WindowsApps package cannot be launched by
external processes. Verify the installation with `codex --version` before using
those commands.
those commands. `CODEX_BINARY` and `--codex-binary` remain explicit overrides.

WSL2 is supported by the Linux archive. It uses the Windows user's DPAPI
protection through the built-in `powershell.exe`; a Linux desktop Secret Service
Expand Down
5 changes: 5 additions & 0 deletions docs/compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ searching `PATH` and returns an actionable error when no standalone CLI is
available. A standalone CLI can also be selected with `CODEX_BINARY` or
`--codex-binary`.

On macOS, discovery checks `PATH`, `/Applications`, and `~/Applications`. Both
the retained `Codex.app/Contents/Resources/codex` compatibility path and the
nested `ChatGPT.app/Contents/Resources/codex-cli/CodexCLI.app/Contents/MacOS/codex`
layout are supported, along with their direct/nested bundle variants.

Usage querying initializes `codex app-server` and calls:

- `account/read`
Expand Down
7 changes: 7 additions & 0 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,13 @@ codex --version
codex-switch account usage <alias>
```

On macOS, a standalone `codex` command is optional when the official Codex or
ChatGPT desktop app is installed in `/Applications` or `~/Applications`.
`codex-switch` automatically discovers both the retained Codex.app compatibility
path and the current nested ChatGPT.app CLI. Run `codex-switch doctor` to see the
exact executable path selected. Nonstandard application locations still require
`--codex-binary <path>` or `CODEX_BINARY`.

On Windows, a path under
`C:\Program Files\WindowsApps\OpenAI.Codex_*\app\resources\codex.exe` belongs to
the desktop app package. Windows does not allow `codex-switch` to launch that
Expand Down
47 changes: 39 additions & 8 deletions internal/codexlogin/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,46 @@ func FindBinary(override string) (string, error) {
}
return validateBinary(path)
}
if path := "/Applications/ChatGPT.app/Contents/Resources/codex"; fileExists(path) {
return path, nil
if runtime.GOOS == "darwin" {
if path := findMacOSBundledBinary(macOSApplicationDirectories()); path != "" {
return path, nil
}
}
return "", fmt.Errorf("codex executable not found; install Codex CLI or set CODEX_BINARY")
return "", fmt.Errorf("codex executable not found on PATH or in a supported app bundle; install Codex CLI or set CODEX_BINARY")
}

func macOSApplicationDirectories() []string {
directories := []string{"/Applications"}
if home, err := os.UserHomeDir(); err == nil && home != "" {
directories = append(directories, filepath.Join(home, "Applications"))
}
return directories
}

func findMacOSBundledBinary(applicationDirectories []string) string {
relativePaths := []string{
filepath.Join("Codex.app", "Contents", "Resources", "codex"),
filepath.Join("Codex.app", "Contents", "Resources", "codex-cli", "CodexCLI.app", "Contents", "MacOS", "codex"),
filepath.Join("ChatGPT.app", "Contents", "Resources", "codex-cli", "CodexCLI.app", "Contents", "MacOS", "codex"),
filepath.Join("ChatGPT.app", "Contents", "Resources", "codex"),
}
for _, directory := range applicationDirectories {
for _, relative := range relativePaths {
path := filepath.Join(directory, relative)
if executableFileExists(path) {
return path
}
}
}
return ""
}

func executableFileExists(path string) bool {
info, err := os.Stat(path)
if err != nil || info.IsDir() || info.Mode()&0o111 == 0 {
return false
}
return true
}

func validateBinary(path string) (string, error) {
Expand Down Expand Up @@ -123,8 +159,3 @@ func withEnvironment(environment []string, key, value string) []string {
}
return append(result, prefix+value)
}

func fileExists(path string) bool {
info, err := os.Stat(path)
return err == nil && !info.IsDir()
}
34 changes: 34 additions & 0 deletions internal/codexlogin/login_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,40 @@ func TestFindBinaryAcceptsExplicitStandaloneBinary(t *testing.T) {
}
}

func TestFindMacOSBundledBinarySupportsCurrentChatGPTLayout(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("executable mode bits are not available on Windows")
}
applications := t.TempDir()
want := filepath.Join(applications, "ChatGPT.app", "Contents", "Resources", "codex-cli", "CodexCLI.app", "Contents", "MacOS", "codex")
if err := os.MkdirAll(filepath.Dir(want), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(want, []byte("test"), 0o755); err != nil {
t.Fatal(err)
}
if got := findMacOSBundledBinary([]string{applications}); got != want {
t.Fatalf("findMacOSBundledBinary() = %q, want %q", got, want)
}
}

func TestFindMacOSBundledBinaryIgnoresNonExecutableFiles(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("executable mode bits are not available on Windows")
}
applications := t.TempDir()
path := filepath.Join(applications, "ChatGPT.app", "Contents", "Resources", "codex")
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte("test"), 0o644); err != nil {
t.Fatal(err)
}
if got := findMacOSBundledBinary([]string{applications}); got != "" {
t.Fatalf("findMacOSBundledBinary() returned non-executable file %q", got)
}
}

func TestFindBinarySkipsWindowsDesktopBundleOnPath(t *testing.T) {
if runtime.GOOS != "windows" {
t.Skip("Windows package paths are only rejected on Windows")
Expand Down
2 changes: 1 addition & 1 deletion internal/doctor/doctor.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ func Run(home codexhome.Home, paths appconfig.Paths, manager *vault.Manager, cod
if version, err := runner.Version(); err != nil {
add("codex_cli", "error", err.Error())
} else {
add("codex_cli", "ok", version)
add("codex_cli", "ok", fmt.Sprintf("%s (%s)", version, codexBinary))
}
}

Expand Down
Loading