#!/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" MCP_DIR = "mcp" INDENT = " " OLD_DIRS = ("libs", "apis", "wiki", "tutorials") # ── 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)}" def _find_home(dest: Path) -> str: """After copying a site into *dest*, return the relative URL path where index.html lives. Checks root, lib/, api/ in order.""" if (dest / "index.html").exists(): return "" if (dest / "lib" / "index.html").exists(): return "lib/" if (dest / "api" / "index.html").exists(): return "api/" return "" def _find_home_for_kind(dest: Path, kind: str) -> str: """After copying a site into *dest*, return the relative URL path for a specific doc kind. A root index.html serves every kind; otherwise each kind is found under its own subdirectory (lib/, api/, wiki/).""" if (dest / "index.html").exists(): return "" sub = dest / kind if kind in ("lib", "api", "wiki") and (sub / "index.html").exists(): return f"{kind}/" return _find_home(dest) # ── 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: 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, home_urls: dict[str, str], kind_urls: dict[tuple[str, str], str] | None = None, ) -> 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]] = [] kind_urls = kind_urls or {} for entry in config.get("repos", []): docs = entry.get("docs") or {} name = entry["name"] title = _safe_title(entry) desc = _safe_description(entry) base_url = home_urls.get(name, f"/{name}/") if docs.get("lib"): url = kind_urls.get((name, "lib"), base_url) lib_cards.append((title, desc, url, "View Documentation")) if docs.get("wiki"): url = kind_urls.get((name, "wiki"), base_url) wiki_cards.append((title, desc, url, "View Documentation")) if docs.get("api"): url = kind_urls.get((name, "api"), base_url) api_cards.append((title, desc, url, "View Documentation")) for entry in config.get("static", []): kind = entry.get("kind") name = entry["repo"] title = _safe_title(entry) desc = _safe_description(entry) url = home_urls.get(name, f"/{name}/") if kind == "api": api_cards.append((title, desc, url, "View Documentation")) elif kind == "tutorial": tutorial_cards.append((title, desc, url, "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 lib/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] = [] home_urls: dict[str, 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 / name 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", []): name = entry["repo"] p = ROOT / entry["path"] dst = ROOT / name if not p.is_dir(): errors.append(f"static {name}: path missing ({p})") elif p == dst: copied.append(f"static {name} already vendored at {dst.relative_to(ROOT)}/") else: copied.append(f"static {name} {dst.relative_to(ROOT)}/") if not dry_run: _copy_dir(p, dst) # ── compute home URLs ─────────────────────────────────────────────────── if not dry_run: kind_urls: dict[tuple[str, str], str] = {} for entry in config.get("repos", []): name = entry["name"] dest = ROOT / name home_urls[name] = f"/{name}/{_find_home(dest)}" for kind in ("lib", "api", "wiki"): if (entry.get("docs") or {}).get(kind): kind_urls[(name, kind)] = f"/{name}/{_find_home_for_kind(dest, kind)}" for entry in config.get("static", []): home_urls[entry["repo"]] = f"/{entry['repo']}/{_find_home(ROOT / entry['repo'])}" # ── regenerate outputs ────────────────────────────────────────────── index_html = build_index_html(config, home_urls, kind_urls) (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 name, url in sorted(home_urls.items()): print(f"[URL] {name:20s} -> {url}") for line in copied: print(f"[COP]{line}") # ── clean stale category dirs ─────────────────────────────────────────── if not dry_run: for d in OLD_DIRS: p = ROOT / d if p.exists(): shutil.rmtree(p, ignore_errors=True) print(f"[DEL] {d}/ (stale category dir removed)") 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()