merge: quick/snapshot-check-update — snapshot check/update con diff
This commit is contained in:
+127
-2
@@ -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 {
|
||||
|
||||
Executable
+14
@@ -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.
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user