feat: externalize apps/analysis to Gitea repos, add analysis table

- Migration 007: repo_url on apps table + analysis table with FTS5
- Analysis struct, parser, CRUD, validation, hash computation
- Selective purge: remote-only apps/analysis preserved across fn index
- CLI: fn app list/clone/pull, fn analysis list/clone/pull
- search/show/list now include analysis results
- Apps removed from git tracking (content lives in Gitea repos)
- .gitkeep for apps/ and analysis/ dirs
- Bash functions: jupyter analysis pipeline, shell utilities
- Browser domain: CDP functions moved from infra to browser

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-01 04:23:51 +02:00
parent 722f29b71b
commit bf1efb2099
111 changed files with 2766 additions and 5043 deletions
+84 -5
View File
@@ -35,6 +35,10 @@ func main() {
cmdProposal(os.Args[2:])
case "run":
cmdRun(os.Args[2:])
case "app":
cmdApp(os.Args[2:])
case "analysis":
cmdAnalysis(os.Args[2:])
case "help", "-h", "--help":
printUsage()
default:
@@ -55,7 +59,9 @@ Usage:
fn add [-k kind] Abre $EDITOR con template
fn run <id_or_name> [args...] Ejecuta funcion/pipeline (go/py/bash)
fn ops <subcommand> Gestiona operations.db (fn ops help)
fn proposal <add|list|show|update> Gestiona proposals`)
fn proposal <add|list|show|update> Gestiona proposals
fn app <list|clone|pull> Gestiona apps externas (Gitea)
fn analysis <list|clone|pull> Gestiona analyses externas (Gitea)`)
}
func root() string {
@@ -117,7 +123,7 @@ func cmdIndex() {
}
}
fmt.Printf("Indexed %d functions, %d types, %d apps\n", result.Functions, result.Types, result.Apps)
fmt.Printf("Indexed %d functions, %d types, %d apps, %d analysis\n", result.Functions, result.Types, result.Apps, result.Analysis)
for _, e := range result.ValidationErrors {
fmt.Fprintf(os.Stderr, " INVALID: %s\n", e)
}
@@ -172,7 +178,13 @@ func cmdSearch(args []string) {
os.Exit(1)
}
if len(fns) == 0 && len(types) == 0 && len(apps) == 0 {
analyses, err := db.SearchAnalysis(query, lang, domain)
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 {
fmt.Println("No results.")
return
}
@@ -205,6 +217,16 @@ func cmdSearch(args []string) {
fmt.Fprintf(w, "app\t%s\t%s\t%s\n", a.ID, a.Lang, desc)
}
}
if len(analyses) > 0 {
if len(fns) > 0 || len(types) > 0 || len(apps) > 0 {
fmt.Fprintln(w)
}
fmt.Fprintln(w, "ANALYSIS\tID\tLANG\tDESCRIPTION")
for _, a := range analyses {
desc := truncate(a.Description, 60)
fmt.Fprintf(w, "analysis\t%s\t%s\t%s\n", a.ID, a.Lang, desc)
}
}
w.Flush()
}
@@ -249,6 +271,12 @@ func cmdList(args []string) {
os.Exit(1)
}
analyses, err := db.SearchAnalysis("", 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")
@@ -274,7 +302,16 @@ func cmdList(args []string) {
fmt.Fprintf(w, "app\t%s\t%s\t%s\n", a.ID, a.Lang, a.Domain)
}
}
if len(fns) == 0 && len(types) == 0 && len(apps) == 0 {
if len(analyses) > 0 {
if len(fns) > 0 || len(types) > 0 || len(apps) > 0 {
fmt.Fprintln(w)
}
fmt.Fprintln(w, "ANALYSIS\tID\tLANG\tDOMAIN")
for _, a := range analyses {
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 {
fmt.Println("Registry is empty. Run 'fn index' first.")
}
w.Flush()
@@ -310,6 +347,12 @@ func cmdShow(args []string) {
return
}
an, errAn := db.GetAnalysis(id)
if errAn == nil {
printAnalysisEntry(an)
return
}
fmt.Fprintf(os.Stderr, "not found: %s\n", id)
os.Exit(1)
}
@@ -412,6 +455,40 @@ func printApp(a *registry.App) {
fmt.Printf("Description: %s\n", a.Description)
fmt.Printf("Tags: %s\n", strings.Join(a.Tags, ", "))
fmt.Printf("Dir: %s\n", a.DirPath)
if a.RepoURL != "" {
fmt.Printf("Repo URL: %s\n", a.RepoURL)
}
if a.Framework != "" {
fmt.Printf("Framework: %s\n", a.Framework)
}
if a.EntryPoint != "" {
fmt.Printf("Entry point: %s\n", a.EntryPoint)
}
if len(a.UsesFunctions) > 0 {
fmt.Printf("Uses fns: %s\n", strings.Join(a.UsesFunctions, ", "))
}
if len(a.UsesTypes) > 0 {
fmt.Printf("Uses types: %s\n", strings.Join(a.UsesTypes, ", "))
}
if a.Notes != "" {
fmt.Printf("\nNotes:\n%s\n", a.Notes)
}
if a.Documentation != "" {
fmt.Printf("\nDocumentation:\n%s\n", a.Documentation)
}
}
func printAnalysisEntry(a *registry.Analysis) {
fmt.Printf("ID: %s\n", a.ID)
fmt.Printf("Name: %s\n", a.Name)
fmt.Printf("Lang: %s\n", a.Lang)
fmt.Printf("Domain: %s\n", a.Domain)
fmt.Printf("Description: %s\n", a.Description)
fmt.Printf("Tags: %s\n", strings.Join(a.Tags, ", "))
fmt.Printf("Dir: %s\n", a.DirPath)
if a.RepoURL != "" {
fmt.Printf("Repo URL: %s\n", a.RepoURL)
}
if a.Framework != "" {
fmt.Printf("Framework: %s\n", a.Framework)
}
@@ -459,8 +536,10 @@ func cmdAdd(args []string) {
templatePath = filepath.Join(r, "docs", "templates", "component.md")
case "app":
templatePath = filepath.Join(r, "docs", "templates", "app.md")
case "analysis":
templatePath = filepath.Join(r, "docs", "templates", "analysis.md")
default:
fmt.Fprintf(os.Stderr, "unknown kind: %s (use function, pipeline, component, or app)\n", kind)
fmt.Fprintf(os.Stderr, "unknown kind: %s (use function, pipeline, component, app, or analysis)\n", kind)
os.Exit(1)
}