merge: quick/snapshot-check-update — snapshot check/update con diff

This commit is contained in:
2026-03-28 13:41:39 +01:00
5 changed files with 243 additions and 2 deletions
+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 {
+14
View File
@@ -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"
Binary file not shown.
+88
View File
@@ -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 {
+14
View File
@@ -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)