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 8f24157096
commit d7f2c00d7b
111 changed files with 2766 additions and 5043 deletions
+63
View File
@@ -74,6 +74,22 @@ type rawApp struct {
Framework string `yaml:"framework"`
EntryPoint string `yaml:"entry_point"`
DirPath string `yaml:"dir_path"`
RepoURL string `yaml:"repo_url"`
}
// rawAnalysis mirrors the YAML frontmatter of an analysis .md file.
type rawAnalysis struct {
Name string `yaml:"name"`
Lang string `yaml:"lang"`
Domain string `yaml:"domain"`
Description string `yaml:"description"`
Tags []string `yaml:"tags"`
UsesFunctions []string `yaml:"uses_functions"`
UsesTypes []string `yaml:"uses_types"`
Framework string `yaml:"framework"`
EntryPoint string `yaml:"entry_point"`
DirPath string `yaml:"dir_path"`
RepoURL string `yaml:"repo_url"`
}
// extractFrontmatter splits a .md file into YAML frontmatter and body.
@@ -266,11 +282,58 @@ func ParseAppMD(path string, root string) (*App, error) {
Documentation: sections.documentation,
Notes: sections.notes,
DirPath: raw.DirPath,
RepoURL: raw.RepoURL,
}
return a, nil
}
// ParseAnalysisMD parses an analysis .md file into an Analysis.
func ParseAnalysisMD(path string, root string) (*Analysis, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("reading %s: %w", path, err)
}
fm, body, err := extractFrontmatter(data)
if err != nil {
return nil, fmt.Errorf("parsing %s: %w", path, err)
}
var raw rawAnalysis
if err := yaml.Unmarshal(fm, &raw); err != nil {
return nil, fmt.Errorf("parsing YAML in %s: %w", path, err)
}
if raw.Name == "" {
return nil, fmt.Errorf("%s: name is required", path)
}
if raw.Description == "" {
return nil, fmt.Errorf("%s: description is required", path)
}
sections := extractSections(body)
an := &Analysis{
ID: GenerateID(raw.Name, raw.Lang, raw.Domain),
Name: raw.Name,
Lang: raw.Lang,
Domain: raw.Domain,
Description: raw.Description,
Tags: raw.Tags,
UsesFunctions: raw.UsesFunctions,
UsesTypes: raw.UsesTypes,
Framework: raw.Framework,
EntryPoint: raw.EntryPoint,
Documentation: sections.documentation,
Notes: sections.notes,
DirPath: raw.DirPath,
RepoURL: raw.RepoURL,
}
return an, nil
}
// bodySections holds the extracted sections from a .md body.
type bodySections struct {
example string // content under ## Ejemplo