From 0cc1acb446757129739a994fa2ff420d852f8eac Mon Sep 17 00:00:00 2001 From: Egutierrez Date: Sun, 12 Apr 2026 17:29:46 +0200 Subject: [PATCH] feat: subcomando project en CLI con busqueda y listado integrado MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Añade cmd/fn/project.go con subcomandos init, list, show y status para gestionar proyectos desde la CLI. Integra projects en fn search, fn list y fn show para que aparezcan junto a functions, types y apps. También añade soporte para vaults en fn show y template project en fn add -k project. --- cmd/fn/main.go | 88 ++++++++++++++++- cmd/fn/project.go | 247 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 331 insertions(+), 4 deletions(-) create mode 100644 cmd/fn/project.go diff --git a/cmd/fn/main.go b/cmd/fn/main.go index 9ad95e89..84a2db7d 100644 --- a/cmd/fn/main.go +++ b/cmd/fn/main.go @@ -37,6 +37,8 @@ func main() { cmdRun(os.Args[2:]) case "check": cmdCheck(os.Args[2:]) + case "project": + cmdProject(os.Args[2:]) case "app": cmdApp(os.Args[2:]) case "analysis": @@ -63,6 +65,7 @@ Usage: fn check params Lista funciones sin params_schema fn ops Gestiona operations.db (fn ops help) fn proposal Gestiona proposals + fn project Gestiona proyectos fn app Gestiona apps externas (Gitea) fn analysis Gestiona analyses externas (Gitea)`) } @@ -126,7 +129,8 @@ func cmdIndex() { } } - fmt.Printf("Indexed %d functions, %d types, %d apps, %d analysis, %d unit_tests\n", result.Functions, result.Types, result.Apps, result.Analysis, result.UnitTests) + fmt.Printf("Indexed %d functions, %d types, %d apps, %d analysis, %d projects, %d vaults, %d unit_tests\n", + result.Functions, result.Types, result.Apps, result.Analysis, result.Projects, result.Vaults, result.UnitTests) for _, e := range result.ValidationErrors { fmt.Fprintf(os.Stderr, " INVALID: %s\n", e) } @@ -190,7 +194,13 @@ func cmdSearch(args []string) { os.Exit(1) } - if len(fns) == 0 && len(types) == 0 && len(apps) == 0 && len(analyses) == 0 { + projects, err := db.SearchProjects(query) + 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 && len(projects) == 0 { fmt.Println("No results.") return } @@ -233,6 +243,16 @@ func cmdSearch(args []string) { fmt.Fprintf(w, "analysis\t%s\t%s\t%s\n", a.ID, a.Lang, desc) } } + if len(projects) > 0 { + if len(fns) > 0 || len(types) > 0 || len(apps) > 0 || len(analyses) > 0 { + fmt.Fprintln(w) + } + fmt.Fprintln(w, "PROJECT\tID\tDESCRIPTION") + for _, p := range projects { + desc := truncate(p.Description, 60) + fmt.Fprintf(w, "project\t%s\t%s\n", p.ID, desc) + } + } w.Flush() } @@ -317,7 +337,22 @@ func cmdList(args []string) { 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 { + projects, err := db.ListAllProjects() + if err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } + + if len(projects) > 0 { + if len(fns) > 0 || len(types) > 0 || len(apps) > 0 || len(analyses) > 0 { + fmt.Fprintln(w) + } + fmt.Fprintln(w, "PROJECT\tID\tDESCRIPTION") + for _, p := range projects { + fmt.Fprintf(w, "project\t%s\t%s\n", p.ID, truncate(p.Description, 60)) + } + } + if len(fns) == 0 && len(types) == 0 && len(apps) == 0 && len(analyses) == 0 && len(projects) == 0 { fmt.Println("Registry is empty. Run 'fn index' first.") } w.Flush() @@ -359,6 +394,18 @@ func cmdShow(args []string) { return } + p, errP := db.GetProject(id) + if errP == nil { + printProjectEntry(p) + return + } + + v, errV := db.GetVault(id) + if errV == nil { + printVaultEntry(v) + return + } + fmt.Fprintf(os.Stderr, "not found: %s\n", id) os.Exit(1) } @@ -518,6 +565,37 @@ func printAnalysisEntry(a *registry.Analysis) { } } +func printProjectEntry(p *registry.Project) { + fmt.Printf("ID: %s\n", p.ID) + fmt.Printf("Name: %s\n", p.Name) + fmt.Printf("Description: %s\n", p.Description) + fmt.Printf("Tags: %s\n", strings.Join(p.Tags, ", ")) + fmt.Printf("Dir: %s\n", p.DirPath) + if p.RepoURL != "" { + fmt.Printf("Repo URL: %s\n", p.RepoURL) + } + if p.Notes != "" { + fmt.Printf("\nNotes:\n%s\n", p.Notes) + } + if p.Documentation != "" { + fmt.Printf("\nDocumentation:\n%s\n", p.Documentation) + } +} + +func printVaultEntry(v *registry.Vault) { + fmt.Printf("ID: %s\n", v.ID) + fmt.Printf("Name: %s\n", v.Name) + if v.ProjectID != "" { + fmt.Printf("Project: %s\n", v.ProjectID) + } + fmt.Printf("Description: %s\n", v.Description) + if v.Path != "" { + fmt.Printf("Path: %s\n", v.Path) + } + fmt.Printf("Symlink: %v\n", v.Symlink) + fmt.Printf("Tags: %s\n", strings.Join(v.Tags, ", ")) +} + // --- check --- func cmdCheck(args []string) { @@ -594,8 +672,10 @@ func cmdAdd(args []string) { templatePath = filepath.Join(r, "docs", "templates", "app.md") case "analysis": templatePath = filepath.Join(r, "docs", "templates", "analysis.md") + case "project": + templatePath = filepath.Join(r, "docs", "templates", "project.md") default: - fmt.Fprintf(os.Stderr, "unknown kind: %s (use function, pipeline, component, app, or analysis)\n", kind) + fmt.Fprintf(os.Stderr, "unknown kind: %s (use function, pipeline, component, app, analysis, or project)\n", kind) os.Exit(1) } diff --git a/cmd/fn/project.go b/cmd/fn/project.go new file mode 100644 index 00000000..635240bd --- /dev/null +++ b/cmd/fn/project.go @@ -0,0 +1,247 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "text/tabwriter" +) + +func cmdProject(args []string) { + if len(args) < 1 { + printProjectUsage() + os.Exit(1) + } + + switch args[0] { + case "init": + cmdProjectInit(args[1:]) + case "list": + cmdProjectList() + case "show": + cmdProjectShow(args[1:]) + case "status": + cmdProjectStatus(args[1:]) + case "help", "-h", "--help": + printProjectUsage() + default: + fmt.Fprintf(os.Stderr, "unknown project subcommand: %s\n", args[0]) + printProjectUsage() + os.Exit(1) + } +} + +func printProjectUsage() { + fmt.Println(`fn project — manage project workspaces + +Usage: + fn project init Crea scaffold de proyecto + fn project list Lista proyectos del registry + fn project show Muestra proyecto con apps, analysis y vaults + fn project status [] Estado resumido de un proyecto`) +} + +func cmdProjectInit(args []string) { + if len(args) < 1 { + fmt.Fprintln(os.Stderr, "usage: fn project init ") + os.Exit(1) + } + name := args[0] + r := root() + projDir := filepath.Join(r, "projects", name) + + if _, err := os.Stat(projDir); err == nil { + fmt.Fprintf(os.Stderr, "project %q already exists at %s\n", name, projDir) + os.Exit(1) + } + + // Create directory structure + dirs := []string{ + projDir, + filepath.Join(projDir, "apps"), + filepath.Join(projDir, "analysis"), + filepath.Join(projDir, "vaults"), + } + for _, d := range dirs { + if err := os.MkdirAll(d, 0o755); err != nil { + fmt.Fprintf(os.Stderr, "error creating %s: %v\n", d, err) + os.Exit(1) + } + } + + // Create project.md + projectMD := fmt.Sprintf(`--- +name: %s +description: "" +tags: [] +repo_url: "" +--- + +## Notas + +`, name) + if err := os.WriteFile(filepath.Join(projDir, "project.md"), []byte(projectMD), 0o644); err != nil { + fmt.Fprintf(os.Stderr, "error writing project.md: %v\n", err) + os.Exit(1) + } + + // Create vault.yaml + vaultYAML := `vaults: [] +` + if err := os.WriteFile(filepath.Join(projDir, "vaults", "vault.yaml"), []byte(vaultYAML), 0o644); err != nil { + fmt.Fprintf(os.Stderr, "error writing vault.yaml: %v\n", err) + os.Exit(1) + } + + fmt.Printf("Project %q created at %s\n", name, projDir) + fmt.Println("\nStructure:") + fmt.Printf(" %s/\n", filepath.Join("projects", name)) + fmt.Printf(" project.md\n") + fmt.Printf(" apps/\n") + fmt.Printf(" analysis/\n") + fmt.Printf(" vaults/\n") + fmt.Printf(" vault.yaml\n") + fmt.Println("\nNext steps:") + fmt.Printf(" 1. Edit projects/%s/project.md (add description and tags)\n", name) + fmt.Printf(" 2. Create apps or analysis inside the project\n") + fmt.Printf(" 3. Run 'fn index' to register the project\n") +} + +func cmdProjectList() { + db := openDB() + defer db.Close() + + projects, err := db.ListAllProjects() + if err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } + + if len(projects) == 0 { + fmt.Println("No projects found. Create one with 'fn project init '.") + return + } + + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + fmt.Fprintln(w, "ID\tDESCRIPTION\tTAGS") + for _, p := range projects { + desc := truncate(p.Description, 50) + tags := strings.Join(p.Tags, ", ") + fmt.Fprintf(w, "%s\t%s\t%s\n", p.ID, desc, tags) + } + w.Flush() +} + +func cmdProjectShow(args []string) { + if len(args) < 1 { + fmt.Fprintln(os.Stderr, "usage: fn project show ") + os.Exit(1) + } + id := args[0] + + db := openDB() + defer db.Close() + + p, err := db.GetProject(id) + if err != nil { + fmt.Fprintf(os.Stderr, "project not found: %s\n", id) + os.Exit(1) + } + + fmt.Printf("ID: %s\n", p.ID) + fmt.Printf("Name: %s\n", p.Name) + fmt.Printf("Description: %s\n", p.Description) + fmt.Printf("Tags: %s\n", strings.Join(p.Tags, ", ")) + fmt.Printf("Dir: %s\n", p.DirPath) + if p.RepoURL != "" { + fmt.Printf("Repo URL: %s\n", p.RepoURL) + } + if p.Notes != "" { + fmt.Printf("\nNotes:\n%s\n", p.Notes) + } + if p.Documentation != "" { + fmt.Printf("\nDocumentation:\n%s\n", p.Documentation) + } + + // Show project apps + apps, _ := db.GetProjectApps(id) + if len(apps) > 0 { + fmt.Printf("\nApps (%d):\n", len(apps)) + for _, a := range apps { + fmt.Printf(" - %s (%s) — %s\n", a.ID, a.Lang, truncate(a.Description, 60)) + } + } + + // Show project analysis + analyses, _ := db.GetProjectAnalysis(id) + if len(analyses) > 0 { + fmt.Printf("\nAnalysis (%d):\n", len(analyses)) + for _, an := range analyses { + fmt.Printf(" - %s (%s) — %s\n", an.ID, an.Lang, truncate(an.Description, 60)) + } + } + + // Show project vaults + vaults, _ := db.GetProjectVaults(id) + if len(vaults) > 0 { + fmt.Printf("\nVaults (%d):\n", len(vaults)) + for _, v := range vaults { + sym := "" + if v.Symlink { + sym = fmt.Sprintf(" -> %s", v.Path) + } + fmt.Printf(" - %s — %s%s\n", v.Name, v.Description, sym) + } + } +} + +func cmdProjectStatus(args []string) { + db := openDB() + defer db.Close() + + if len(args) > 0 { + // Status for a specific project + id := args[0] + p, err := db.GetProject(id) + if err != nil { + fmt.Fprintf(os.Stderr, "project not found: %s\n", id) + os.Exit(1) + } + + apps, _ := db.GetProjectApps(id) + analyses, _ := db.GetProjectAnalysis(id) + vaults, _ := db.GetProjectVaults(id) + + fmt.Printf("%s — %s\n", p.Name, p.Description) + fmt.Printf(" Apps: %d\n", len(apps)) + fmt.Printf(" Analysis: %d\n", len(analyses)) + fmt.Printf(" Vaults: %d\n", len(vaults)) + fmt.Printf(" Updated: %s\n", p.UpdatedAt.Format("2006-01-02 15:04")) + return + } + + // Status for all projects + projects, err := db.ListAllProjects() + if err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } + + if len(projects) == 0 { + fmt.Println("No projects found.") + return + } + + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + fmt.Fprintln(w, "PROJECT\tAPPS\tANALYSIS\tVAULTS\tUPDATED") + for _, p := range projects { + apps, _ := db.GetProjectApps(p.ID) + analyses, _ := db.GetProjectAnalysis(p.ID) + vaults, _ := db.GetProjectVaults(p.ID) + fmt.Fprintf(w, "%s\t%d\t%d\t%d\t%s\n", + p.Name, len(apps), len(analyses), len(vaults), + p.UpdatedAt.Format("2006-01-02")) + } + w.Flush() +}