feat: CLI fn con subcomandos index, search, list, show y add
Implementa el binario CLI definido en docs/architecture.md: - fn index: regenera registry.db parseando todos los .md - fn search: busqueda FTS con filtros -k kind -p purity -l lang -d domain - fn list: lista entradas con filtros opcionales - fn show: muestra entrada completa por ID (busca en functions y types) - fn add: imprime instrucciones para copiar template y registrar Corrige .gitignore para ignorar solo /fn en raiz, no el directorio cmd/fn/. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -4,7 +4,7 @@ registry.db-journal
|
||||
registry.db-wal
|
||||
|
||||
# Binario CLI
|
||||
fn
|
||||
/fn
|
||||
|
||||
# Go
|
||||
*.exe
|
||||
|
||||
+359
@@ -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] <query>
|
||||
fn list [-k kind] [-d domain] [-l lang]
|
||||
fn show <id> 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 <id>")
|
||||
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/<domain>/<name>.md\n $EDITOR functions/<domain>/<name>.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] + "..."
|
||||
}
|
||||
Reference in New Issue
Block a user