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 { diff --git a/fn_operations/docker_tui/build.sh b/fn_operations/docker_tui/build.sh new file mode 100755 index 00000000..e877df85 --- /dev/null +++ b/fn_operations/docker_tui/build.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -euo pipefail + +cd "$(dirname "$0")" + +echo "==> Tidying modules..." +go mod tidy + +echo "==> Building docker-tui..." +mkdir -p build +go build -trimpath -ldflags='-s -w' -o build/docker-tui . + +echo "==> Done: build/docker-tui ($(du -h build/docker-tui | cut -f1))" +echo " Run with: ./build/docker-tui" diff --git a/fn_operations/docker_tui/operations.db b/fn_operations/docker_tui/operations.db index 5196b536..e730dc7b 100644 Binary files a/fn_operations/docker_tui/operations.db and b/fn_operations/docker_tui/operations.db differ diff --git a/fn_operations/operations.go b/fn_operations/operations.go index d52fc9a5..973c3f64 100644 --- a/fn_operations/operations.go +++ b/fn_operations/operations.go @@ -189,6 +189,94 @@ func GetEntityGraph(db *DB) (*Graph, error) { }, nil } +// SnapshotStatus describes the state of a snapshot vs the registry. +type SnapshotStatus string + +const ( + SnapshotUpToDate SnapshotStatus = "up_to_date" + SnapshotOutdated SnapshotStatus = "outdated" + SnapshotMissing SnapshotStatus = "missing" // exists locally but not in registry +) + +// SnapshotCheckResult holds the comparison for one type snapshot. +type SnapshotCheckResult struct { + ID string + LocalVersion string + RegistryVersion string + Status SnapshotStatus +} + +// CheckSnapshots compares all local snapshots against the registry. +func CheckSnapshots(opsDB *DB, registryDB *registry.DB) ([]SnapshotCheckResult, error) { + snaps, err := opsDB.ListTypeSnapshots() + if err != nil { + return nil, fmt.Errorf("listing snapshots: %w", err) + } + + var results []SnapshotCheckResult + for _, snap := range snaps { + regType, err := registryDB.GetType(snap.ID) + if err != nil { + // Not found in registry + results = append(results, SnapshotCheckResult{ + ID: snap.ID, + LocalVersion: snap.Version, + Status: SnapshotMissing, + }) + continue + } + + status := SnapshotUpToDate + if regType.Version != snap.Version { + status = SnapshotOutdated + } + + results = append(results, SnapshotCheckResult{ + ID: snap.ID, + LocalVersion: snap.Version, + RegistryVersion: regType.Version, + Status: status, + }) + } + return results, nil +} + +// UpdateSnapshot re-snapshots a type from the registry, replacing the local copy. +// Returns the old and new definitions for diffing. +func UpdateSnapshot(opsDB *DB, registryDB *registry.DB, typeID string) (old, new_ *TypeSnapshot, err error) { + // Get current local snapshot + oldSnap, err := opsDB.GetTypeSnapshot(typeID) + if err != nil { + return nil, nil, fmt.Errorf("reading local snapshot: %w", err) + } + if oldSnap == nil { + return nil, nil, fmt.Errorf("type %q not found in local snapshots", typeID) + } + + // Get current registry type + regType, err := registryDB.GetType(typeID) + if err != nil { + return nil, nil, fmt.Errorf("fetching type %q from registry: %w", typeID, err) + } + + // Build new snapshot + newSnap := &TypeSnapshot{ + ID: regType.ID, + Version: regType.Version, + Lang: regType.Lang, + Algebraic: string(regType.Algebraic), + Definition: regType.Definition, + Description: regType.Description, + SnappedAt: time.Now().UTC(), + } + + if err := opsDB.UpdateTypeSnapshot(newSnap); err != nil { + return nil, nil, fmt.Errorf("updating snapshot: %w", err) + } + + return oldSnap, newSnap, nil +} + func buildEntitySet(db *DB) (map[string]bool, error) { all, err := db.ListEntities("", "") if err != nil { diff --git a/fn_operations/store.go b/fn_operations/store.go index 52f2389a..ed1f47c3 100644 --- a/fn_operations/store.go +++ b/fn_operations/store.go @@ -58,6 +58,20 @@ func (db *DB) InsertTypeSnapshot(ts *TypeSnapshot) error { return err } +// UpdateTypeSnapshot replaces an existing snapshot with a new version. +func (db *DB) UpdateTypeSnapshot(ts *TypeSnapshot) error { + if ts.SnappedAt.IsZero() { + ts.SnappedAt = time.Now().UTC() + } + _, err := db.conn.Exec(` + UPDATE types_snapshot SET version=?, lang=?, algebraic=?, definition=?, description=?, snapped_at=? + WHERE id=?`, + ts.Version, ts.Lang, ts.Algebraic, ts.Definition, ts.Description, + ts.SnappedAt.Format(time.RFC3339), ts.ID, + ) + return err +} + // GetTypeSnapshot returns a type snapshot by ID. func (db *DB) GetTypeSnapshot(id string) (*TypeSnapshot, error) { row := db.conn.QueryRow("SELECT id, version, lang, algebraic, definition, description, snapped_at FROM types_snapshot WHERE id = ?", id)