Files
fn_registry/cmd/fn/doctor.go
T

291 lines
6.7 KiB
Go

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)
case "cpp-apps":
doctorCppApps(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
cpp-apps Conformidad de apps C++ con cpp/PATTERNS.md (cfg.about, dockspace, menubar)
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()
}
if v, err := infra.AuditCppApps(root); err == nil {
all["cpp_apps"] = v
} else {
all["cpp_apps_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)
fmt.Println("\n=== C++ apps standard conformance ===")
doctorCppApps(root, false)
}
func doctorCppApps(root string, jsonOut bool) {
audits, err := infra.AuditCppApps(root)
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
if jsonOut {
emit(audits)
return
}
bad := 0
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
fmt.Fprintln(w, "STATUS\tAPP\tISSUES")
for _, a := range audits {
status := "OK"
issues := "-"
if !a.OK {
status = "FAIL"
issues = strings.Join(a.Issues, "; ")
bad++
}
fmt.Fprintf(w, "%s\t%s\t%s\n", status, a.AppID, issues)
}
w.Flush()
fmt.Printf("\n%d/%d C++ apps conform.\n", len(audits)-bad, len(audits))
}
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))
}