diff --git a/.gitignore b/.gitignore index f9c03ed3..35b6654d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ registry.db-journal registry.db-wal # Binario CLI -fn +/fn # Go *.exe diff --git a/cmd/fn/main.go b/cmd/fn/main.go new file mode 100644 index 00000000..74f836ea --- /dev/null +++ b/cmd/fn/main.go @@ -0,0 +1,359 @@ +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 "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`) +} + +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) + } + + fmt.Printf("Indexed %d functions, %d types\n", result.Functions, result.Types) + for _, e := range result.Errors { + fmt.Fprintf(os.Stderr, " warn: %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) + } + + if len(fns) == 0 && len(types) == 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) + } + } + 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) + } + + 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(fns) == 0 && len(types) == 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 + } + + 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.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.Definition != "" { + fmt.Printf("\nDefinition:\n%s\n", t.Definition) + } +} + +// --- 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") + default: + fmt.Fprintf(os.Stderr, "unknown kind: %s (use function, pipeline, or component)\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] + "..." +}