hugin/cli/internal/tools/tools.go
2026-10-27 08:33:21 +01:00

115 lines
3.1 KiB
Go

// Package werkzeuge erkennt Claude Code und OpenCode im Repo und auf dem
// Rechner und prüft ihre Versionen gegen die Untergrenzen (→ KONZEPT.md 5.4, 5.5).
package tools
import (
"context"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
)
// Untergrenzen, solange der Server keine anderen liefert (→ KONZEPT.md 5.5).
var Floors = map[string]string{
"claude": "2.1.105",
"opencode": "1.2.11",
}
var hints = map[string][]string{
"claude": {".claude", "CLAUDE.md", ".mcp.json"},
"opencode": {".opencode", "opencode.json", "opencode.jsonc", "AGENTS.md"},
}
// InRepo erkennt die Werkzeuge an vorhandenen Dateien. Ohne Hinweise: beide.
func InRepo(repo string) []string {
var found []string
for _, name := range []string{"claude", "opencode"} {
for _, file := range hints[name] {
if _, err := os.Stat(filepath.Join(repo, file)); err == nil {
found = append(found, name)
break
}
}
}
if len(found) == 0 {
return []string{"claude", "opencode"}
}
return found
}
var semver = regexp.MustCompile(`\d+\.\d+\.\d+`)
// Version fragt das installierte Werkzeug nach seiner Version; leer, wenn es
// nicht installiert ist oder nicht antwortet.
func Version(name string) string {
ctx, abort := context.WithTimeout(context.Background(), 5*time.Second)
defer abort()
from, err := exec.CommandContext(ctx, name, "--version").Output()
if err != nil {
return ""
}
return semver.FindString(string(from))
}
// Less vergleicht zwei Versionen a.b.c.
func Less(a, b string) bool {
pa, pb := parts(a), parts(b)
for i := range 3 {
if pa[i] != pb[i] {
return pa[i] < pb[i]
}
}
return false
}
func parts(v string) [3]int {
var t [3]int
for i, s := range strings.SplitN(v, ".", 3) {
t[i], _ = strconv.Atoi(s)
}
return t
}
// Target ist der Name im Bundle: claude, opencode@1 oder opencode@2.
func Target(name, version string) string {
if name != "opencode" {
return name
}
if version != "" && !Less(version, "2.0.0") {
return "opencode@2"
}
return "opencode@1"
}
var ansi = regexp.MustCompile(`\x1b\[[0-9;]*m`)
// CheckOpenCode lässt OpenCode die zusammengeführte Konfiguration im Repo laden. OpenCode
// startet nicht, wenn opencode.json ein falsches Feld hat; das soll hugin melden, bevor es
// jemand beim Start merkt. Leer bei Erfolg oder wenn OpenCode nicht installiert ist.
// --pure: globale Plugins nicht mitladen (sie können eigene Fehler haben).
func CheckOpenCode(repo string) string {
if _, err := exec.LookPath("opencode"); err != nil {
return ""
}
ctx, abort := context.WithTimeout(context.Background(), 30*time.Second)
defer abort()
cmd := exec.CommandContext(ctx, "opencode", "debug", "config", "--pure")
cmd.Dir = repo
var stderr strings.Builder
cmd.Stderr = &stderr
if err := cmd.Run(); err == nil {
return ""
} else if ctx.Err() != nil {
return "opencode debug config antwortet nicht (Zeitlimit 30 s)"
}
text := strings.TrimSpace(ansi.ReplaceAllString(stderr.String(), ""))
text = strings.TrimPrefix(text, "Error: ")
if text == "" {
return "opencode debug config ist fehlgeschlagen"
}
return text
}