|
10 | 10 |
|
11 | 11 | import argparse |
12 | 12 | import os |
| 13 | +import shutil |
13 | 14 | import signal |
14 | 15 | import subprocess |
15 | 16 | import sys |
@@ -230,6 +231,58 @@ def cmd_continue(args: argparse.Namespace) -> int: |
230 | 231 | return 0 |
231 | 232 |
|
232 | 233 |
|
| 234 | +def cmd_install(args: argparse.Namespace) -> int: |
| 235 | + """Deploy the bundled skills and Runner agent into ~/.claude. |
| 236 | +
|
| 237 | + Makes `/goal-init`, `/goal-run`, `/goal-flash` and the `goal-runner` |
| 238 | + subagent available to every Claude Code session. The assets ship inside |
| 239 | + the installed package (`resources/`), so this works from a `uv tool |
| 240 | + install` / `pip install` with no source checkout. |
| 241 | + """ |
| 242 | + resources = Path(__file__).resolve().parent / "resources" |
| 243 | + claude = Path.home() / ".claude" |
| 244 | + |
| 245 | + copied: list[str] = [] |
| 246 | + skipped: list[str] = [] |
| 247 | + |
| 248 | + def deploy(src: Path, dst: Path) -> None: |
| 249 | + if dst.exists() and not args.force: |
| 250 | + skipped.append(str(dst)) |
| 251 | + return |
| 252 | + if dst.exists(): |
| 253 | + if dst.is_dir(): |
| 254 | + shutil.rmtree(dst) |
| 255 | + else: |
| 256 | + dst.unlink() |
| 257 | + dst.parent.mkdir(parents=True, exist_ok=True) |
| 258 | + if src.is_dir(): |
| 259 | + shutil.copytree(src, dst) |
| 260 | + else: |
| 261 | + shutil.copy2(src, dst) |
| 262 | + copied.append(str(dst)) |
| 263 | + |
| 264 | + skills_src = resources / "skills" |
| 265 | + if skills_src.is_dir(): |
| 266 | + for skill in sorted(p for p in skills_src.iterdir() if p.is_dir()): |
| 267 | + deploy(skill, claude / "skills" / skill.name) |
| 268 | + |
| 269 | + agents_src = resources / "agents" |
| 270 | + if agents_src.is_dir(): |
| 271 | + for agent in sorted(agents_src.glob("*.md")): |
| 272 | + deploy(agent, claude / "agents" / agent.name) |
| 273 | + |
| 274 | + for path in copied: |
| 275 | + print(f"installed {path}") |
| 276 | + for path in skipped: |
| 277 | + print(f"exists {path} (use --force to overwrite)") |
| 278 | + if not copied and not skipped: |
| 279 | + print("Nothing to install — no bundled resources found.", file=sys.stderr) |
| 280 | + return 1 |
| 281 | + if copied: |
| 282 | + print("\nDone. Open Claude Code and type /goal-init to verify.") |
| 283 | + return 0 |
| 284 | + |
| 285 | + |
233 | 286 | def main(argv: list[str] | None = None) -> int: |
234 | 287 | parser = argparse.ArgumentParser(prog="goaloop", description=__doc__) |
235 | 288 | sub = parser.add_subparsers(dest="command", required=True) |
@@ -265,6 +318,12 @@ def main(argv: list[str] | None = None) -> int: |
265 | 318 | p_continue.add_argument("workspace") |
266 | 319 | p_continue.set_defaults(func=cmd_continue) |
267 | 320 |
|
| 321 | + p_install = sub.add_parser( |
| 322 | + "install", help="deploy bundled skills + agent into ~/.claude") |
| 323 | + p_install.add_argument("--force", action="store_true", |
| 324 | + help="overwrite existing skills/agents") |
| 325 | + p_install.set_defaults(func=cmd_install) |
| 326 | + |
268 | 327 | args = parser.parse_args(argv) |
269 | 328 | return args.func(args) |
270 | 329 |
|
|
0 commit comments