import argparse
import shutil
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import List, Optional
@dataclass
class Result:
success: bool
message: str
data: Optional[dict] = None
SKILLS = [
"task-graph-basics",
"task-graph-coordinator",
"task-graph-worker",
"task-graph-reporting",
"task-graph-migration",
"task-graph-repair",
]
DEFAULT_TARGET = Path.home() / ".claude" / "skills"
def get_script_dir() -> Path:
return Path(__file__).parent.resolve()
def get_skills_dir() -> Path:
return get_script_dir().parent
def list_skills() -> Result:
skills_dir = get_skills_dir()
available = []
for skill in SKILLS:
skill_path = skills_dir / skill / "SKILL.md"
if skill_path.exists():
available.append(skill)
return Result(
success=True,
message=f"Found {len(available)} skills",
data={"skills": available}
)
def validate_skill(skill_path: Path) -> Result:
skill_md = skill_path / "SKILL.md"
if not skill_path.exists():
return Result(False, f"Skill directory not found: {skill_path}")
if not skill_md.exists():
return Result(False, f"SKILL.md not found in {skill_path}")
content = skill_md.read_text(encoding="utf-8")
if not content.startswith("---"):
return Result(False, f"Missing frontmatter in {skill_md}")
return Result(True, "Valid skill")
def install_skill(
skill_name: str,
target_dir: Path,
dry_run: bool = False
) -> Result:
skills_dir = get_skills_dir()
source = skills_dir / skill_name
dest = target_dir / skill_name
validation = validate_skill(source)
if not validation.success:
return validation
if dry_run:
return Result(
True,
f"Would install: {source} -> {dest}",
data={"source": str(source), "dest": str(dest)}
)
target_dir.mkdir(parents=True, exist_ok=True)
if dest.exists():
shutil.rmtree(dest)
shutil.copytree(source, dest)
return Result(
True,
f"Installed: {skill_name} -> {dest}",
data={"source": str(source), "dest": str(dest)}
)
def uninstall_skill(skill_name: str, target_dir: Path, dry_run: bool = False) -> Result:
dest = target_dir / skill_name
if not dest.exists():
return Result(True, f"Not installed: {skill_name}")
if dry_run:
return Result(True, f"Would remove: {dest}")
shutil.rmtree(dest)
return Result(True, f"Removed: {dest}")
def install_all(
target_dir: Path,
skills: Optional[List[str]] = None,
dry_run: bool = False
) -> Result:
to_install = skills if skills else SKILLS
results = []
failed = []
for skill in to_install:
if skill not in SKILLS:
results.append(f"Unknown skill: {skill}")
failed.append(skill)
continue
result = install_skill(skill, target_dir, dry_run)
results.append(result.message)
if not result.success:
failed.append(skill)
success = len(failed) == 0
summary = f"Installed {len(to_install) - len(failed)}/{len(to_install)} skills"
if dry_run:
summary = f"[DRY RUN] {summary}"
return Result(
success=success,
message=summary,
data={"results": results, "failed": failed}
)
def uninstall_all(
target_dir: Path,
skills: Optional[List[str]] = None,
dry_run: bool = False
) -> Result:
to_uninstall = skills if skills else SKILLS
results = []
for skill in to_uninstall:
result = uninstall_skill(skill, target_dir, dry_run)
results.append(result.message)
summary = f"Uninstalled {len(to_uninstall)} skills"
if dry_run:
summary = f"[DRY RUN] {summary}"
return Result(
success=True,
message=summary,
data={"results": results}
)
def main():
parser = argparse.ArgumentParser(
description="Install task-graph-mcp skills",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
%(prog)s Install all to ~/.claude/skills/
%(prog)s --target ~/my-skills/ Install to custom directory
%(prog)s --skills worker,coordinator Install specific skills only
%(prog)s --list Show available skills
%(prog)s --dry-run Preview installation
%(prog)s --uninstall Remove installed skills
"""
)
parser.add_argument(
"--target", "-t",
type=Path,
default=DEFAULT_TARGET,
help=f"Installation directory (default: {DEFAULT_TARGET})"
)
parser.add_argument(
"--skills", "-s",
type=str,
help="Comma-separated list of skills to install"
)
parser.add_argument(
"--list", "-l",
action="store_true",
help="List available skills"
)
parser.add_argument(
"--dry-run", "-n",
action="store_true",
help="Show what would be done without doing it"
)
parser.add_argument(
"--uninstall", "-u",
action="store_true",
help="Uninstall skills instead of installing"
)
parser.add_argument(
"--quiet", "-q",
action="store_true",
help="Minimal output"
)
args = parser.parse_args()
if args.list:
result = list_skills()
if not args.quiet:
print("Available skills:")
skills_list = (result.data or {}).get("skills", [])
for skill in skills_list:
print(f" - {skill}")
return 0
skills = None
if args.skills:
skills = [s.strip() for s in args.skills.split(",")]
if args.uninstall:
result = uninstall_all(args.target, skills, args.dry_run)
else:
result = install_all(args.target, skills, args.dry_run)
if not args.quiet:
print(result.message)
data = result.data or {}
if "results" in data:
for r in data["results"]:
print(f" {r}")
if data.get("failed"):
print(f"\nFailed: {', '.join(data['failed'])}")
return 0 if result.success else 1
if __name__ == "__main__":
sys.exit(main())