// 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 }