Files
deep_research/scripts/deploy_adapters.py
T

291 lines
11 KiB
Python

#!/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"
ANTIGRAVITY_RULES_DIR = REPO_ROOT / ".agents" / "rules"
ANTIGRAVITY_WORKFLOWS_DIR = REPO_ROOT / ".agents" / "workflows"
ANTIGRAVITY_AGENTS_FILE = REPO_ROOT / ".agents" / "agents.md"
@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 copy_antigravity_rules(dst: Path, *, force: bool, dry_run: bool = False) -> DeployResult:
result = DeployResult(platform="rules", target=dst)
if not ANTIGRAVITY_RULES_DIR.exists():
return result
part = copy_tree_contents(ANTIGRAVITY_RULES_DIR, dst, 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 copy_antigravity_workflows(dst: Path, *, force: bool, dry_run: bool = False) -> DeployResult:
result = DeployResult(platform="workflows", target=dst)
if not ANTIGRAVITY_WORKFLOWS_DIR.exists():
return result
part = copy_tree_contents(ANTIGRAVITY_WORKFLOWS_DIR, dst, 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 copy_antigravity_agents_file(dst: Path, *, force: bool, dry_run: bool = False) -> DeployResult:
result = DeployResult(platform="agents", target=dst)
if not ANTIGRAVITY_AGENTS_FILE.exists():
return result
target = dst / "agents.md"
if target.exists() and not force:
result.skipped.append(target)
return result
result.planned.append(target)
if dry_run:
return result
target.parent.mkdir(parents=True, exist_ok=True)
if target.exists() and force:
backup = target.with_name("agents.md.bak")
shutil.copy2(target, backup)
result.backups.append(backup)
shutil.copy2(ANTIGRAVITY_AGENTS_FILE, target)
result.written.append(target)
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 deploy_antigravity(
*,
target: Path | None = None,
force: bool = False,
skip_skills: bool = False,
skip_agents: bool = False,
skip_rules: bool = False,
skip_workflows: bool = False,
dry_run: bool = False,
repo_root: Path = REPO_ROOT,
) -> DeployResult:
"""Deploy Antigravity workspace rules and skills into a workspace root.
This intentionally deploys to a workspace-local `.agents` directory, not
global Antigravity/Gemini settings, so existing user configuration is not
touched. Existing files are skipped unless `force=True`.
"""
workspace = (target or repo_root).expanduser()
parts: list[DeployResult] = []
if not skip_agents:
parts.append(copy_antigravity_agents_file(workspace / ".agents", force=force, dry_run=dry_run))
if not skip_skills:
parts.append(copy_registered_skills(workspace / ".agents" / "skills", force=force, dry_run=dry_run))
if not skip_rules:
parts.append(copy_antigravity_rules(workspace / ".agents" / "rules", force=force, dry_run=dry_run))
if not skip_workflows:
parts.append(copy_antigravity_workflows(workspace / ".agents" / "workflows", force=force, dry_run=dry_run))
return _merge_results("antigravity", workspace, 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")
antigravity = sub.add_parser("antigravity", help="Deploy Antigravity workspace rules and skills")
antigravity.add_argument("--target", type=Path, help="Workspace root; defaults to this repository")
antigravity.add_argument("--force", action="store_true", help="overwrite existing files and create .bak backups")
antigravity.add_argument("--skip-agents", action="store_true", help="do not copy role definitions into target/.agents/agents.md")
antigravity.add_argument("--skip-skills", action="store_true", help="do not copy canonical skills into target/.agents/skills")
antigravity.add_argument("--skip-rules", action="store_true", help="do not copy workspace rules into target/.agents/rules")
antigravity.add_argument("--skip-workflows", action="store_true", help="do not copy workflows into target/.agents/workflows")
antigravity.add_argument("--dry-run", action="store_true", help="show files that would be written without writing")
return parser
def main() -> int:
args = build_parser().parse_args()
if args.platform == "codex":
result = deploy_codex(
target=args.target,
force=args.force,
skip_agents=args.skip_agents,
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
if args.platform == "antigravity":
result = deploy_antigravity(
target=args.target,
force=args.force,
skip_skills=args.skip_skills,
skip_rules=args.skip_rules,
skip_workflows=args.skip_workflows,
dry_run=args.dry_run,
)
print_result(result)
print()
print("Open the target workspace in Antigravity and enable/mention the workspace rule if needed:")
print(" .agents/rules/deep-research-antigravity.md")
print("Workflow installed when supported by your Antigravity build:")
print(" .agents/workflows/deep-research-native.md")
print("Existing files are skipped by default. Use --force only when you want .bak backups and replacement.")
return 0
raise SystemExit(f"unsupported platform: {args.platform}")
if __name__ == "__main__":
raise SystemExit(main())