#!/usr/bin/env python3 """Deploy platform adapter templates outside the repository checkout.""" from __future__ import annotations import argparse import os import shutil import sys from dataclasses import dataclass, field from pathlib import Path REPO_ROOT = Path(__file__).resolve().parent.parent if str(REPO_ROOT) not in sys.path: sys.path.insert(0, str(REPO_ROOT)) from scripts.runtime.skills import SkillRegistry CODEX_TEMPLATE = REPO_ROOT / "codex_adapter_templates" / "codex" @dataclass class DeployResult: platform: str target: Path written: list[Path] = field(default_factory=list) skipped: list[Path] = field(default_factory=list) planned: list[Path] = field(default_factory=list) backups: list[Path] = field(default_factory=list) def default_codex_home( *, env: dict[str, str] | None = None, user_home: Path | None = None, ) -> Path: values = os.environ if env is None else env if values.get("CODEX_HOME"): return Path(values["CODEX_HOME"]).expanduser() home = Path.home() if user_home is None else user_home return home / ".codex" def copy_tree_contents( src: Path, dst: Path, *, force: bool, dry_run: bool = False, backup_existing: bool = True, exclude: set[Path] | None = None, ) -> DeployResult: if not src.exists(): raise FileNotFoundError(f"adapter template source not found: {src}") result = DeployResult(platform="copy", target=dst) excluded = exclude or set() for item in sorted(src.rglob("*")): rel = item.relative_to(src) if rel in excluded: continue target = dst / rel if item.is_dir(): if not dry_run: target.mkdir(parents=True, exist_ok=True) continue if target.exists() and not force: result.skipped.append(target) continue result.planned.append(target) if dry_run: continue target.parent.mkdir(parents=True, exist_ok=True) if target.exists() and force and backup_existing: backup = target.with_name(f"{target.name}.bak") shutil.copy2(target, backup) result.backups.append(backup) shutil.copy2(item, target) result.written.append(target) return result def _merge_results(platform: str, target: Path, parts: list[DeployResult]) -> DeployResult: merged = DeployResult(platform=platform, target=target) for part in parts: merged.written.extend(part.written) merged.skipped.extend(part.skipped) merged.planned.extend(part.planned) merged.backups.extend(part.backups) return merged def copy_registered_skills(dst: Path, *, force: bool, dry_run: bool = False) -> DeployResult: result = DeployResult(platform="skills", target=dst) for skill in SkillRegistry().list(): part = copy_tree_contents(skill.path.parent, dst / skill.name, force=force, dry_run=dry_run) result.written.extend(part.written) result.skipped.extend(part.skipped) result.planned.extend(part.planned) result.backups.extend(part.backups) return result def deploy_codex( *, target: Path | None = None, force: bool = False, skip_skills: bool = False, dry_run: bool = False, include_config: bool = False, repo_root: Path = REPO_ROOT, ) -> DeployResult: codex_home = (target or default_codex_home()).expanduser() template = repo_root / "codex_adapter_templates" / "codex" parts = [ copy_tree_contents( template, codex_home, force=force, dry_run=dry_run, exclude=set() if include_config else {Path("config.toml")}, ), ] if not skip_skills: parts.append(copy_registered_skills(codex_home / "skills", force=force, dry_run=dry_run)) return _merge_results("codex", codex_home, parts) def print_result(result: DeployResult) -> None: action = "planned" if result.planned and not result.written else "written" print(f"{result.platform} adapter deployment") print(f" target: {result.target}") print(f" files {action}: {len(result.planned if action == 'planned' else result.written)}") print(f" files skipped: {len(result.skipped)}") print(f" backups: {len(result.backups)}") if result.written: for path in result.written: print(f" {path}") elif result.planned: for path in result.planned: print(f" {path}") def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description="Deploy Deep Research surface adapters") sub = parser.add_subparsers(dest="platform", required=True) codex = sub.add_parser("codex", help="Deploy Codex adapter to CODEX_HOME or a target directory") codex.add_argument("--target", type=Path, help="Codex home target; defaults to $CODEX_HOME or ~/.codex") codex.add_argument("--force", action="store_true", help="overwrite existing files and create .bak backups") codex.add_argument("--skip-skills", action="store_true", help="do not copy canonical skills into target/skills") codex.add_argument("--include-config", action="store_true", help="also copy config.toml; off by default to avoid overwriting global Codex config") codex.add_argument("--dry-run", action="store_true", help="show files that would be written") return parser def main() -> int: args = build_parser().parse_args() if args.platform == "codex": result = deploy_codex( target=args.target, force=args.force, skip_skills=args.skip_skills, dry_run=args.dry_run, include_config=args.include_config, ) print_result(result) print() print("Run Codex from this repository after deployment:") if args.include_config: print(" codex --profile deep-research") else: print(" codex") print("Note: config.toml is not copied by default. Use --include-config only if you want the bundled profile.") return 0 raise SystemExit(f"unsupported platform: {args.platform}") if __name__ == "__main__": raise SystemExit(main())