package main import ( "encoding/json" "fmt" "os" "strings" "text/tabwriter" "fn-registry/functions/infra" ) func cmdDoctor(args []string) { jsonOut := false sub := "" for _, a := range args { switch a { case "--json": jsonOut = true case "-h", "--help": doctorUsage() return default: if sub == "" { sub = a } } } r := root() switch sub { case "", "all": doctorAll(r, jsonOut) case "artefacts": doctorArtefacts(r, jsonOut) case "services": doctorServices(r, jsonOut) case "sync": doctorSync(r, jsonOut) case "uses-functions": doctorUsesFunctions(r, jsonOut) case "unused": doctorUnused(r, jsonOut) default: fmt.Fprintf(os.Stderr, "unknown doctor subcommand: %s\n", sub) doctorUsage() os.Exit(1) } } func doctorUsage() { fmt.Println(`fn doctor — diagnostico read-only del registry y artefactos Usage: fn doctor [subcommand] [--json] Subcommands: (none)|all Corre todos los checks artefacts Salud de apps y analyses (git, venv, app.md, upstream) services Estado de apps con tag 'service' (systemd + puerto) sync Drift entre pc_locations BD y disco uses-functions Audit imports reales vs uses_functions del app.md unused Funciones del registry sin consumidores Flags: --json Salida JSON (para scripting/agentes)`) } func doctorAll(root string, jsonOut bool) { if jsonOut { all := map[string]any{} if v, err := infra.ArtefactDoctor(root); err == nil { all["artefacts"] = v } else { all["artefacts_error"] = err.Error() } if v, err := infra.ServicesStatus(root); err == nil { all["services"] = v } else { all["services_error"] = err.Error() } if v, err := infra.PcLocationsDrift(root, ""); err == nil { all["sync"] = v } else { all["sync_error"] = err.Error() } if v, err := infra.AuditUsesFunctions(root); err == nil { all["uses_functions"] = v } else { all["uses_functions_error"] = err.Error() } if v, err := infra.FindUnusedFunctions(root); err == nil { all["unused"] = v } else { all["unused_error"] = err.Error() } emit(all) return } fmt.Println("=== Artefacts ===") doctorArtefacts(root, false) fmt.Println("\n=== Services ===") doctorServices(root, false) fmt.Println("\n=== Sync (pc_locations drift) ===") doctorSync(root, false) fmt.Println("\n=== uses_functions audit ===") doctorUsesFunctions(root, false) fmt.Println("\n=== Unused functions ===") doctorUnused(root, false) } func doctorArtefacts(root string, jsonOut bool) { checks, err := infra.ArtefactDoctor(root) if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } if jsonOut { emit(checks) return } bad := 0 w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) fmt.Fprintln(w, "STATUS\tTYPE\tID\tISSUES") for _, c := range checks { status := "OK" issues := "-" if !c.OK { status = "FAIL" issues = strings.Join(c.Issues, "; ") bad++ } fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", status, c.Type, c.ID, issues) } w.Flush() fmt.Printf("\n%d/%d artefacts healthy.\n", len(checks)-bad, len(checks)) } func doctorServices(root string, jsonOut bool) { statuses, err := infra.ServicesStatus(root) if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } if jsonOut { emit(statuses) return } if len(statuses) == 0 { fmt.Println("No services registered (no apps with tag 'service').") return } w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) fmt.Fprintln(w, "NAME\tUNIT\tACTIVE\tPORT\tLISTENING") for _, s := range statuses { port := "-" listen := "-" if s.Port > 0 { port = fmt.Sprintf("%d", s.Port) listen = fmt.Sprintf("%v", s.PortListening) } fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", s.Name, s.UnitName, s.UnitActive, port, listen) } w.Flush() } func doctorSync(root string, jsonOut bool) { drifts, err := infra.PcLocationsDrift(root, "") if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } if jsonOut { emit(drifts) return } if len(drifts) == 0 { fmt.Println("No drift detected: pc_locations matches disk.") return } w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) fmt.Fprintln(w, "TYPE\tID\tDIR\tSTATUS\tISSUE") for _, d := range drifts { fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", d.EntityType, d.EntityID, d.DirPath, d.Status, d.Issue) } w.Flush() fmt.Printf("\n%d drift(s) detected.\n", len(drifts)) } func doctorUsesFunctions(root string, jsonOut bool) { audits, err := infra.AuditUsesFunctions(root) if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } if jsonOut { emit(audits) return } bad := 0 for _, a := range audits { if len(a.Missing) == 0 && len(a.Unused) == 0 { continue } bad++ fmt.Printf("\n%s (%s)\n", a.AppID, a.Lang) if len(a.Missing) > 0 { fmt.Printf(" missing in app.md: %s\n", strings.Join(a.Missing, ", ")) } if len(a.Unused) > 0 { fmt.Printf(" declared but unused: %s\n", strings.Join(a.Unused, ", ")) } } if bad == 0 { fmt.Println("All apps have matching uses_functions vs imports.") } else { fmt.Printf("\n%d/%d apps have drift.\n", bad, len(audits)) } } func doctorUnused(root string, jsonOut bool) { unused, err := infra.FindUnusedFunctions(root) if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } if jsonOut { emit(unused) return } if len(unused) == 0 { fmt.Println("No unused functions.") return } w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) fmt.Fprintln(w, "ID\tLANG\tDOMAIN\tAGE_DAYS") for _, u := range unused { fmt.Fprintf(w, "%s\t%s\t%s\t%d\n", u.ID, u.Lang, u.Domain, u.AgeDays) } w.Flush() fmt.Printf("\n%d unused functions (candidates to remove).\n", len(unused)) } func emit(v any) { b, err := json.MarshalIndent(v, "", " ") if err != nil { fmt.Fprintf(os.Stderr, "json error: %v\n", err) os.Exit(1) } fmt.Println(string(b)) }