package main import ( "encoding/json" "fmt" "io" "os" "path/filepath" "strings" "text/tabwriter" ops "fn-registry/fn_operations" "fn-registry/registry" ) const opsDBName = "operations.db" func cmdOps(args []string) { if len(args) < 1 { printOpsUsage() os.Exit(1) } switch args[0] { case "init": cmdOpsInit(args[1:]) case "entity": cmdOpsEntity(args[1:]) case "relation": cmdOpsRelation(args[1:]) case "graph": cmdOpsGraph() case "snapshot": cmdOpsSnapshot(args[1:]) case "help", "-h", "--help": printOpsUsage() default: fmt.Fprintf(os.Stderr, "unknown ops command: %s\n", args[0]) printOpsUsage() os.Exit(1) } } func printOpsUsage() { fmt.Println(`fn ops — operations CLI Usage: fn ops init [path] Crea operations.db en path (default: .) fn ops entity add Añade entity fn ops entity list [-d domain] [-s status] Lista entities fn ops entity show Muestra entity fn ops entity delete Elimina entity fn ops relation add Añade relation fn ops relation list [entity_id] Lista relations fn ops relation show Muestra relation fn ops relation delete Elimina relation fn ops graph Grafo ASCII de entities y relations fn ops snapshot list Lista tipos snapshotted Entity flags: --id --name --type-ref --source --domain --status --description --tags --metadata --notes Relation flags: --id --name --from --to --via --direction --status --purity --weight <0.0-1.0> --description --tags --notes `) } // --- ops init --- func cmdOpsInit(args []string) { dir := "." if len(args) > 0 { dir = args[0] } path := filepath.Join(dir, opsDBName) if _, err := os.Stat(path); err == nil { fmt.Fprintf(os.Stderr, "operations.db already exists at %s\n", path) os.Exit(1) } // Copy from template if available, otherwise create fresh templatePath := filepath.Join(root(), "fn_operations", "project_template", "operations.db") if _, err := os.Stat(templatePath); err == nil { src, err := os.Open(templatePath) if err != nil { fmt.Fprintf(os.Stderr, "error opening template: %v\n", err) os.Exit(1) } defer src.Close() if err := os.MkdirAll(dir, 0o755); err != nil { fmt.Fprintf(os.Stderr, "error creating directory: %v\n", err) os.Exit(1) } dst, err := os.Create(path) if err != nil { fmt.Fprintf(os.Stderr, "error creating db: %v\n", err) os.Exit(1) } defer dst.Close() if _, err := io.Copy(dst, src); err != nil { fmt.Fprintf(os.Stderr, "error copying template: %v\n", err) os.Exit(1) } fmt.Printf("operations.db created at %s (from template)\n", path) return } // Create fresh db, err := ops.Open(path) if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } db.Close() fmt.Printf("operations.db created at %s\n", path) } // --- ops entity --- func cmdOpsEntity(args []string) { if len(args) < 1 { fmt.Fprintln(os.Stderr, "usage: fn ops entity ...") os.Exit(1) } switch args[0] { case "add": cmdOpsEntityAdd(args[1:]) case "list": cmdOpsEntityList(args[1:]) case "show": cmdOpsEntityShow(args[1:]) case "delete": cmdOpsEntityDelete(args[1:]) default: fmt.Fprintf(os.Stderr, "unknown entity command: %s\n", args[0]) os.Exit(1) } } func cmdOpsEntityAdd(args []string) { var e ops.Entity e.Status = ops.StatusActive var tagsStr, metadataStr string for i := 0; i < len(args); i++ { switch args[i] { case "--id": i++; e.ID = args[i] case "--name": i++; e.Name = args[i] case "--type-ref": i++; e.TypeRef = args[i] case "--source": i++; e.Source = args[i] case "--domain": i++; e.Domain = args[i] case "--status": i++; e.Status = ops.EntityStatus(args[i]) case "--description": i++; e.Description = args[i] case "--tags": i++; tagsStr = args[i] case "--metadata": i++; metadataStr = args[i] case "--notes": i++; e.Notes = args[i] } } if e.Name == "" || e.TypeRef == "" || e.Source == "" { fmt.Fprintln(os.Stderr, "required: --name, --type-ref, --source") os.Exit(1) } if e.ID == "" { e.ID = e.Name } if tagsStr != "" { e.Tags = strings.Split(tagsStr, ",") } if metadataStr != "" { json.Unmarshal([]byte(metadataStr), &e.Metadata) } opsDB := openOpsDB() defer opsDB.Close() // Try to open registry for type snapshot regDB := tryOpenRegistryDB() if regDB != nil { defer regDB.Close() } if err := ops.InsertEntityWithSnapshot(opsDB, regDB, &e); err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } fmt.Printf("Entity %s added\n", e.ID) } func cmdOpsEntityList(args []string) { var domain string var status ops.EntityStatus for i := 0; i < len(args); i++ { switch args[i] { case "-d": i++; domain = args[i] case "-s": i++; status = ops.EntityStatus(args[i]) } } db := openOpsDB() defer db.Close() entities, err := db.ListEntities(domain, status) if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } if len(entities) == 0 { fmt.Println("No entities.") return } w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) fmt.Fprintln(w, "ID\tTYPE_REF\tSTATUS\tSOURCE\tDOMAIN") for _, e := range entities { fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", e.ID, e.TypeRef, e.Status, e.Source, e.Domain) } w.Flush() } func cmdOpsEntityShow(args []string) { if len(args) < 1 { fmt.Fprintln(os.Stderr, "usage: fn ops entity show ") os.Exit(1) } db := openOpsDB() defer db.Close() e, err := db.GetEntity(args[0]) if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } if e == nil { fmt.Fprintf(os.Stderr, "entity not found: %s\n", args[0]) os.Exit(1) } fmt.Printf("ID: %s\n", e.ID) fmt.Printf("Name: %s\n", e.Name) fmt.Printf("Type ref: %s\n", e.TypeRef) fmt.Printf("Status: %s\n", e.Status) fmt.Printf("Source: %s\n", e.Source) fmt.Printf("Domain: %s\n", e.Domain) fmt.Printf("Description: %s\n", e.Description) fmt.Printf("Tags: %s\n", strings.Join(e.Tags, ", ")) if len(e.Metadata) > 0 { meta, _ := json.MarshalIndent(e.Metadata, " ", " ") fmt.Printf("Metadata: %s\n", meta) } if e.Notes != "" { fmt.Printf("Notes: %s\n", e.Notes) } fmt.Printf("Created: %s\n", e.CreatedAt.Format("2006-01-02 15:04:05")) fmt.Printf("Updated: %s\n", e.UpdatedAt.Format("2006-01-02 15:04:05")) } func cmdOpsEntityDelete(args []string) { if len(args) < 1 { fmt.Fprintln(os.Stderr, "usage: fn ops entity delete ") os.Exit(1) } db := openOpsDB() defer db.Close() if err := db.DeleteEntity(args[0]); err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } fmt.Printf("Entity %s deleted\n", args[0]) } // --- ops relation --- func cmdOpsRelation(args []string) { if len(args) < 1 { fmt.Fprintln(os.Stderr, "usage: fn ops relation ...") os.Exit(1) } switch args[0] { case "add": cmdOpsRelationAdd(args[1:]) case "list": cmdOpsRelationList(args[1:]) case "show": cmdOpsRelationShow(args[1:]) case "delete": cmdOpsRelationDelete(args[1:]) default: fmt.Fprintf(os.Stderr, "unknown relation command: %s\n", args[0]) os.Exit(1) } } func cmdOpsRelationAdd(args []string) { var r ops.Relation r.Direction = ops.DirUnidirectional r.Status = ops.RelDesigned var tagsStr string for i := 0; i < len(args); i++ { switch args[i] { case "--id": i++; r.ID = args[i] case "--name": i++; r.Name = args[i] case "--from": i++; r.FromEntity = args[i] case "--to": i++; r.ToEntity = args[i] case "--via": i++; r.Via = args[i] case "--direction": i++; r.Direction = ops.Direction(args[i]) case "--status": i++; r.Status = ops.RelationStatus(args[i]) case "--purity": i++; r.Purity = args[i] case "--weight": i++ var w float64 fmt.Sscanf(args[i], "%f", &w) r.Weight = &w case "--description": i++; r.Description = args[i] case "--tags": i++; tagsStr = args[i] case "--notes": i++; r.Notes = args[i] } } if r.Name == "" || r.ToEntity == "" { fmt.Fprintln(os.Stderr, "required: --name, --to (and --from for simple relations)") os.Exit(1) } if r.ID == "" && r.FromEntity != "" { via := "semantic" if r.Via != "" { via = r.Via } r.ID = fmt.Sprintf("%s__to__%s__via__%s", r.FromEntity, r.ToEntity, via) } if tagsStr != "" { r.Tags = strings.Split(tagsStr, ",") } db := openOpsDB() defer db.Close() if err := ops.InsertRelationSafe(db, &r); err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } fmt.Printf("Relation %s added\n", r.ID) } func cmdOpsRelationList(args []string) { var entityID string if len(args) > 0 { entityID = args[0] } db := openOpsDB() defer db.Close() rels, err := db.ListRelations(entityID) if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } if len(rels) == 0 { fmt.Println("No relations.") return } w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) fmt.Fprintln(w, "ID\tNAME\tFROM\tTO\tVIA\tDIRECTION\tSTATUS") for _, r := range rels { from := r.FromEntity if from == "" { from = "(inputs)" } via := r.Via if via == "" { via = "-" } fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", r.ID, r.Name, from, r.ToEntity, via, r.Direction, r.Status) } w.Flush() } func cmdOpsRelationShow(args []string) { if len(args) < 1 { fmt.Fprintln(os.Stderr, "usage: fn ops relation show ") os.Exit(1) } db := openOpsDB() defer db.Close() r, err := db.GetRelation(args[0]) if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } if r == nil { fmt.Fprintf(os.Stderr, "relation not found: %s\n", args[0]) os.Exit(1) } fmt.Printf("ID: %s\n", r.ID) fmt.Printf("Name: %s\n", r.Name) fmt.Printf("From: %s\n", r.FromEntity) fmt.Printf("To: %s\n", r.ToEntity) fmt.Printf("Via: %s\n", r.Via) fmt.Printf("Description: %s\n", r.Description) fmt.Printf("Purity: %s\n", r.Purity) fmt.Printf("Direction: %s\n", r.Direction) if r.Weight != nil { fmt.Printf("Weight: %.2f\n", *r.Weight) } fmt.Printf("Status: %s\n", r.Status) fmt.Printf("Tags: %s\n", strings.Join(r.Tags, ", ")) if r.Notes != "" { fmt.Printf("Notes: %s\n", r.Notes) } // Show inputs if any inputs, _ := db.GetRelationInputs(r.ID) if len(inputs) > 0 { fmt.Println("\nInputs:") for _, ri := range inputs { ord := "" if ri.Order != nil { ord = fmt.Sprintf(" (order: %d)", *ri.Order) } fmt.Printf(" %s [%s]%s\n", ri.EntityID, ri.Role, ord) } } } func cmdOpsRelationDelete(args []string) { if len(args) < 1 { fmt.Fprintln(os.Stderr, "usage: fn ops relation delete ") os.Exit(1) } db := openOpsDB() defer db.Close() if err := db.DeleteRelation(args[0]); err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } fmt.Printf("Relation %s deleted\n", args[0]) } // --- ops graph --- func cmdOpsGraph() { db := openOpsDB() defer db.Close() g, err := ops.GetEntityGraph(db) if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } if len(g.Entities) == 0 { fmt.Println("Empty graph.") return } fmt.Println("Entities:") for _, e := range g.Entities { fmt.Printf(" [%s] (%s) status:%s source:%s\n", e.ID, e.TypeRef, e.Status, e.Source) } if len(g.Relations) > 0 { fmt.Println("\nRelations:") for _, r := range g.Relations { via := "" if r.Via != "" { via = fmt.Sprintf(" via:%s", r.Via) } inputs, hasInputs := g.Inputs[r.ID] if hasInputs { sources := make([]string, len(inputs)) for i, ri := range inputs { sources[i] = fmt.Sprintf("%s[%s]", ri.EntityID, ri.Role) } fmt.Printf(" (%s) %s → %s%s\n", strings.Join(sources, " + "), r.Name, r.ToEntity, via) } else { from := r.FromEntity if from == "" { from = "?" } dir := "→" if r.Direction == ops.DirBidirectional { dir = "↔" } fmt.Printf(" %s %s %s %s%s\n", from, dir, r.Name, r.ToEntity, via) } } } } // --- ops snapshot --- func cmdOpsSnapshot(args []string) { if len(args) < 1 || args[0] != "list" { fmt.Fprintln(os.Stderr, "usage: fn ops snapshot list") os.Exit(1) } db := openOpsDB() defer db.Close() snaps, err := db.ListTypeSnapshots() if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } if len(snaps) == 0 { fmt.Println("No type snapshots.") return } w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) fmt.Fprintln(w, "ID\tVERSION\tLANG\tALGEBRAIC\tSNAPPED_AT") for _, s := range snaps { fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", s.ID, s.Version, s.Lang, s.Algebraic, s.SnappedAt.Format("2006-01-02 15:04")) } w.Flush() } // --- helpers --- func findOpsDB() string { dir, _ := os.Getwd() for { path := filepath.Join(dir, opsDBName) if _, err := os.Stat(path); err == nil { return path } parent := filepath.Dir(dir) if parent == dir { break } dir = parent } return filepath.Join(".", opsDBName) } func openOpsDB() *ops.DB { path := findOpsDB() db, err := ops.Open(path) if err != nil { fmt.Fprintf(os.Stderr, "error opening operations.db: %v\n", err) fmt.Fprintln(os.Stderr, "Run 'fn ops init' first to create one.") os.Exit(1) } return db } func tryOpenRegistryDB() *registry.DB { // Try FN_REGISTRY_ROOT env var first if envRoot := os.Getenv("FN_REGISTRY_ROOT"); envRoot != "" { path := filepath.Join(envRoot, dbName) if _, err := os.Stat(path); err == nil { db, err := registry.Open(path) if err == nil { return db } } } // Try root() (finds go.mod walking up from cwd) path := filepath.Join(root(), dbName) if _, err := os.Stat(path); err == nil { db, err := registry.Open(path) if err == nil { return db } } // Try executable's directory if exe, err := os.Executable(); err == nil { exeDir := filepath.Dir(exe) path := filepath.Join(exeDir, dbName) if _, err := os.Stat(path); err == nil { db, err := registry.Open(path) if err == nil { return db } } } return nil }