package infra import ( "fmt" "log" "os" "path/filepath" "sort" "strings" "gopkg.in/yaml.v3" ) // ScanFlowsDir escanea el directorio root (dev/flows/) y devuelve todos los Flows // encontrados en *.md directos. // Si un archivo falla al parsearse, se emite un warning al log y se continua. // Los flows se devuelven ordenados por ID ascendente. func ScanFlowsDir(root string) ([]Flow, error) { matches, err := filepath.Glob(filepath.Join(root, "*.md")) if err != nil { return nil, fmt.Errorf("scan_flows_dir: glob: %w", err) } var flows []Flow for _, path := range matches { base := filepath.Base(path) if strings.EqualFold(base, "INDEX.md") || strings.EqualFold(base, "README.md") || strings.EqualFold(base, "AGENT_GUIDE.md") { continue } info, err := os.Stat(path) if err != nil || !info.Mode().IsRegular() { continue } f, err := parseFlowMd(path) if err != nil { log.Printf("scan_flows_dir: warning: skip %s: %v", path, err) continue } flows = append(flows, f) } sort.Slice(flows, func(i, j int) bool { return flows[i].ID < flows[j].ID }) return flows, nil } // parseFlowMd parsea el frontmatter de un archivo dev/flows/*.md en un struct Flow. func parseFlowMd(path string) (Flow, error) { data, err := os.ReadFile(path) if err != nil { return Flow{}, fmt.Errorf("read %s: %w", path, err) } info, err := os.Stat(path) if err != nil { return Flow{}, fmt.Errorf("stat %s: %w", path, err) } fm, _, err := splitFrontmatter(data) if err != nil { return Flow{}, fmt.Errorf("frontmatter %s: %w", path, err) } var f Flow if err := yaml.Unmarshal(fm, &f); err != nil { return Flow{}, fmt.Errorf("yaml %s: %w", path, err) } // Algunos flows usan "name" y no "title" — normalizar if f.Title == "" && f.Name != "" { f.Title = f.Name } // Algunos flows usan entero como ID en el YAML — yaml.v3 lo convierte a string OK f.FilePath = path f.MtimeNs = info.ModTime().UnixNano() return f, nil }