From d17f482d50ed46957ae11b534e3593d9786482c0 Mon Sep 17 00:00:00 2001 From: Eva H <63033505+hoyyeva@users.noreply.github.com> Date: Wed, 8 Apr 2026 13:54:51 -0700 Subject: [PATCH] launch/opencode: detect `curl` installed opencode at `~/.opencode/bin` (#15197) --- cmd/launch/opencode.go | 27 +++++++++++++++++++++++++-- cmd/launch/opencode_test.go | 35 +++++++++++++++++++++++++++++++++++ cmd/launch/registry.go | 4 ++-- 3 files changed, 62 insertions(+), 4 deletions(-) diff --git a/cmd/launch/opencode.go b/cmd/launch/opencode.go index 48a004e64..9e89b95b1 100644 --- a/cmd/launch/opencode.go +++ b/cmd/launch/opencode.go @@ -7,6 +7,7 @@ import ( "os" "os/exec" "path/filepath" + "runtime" "slices" "strings" @@ -19,12 +20,34 @@ type OpenCode struct{} func (o *OpenCode) String() string { return "OpenCode" } +// findOpenCode returns the opencode binary path, checking PATH first then the +// curl installer location (~/.opencode/bin) which may not be on PATH yet. +func findOpenCode() (string, bool) { + if p, err := exec.LookPath("opencode"); err == nil { + return p, true + } + home, err := os.UserHomeDir() + if err != nil { + return "", false + } + name := "opencode" + if runtime.GOOS == "windows" { + name = "opencode.exe" + } + fallback := filepath.Join(home, ".opencode", "bin", name) + if _, err := os.Stat(fallback); err == nil { + return fallback, true + } + return "", false +} + func (o *OpenCode) Run(model string, args []string) error { - if _, err := exec.LookPath("opencode"); err != nil { + opencodePath, ok := findOpenCode() + if !ok { return fmt.Errorf("opencode is not installed, install from https://opencode.ai") } - cmd := exec.Command("opencode", args...) + cmd := exec.Command(opencodePath, args...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr diff --git a/cmd/launch/opencode_test.go b/cmd/launch/opencode_test.go index c2e9b3fb0..fdaea4f28 100644 --- a/cmd/launch/opencode_test.go +++ b/cmd/launch/opencode_test.go @@ -7,6 +7,7 @@ import ( "net/http/httptest" "os" "path/filepath" + "runtime" "testing" ) @@ -771,6 +772,40 @@ func TestLookupCloudModelLimit(t *testing.T) { } } +func TestFindOpenCode(t *testing.T) { + t.Run("fallback to ~/.opencode/bin", func(t *testing.T) { + tmpDir := t.TempDir() + setTestHome(t, tmpDir) + + // Ensure opencode is not on PATH + t.Setenv("PATH", tmpDir) + + // Without the fallback binary, findOpenCode should fail + if _, ok := findOpenCode(); ok { + t.Fatal("findOpenCode should fail when binary is not on PATH or in fallback location") + } + + // Create a fake binary at the curl install fallback location + binDir := filepath.Join(tmpDir, ".opencode", "bin") + os.MkdirAll(binDir, 0o755) + name := "opencode" + if runtime.GOOS == "windows" { + name = "opencode.exe" + } + fakeBin := filepath.Join(binDir, name) + os.WriteFile(fakeBin, []byte("#!/bin/sh\n"), 0o755) + + // Now findOpenCode should succeed via fallback + path, ok := findOpenCode() + if !ok { + t.Fatal("findOpenCode should succeed with fallback binary") + } + if path != fakeBin { + t.Errorf("findOpenCode = %q, want %q", path, fakeBin) + } + }) +} + func TestOpenCodeModels_NoConfig(t *testing.T) { o := &OpenCode{} tmpDir := t.TempDir() diff --git a/cmd/launch/registry.go b/cmd/launch/registry.go index 482aac02c..8d70b3db1 100644 --- a/cmd/launch/registry.go +++ b/cmd/launch/registry.go @@ -92,8 +92,8 @@ var integrationSpecs = []*IntegrationSpec{ Description: "Anomaly's open-source coding agent", Install: IntegrationInstallSpec{ CheckInstalled: func() bool { - _, err := exec.LookPath("opencode") - return err == nil + _, ok := findOpenCode() + return ok }, URL: "https://opencode.ai", },