From 9ea13870cfd005b001c463b2640dc5881e8061d5 Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Sat, 28 Mar 2026 13:41:35 +0100 Subject: [PATCH] feat: CLI fn ops snapshot check y snapshot update Comando check compara versiones locales vs registry y marca outdated. Comando update re-snapshottea por ID o --all, muestra diff de definition y description. Requiere registry accesible via FN_REGISTRY_ROOT. --- cmd/fn/ops.go | 129 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 127 insertions(+), 2 deletions(-) diff --git a/cmd/fn/ops.go b/cmd/fn/ops.go index 385edbdc..bb57331e 100644 --- a/cmd/fn/ops.go +++ b/cmd/fn/ops.go @@ -56,6 +56,8 @@ Usage: fn ops relation delete Elimina relation fn ops graph Grafo ASCII de entities y relations fn ops snapshot list Lista tipos snapshotted + fn ops snapshot check Compara snapshots vs registry + fn ops snapshot update |--all Re-snapshot desde registry Entity flags: --id --name --type-ref --source @@ -540,11 +542,25 @@ func cmdOpsGraph() { // --- ops snapshot --- func cmdOpsSnapshot(args []string) { - if len(args) < 1 || args[0] != "list" { - fmt.Fprintln(os.Stderr, "usage: fn ops snapshot list") + if len(args) < 1 { + fmt.Fprintln(os.Stderr, "usage: fn ops snapshot [id|--all]") os.Exit(1) } + switch args[0] { + case "list": + cmdOpsSnapshotList() + case "check": + cmdOpsSnapshotCheck() + case "update": + cmdOpsSnapshotUpdate(args[1:]) + default: + fmt.Fprintf(os.Stderr, "unknown snapshot command: %s\n", args[0]) + os.Exit(1) + } +} + +func cmdOpsSnapshotList() { db := openOpsDB() defer db.Close() @@ -568,6 +584,115 @@ func cmdOpsSnapshot(args []string) { w.Flush() } +func cmdOpsSnapshotCheck() { + opsDB := openOpsDB() + defer opsDB.Close() + + regDB := requireRegistryDB() + defer regDB.Close() + + results, err := ops.CheckSnapshots(opsDB, regDB) + if err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } + + if len(results) == 0 { + fmt.Println("No type snapshots to check.") + return + } + + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + fmt.Fprintln(w, "ID\tLOCAL\tREGISTRY\tSTATUS") + for _, r := range results { + regVer := r.RegistryVersion + if regVer == "" { + regVer = "-" + } + status := "✓" + switch r.Status { + case ops.SnapshotOutdated: + status = "← OUTDATED" + case ops.SnapshotMissing: + status = "⚠ not in registry" + } + fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", r.ID, r.LocalVersion, regVer, status) + } + w.Flush() +} + +func cmdOpsSnapshotUpdate(args []string) { + if len(args) < 1 { + fmt.Fprintln(os.Stderr, "usage: fn ops snapshot update | --all") + os.Exit(1) + } + + opsDB := openOpsDB() + defer opsDB.Close() + + regDB := requireRegistryDB() + defer regDB.Close() + + if args[0] == "--all" { + // Update all outdated + results, err := ops.CheckSnapshots(opsDB, regDB) + if err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } + updated := 0 + for _, r := range results { + if r.Status != ops.SnapshotOutdated { + continue + } + old, new_, err := ops.UpdateSnapshot(opsDB, regDB, r.ID) + if err != nil { + fmt.Fprintf(os.Stderr, " error updating %s: %v\n", r.ID, err) + continue + } + printSnapshotDiff(r.ID, old, new_) + updated++ + } + if updated == 0 { + fmt.Println("All snapshots are up to date.") + } else { + fmt.Printf("\n%d snapshot(s) updated.\n", updated) + } + return + } + + // Update single + typeID := args[0] + old, new_, err := ops.UpdateSnapshot(opsDB, regDB, typeID) + if err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } + printSnapshotDiff(typeID, old, new_) +} + +func printSnapshotDiff(id string, old, new_ *ops.TypeSnapshot) { + fmt.Printf("Updated %s: %s → %s\n", id, old.Version, new_.Version) + if old.Definition != new_.Definition { + fmt.Println(" Definition changed:") + fmt.Printf(" - %s\n", strings.ReplaceAll(strings.TrimSpace(old.Definition), "\n", "\n - ")) + fmt.Printf(" + %s\n", strings.ReplaceAll(strings.TrimSpace(new_.Definition), "\n", "\n + ")) + } + if old.Description != new_.Description { + fmt.Printf(" Description: %q → %q\n", old.Description, new_.Description) + } +} + +func requireRegistryDB() *registry.DB { + db := tryOpenRegistryDB() + if db == nil { + fmt.Fprintln(os.Stderr, "error: cannot find registry.db") + fmt.Fprintln(os.Stderr, "Set FN_REGISTRY_ROOT or run from the registry directory.") + os.Exit(1) + } + return db +} + // --- helpers --- func findOpsDB() string {