Files
codex-game-studios/scripts/install_codex_runtime.py
2026-05-19 01:29:15 -07:00

208 lines
6.7 KiB
Python
Executable File

#!/usr/bin/env python3
"""Install Codex Game Studios runtime files into a game project.
Current runtime scope:
- Copy bundled Codex custom agents from this plugin's `runtime/agents/`
directory into the target project's `.codex/agents/` directory. This includes
director agents plus Godot, Unity, and Unreal engine specialists.
- Copy the Codex project guide template from `project-template/AGENTS.md`
into the target project's root `AGENTS.md`.
This installer intentionally does not install hooks, rules, MCP servers, or apps
yet.
"""
from __future__ import annotations
import argparse
import filecmp
import shutil
import sys
from dataclasses import dataclass
from pathlib import Path
def find_plugin_root(start: Path) -> Path:
"""Walk upward until the Codex plugin manifest is found."""
for candidate in [start, *start.parents]:
if (candidate / ".codex-plugin" / "plugin.json").is_file():
return candidate
raise FileNotFoundError(
f"Could not find .codex-plugin/plugin.json above: {start}"
)
PLUGIN_ROOT = find_plugin_root(Path(__file__).resolve())
SOURCE_AGENTS_DIR = PLUGIN_ROOT / "runtime" / "agents"
PROJECT_AGENTS_TEMPLATE = PLUGIN_ROOT / "project-template" / "AGENTS.md"
@dataclass(frozen=True)
class InstallResult:
installed: list[Path]
unchanged: list[Path]
conflicts: list[Path]
def relative_to_target(path: Path, target: Path) -> str:
try:
return str(path.relative_to(target))
except ValueError:
return str(path)
def discover_agent_files() -> list[Path]:
if not SOURCE_AGENTS_DIR.exists():
raise FileNotFoundError(f"Source agent directory not found: {SOURCE_AGENTS_DIR}")
agent_files = sorted(SOURCE_AGENTS_DIR.glob("*.toml"))
if not agent_files:
raise FileNotFoundError(f"No custom agent TOML files found in: {SOURCE_AGENTS_DIR}")
return agent_files
def install_agents(target: Path, *, force: bool, dry_run: bool) -> InstallResult:
agent_files = discover_agent_files()
target_agents_dir = target / ".codex" / "agents"
planned: list[tuple[Path, Path]] = []
unchanged: list[Path] = []
conflicts: list[Path] = []
for source in agent_files:
destination = target_agents_dir / source.name
if source.resolve() == destination.resolve():
unchanged.append(destination)
continue
if destination.exists():
if filecmp.cmp(source, destination, shallow=False):
unchanged.append(destination)
continue
if not force:
conflicts.append(destination)
continue
planned.append((source, destination))
if conflicts and not force:
return InstallResult(installed=[], unchanged=unchanged, conflicts=conflicts)
installed = [destination for _, destination in planned]
if not dry_run:
target_agents_dir.mkdir(parents=True, exist_ok=True)
for source, destination in planned:
shutil.copy2(source, destination)
return InstallResult(installed=installed, unchanged=unchanged, conflicts=conflicts)
def install_project_guide(
target: Path, *, force: bool, dry_run: bool
) -> InstallResult:
if not PROJECT_AGENTS_TEMPLATE.is_file():
raise FileNotFoundError(f"Missing project guide template: {PROJECT_AGENTS_TEMPLATE}")
destination = target / "AGENTS.md"
if destination.exists():
if filecmp.cmp(PROJECT_AGENTS_TEMPLATE, destination, shallow=False):
return InstallResult(installed=[], unchanged=[destination], conflicts=[])
if not force:
return InstallResult(installed=[], unchanged=[], conflicts=[destination])
if not dry_run:
shutil.copy2(PROJECT_AGENTS_TEMPLATE, destination)
return InstallResult(installed=[destination], unchanged=[], conflicts=[])
def print_path_list(label: str, paths: list[Path], target: Path) -> None:
if not paths:
return
print(f"{label}:")
for path in paths:
print(f"- {relative_to_target(path, target)}")
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"target",
nargs="?",
default=".",
help="Target game project root. Defaults to the current directory.",
)
parser.add_argument(
"--force",
action="store_true",
help="Overwrite existing agent files when their content differs.",
)
parser.add_argument(
"--force-agents-md",
action="store_true",
help="Overwrite an existing target AGENTS.md when its content differs.",
)
parser.add_argument(
"--dry-run",
action="store_true",
help="Show what would be installed without writing files.",
)
return parser.parse_args()
def main() -> int:
args = parse_args()
target = Path(args.target).expanduser().resolve()
print("Codex Game Studios runtime installer")
print(f"Plugin root: {PLUGIN_ROOT}")
print(f"Target project: {target}")
print("Runtime scope: director agents, engine specialists, plus project AGENTS.md")
try:
agent_result = install_agents(target, force=args.force, dry_run=True)
guide_result = install_project_guide(
target, force=args.force_agents_md, dry_run=True
)
except FileNotFoundError as error:
print(f"ERROR: {error}", file=sys.stderr)
return 1
print_path_list("Agent conflicts", agent_result.conflicts, target)
print_path_list("Project guide conflicts", guide_result.conflicts, target)
if agent_result.conflicts or guide_result.conflicts:
if agent_result.conflicts:
print(
"ERROR: Existing agent files differ. Re-run with --force to overwrite them.",
file=sys.stderr,
)
if guide_result.conflicts:
print(
"ERROR: Target AGENTS.md differs. Re-run with --force-agents-md to overwrite it.",
file=sys.stderr,
)
return 2
if not args.dry_run:
agent_result = install_agents(target, force=args.force, dry_run=False)
guide_result = install_project_guide(
target, force=args.force_agents_md, dry_run=False
)
print_path_list("Installed agents" if not args.dry_run else "Would install agents", agent_result.installed, target)
print_path_list("Unchanged agents", agent_result.unchanged, target)
print_path_list("Installed project guide" if not args.dry_run else "Would install project guide", guide_result.installed, target)
print_path_list("Unchanged project guide", guide_result.unchanged, target)
print("Done.")
return 0
if __name__ == "__main__":
raise SystemExit(main())