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 relation delete <id> Elimina relation
|
||||||
fn ops graph Grafo ASCII de entities y relations
|
fn ops graph Grafo ASCII de entities y relations
|
||||||
fn ops snapshot list Lista tipos snapshotted
|
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:
|
Entity flags:
|
||||||
--id <id> --name <name> --type-ref <type_id> --source <source>
|
--id <id> --name <name> --type-ref <type_id> --source <source>
|
||||||
@@ -540,11 +542,25 @@ func cmdOpsGraph() {
|
|||||||
// --- ops snapshot ---
|
// --- ops snapshot ---
|
||||||
|
|
||||||
func cmdOpsSnapshot(args []string) {
|
func cmdOpsSnapshot(args []string) {
|
||||||
if len(args) < 1 || args[0] != "list" {
|
if len(args) < 1 {
|
||||||
fmt.Fprintln(os.Stderr, "usage: fn ops snapshot list")
|
fmt.Fprintln(os.Stderr, "usage: fn ops snapshot <list|check|update> [id|--all]")
|
||||||
os.Exit(1)
|
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()
|
db := openOpsDB()
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
|
|
||||||
@@ -568,6 +584,115 @@ func cmdOpsSnapshot(args []string) {
|
|||||||
w.Flush()
|
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 ---
|
// --- helpers ---
|
||||||
|
|
||||||
func findOpsDB() string {
|
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
|
}, 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) {
|
func buildEntitySet(db *DB) (map[string]bool, error) {
|
||||||
all, err := db.ListEntities("", "")
|
all, err := db.ListEntities("", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -58,6 +58,20 @@ func (db *DB) InsertTypeSnapshot(ts *TypeSnapshot) error {
|
|||||||
return err
|
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.
|
// GetTypeSnapshot returns a type snapshot by ID.
|
||||||
func (db *DB) GetTypeSnapshot(id string) (*TypeSnapshot, error) {
|
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)
|
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