"""export_hub_manifest — genera el TSV sidecar para app_hub_launcher.""" from __future__ import annotations import os import sqlite3 import sys from pathlib import Path from typing import Any def _read_frontmatter(md_path: Path) -> dict[str, Any]: """Parse YAML frontmatter from a .md file. Returns {} on any error.""" try: import yaml # PyYAML — available in python/.venv text = md_path.read_text(encoding="utf-8") if not text.startswith("---"): return {} # Find the closing --- end = text.find("\n---", 3) if end == -1: return {} yaml_block = text[3:end].strip() data = yaml.safe_load(yaml_block) return data if isinstance(data, dict) else {} except Exception as exc: print(f"[export_hub_manifest] WARN: could not parse {md_path}: {exc}", file=sys.stderr) return {} def _snake_to_display(name: str) -> str: """Convert snake_case name to Title Case With Spaces. Examples: graph_explorer -> Graph Explorer dag_engine_ui -> Dag Engine Ui app_hub_launcher -> App Hub Launcher """ return " ".join(part.capitalize() for part in name.split("_")) def export_hub_manifest(out_path: str, *, registry_root: str | None = None) -> dict: """Generate TSV sidecar manifest for app_hub_launcher. Queries registry.db for all cpp/imgui apps, reads their app.md frontmatter to extract name, description and accent color, then writes a UTF-8 TSV to out_path. Args: out_path: Destination path for the TSV manifest file. registry_root: Path to the fn_registry root directory. Defaults to FN_REGISTRY_ROOT env var or /home/lucas/fn_registry. Returns: {"ok": True, "count": N, "out_path": ""} """ root = Path( registry_root or os.environ.get("FN_REGISTRY_ROOT", "/home/lucas/fn_registry") ).resolve() db_path = root / "registry.db" if not db_path.exists(): raise FileNotFoundError(f"registry.db not found at {db_path}") con = sqlite3.connect(str(db_path)) con.row_factory = sqlite3.Row try: rows = con.execute( "SELECT id, name, dir_path FROM apps WHERE lang='cpp' AND framework='imgui' ORDER BY name" ).fetchall() finally: con.close() DEFAULT_ACCENT = "#64748b" TSV_HEADER = "name\tdisplay_name\tdescription\taccent_hex\n" lines: list[str] = [TSV_HEADER] count = 0 for row in rows: app_name: str = row["name"] dir_path: str = row["dir_path"] # Derive defaults in case app.md is missing / malformed display_name = _snake_to_display(app_name) description = "" accent_hex = DEFAULT_ACCENT md_path = root / dir_path / "app.md" if md_path.exists(): fm = _read_frontmatter(md_path) if fm: description = fm.get("description", "") or "" icon_block = fm.get("icon") if isinstance(icon_block, dict): accent_hex = icon_block.get("accent", DEFAULT_ACCENT) or DEFAULT_ACCENT else: print( f"[export_hub_manifest] WARN: empty/malformed frontmatter in {md_path}", file=sys.stderr, ) else: print( f"[export_hub_manifest] WARN: app.md missing for {app_name} at {md_path}", file=sys.stderr, ) # Sanitize: TSV values must not contain tabs or newlines def clean(s: str) -> str: return s.replace("\t", " ").replace("\n", " ").replace("\r", "") lines.append( f"{clean(app_name)}\t{clean(display_name)}\t{clean(description)}\t{clean(accent_hex)}\n" ) count += 1 out = Path(out_path).resolve() out.parent.mkdir(parents=True, exist_ok=True) out.write_text("".join(lines), encoding="utf-8") return {"ok": True, "count": count, "out_path": str(out)} if __name__ == "__main__": import argparse import json parser = argparse.ArgumentParser( description="Export hub manifest TSV for app_hub_launcher." ) parser.add_argument("out_path", help="Destination .tsv file path") parser.add_argument( "--registry-root", default=None, help="Path to fn_registry root (default: FN_REGISTRY_ROOT env or /home/lucas/fn_registry)", ) args = parser.parse_args() result = export_hub_manifest(args.out_path, registry_root=args.registry_root) print(json.dumps(result, indent=2))