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.
This commit is contained in:
2026-03-28 13:41:35 +01:00
parent 0095de2ce7
commit 9ea13870cf
+127 -2
View File
@@ -56,6 +56,8 @@ Usage:
fn ops relation delete <id> 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 <id>|--all Re-snapshot desde registry
Entity flags:
--id <id> --name <name> --type-ref <type_id> --source <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 <list|check|update> [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 <type_id> | --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 {