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() }