feat: subcomando project en CLI con busqueda y listado integrado

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.
This commit is contained in:
2026-04-12 17:29:46 +02:00
parent 5992d78941
commit 0cc1acb446
2 changed files with 331 additions and 4 deletions
+84 -4
View File
@@ -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 <subcommand> Gestiona operations.db (fn ops help)
fn proposal <add|list|show|update> Gestiona proposals
fn project <init|list|show|status> Gestiona proyectos
fn app <list|clone|pull> Gestiona apps externas (Gitea)
fn analysis <list|clone|pull> 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)
}
+247
View File
@@ -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 <nombre> Crea scaffold de proyecto
fn project list Lista proyectos del registry
fn project show <id> Muestra proyecto con apps, analysis y vaults
fn project status [<id>] Estado resumido de un proyecto`)
}
func cmdProjectInit(args []string) {
if len(args) < 1 {
fmt.Fprintln(os.Stderr, "usage: fn project init <nombre>")
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 <nombre>'.")
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 <id>")
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()
}