115 lines
3.1 KiB
Go
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
|
|
}
|