mirror of
https://github.com/lllllllama/RigorPilot-Skills.git
synced 2026-09-14 13:43:27 +08:00
237 lines
8.2 KiB
Python
237 lines
8.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Install this repository's skill folders into a client-neutral or client-specific skills directory."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import os
|
|
import shutil
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Iterable, List, Mapping
|
|
|
|
|
|
SHARED_REFERENCE_FILES = [
|
|
"agent-operating-principles.md",
|
|
"research-rigor-principles.md",
|
|
"deep-learning-experiment-principles.md",
|
|
"explore-variant-spec.md",
|
|
"research-pitfall-checklist.md",
|
|
"research-thinking-loop.md",
|
|
"continuous-learning-policy.md",
|
|
]
|
|
|
|
# Runtime modules loaded by installed skills via parents[3]/shared/scripts;
|
|
# they must be installed next to the skills.
|
|
SHARED_SCRIPT_FILES = [
|
|
"agent_provider.py",
|
|
"command_utils.py",
|
|
"model_adapter.py",
|
|
"resource_monitor.py",
|
|
"runtime_runner.py",
|
|
"task_queue.py",
|
|
"write_run_bundle.py",
|
|
"write_explore_bundle.py",
|
|
"lessons_store.py",
|
|
]
|
|
|
|
|
|
def default_target(client: str, env: Mapping[str, str] | None = None, home: Path | None = None) -> Path:
|
|
env_map = os.environ if env is None else env
|
|
home_path = (home or Path.home()).expanduser()
|
|
|
|
if client == "agents":
|
|
agents_home = env_map.get("AGENTS_HOME")
|
|
base = Path(agents_home).expanduser() if agents_home else home_path / ".agents"
|
|
return (base / "skills").resolve()
|
|
|
|
if client == "codex":
|
|
codex_home = env_map.get("CODEX_HOME")
|
|
base = Path(codex_home).expanduser() if codex_home else home_path / ".codex"
|
|
return (base / "skills").resolve()
|
|
|
|
if client == "claude":
|
|
claude_home = env_map.get("CLAUDE_HOME")
|
|
base = Path(claude_home).expanduser() if claude_home else home_path / ".claude"
|
|
return (base / "skills").resolve()
|
|
|
|
raise ValueError(f"Unsupported client: {client}")
|
|
|
|
|
|
def discover_skills(skills_root: Path) -> List[Path]:
|
|
return sorted(
|
|
path for path in skills_root.iterdir()
|
|
if path.is_dir() and (path / "SKILL.md").exists()
|
|
)
|
|
|
|
|
|
def safe_remove(path: Path, root: Path) -> None:
|
|
# Validate containment on the entry itself, without following a symlink:
|
|
# resolving first would reject (or worse, rmtree) the symlink's target
|
|
# instead of the entry inside the target root.
|
|
if path.parent.resolve() != root.resolve():
|
|
raise ValueError(f"Refusing to remove path outside target root: {path}")
|
|
if path.is_symlink():
|
|
path.unlink()
|
|
return
|
|
if path.exists():
|
|
if path.is_file():
|
|
path.unlink()
|
|
else:
|
|
shutil.rmtree(path)
|
|
|
|
|
|
def copy_skill(source: Path, target: Path) -> None:
|
|
shutil.copytree(
|
|
source,
|
|
target,
|
|
ignore=shutil.ignore_patterns("__pycache__", "*.pyc", ".DS_Store"),
|
|
dirs_exist_ok=False,
|
|
)
|
|
|
|
|
|
def install_shared_references(repo_root: Path, target_root: Path, force: bool) -> List[Path]:
|
|
source_root = repo_root / "references"
|
|
target_reference_root = target_root.parent / "references"
|
|
installed: List[Path] = []
|
|
|
|
for filename in SHARED_REFERENCE_FILES:
|
|
source_path = source_root / filename
|
|
target_path = target_reference_root / filename
|
|
if not source_path.exists():
|
|
raise FileNotFoundError(f"Shared reference does not exist: {source_path}")
|
|
if target_path.exists():
|
|
if target_path.read_bytes() == source_path.read_bytes():
|
|
installed.append(target_path)
|
|
continue
|
|
if not force:
|
|
raise FileExistsError(
|
|
f"Shared reference already exists with different content: {target_path}. "
|
|
"Re-run with --force to replace it."
|
|
)
|
|
target_path.parent.mkdir(parents=True, exist_ok=True)
|
|
shutil.copy2(source_path, target_path)
|
|
installed.append(target_path)
|
|
|
|
return installed
|
|
|
|
|
|
def install_shared_scripts(repo_root: Path, target_root: Path, force: bool) -> List[Path]:
|
|
source_root = repo_root / "shared" / "scripts"
|
|
target_script_root = target_root.parent / "shared" / "scripts"
|
|
installed: List[Path] = []
|
|
|
|
for filename in SHARED_SCRIPT_FILES:
|
|
source_path = source_root / filename
|
|
target_path = target_script_root / filename
|
|
if not source_path.exists():
|
|
raise FileNotFoundError(f"Shared script does not exist: {source_path}")
|
|
if target_path.exists():
|
|
if target_path.read_bytes() == source_path.read_bytes():
|
|
installed.append(target_path)
|
|
continue
|
|
if not force:
|
|
raise FileExistsError(
|
|
f"Shared script already exists with different content: {target_path}. "
|
|
"Re-run with --force to replace it."
|
|
)
|
|
target_path.parent.mkdir(parents=True, exist_ok=True)
|
|
shutil.copy2(source_path, target_path)
|
|
installed.append(target_path)
|
|
|
|
return installed
|
|
|
|
|
|
def install_skills(
|
|
repo_root: Path,
|
|
target_root: Path,
|
|
mode: str,
|
|
force: bool,
|
|
) -> List[Path]:
|
|
skills_root = repo_root / "skills"
|
|
skill_dirs = discover_skills(skills_root)
|
|
target_root.mkdir(parents=True, exist_ok=True)
|
|
install_shared_references(repo_root, target_root, force)
|
|
install_shared_scripts(repo_root, target_root, force)
|
|
|
|
installed: List[Path] = []
|
|
for skill_dir in skill_dirs:
|
|
target_path = target_root / skill_dir.name
|
|
if target_path.exists() or target_path.is_symlink():
|
|
if not force:
|
|
raise FileExistsError(
|
|
f"Target already exists: {target_path}. Re-run with --force to replace it."
|
|
)
|
|
safe_remove(target_path, target_root)
|
|
|
|
if mode == "copy":
|
|
copy_skill(skill_dir, target_path)
|
|
else:
|
|
try:
|
|
target_path.symlink_to(skill_dir.resolve(), target_is_directory=True)
|
|
except OSError as exc:
|
|
# Windows commonly denies symlink creation unless Developer Mode
|
|
# or elevated privileges are enabled. A portable copy is safer
|
|
# than failing an otherwise valid local installation.
|
|
if os.name != "nt" or getattr(exc, "winerror", None) != 1314:
|
|
raise
|
|
copy_skill(skill_dir, target_path)
|
|
|
|
installed.append(target_path)
|
|
|
|
return installed
|
|
|
|
|
|
def format_paths(paths: Iterable[Path]) -> str:
|
|
return "\n".join(f"- {path}" for path in paths)
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="Install all skill folders into a neutral, Codex, or Claude Code skills directory.")
|
|
parser.add_argument(
|
|
"--client",
|
|
choices=["agents", "codex", "claude"],
|
|
default="agents",
|
|
help="Which skill directory convention to target. Defaults to the neutral Agent Skills directory for cross-client compatibility.",
|
|
)
|
|
parser.add_argument(
|
|
"--target",
|
|
default=None,
|
|
help="Override the skills directory. Defaults to AGENTS_HOME/skills or ~/.agents/skills for neutral installs, CODEX_HOME/skills or ~/.codex/skills for Codex, and CLAUDE_HOME/skills or ~/.claude/skills for Claude Code.",
|
|
)
|
|
parser.add_argument(
|
|
"--mode",
|
|
choices=["copy", "symlink"],
|
|
default="copy",
|
|
help="Installation mode. Use symlink for local development, copy for portable installs.",
|
|
)
|
|
parser.add_argument(
|
|
"--force",
|
|
action="store_true",
|
|
help="Replace existing target skill folders.",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
repo_root = Path(__file__).resolve().parents[1]
|
|
target_root = (
|
|
Path(args.target).expanduser().resolve()
|
|
if args.target
|
|
else default_target(args.client)
|
|
)
|
|
installed = install_skills(repo_root, target_root, args.mode, args.force)
|
|
|
|
print(f"Installed {len(installed)} skills to {target_root} for {args.client}")
|
|
if args.mode == "symlink":
|
|
copied = [path for path in installed if not path.is_symlink()]
|
|
if copied:
|
|
print(
|
|
f"Warning: symlink permission was unavailable; installed {len(copied)} skill(s) as copies instead.",
|
|
file=sys.stderr,
|
|
)
|
|
print(format_paths(installed))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|