#!/usr/bin/env python """Collect documentation from source repos into this repository and regenerate the static index and nginx configuration. Usage: python collect.py # copy artifacts, regenerate outputs python collect.py --dry-run # list planned actions without writing """ from __future__ import annotations import argparse import shutil import sys from pathlib import Path import yaml ROOT = Path(__file__).resolve().parent CONFIG_FILE = ROOT / "config.yml" KIND_DIR = {"lib": "libs", "api": "apis", "wiki": "wiki"} SITE_DIR = "site" MCP_DIR = "mcp" TITLE_CSS = "font-size:1.6rem;margin-bottom:1.2rem;color:#2dd4bf;" CARD_CSS = ( "background:#161b22;border:1px solid #30363d;border-radius:12px;" "padding:1.8rem;text-align:left;" "transition:transform 0.15s ease,box-shadow 0.15s ease;" ) INDENT = " " # ── helpers ────────────────────────────────────────────────────────────────── def load_config() -> dict: data = yaml.safe_load(CONFIG_FILE.read_text(encoding="utf-8")) if "service" not in data or "repos" not in data: print("ERROR: config.yml must define 'service' and 'repos' keys", file=sys.stderr) sys.exit(1) return data def _copy_dir(src: Path, dst: Path) -> None: if dst.exists(): shutil.rmtree(dst, ignore_errors=True) dst.parent.mkdir(parents=True, exist_ok=True) shutil.copytree(src, dst) def _safe_title(entry: dict) -> str: return entry.get("title") or entry["name"].replace("-", " ").title() def _safe_description(entry: dict) -> str: return entry.get("description") or f"Documentation for {_safe_title(entry)}" # ── index.html generator ──────────────────────────────────────────────────── INDEX_TEMPLATE = r""" Aetoskia Developer Documentation

Aetoskia Developer Docs

Central documentation hub for Aetoskia projects

{body} """ def _render_section(title: str, cards: list[tuple[str, str, str, str]]) -> str: """cards = [(safe_title, description, href, label), ...]""" if not cards: return "" lines = [f'
', f"

{title}

", '
'] for t, desc, href, label in cards: lines.append(f'
') lines.append(f"

{t}

") lines.append(f"

{desc}

") lines.append(f' {label} →') lines.append(f"
") lines.append("
") lines.append("
") return "\n".join(lines) def build_index_html(config: dict) -> str: lib_cards: list[tuple[str, str, str, str]] = [] api_cards: list[tuple[str, str, str, str]] = [] wiki_cards: list[tuple[str, str, str, str]] = [] tutorial_cards: list[tuple[str, str, str, str]] = [] for entry in config.get("repos", []): docs = entry.get("docs") or {} name = entry["name"] title = _safe_title(entry) desc = _safe_description(entry) if docs.get("lib"): lib_cards.append((title, desc, f"/libs/{name}/", "View Documentation")) if docs.get("api"): api_cards.append((title, desc, f"/apis/{name}/", "View Documentation")) if docs.get("wiki"): wiki_cards.append((title, desc, f"/wiki/{name}/", "View Documentation")) for entry in config.get("static", []): kind = entry.get("kind") name = entry["repo"] title = _safe_title(entry) desc = _safe_description(entry) if kind == "api": api_cards.append((title, desc, f"/apis/{name}/", "View Documentation")) elif kind == "tutorial": tutorial_cards.append((title, desc, f"/tutorials/{name}/", "View Tutorial")) body = "\n\n".join(filter(None, [ _render_section("Libraries", lib_cards), _render_section("APIs", api_cards), _render_section("Wiki", wiki_cards), _render_section("Tutorials", tutorial_cards), ])) return INDEX_TEMPLATE.format(body=body) # ── nginx.conf generator ──────────────────────────────────────────────────── NGINX_TEMPLATE = r"""server {{ listen {port}; server_name _; root /usr/share/nginx/html; index index.html api/index.html; location / {{ try_files $uri $uri/ /index.html; }} access_log /var/log/nginx/access.log; error_log /var/log/nginx/error.log; }} """ def build_nginx_conf(port: int) -> str: return NGINX_TEMPLATE.format(port=port) # ── collect logic ──────────────────────────────────────────────────────────── def collect(config: dict, dry_run: bool = False) -> list[str]: service = config["service"] errors: list[str] = [] copied: list[str] = [] for entry in config.get("repos", []): name = entry["name"] source = Path(entry["source"]).resolve() docs = entry.get("docs") or {} if not source.is_dir(): errors.append(f"{name}: source repo not found ({source})") continue for kind in ("lib", "api", "wiki"): site_rel = docs.get(kind) if not site_rel: continue src = source / site_rel if not src.is_dir(): errors.append(f"{name}: {kind} site missing ({src})") continue dst = ROOT / KIND_DIR[kind] / name / SITE_DIR copied.append(f"{kind:>4} {name} {dst.relative_to(ROOT)}") if not dry_run: _copy_dir(src, dst) mcp = docs.get("mcp") if mcp: src = source / mcp["bundle"] if not src.is_dir(): errors.append(f"{name}: mcp bundle missing ({src})") continue dst = ROOT / MCP_DIR / name copied.append(f" mcp {name} {dst.relative_to(ROOT)}") if not dry_run: _copy_dir(src, dst) for entry in config.get("static", []): p = ROOT / entry["path"] if not p.is_dir(): errors.append(f"static {entry['repo']}: path missing ({p})") else: copied.append(f"static {entry['repo']} {entry['path']}") if not dry_run: index_html = build_index_html(config) (ROOT / "_index" / "index.html").write_text(index_html, encoding="utf-8") nginx_conf = build_nginx_conf(service["html_port"]) (ROOT / "nginx.conf").write_text(nginx_conf, encoding="utf-8") print(f"[OK] _index/index.html regenerated") print(f"[OK] nginx.conf regenerated (listen {service['html_port']})") for line in copied: print(f"[COP]{line}") if errors: print("\nERRORS:", file=sys.stderr) for e in errors: print(f" - {e}", file=sys.stderr) return errors # ── CLI ────────────────────────────────────────────────────────────────────── def main() -> None: parser = argparse.ArgumentParser(description="Collect docs into the aetos-docs service repo") parser.add_argument("--dry-run", action="store_true", help="Show planned actions without writing") args = parser.parse_args() config = load_config() errors = collect(config, dry_run=args.dry_run) if errors: sys.exit(1) if __name__ == "__main__": main()