#!/usr/bin/env python3 """Install the approved O11Y Skills into the user's Codex skills directory.""" from __future__ import annotations import argparse import json import os import subprocess import sys from pathlib import Path REPO = "tidbcloud/nutshell-skills" SKILL_PATHS = ( "skills/platform/clinic-api", "skills/platform/o11y-auth", "skills/platform/o11y-data", "skills/platform/o11y-data-export", "skills/platform/o11y-metrics-api", ) def codex_home() -> Path: return Path(os.environ.get("CODEX_HOME", Path.home() / ".codex")).expanduser().resolve() def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Install Manual Invoice O11Y Skills.") parser.add_argument("--ref", default="main") parser.add_argument("--check", action="store_true") return parser.parse_args() def main() -> int: args = parse_args() skills_root = codex_home() / "skills" status = { Path(source).name: (skills_root / Path(source).name / "SKILL.md").is_file() for source in SKILL_PATHS } if args.check: print( json.dumps( { "status": "ready" if all(status.values()) else "missing", "skills_root": str(skills_root), "skills": status, }, ensure_ascii=False, ) ) return 0 if all(status.values()) else 3 missing_paths = [source for source in SKILL_PATHS if not status[Path(source).name]] installed: list[str] = [] if missing_paths: token = os.environ.get("GITHUB_PAT") or os.environ.get("GITHUB_TOKEN") or os.environ.get( "GH_TOKEN" ) if not token: raise ValueError("GITHUB_PAT is required to install private O11Y Skills.") installer = ( Path.home() / ".codex" / "skills" / ".system" / "skill-installer" / "scripts" / "install-skill-from-github.py" ) if not installer.is_file(): raise FileNotFoundError(f"Codex Skill installer not found: {installer}") child_env = os.environ.copy() child_env["GITHUB_TOKEN"] = token command = [ sys.executable, str(installer), "--repo", REPO, "--ref", args.ref, "--path", *missing_paths, ] result = subprocess.run( command, capture_output=True, text=True, env=child_env, ) if result.returncode != 0: raise RuntimeError(result.stderr.strip() or "O11Y Skill installation failed.") installed = [Path(source).name for source in missing_paths] final_status = { Path(source).name: (skills_root / Path(source).name / "SKILL.md").is_file() for source in SKILL_PATHS } print( json.dumps( { "status": "ready" if all(final_status.values()) else "missing", "repo": REPO, "ref": args.ref, "skills_root": str(skills_root), "installed": installed, "already_installed": [name for name, ready in status.items() if ready], "skills": final_status, }, ensure_ascii=False, ) ) return 0 if all(final_status.values()) else 3 if __name__ == "__main__": raise SystemExit(main())