package main import ( "fmt" "os" "path/filepath" "strings" "text/tabwriter" "fn-registry/registry" ) const dbName = "registry.db" func main() { if len(os.Args) < 2 { printUsage() os.Exit(1) } switch os.Args[1] { case "index": cmdIndex() case "search": cmdSearch(os.Args[2:]) case "list": cmdList(os.Args[2:]) case "show": cmdShow(os.Args[2:]) case "add": cmdAdd(os.Args[2:]) case "ops": cmdOps(os.Args[2:]) case "proposal": cmdProposal(os.Args[2:]) case "run": cmdRun(os.Args[2:]) case "app": cmdApp(os.Args[2:]) case "analysis": cmdAnalysis(os.Args[2:]) case "help", "-h", "--help": printUsage() default: fmt.Fprintf(os.Stderr, "unknown command: %s\n", os.Args[1]) printUsage() os.Exit(1) } } func printUsage() { fmt.Println(`fn — registry CLI Usage: fn index Regenera registry.db desde los .md fn search [-k kind] [-p purity] [-l lang] [-d domain] fn list [-k kind] [-d domain] [-l lang] fn show Muestra entrada completa fn add [-k kind] Abre $EDITOR con template fn run [args...] Ejecuta funcion/pipeline (go/py/bash) fn ops Gestiona operations.db (fn ops help) fn proposal Gestiona proposals fn app Gestiona apps externas (Gitea) fn analysis Gestiona analyses externas (Gitea)`) } func root() string { // Walk up from the binary or cwd to find go.mod dir, _ := os.Getwd() for { if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { return dir } parent := filepath.Dir(dir) if parent == dir { break } dir = parent } // Fallback to cwd cwd, _ := os.Getwd() return cwd } func openDB() *registry.DB { db, err := registry.Open(filepath.Join(root(), dbName)) if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } return db } // --- index --- func cmdIndex() { r := root() db, err := registry.Open(filepath.Join(r, dbName)) if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } defer db.Close() result, err := registry.Index(db, r) if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } // Flush WAL to main db file so external readers (e.g. Metabase) see changes. db.WalCheckpoint() // Sync registry.db to Metabase mount directory if it exists. metabaseCopy := filepath.Join(r, ".metabase-registry", "registry.db") if _, err := os.Stat(filepath.Dir(metabaseCopy)); err == nil { src := filepath.Join(r, dbName) data, err := os.ReadFile(src) if err == nil { if err := os.WriteFile(metabaseCopy, data, 0666); err != nil { fmt.Fprintf(os.Stderr, "warning: could not sync to metabase: %v\n", err) } } } fmt.Printf("Indexed %d functions, %d types, %d apps, %d analysis\n", result.Functions, result.Types, result.Apps, result.Analysis) for _, e := range result.ValidationErrors { fmt.Fprintf(os.Stderr, " INVALID: %s\n", e) } for _, e := range result.Errors { fmt.Fprintf(os.Stderr, " ERROR: %s\n", e) } } // --- search --- func cmdSearch(args []string) { var kind, purity, lang, domain, query string i := 0 for i < len(args) { switch args[i] { case "-k": i++ kind = args[i] case "-p": i++ purity = args[i] case "-l": i++ lang = args[i] case "-d": i++ domain = args[i] default: query = args[i] } i++ } db := openDB() defer db.Close() fns, err := db.SearchFunctions(query, registry.Kind(kind), registry.Purity(purity), lang, domain) if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } types, err := db.SearchTypes(query, lang, domain) if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } apps, err := db.SearchApps(query, lang, domain) if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } analyses, err := db.SearchAnalysis(query, lang, domain) if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } if len(fns) == 0 && len(types) == 0 && len(apps) == 0 && len(analyses) == 0 { fmt.Println("No results.") return } w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) if len(fns) > 0 { fmt.Fprintln(w, "KIND\tID\tPURITY\tDESCRIPTION") for _, f := range fns { desc := truncate(f.Description, 60) fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", f.Kind, f.ID, f.Purity, desc) } } if len(types) > 0 { if len(fns) > 0 { fmt.Fprintln(w) } fmt.Fprintln(w, "ALGEBRAIC\tID\tDESCRIPTION") for _, t := range types { desc := truncate(t.Description, 60) fmt.Fprintf(w, "%s\t%s\t%s\n", t.Algebraic, t.ID, desc) } } if len(apps) > 0 { if len(fns) > 0 || len(types) > 0 { fmt.Fprintln(w) } fmt.Fprintln(w, "APP\tID\tLANG\tDESCRIPTION") for _, a := range apps { desc := truncate(a.Description, 60) fmt.Fprintf(w, "app\t%s\t%s\t%s\n", a.ID, a.Lang, desc) } } if len(analyses) > 0 { if len(fns) > 0 || len(types) > 0 || len(apps) > 0 { fmt.Fprintln(w) } fmt.Fprintln(w, "ANALYSIS\tID\tLANG\tDESCRIPTION") for _, a := range analyses { desc := truncate(a.Description, 60) fmt.Fprintf(w, "analysis\t%s\t%s\t%s\n", a.ID, a.Lang, desc) } } w.Flush() } // --- list --- func cmdList(args []string) { var kind, domain, lang string i := 0 for i < len(args) { switch args[i] { case "-k": i++ kind = args[i] case "-d": i++ domain = args[i] case "-l": i++ lang = args[i] } i++ } db := openDB() defer db.Close() fns, err := db.SearchFunctions("", registry.Kind(kind), "", lang, domain) if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } types, err := db.SearchTypes("", lang, domain) if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } apps, err := db.SearchApps("", lang, domain) if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } analyses, err := db.SearchAnalysis("", lang, domain) if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) os.Exit(1) } w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) if len(fns) > 0 { fmt.Fprintln(w, "KIND\tID\tPURITY\tVERSION\tDOMAIN") for _, f := range fns { fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", f.Kind, f.ID, f.Purity, f.Version, f.Domain) } } if len(types) > 0 { if len(fns) > 0 { fmt.Fprintln(w) } fmt.Fprintln(w, "ALGEBRAIC\tID\tVERSION\tDOMAIN") for _, t := range types { fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", t.Algebraic, t.ID, t.Version, t.Domain) } } if len(apps) > 0 { if len(fns) > 0 || len(types) > 0 { fmt.Fprintln(w) } fmt.Fprintln(w, "APP\tID\tLANG\tDOMAIN") for _, a := range apps { fmt.Fprintf(w, "app\t%s\t%s\t%s\n", a.ID, a.Lang, a.Domain) } } if len(analyses) > 0 { if len(fns) > 0 || len(types) > 0 || len(apps) > 0 { fmt.Fprintln(w) } fmt.Fprintln(w, "ANALYSIS\tID\tLANG\tDOMAIN") for _, a := range analyses { fmt.Fprintf(w, "analysis\t%s\t%s\t%s\n", a.ID, a.Lang, a.Domain) } } if len(fns) == 0 && len(types) == 0 && len(apps) == 0 && len(analyses) == 0 { fmt.Println("Registry is empty. Run 'fn index' first.") } w.Flush() } // --- show --- func cmdShow(args []string) { if len(args) < 1 { fmt.Fprintln(os.Stderr, "usage: fn show ") os.Exit(1) } id := args[0] db := openDB() defer db.Close() f, errF := db.GetFunction(id) if errF == nil { printFunction(f) return } t, errT := db.GetType(id) if errT == nil { printType(t) return } a, errA := db.GetApp(id) if errA == nil { printApp(a) return } an, errAn := db.GetAnalysis(id) if errAn == nil { printAnalysisEntry(an) return } fmt.Fprintf(os.Stderr, "not found: %s\n", id) os.Exit(1) } func printFunction(f *registry.Function) { fmt.Printf("ID: %s\n", f.ID) fmt.Printf("Name: %s\n", f.Name) fmt.Printf("Kind: %s\n", f.Kind) fmt.Printf("Lang: %s\n", f.Lang) fmt.Printf("Domain: %s\n", f.Domain) fmt.Printf("Version: %s\n", f.Version) fmt.Printf("Purity: %s\n", f.Purity) fmt.Printf("Signature: %s\n", f.Signature) fmt.Printf("Description: %s\n", f.Description) fmt.Printf("Tags: %s\n", strings.Join(f.Tags, ", ")) fmt.Printf("File: %s\n", f.FilePath) if len(f.UsesFunctions) > 0 { fmt.Printf("Uses fns: %s\n", strings.Join(f.UsesFunctions, ", ")) } if len(f.UsesTypes) > 0 { fmt.Printf("Uses types: %s\n", strings.Join(f.UsesTypes, ", ")) } if len(f.Returns) > 0 { fmt.Printf("Returns: %s\n", strings.Join(f.Returns, ", ")) } if f.ErrorType != "" { fmt.Printf("Error type: %s\n", f.ErrorType) } if len(f.Imports) > 0 { fmt.Printf("Imports: %s\n", strings.Join(f.Imports, ", ")) } if f.Example != "" { fmt.Printf("\nExample:\n%s\n", f.Example) } if f.Notes != "" { fmt.Printf("\nNotes:\n%s\n", f.Notes) } if f.Documentation != "" { fmt.Printf("\nDocumentation:\n%s\n", f.Documentation) } if f.Code != "" { fmt.Printf("\nCode:\n%s\n", f.Code) } if f.SourceRepo != "" { fmt.Printf("Source repo: %s\n", f.SourceRepo) fmt.Printf("Source license: %s\n", f.SourceLicense) fmt.Printf("Source file: %s\n", f.SourceFile) } if f.Kind == registry.KindComponent { fmt.Printf("Framework: %s\n", f.Framework) if f.HasState != nil { fmt.Printf("Has state: %v\n", *f.HasState) } if len(f.Emits) > 0 { fmt.Printf("Emits: %s\n", strings.Join(f.Emits, ", ")) } } } func printType(t *registry.Type) { fmt.Printf("ID: %s\n", t.ID) fmt.Printf("Name: %s\n", t.Name) fmt.Printf("Lang: %s\n", t.Lang) fmt.Printf("Domain: %s\n", t.Domain) fmt.Printf("Version: %s\n", t.Version) fmt.Printf("Algebraic: %s\n", t.Algebraic) fmt.Printf("Description: %s\n", t.Description) fmt.Printf("Tags: %s\n", strings.Join(t.Tags, ", ")) fmt.Printf("File: %s\n", t.FilePath) if len(t.UsesTypes) > 0 { fmt.Printf("Uses types: %s\n", strings.Join(t.UsesTypes, ", ")) } if t.SourceRepo != "" { fmt.Printf("Source repo: %s\n", t.SourceRepo) fmt.Printf("Source license: %s\n", t.SourceLicense) fmt.Printf("Source file: %s\n", t.SourceFile) } if t.Definition != "" { fmt.Printf("\nDefinition:\n%s\n", t.Definition) } if t.Examples != "" { fmt.Printf("\nExamples:\n%s\n", t.Examples) } if t.Notes != "" { fmt.Printf("\nNotes:\n%s\n", t.Notes) } if t.Documentation != "" { fmt.Printf("\nDocumentation:\n%s\n", t.Documentation) } if t.Code != "" { fmt.Printf("\nCode:\n%s\n", t.Code) } } func printApp(a *registry.App) { fmt.Printf("ID: %s\n", a.ID) fmt.Printf("Name: %s\n", a.Name) fmt.Printf("Lang: %s\n", a.Lang) fmt.Printf("Domain: %s\n", a.Domain) fmt.Printf("Description: %s\n", a.Description) fmt.Printf("Tags: %s\n", strings.Join(a.Tags, ", ")) fmt.Printf("Dir: %s\n", a.DirPath) if a.RepoURL != "" { fmt.Printf("Repo URL: %s\n", a.RepoURL) } if a.Framework != "" { fmt.Printf("Framework: %s\n", a.Framework) } if a.EntryPoint != "" { fmt.Printf("Entry point: %s\n", a.EntryPoint) } if len(a.UsesFunctions) > 0 { fmt.Printf("Uses fns: %s\n", strings.Join(a.UsesFunctions, ", ")) } if len(a.UsesTypes) > 0 { fmt.Printf("Uses types: %s\n", strings.Join(a.UsesTypes, ", ")) } if a.Notes != "" { fmt.Printf("\nNotes:\n%s\n", a.Notes) } if a.Documentation != "" { fmt.Printf("\nDocumentation:\n%s\n", a.Documentation) } } func printAnalysisEntry(a *registry.Analysis) { fmt.Printf("ID: %s\n", a.ID) fmt.Printf("Name: %s\n", a.Name) fmt.Printf("Lang: %s\n", a.Lang) fmt.Printf("Domain: %s\n", a.Domain) fmt.Printf("Description: %s\n", a.Description) fmt.Printf("Tags: %s\n", strings.Join(a.Tags, ", ")) fmt.Printf("Dir: %s\n", a.DirPath) if a.RepoURL != "" { fmt.Printf("Repo URL: %s\n", a.RepoURL) } if a.Framework != "" { fmt.Printf("Framework: %s\n", a.Framework) } if a.EntryPoint != "" { fmt.Printf("Entry point: %s\n", a.EntryPoint) } if len(a.UsesFunctions) > 0 { fmt.Printf("Uses fns: %s\n", strings.Join(a.UsesFunctions, ", ")) } if len(a.UsesTypes) > 0 { fmt.Printf("Uses types: %s\n", strings.Join(a.UsesTypes, ", ")) } if a.Notes != "" { fmt.Printf("\nNotes:\n%s\n", a.Notes) } if a.Documentation != "" { fmt.Printf("\nDocumentation:\n%s\n", a.Documentation) } } // --- add --- func cmdAdd(args []string) { kind := "function" for i := 0; i < len(args); i++ { if args[i] == "-k" { i++ kind = args[i] } } editor := os.Getenv("EDITOR") if editor == "" { editor = "vi" } r := root() var templatePath string switch kind { case "function": templatePath = filepath.Join(r, "docs", "templates", "function.md") case "pipeline": templatePath = filepath.Join(r, "docs", "templates", "pipeline.md") case "component": templatePath = filepath.Join(r, "docs", "templates", "component.md") case "app": templatePath = filepath.Join(r, "docs", "templates", "app.md") case "analysis": templatePath = filepath.Join(r, "docs", "templates", "analysis.md") default: fmt.Fprintf(os.Stderr, "unknown kind: %s (use function, pipeline, component, app, or analysis)\n", kind) os.Exit(1) } if _, err := os.Stat(templatePath); os.IsNotExist(err) { fmt.Fprintf(os.Stderr, "template not found: %s\n", templatePath) os.Exit(1) } fmt.Printf("Template: %s\n", templatePath) fmt.Printf("Copy and edit the template, then run 'fn index' to register it.\n") fmt.Printf("\n cp %s functions//.md\n $EDITOR functions//.md\n fn index\n", templatePath) } // --- add type --- func truncate(s string, max int) string { if len(s) <= max { return s } return s[:max-3] + "..." }