180 lines
4.9 KiB
Go
180 lines
4.9 KiB
Go
package handlers
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// AgentToolInfo holds version info for an AI coding agent tool.
|
|
type AgentToolInfo struct {
|
|
Name string `json:"name"`
|
|
Found bool `json:"found"`
|
|
Version string `json:"version,omitempty"`
|
|
Path string `json:"path,omitempty"`
|
|
}
|
|
|
|
// RuntimeInfo holds version info for a programming language runtime or compiler.
|
|
type RuntimeInfo struct {
|
|
Name string `json:"name"`
|
|
Found bool `json:"found"`
|
|
Version string `json:"version,omitempty"`
|
|
Path string `json:"path,omitempty"`
|
|
}
|
|
|
|
type agentToolDef struct {
|
|
Name string
|
|
Binaries []string
|
|
Args []string
|
|
}
|
|
|
|
type runtimeDef struct {
|
|
Name string
|
|
Binaries []string // try in order
|
|
Args []string
|
|
}
|
|
|
|
var agentToolDefs = []agentToolDef{
|
|
{Name: "Codex", Binaries: []string{"codex"}, Args: []string{"--version"}},
|
|
{Name: "Claude Code", Binaries: []string{"claude"}, Args: []string{"--version"}},
|
|
{Name: "OpenCode", Binaries: []string{"opencode"}, Args: []string{"--version"}},
|
|
}
|
|
|
|
var runtimeDefs = []runtimeDef{
|
|
// python3 first; fall back to python on Windows where python3 may not exist
|
|
{Name: "Python", Binaries: []string{"python3", "python"}, Args: []string{"--version"}},
|
|
{Name: "Node.js", Binaries: []string{"node"}, Args: []string{"--version"}},
|
|
{Name: "Bun", Binaries: []string{"bun"}, Args: []string{"--version"}},
|
|
{Name: "Go", Binaries: []string{"go"}, Args: []string{"version"}},
|
|
// Java outputs version to stderr — runVersionOutput handles this
|
|
{Name: "Java", Binaries: []string{"java"}, Args: []string{"-version"}},
|
|
{Name: "Rust", Binaries: []string{"rustc"}, Args: []string{"--version"}},
|
|
// C compilers: detect both, show whichever is present
|
|
{Name: "GCC", Binaries: []string{"gcc"}, Args: []string{"--version"}},
|
|
{Name: "Clang", Binaries: []string{"clang"}, Args: []string{"--version"}},
|
|
}
|
|
|
|
// extraSearchPaths returns common installation directories not always in PATH.
|
|
func extraSearchPaths() []string {
|
|
home, _ := os.UserHomeDir()
|
|
paths := []string{
|
|
filepath.Join(home, "bin"),
|
|
filepath.Join(home, ".local", "bin"),
|
|
filepath.Join(home, ".bun", "bin"),
|
|
filepath.Join(home, "go", "bin"),
|
|
filepath.Join(home, ".cargo", "bin"),
|
|
"/usr/local/go/bin",
|
|
"/usr/local/bin",
|
|
"/usr/bin",
|
|
}
|
|
if runtime.GOOS == "windows" {
|
|
appdata := os.Getenv("APPDATA")
|
|
paths = append(paths,
|
|
filepath.Join(appdata, "npm"),
|
|
filepath.Join(home, "AppData", "Local", "Programs"),
|
|
filepath.Join(home, "AppData", "Roaming", "npm"),
|
|
)
|
|
}
|
|
return paths
|
|
}
|
|
|
|
// lookupBinary searches PATH then extra directories for the binary.
|
|
func lookupBinary(name string) string {
|
|
if p, err := exec.LookPath(name); err == nil {
|
|
return p
|
|
}
|
|
for _, dir := range extraSearchPaths() {
|
|
candidate := filepath.Join(dir, name)
|
|
if runtime.GOOS == "windows" {
|
|
candidate += ".exe"
|
|
}
|
|
if _, err := os.Stat(candidate); err == nil {
|
|
return candidate
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// runVersionOutput runs the binary and returns the first non-empty line from stdout
|
|
// or stderr (some tools like Java output version to stderr).
|
|
func runVersionOutput(binaryPath string, args []string) string {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
|
|
defer cancel()
|
|
cmd := exec.CommandContext(ctx, binaryPath, args...)
|
|
var stdout, stderr bytes.Buffer
|
|
cmd.Stdout = &stdout
|
|
cmd.Stderr = &stderr
|
|
_ = cmd.Run()
|
|
out := strings.TrimSpace(stdout.String())
|
|
if out == "" {
|
|
out = strings.TrimSpace(stderr.String())
|
|
}
|
|
if idx := strings.IndexByte(out, '\n'); idx >= 0 {
|
|
out = strings.TrimSpace(out[:idx])
|
|
}
|
|
return out
|
|
}
|
|
|
|
// runVersion is kept for backward compatibility.
|
|
func runVersion(binaryPath string, args []string) string {
|
|
return runVersionOutput(binaryPath, args)
|
|
}
|
|
|
|
// detectAgentTools detects all AI agent tools in parallel.
|
|
func detectAgentTools() []AgentToolInfo {
|
|
results := make([]AgentToolInfo, len(agentToolDefs))
|
|
var wg sync.WaitGroup
|
|
for i, def := range agentToolDefs {
|
|
wg.Add(1)
|
|
go func(idx int, d agentToolDef) {
|
|
defer wg.Done()
|
|
info := AgentToolInfo{Name: d.Name}
|
|
for _, bin := range d.Binaries {
|
|
p := lookupBinary(bin)
|
|
if p == "" {
|
|
continue
|
|
}
|
|
info.Found = true
|
|
info.Path = p
|
|
info.Version = runVersionOutput(p, d.Args)
|
|
break
|
|
}
|
|
results[idx] = info
|
|
}(i, def)
|
|
}
|
|
wg.Wait()
|
|
return results
|
|
}
|
|
|
|
// detectRuntimes detects all language runtimes in parallel.
|
|
func detectRuntimes() []RuntimeInfo {
|
|
results := make([]RuntimeInfo, len(runtimeDefs))
|
|
var wg sync.WaitGroup
|
|
for i, def := range runtimeDefs {
|
|
wg.Add(1)
|
|
go func(idx int, d runtimeDef) {
|
|
defer wg.Done()
|
|
info := RuntimeInfo{Name: d.Name}
|
|
for _, bin := range d.Binaries {
|
|
p := lookupBinary(bin)
|
|
if p == "" {
|
|
continue
|
|
}
|
|
info.Found = true
|
|
info.Path = p
|
|
info.Version = runVersionOutput(p, d.Args)
|
|
break
|
|
}
|
|
results[idx] = info
|
|
}(i, def)
|
|
}
|
|
wg.Wait()
|
|
return results
|
|
}
|