Skip to content

Commit 6f2074f

Browse files
committed
feat: add agent install lifecycle support
1 parent 40be65e commit 6f2074f

25 files changed

Lines changed: 1035 additions & 33 deletions

core/cmd/desktop.go

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,7 @@ func cmdDesktopAgents() *cobra.Command {
241241
Use: "agents",
242242
Short: "Detect local agent CLI installations",
243243
}
244-
c.AddCommand(cmdDesktopAgentsStatus(), cmdDesktopAgentsWhich())
244+
c.AddCommand(cmdDesktopAgentsStatus(), cmdDesktopAgentsWhich(), cmdDesktopAgentsInstall(), cmdDesktopAgentsUninstall())
245245
return c
246246
}
247247

@@ -268,6 +268,31 @@ func cmdDesktopAgentsWhich() *cobra.Command {
268268
return c
269269
}
270270

271+
func cmdDesktopAgentsInstall() *cobra.Command {
272+
var kind string
273+
c := &cobra.Command{
274+
Use: "install",
275+
Short: "Install one local agent CLI when supported",
276+
RunE: func(cmd *cobra.Command, args []string) error {
277+
return writeDesktopJSON(desktop.AgentInstall(kind))
278+
},
279+
}
280+
c.Flags().StringVar(&kind, "cli", "", "Agent CLI kind to install")
281+
return c
282+
}
283+
284+
func cmdDesktopAgentsUninstall() *cobra.Command {
285+
var kind string
286+
c := &cobra.Command{
287+
Use: "uninstall",
288+
Short: "Uninstall one local agent CLI when supported",
289+
RunE: func(cmd *cobra.Command, args []string) error {
290+
return writeDesktopJSON(desktop.AgentUninstall(kind))
291+
},
292+
}
293+
c.Flags().StringVar(&kind, "cli", "", "Agent CLI kind to uninstall")
294+
return c
295+
}
271296
func applyBindingSwitch(kind agentkind.Kind, binding string) error {
272297
if err := desktop.ApplyBinding(kind, binding); err != nil {
273298
return err

core/cmd/proxy_cmd.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ func shouldSkipAutoProxy(cmd *cobra.Command) bool {
242242
}
243243
for c := cmd; c != nil; c = c.Parent() {
244244
switch c.Name() {
245-
case "__proxy-daemon", "update", "version":
245+
case "__proxy-daemon", "desktop", "update", "version":
246246
return true
247247
}
248248
}

core/internal/apply/codex_home_test.go

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package apply
22

33
import (
4+
"os"
45
"path/filepath"
6+
"runtime"
57
"testing"
68
)
79

@@ -28,4 +30,28 @@ func TestCodexConfigPathUsesCODEX_HOME(t *testing.T) {
2830
if got != want {
2931
t.Fatalf("CodexConfigPath() = %q, want %q", got, want)
3032
}
31-
}
33+
}
34+
func TestResolveCodexExecutableIgnoresWindowsStoreClient(t *testing.T) {
35+
if runtime.GOOS != "windows" {
36+
t.Skip("Windows Store Codex layout is Windows-only")
37+
}
38+
root := t.TempDir()
39+
storeBin := filepath.Join(root, "WindowsApps", "OpenAI.Codex_1.0.0.0_x64__2p2nqsd0c76g0", "app", "resources")
40+
if err := os.MkdirAll(storeBin, 0o755); err != nil {
41+
t.Fatal(err)
42+
}
43+
if err := os.WriteFile(filepath.Join(storeBin, "codex.exe"), []byte(""), 0o755); err != nil {
44+
t.Fatal(err)
45+
}
46+
t.Setenv("PATH", storeBin)
47+
t.Setenv("HOME", root)
48+
t.Setenv("USERPROFILE", root)
49+
t.Setenv("APPDATA", filepath.Join(root, "AppData"))
50+
t.Setenv("LOCALAPPDATA", filepath.Join(root, "LocalAppData"))
51+
if got, ok := ResolveCodexExecutable(); ok {
52+
t.Fatalf("ResolveCodexExecutable() = %q, true; want Windows Store client ignored", got)
53+
}
54+
if CodexInstalled() {
55+
t.Fatal("CodexInstalled() = true for Windows Store client; want false")
56+
}
57+
}

core/internal/apply/codex_install.go

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ func CodexSearchDirs(home string) []string {
3131
}
3232
addWindowsCodexSearchDirs(add)
3333
addDarwinCodexSearchDirs(add, home)
34+
if prefix, ok := npmGlobalPrefix(); ok {
35+
add(prefix)
36+
}
3437
return dirs
3538
}
3639

@@ -54,31 +57,24 @@ func codexExecutableFile(path string) bool {
5457

5558
// ResolveCodexExecutable finds the Codex CLI binary from PATH or common install roots.
5659
func ResolveCodexExecutable() (string, bool) {
57-
if p, err := exec.LookPath("codex"); err == nil && strings.TrimSpace(p) != "" {
60+
if p, err := exec.LookPath("codex"); err == nil && strings.TrimSpace(p) != "" && !codexClientExecutablePath(p) {
5861
return p, true
5962
}
6063
home, _ := os.UserHomeDir()
6164
for _, dir := range CodexSearchDirs(home) {
6265
for _, name := range codexCommandNames() {
6366
candidate := filepath.Join(dir, name)
64-
if codexExecutableFile(candidate) {
67+
if codexExecutableFile(candidate) && !codexClientExecutablePath(candidate) {
6568
return candidate, true
6669
}
6770
}
6871
}
69-
for _, candidate := range codexExtraExecutableCandidates() {
70-
if codexExecutableFile(candidate) {
71-
return candidate, true
72-
}
73-
}
7472
return "", false
7573
}
7674

77-
// CodexInstalled reports whether Codex is usable (CLI on disk or Codex app install layout).
78-
// CLI and desktop app share CODEX_HOME; they are not distinguished here.
75+
// CodexInstalled reports whether the Codex CLI is available.
76+
// Desktop/Store app installs are intentionally ignored here.
7977
func CodexInstalled() bool {
80-
if _, ok := ResolveCodexExecutable(); ok {
81-
return true
82-
}
83-
return codexRuntimePresent()
78+
_, ok := ResolveCodexExecutable()
79+
return ok
8480
}

core/internal/apply/codex_install_other.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package apply
55
import (
66
"os"
77
"path/filepath"
8+
"strings"
89
)
910

1011
func addWindowsCodexSearchDirs(add func(string)) {}
@@ -16,6 +17,16 @@ func addDarwinCodexSearchDirs(add func(string), home string) {
1617
add(filepath.Join(home, ".npm-global", "bin"))
1718
}
1819

20+
func codexClientExecutablePath(path string) bool {
21+
cleaned := filepath.Clean(path)
22+
for _, appRoot := range darwinCodexAppRoots() {
23+
if strings.HasPrefix(cleaned, filepath.Clean(appRoot)+string(filepath.Separator)) {
24+
return true
25+
}
26+
}
27+
return false
28+
}
29+
1930
func codexExtraExecutableCandidates() []string {
2031
var out []string
2132
for _, appRoot := range darwinCodexAppRoots() {

core/internal/apply/codex_install_windows.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,23 @@ import (
99
)
1010

1111
func addWindowsCodexSearchDirs(add func(string)) {
12-
if localAppData := strings.TrimSpace(os.Getenv("LOCALAPPDATA")); localAppData != "" {
13-
add(filepath.Join(localAppData, "Programs", "OpenAI", "Codex", "bin"))
14-
}
1512
if appData := strings.TrimSpace(os.Getenv("APPDATA")); appData != "" {
1613
add(filepath.Join(appData, "npm"))
1714
}
1815
}
1916

2017
func addDarwinCodexSearchDirs(add func(string), home string) {}
2118

19+
func codexClientExecutablePath(path string) bool {
20+
cleaned := strings.ToLower(filepath.Clean(strings.TrimSpace(path)))
21+
if cleaned == "" {
22+
return false
23+
}
24+
return strings.Contains(cleaned, strings.ToLower(filepath.Clean(filepath.Join("WindowsApps", "OpenAI.Codex_")))) ||
25+
strings.Contains(cleaned, strings.ToLower(filepath.Clean(filepath.Join("Packages", "OpenAI.Codex_")))) ||
26+
strings.Contains(cleaned, strings.ToLower(filepath.Clean(filepath.Join("Programs", "OpenAI", "Codex"))))
27+
}
28+
2229
func codexExtraExecutableCandidates() []string {
2330
var out []string
2431
if localAppData := strings.TrimSpace(os.Getenv("LOCALAPPDATA")); localAppData != "" {

0 commit comments

Comments
 (0)