// Package doctor reports module convergence and pinned-tool version drift. package doctor import ( "context" "encoding/json" "fmt" "io" "regexp" "strings" "gitlab.hive-brain.de/MDietermann/Archduino/internal/config" "gitlab.hive-brain.de/MDietermann/Archduino/internal/module" "gitlab.hive-brain.de/MDietermann/Archduino/internal/platform" ) // Run inspects every module in the registry plus pinned tool versions and // writes a human-readable report to w. Returns 0 when everything is // converged with no drift; 1 otherwise. Never panics on module errors. func Run(ctx context.Context, cfg config.Config, reg *module.Registry, runner platform.Runner, w io.Writer) int { var converged, pending, errs, drift int for _, m := range reg.All() { id := m.Meta().ID st, err := m.Detect(ctx) if err != nil { fmt.Fprintf(w, "[!] %s — detect failed: %v\n", id, err) errs++ continue } pl, err := m.Plan(ctx, st) if err != nil { fmt.Fprintf(w, "[!] %s — plan failed: %v\n", id, err) errs++ continue } if pl.IsEmpty() { fmt.Fprintf(w, "[✓] %s\n", id) converged++ } else { fmt.Fprintf(w, "[✗] %s (%d actions pending)\n", id, len(pl.Actions)) pending++ } } drift += checkArduinoCLI(ctx, runner, cfg.ArduinoCLI.Version, w) drift += checkWokwiCLI(ctx, runner, cfg.Renode.WokwiVersion, w) fmt.Fprintf(w, "\n%d converged, %d need-install, %d drift, %d errors\n", converged, pending, drift, errs) if pending > 0 || drift > 0 || errs > 0 { return 1 } return 0 } type arduinoCLIVersion struct { VersionString string `json:"VersionString"` } func checkArduinoCLI(ctx context.Context, runner platform.Runner, want string, w io.Writer) int { if want == "" { return 0 } out, err := runner.Output(ctx, "arduino-cli", "version", "--format", "json") if err != nil { return 0 // not installed → silent skip } var v arduinoCLIVersion if err := json.Unmarshal(out, &v); err != nil || v.VersionString == "" { fmt.Fprintf(w, "[!] arduino-cli — version parse failed\n") return 1 } if v.VersionString != want { fmt.Fprintf(w, "[!] arduino-cli drift: got %s, want %s\n", v.VersionString, want) return 1 } fmt.Fprintf(w, "[✓] arduino-cli %s\n", v.VersionString) return 0 } var wokwiVersionRE = regexp.MustCompile(`v?(\d+\.\d+\.\d+)`) func checkWokwiCLI(ctx context.Context, runner platform.Runner, want string, w io.Writer) int { if want == "" { return 0 } out, err := runner.Output(ctx, "wokwi-cli", "--version") if err != nil { return 0 } m := wokwiVersionRE.FindStringSubmatch(strings.TrimSpace(string(out))) if len(m) < 2 { fmt.Fprintf(w, "[!] wokwi-cli — version parse failed\n") return 1 } got := m[1] if got != want { fmt.Fprintf(w, "[!] wokwi-cli drift: got %s, want %s\n", got, want) return 1 } fmt.Fprintf(w, "[✓] wokwi-cli %s\n", got) return 0 }