291 lines
11 KiB
Python
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())
|