skill-blast 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,4 @@
1
+ """skill-blast: One-click installer for 50 top AI agent skills."""
2
+
3
+ __version__ = "1.0.0"
4
+ __author__ = "skill-blast contributors"
@@ -0,0 +1,5 @@
1
+ """Allow running as: python -m skill_blast"""
2
+ from skill_blast.cli import main
3
+
4
+ if __name__ == "__main__":
5
+ main()
skill_blast/agents.py ADDED
@@ -0,0 +1,108 @@
1
+ """
2
+ Cross-platform AI agent definitions.
3
+ Each agent has: display name, skill directory, detection logic.
4
+ """
5
+
6
+ import os
7
+ import platform
8
+ import shutil
9
+ from pathlib import Path
10
+ from typing import Callable
11
+
12
+ HOME = Path.home()
13
+ IS_WINDOWS = platform.system() == "Windows"
14
+ IS_MAC = platform.system() == "Darwin"
15
+
16
+
17
+ def _appdata() -> Path:
18
+ """Windows %APPDATA%, macOS ~/Library/Application Support, else HOME."""
19
+ if IS_WINDOWS:
20
+ return Path(os.environ.get("APPDATA", HOME / "AppData" / "Roaming"))
21
+ if IS_MAC:
22
+ return HOME / "Library" / "Application Support"
23
+ return HOME
24
+
25
+
26
+ def _has_cmd(*cmds: str) -> bool:
27
+ return any(shutil.which(c) for c in cmds)
28
+
29
+
30
+ def _dir_exists(*paths: Path) -> bool:
31
+ return any(p.exists() for p in paths)
32
+
33
+
34
+ # ── Agent registry ─────────────────────────────────────────────────────────────
35
+ # skills_dir: where skill folders are symlinked/copied for that agent
36
+ # detect: returns True if the agent appears to be installed
37
+
38
+ AGENTS: dict[str, dict] = {
39
+ "claude-code": {
40
+ "name": "Claude Code",
41
+ "icon": "🤖",
42
+ "skills_dir": HOME / ".claude" / "skills",
43
+ "detect": lambda: _dir_exists(HOME / ".claude") or _has_cmd("claude"),
44
+ "docs": "https://docs.anthropic.com/en/docs/claude-code",
45
+ "note": "Load via /skills in Claude Code",
46
+ },
47
+ "opencode": {
48
+ "name": "OpenCode",
49
+ "icon": "⚡",
50
+ "skills_dir": HOME / ".opencode" / "skills",
51
+ "detect": lambda: _dir_exists(HOME / ".opencode") or _has_cmd("opencode"),
52
+ "docs": "https://opencode.ai",
53
+ "note": "Load via /plugin in OpenCode",
54
+ },
55
+ "antigravity": {
56
+ "name": "Antigravity",
57
+ "icon": "🚀",
58
+ "skills_dir": HOME / ".antigravity" / "skills",
59
+ "detect": lambda: _dir_exists(HOME / ".antigravity") or _has_cmd("antigravity"),
60
+ "docs": "https://antigravity.dev",
61
+ "note": "Load via /skills in Antigravity",
62
+ },
63
+ "gemini-cli": {
64
+ "name": "Gemini CLI",
65
+ "icon": "♊",
66
+ "skills_dir": HOME / ".gemini" / "skills",
67
+ "detect": lambda: _dir_exists(HOME / ".gemini") or _has_cmd("gemini", "gemini-cli"),
68
+ "docs": "https://github.com/google-gemini/gemini-cli",
69
+ "note": "Skills auto-loaded from ~/.gemini/skills/",
70
+ },
71
+ "cursor": {
72
+ "name": "Cursor",
73
+ "icon": "🖱️",
74
+ "skills_dir": HOME / ".cursor" / "skills",
75
+ "detect": lambda: _dir_exists(HOME / ".cursor") or _has_cmd("cursor"),
76
+ "docs": "https://cursor.com",
77
+ "note": "Skills available in Cursor agent mode",
78
+ },
79
+ "windsurf": {
80
+ "name": "Windsurf",
81
+ "icon": "🏄",
82
+ "skills_dir": HOME / ".codeium" / "windsurf" / "skills",
83
+ "detect": lambda: _dir_exists(HOME / ".codeium") or _has_cmd("windsurf"),
84
+ "docs": "https://codeium.com/windsurf",
85
+ "note": "Load via Cascade in Windsurf",
86
+ },
87
+ "continue": {
88
+ "name": "Continue.dev",
89
+ "icon": "🔄",
90
+ "skills_dir": HOME / ".continue" / "skills",
91
+ "detect": lambda: _dir_exists(HOME / ".continue") or _has_cmd("continue"),
92
+ "docs": "https://continue.dev",
93
+ "note": "Skills available in Continue sidebar",
94
+ },
95
+ "aider": {
96
+ "name": "Aider",
97
+ "icon": "✏️",
98
+ "skills_dir": HOME / ".aider" / "skills",
99
+ "detect": lambda: _has_cmd("aider"),
100
+ "docs": "https://aider.chat",
101
+ "note": "Reference skill files in your chat",
102
+ },
103
+ }
104
+
105
+
106
+ def detect_agents() -> dict[str, dict]:
107
+ """Return agents that appear to be installed on this machine."""
108
+ return {k: v for k, v in AGENTS.items() if v["detect"]()}
skill_blast/cli.py ADDED
@@ -0,0 +1,528 @@
1
+ """
2
+ skill-blast CLI — works for both developers (flags) and non-developers (wizard).
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import argparse
8
+ import sys
9
+ import platform
10
+ from pathlib import Path
11
+
12
+ from rich.console import Console
13
+ from rich.panel import Panel
14
+ from rich.progress import (BarColumn, Progress, SpinnerColumn,
15
+ TaskProgressColumn, TextColumn, TimeElapsedColumn)
16
+ from rich.prompt import Confirm, Prompt
17
+ from rich.table import Table
18
+ from rich.text import Text
19
+
20
+ from . import __version__
21
+ from .agents import AGENTS, detect_agents
22
+ from .installer import (check_git, install_skill, uninstall_skill,
23
+ health_check, batch_update_repos, CACHE_DIR, STORE_DIR)
24
+ from .skills import (ALL_SKILLS, CATEGORIES, CATEGORY_COLORS,
25
+ CATEGORY_ICONS, Skill, SKILLS_BY_ID)
26
+
27
+ console = Console()
28
+
29
+
30
+ # ── UI helpers ─────────────────────────────────────────────────────────────────
31
+
32
+ BANNER = """[bold cyan]
33
+ ███████╗██╗ ██╗██╗██╗ ██╗ ██████╗ ██╗ █████╗ ███████╗████████╗
34
+ ██╔════╝██║ ██╔╝██║██║ ██║ ██╔══██╗██║ ██╔══██╗██╔════╝╚══██╔══╝
35
+ ███████╗█████╔╝ ██║██║ ██║ █████╗██████╔╝██║ ███████║███████╗ ██║
36
+ ╚════██║██╔═██╗ ██║██║ ██║ ╚════╝██╔══██╗██║ ██╔══██║╚════██║ ██║
37
+ ███████║██║ ██╗██║███████╗███████╗ ██████╔╝███████╗██║ ██║███████║ ██║
38
+ ╚══════╝╚═╝ ╚═╝╚═╝╚══════╝╚══════╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝╚══════╝ ╚═╝
39
+ [/bold cyan]"""
40
+
41
+
42
+ def print_banner() -> None:
43
+ console.print(BANNER)
44
+ console.print(
45
+ "[dim] One-click installer for 50 top AI agent skills[/]"
46
+ " · [cyan]github.com/Venkatesh-6921/skill-blast[/]\n"
47
+ )
48
+
49
+
50
+ def _cat_display(cat: str) -> str:
51
+ icon = CATEGORY_ICONS.get(cat, "•")
52
+ color = CATEGORY_COLORS.get(cat, "white")
53
+ return f"[{color}]{icon} {cat}[/]"
54
+
55
+
56
+ # ── Wizard (non-developer mode) ────────────────────────────────────────────────
57
+
58
+ def run_wizard() -> tuple[list[str], list[str], bool]:
59
+ """
60
+ Interactive step-by-step wizard.
61
+ Returns: (agent_keys, categories, dry_run)
62
+ """
63
+ console.print(Panel(
64
+ "👋 [bold]Welcome![/] This wizard will walk you through installing AI skills.\n"
65
+ " No coding knowledge needed. Just answer a few questions.\n\n"
66
+ " [dim]Press Ctrl+C at any time to cancel.[/]",
67
+ border_style="cyan",
68
+ ))
69
+ console.print()
70
+
71
+ # ── Step 1: Agents ─────────────────────────────────────────────────────────
72
+ detected = detect_agents()
73
+ console.print("[bold]Step 1 of 3[/] — [cyan]Which AI agents do you use?[/]\n")
74
+
75
+ console.print(" Detected on your machine:")
76
+ if detected:
77
+ for key, agent in detected.items():
78
+ console.print(f" [green]✓[/] {agent['icon']} {agent['name']}")
79
+ else:
80
+ console.print(" [yellow]None detected automatically[/]")
81
+ console.print()
82
+
83
+ console.print(" All available agents:")
84
+ agent_keys = list(AGENTS.keys())
85
+ for i, (key, agent) in enumerate(AGENTS.items(), 1):
86
+ mark = "[green]detected[/]" if key in detected else "[dim]not detected[/]"
87
+ console.print(f" [bold]{i}[/]. {agent['icon']} {agent['name']:20} {mark}")
88
+
89
+ console.print()
90
+ console.print(" [dim]Enter numbers separated by commas. Press ENTER to use detected agents.[/]")
91
+ raw = Prompt.ask(" Your choice", default=",".join(str(i) for i, k in enumerate(agent_keys, 1) if k in detected) or "1")
92
+
93
+ chosen_agents: list[str] = []
94
+ if raw.strip().lower() in ("all", "a"):
95
+ chosen_agents = agent_keys
96
+ else:
97
+ for part in raw.split(","):
98
+ try:
99
+ idx = int(part.strip()) - 1
100
+ if 0 <= idx < len(agent_keys):
101
+ chosen_agents.append(agent_keys[idx])
102
+ except ValueError:
103
+ pass
104
+
105
+ if not chosen_agents:
106
+ chosen_agents = list(detected.keys()) or ["claude-code"]
107
+
108
+ console.print()
109
+ console.print("[green]✓[/] Will install to: " + ", ".join(AGENTS[k]["name"] for k in chosen_agents))
110
+ console.print()
111
+
112
+ # ── Step 2: Categories ─────────────────────────────────────────────────────
113
+ console.print("[bold]Step 2 of 3[/] — [cyan]Which skill categories do you want?[/]\n")
114
+
115
+ cats = sorted(CATEGORIES)
116
+ for i, cat in enumerate(cats, 1):
117
+ count = sum(1 for s in ALL_SKILLS if s.category == cat)
118
+ icon = CATEGORY_ICONS.get(cat, "•")
119
+ color = CATEGORY_COLORS.get(cat, "white")
120
+ console.print(f" [bold]{i}[/]. [{color}]{icon} {cat:20}[/] [dim]({count} skills)[/]")
121
+
122
+ console.print()
123
+ console.print(" [dim]Enter numbers, 'all' for everything, or ENTER for all.[/]")
124
+ raw2 = Prompt.ask(" Your choice", default="all")
125
+
126
+ chosen_cats: list[str] = []
127
+ if raw2.strip().lower() in ("all", "a", ""):
128
+ chosen_cats = cats
129
+ else:
130
+ for part in raw2.split(","):
131
+ try:
132
+ idx = int(part.strip()) - 1
133
+ if 0 <= idx < len(cats):
134
+ chosen_cats.append(cats[idx])
135
+ except ValueError:
136
+ pass
137
+
138
+ if not chosen_cats:
139
+ chosen_cats = cats
140
+
141
+ skill_count = sum(1 for s in ALL_SKILLS if s.category in chosen_cats)
142
+ console.print()
143
+ console.print(f"[green]✓[/] Will install [bold]{skill_count}[/] skills from: " + ", ".join(chosen_cats))
144
+ console.print()
145
+
146
+ # ── Step 3: Confirm ────────────────────────────────────────────────────────
147
+ console.print("[bold]Step 3 of 3[/] — [cyan]Ready to install?[/]\n")
148
+ console.print(f" • [bold]{skill_count}[/] skills")
149
+ console.print(f" • Target agents: [bold]{', '.join(AGENTS[k]['name'] for k in chosen_agents)}[/]")
150
+ console.print(f" • Cache location: [dim]{CACHE_DIR}[/]")
151
+ console.print()
152
+
153
+ dry = Confirm.ask(" Preview only (dry run, no files written)?", default=False)
154
+ console.print()
155
+
156
+ if not dry:
157
+ go = Confirm.ask(" [bold green]Start installation?[/]", default=True)
158
+ if not go:
159
+ console.print("[yellow]Cancelled.[/]")
160
+ sys.exit(0)
161
+
162
+ return chosen_agents, chosen_cats, dry
163
+
164
+
165
+ # ── List all skills ─────────────────────────────────────────────────────────────
166
+
167
+ def cmd_list(category_filter: list[str] | None = None) -> None:
168
+ skills = ALL_SKILLS if not category_filter else [s for s in ALL_SKILLS if s.category in category_filter]
169
+ skills = sorted(skills, key=lambda x: (x.category, x.id))
170
+
171
+ table = Table(
172
+ title=f"[bold]skill-blast — {len(skills)} Skills[/]",
173
+ border_style="bright_black",
174
+ show_lines=False,
175
+ expand=True,
176
+ )
177
+ table.add_column("ID", style="dim", width=4, no_wrap=True)
178
+ table.add_column("Category", width=14)
179
+ table.add_column("Name", width=24, style="bold")
180
+ table.add_column("Repo", style="dim cyan", width=34)
181
+ table.add_column("Description")
182
+
183
+ prev_cat = ""
184
+ for s in skills:
185
+ cat_display = ""
186
+ if s.category != prev_cat:
187
+ cat_display = _cat_display(s.category)
188
+ prev_cat = s.category
189
+ table.add_row(str(s.id), cat_display, s.name, s.repo, s.desc)
190
+
191
+ console.print(table)
192
+ console.print(f"\n[dim]Total: {len(skills)} skills across {len(CATEGORIES)} categories[/]")
193
+
194
+
195
+ # ── Uninstall ───────────────────────────────────────────────────────────────────
196
+
197
+ def run_uninstall(
198
+ skills: list[Skill],
199
+ agent_dirs: list[Path],
200
+ ) -> list[dict]:
201
+ results: list[dict] = []
202
+
203
+ with Progress(
204
+ SpinnerColumn(),
205
+ TextColumn("[progress.description]{task.description:<60}"),
206
+ BarColumn(),
207
+ TaskProgressColumn(),
208
+ TimeElapsedColumn(),
209
+ console=console,
210
+ transient=False,
211
+ ) as progress:
212
+ task = progress.add_task("[red]Uninstalling…", total=len(skills))
213
+
214
+ for skill in skills:
215
+ color = CATEGORY_COLORS.get(skill.category, "white")
216
+ progress.update(
217
+ task,
218
+ description=(
219
+ f"[{color}]{CATEGORY_ICONS.get(skill.category, '•')} {skill.category:10}[/] "
220
+ f"[bold]{skill.name}[/]"
221
+ ),
222
+ )
223
+ result = uninstall_skill(skill, agent_dirs)
224
+ results.append(result)
225
+
226
+ if result["ok"]:
227
+ removed = ", ".join(result["removed"])
228
+ console.log(
229
+ f" [green]✓[/] [bold]{skill.name}[/] removed"
230
+ + (f" ← {removed}" if removed else " (not installed)")
231
+ )
232
+ else:
233
+ console.log(f" [red]✗[/] [bold]{skill.name}[/] [red]{result['error']}[/]")
234
+
235
+ progress.advance(task)
236
+
237
+ return results
238
+
239
+
240
+ def print_uninstall_summary(results: list[dict]) -> None:
241
+ ok = [r for r in results if r["ok"]]
242
+ removed_count = sum(len(r["removed"]) for r in ok)
243
+ failed = [r for r in results if not r["ok"]]
244
+
245
+ console.print()
246
+ console.print(Panel(
247
+ f"[bold green]✓ {len(ok)} processed[/] "
248
+ f"[dim]{removed_count} links removed[/] "
249
+ + (f"[bold red]✗ {len(failed)} failed[/]" if failed else "[green]0 failures[/]"),
250
+ title="[bold]Uninstall Summary[/]",
251
+ border_style="bright_black",
252
+ ))
253
+
254
+
255
+ # ── Health check ────────────────────────────────────────────────────────────────
256
+
257
+ def cmd_check() -> None:
258
+ """Run diagnostics on the skill-blast installation."""
259
+ agent_dirs = [a["skills_dir"] for a in AGENTS.values()]
260
+ report = health_check(agent_dirs)
261
+
262
+ console.print(Panel(
263
+ "[bold]skill-blast health check[/]",
264
+ border_style="cyan",
265
+ ))
266
+
267
+ # Git
268
+ git_icon = "[green]✓[/]" if report["git_ok"] else "[red]✗[/]"
269
+ console.print(f" {git_icon} git: {report['git_msg']}")
270
+
271
+ # Cache
272
+ cache_icon = "[green]✓[/]" if report["cache_exists"] else "[yellow]○[/]"
273
+ console.print(f" {cache_icon} Cache: {CACHE_DIR} ({report['cached_repos']} repos)")
274
+
275
+ # Store
276
+ store_icon = "[green]✓[/]" if report["store_exists"] else "[yellow]○[/]"
277
+ console.print(f" {store_icon} Store: {STORE_DIR} ({report['stored_skills']} skills)")
278
+
279
+ # Links
280
+ console.print(f" [green]✓[/] Healthy links: {report['healthy_links']}")
281
+
282
+ if report["broken_links"]:
283
+ console.print(f" [red]✗[/] Broken symlinks: {len(report['broken_links'])}")
284
+ for link in report["broken_links"]:
285
+ console.print(f" [red]→[/] {link}")
286
+ console.print()
287
+ console.print(" [dim]Tip: run [/][bold]skill-blast --update[/][dim] to repair broken links.[/]")
288
+ else:
289
+ console.print(f" [green]✓[/] No broken symlinks")
290
+
291
+ console.print()
292
+
293
+
294
+ # ── Progress install ────────────────────────────────────────────────────────────
295
+
296
+ def run_install(
297
+ skills: list[Skill],
298
+ agent_dirs: list[Path],
299
+ dry_run: bool,
300
+ update: bool,
301
+ ) -> list[dict]:
302
+ results: list[dict] = []
303
+
304
+ with Progress(
305
+ SpinnerColumn(),
306
+ TextColumn("[progress.description]{task.description:<60}"),
307
+ BarColumn(),
308
+ TaskProgressColumn(),
309
+ TimeElapsedColumn(),
310
+ console=console,
311
+ transient=False,
312
+ ) as progress:
313
+ task = progress.add_task("[cyan]Installing…", total=len(skills))
314
+
315
+ for skill in skills:
316
+ color = CATEGORY_COLORS.get(skill.category, "white")
317
+ progress.update(
318
+ task,
319
+ description=(
320
+ f"[{color}]{CATEGORY_ICONS.get(skill.category, '•')} {skill.category:10}[/] "
321
+ f"[bold]{skill.name}[/]"
322
+ ),
323
+ )
324
+ result = install_skill(skill, agent_dirs, dry_run=dry_run, update=update)
325
+ results.append(result)
326
+
327
+ if result["ok"]:
328
+ msg = result["repo_msg"]
329
+ linked = ", ".join(result["linked"])
330
+ console.log(
331
+ f" [green]✓[/] [bold]{skill.name}[/] "
332
+ f"[dim]({msg})[/]"
333
+ + (f" → {linked}" if linked else "")
334
+ )
335
+ else:
336
+ console.log(f" [red]✗[/] [bold]{skill.name}[/] [red]{result['error']}[/]")
337
+
338
+ progress.advance(task)
339
+
340
+ return results
341
+
342
+
343
+ # ── Summary ─────────────────────────────────────────────────────────────────────
344
+
345
+ def print_summary(results: list[dict], agent_names: list[str]) -> None:
346
+ ok = [r for r in results if r["ok"]]
347
+ failed = [r for r in results if not r["ok"]]
348
+ already = sum(len(r["already"]) for r in ok)
349
+
350
+ console.print()
351
+ console.print(Panel(
352
+ f"[bold green]✓ {len(ok)} installed[/] "
353
+ f"[dim]{already} already existed[/] "
354
+ + (f"[bold red]✗ {len(failed)} failed[/]" if failed else "[green]0 failures[/]"),
355
+ title="[bold]Installation Summary[/]",
356
+ border_style="bright_black",
357
+ ))
358
+
359
+ if failed:
360
+ table = Table(title="[red]Failed Skills[/]", border_style="red", show_lines=False)
361
+ table.add_column("ID", width=4, style="dim")
362
+ table.add_column("Name", style="bold red")
363
+ table.add_column("Reason")
364
+ for r in failed:
365
+ table.add_row(str(r["skill"].id), r["skill"].name, r["error"] or "?")
366
+ console.print(table)
367
+
368
+ console.print()
369
+ console.print(Panel(
370
+ f"[bold]Cache:[/] {CACHE_DIR}\n"
371
+ f"[bold]Store:[/] {STORE_DIR}\n\n"
372
+ "[bold]Next steps by agent:[/]\n"
373
+ + "\n".join(
374
+ f" {AGENTS[k]['icon']} [cyan]{AGENTS[k]['name']:15}[/] {AGENTS[k]['note']}"
375
+ for k in agent_names
376
+ if k in AGENTS
377
+ )
378
+ + "\n\n[dim]Run [/][bold]skill-blast --update[/][dim] anytime to pull the latest skill versions.[/]",
379
+ title="[bold green]Done! 🎉[/]",
380
+ border_style="green",
381
+ ))
382
+
383
+
384
+ # ── Entry point ─────────────────────────────────────────────────────────────────
385
+
386
+ def main() -> None:
387
+ parser = argparse.ArgumentParser(
388
+ prog="skill-blast",
389
+ description="One-click installer for 50 top AI agent skills",
390
+ formatter_class=argparse.RawDescriptionHelpFormatter,
391
+ epilog="""
392
+ Examples:
393
+ skill-blast # interactive wizard (recommended for beginners)
394
+ skill-blast --all # install everything, auto-detect agents
395
+ skill-blast --agents claude-code opencode
396
+ skill-blast --only Engineering "UI/Design"
397
+ skill-blast --skip 41 49 50
398
+ skill-blast --update # pull latest for cached repos
399
+ skill-blast --dry-run # preview without writing files
400
+ skill-blast --list # show all 50 skills
401
+ skill-blast --list --only Marketing # filter list by category
402
+ skill-blast --uninstall # remove all installed skills
403
+ skill-blast --uninstall --only Media # remove only Media skills
404
+ skill-blast --check # run health diagnostics
405
+ """,
406
+ )
407
+ parser.add_argument("--version", action="version",
408
+ version=f"%(prog)s {__version__}")
409
+ parser.add_argument("--agents", nargs="+", choices=list(AGENTS), metavar="AGENT",
410
+ help="Target specific agents (default: auto-detect)")
411
+ parser.add_argument("--all", action="store_true",
412
+ help="Install all skills to all detected agents")
413
+ parser.add_argument("--only", nargs="+", metavar="CATEGORY",
414
+ help=f"Install only these categories: {', '.join(CATEGORIES)}")
415
+ parser.add_argument("--skip", nargs="+", type=int, metavar="ID",
416
+ help="Skill IDs to skip")
417
+ parser.add_argument("--update", action="store_true",
418
+ help="Pull latest for already-cached repos")
419
+ parser.add_argument("--dry-run", action="store_true",
420
+ help="Preview without writing any files")
421
+ parser.add_argument("--list", action="store_true",
422
+ help="List all skills and exit")
423
+ parser.add_argument("--uninstall", action="store_true",
424
+ help="Remove installed skills from agent directories")
425
+ parser.add_argument("--check", action="store_true",
426
+ help="Run health diagnostics on your skill-blast installation")
427
+ parser.add_argument("--force-agents", action="store_true",
428
+ help="Install to ALL agents even if not detected")
429
+ parser.add_argument("--no-wizard", action="store_true",
430
+ help="Skip interactive wizard; use flags only")
431
+ args = parser.parse_args()
432
+
433
+ # ── List only ──────────────────────────────────────────────────────────────
434
+ if args.list:
435
+ print_banner()
436
+ cmd_list(category_filter=args.only)
437
+ return
438
+
439
+ # ── Health check ──────────────────────────────────────────────────────────
440
+ if args.check:
441
+ print_banner()
442
+ cmd_check()
443
+ return
444
+
445
+ print_banner()
446
+
447
+ # ── Git check ──────────────────────────────────────────────────────────────
448
+ git_ok, git_msg = check_git()
449
+ if not git_ok:
450
+ console.print(Panel(
451
+ "[bold red]git is not installed or not in PATH.[/]\n\n"
452
+ "Please install git first:\n"
453
+ " • [cyan]Windows[/]: https://git-scm.com/download/win\n"
454
+ " • [cyan]macOS[/]: brew install git (or via Xcode tools)\n"
455
+ " • [cyan]Linux[/]: sudo apt install git / sudo dnf install git\n\n"
456
+ "Then re-run skill-blast.",
457
+ title="[red]Missing dependency[/]",
458
+ border_style="red",
459
+ ))
460
+ sys.exit(1)
461
+
462
+ # ── Decide: wizard or flags ────────────────────────────────────────────────
463
+ use_wizard = not (args.all or args.agents or args.only or args.skip
464
+ or args.dry_run or args.no_wizard or args.uninstall)
465
+
466
+ if use_wizard:
467
+ # Non-developer path
468
+ chosen_agent_keys, chosen_cats, dry_run = run_wizard()
469
+ else:
470
+ # Developer / flag path
471
+ dry_run = args.dry_run
472
+
473
+ if args.agents:
474
+ chosen_agent_keys = args.agents
475
+ elif args.force_agents:
476
+ chosen_agent_keys = list(AGENTS.keys())
477
+ else:
478
+ detected = detect_agents()
479
+ chosen_agent_keys = list(detected.keys()) or ["claude-code"]
480
+ if chosen_agent_keys == ["claude-code"]:
481
+ console.print("[yellow]No agents detected — defaulting to Claude Code.[/]\n")
482
+
483
+ chosen_cats = args.only if args.only else CATEGORIES
484
+
485
+ # ── Filter skills ──────────────────────────────────────────────────────────
486
+ skills = [s for s in ALL_SKILLS if s.category in chosen_cats]
487
+ if args.skip:
488
+ skills = [s for s in skills if s.id not in args.skip]
489
+
490
+ agent_dirs = [AGENTS[k]["skills_dir"] for k in chosen_agent_keys]
491
+
492
+ # ── Status line ────────────────────────────────────────────────────────────
493
+ console.print(f"[bold]Agents:[/] {', '.join(AGENTS[k]['name'] for k in chosen_agent_keys)}")
494
+ console.print(f"[bold]Skills:[/] {len(skills)} "
495
+ f"[dim]({', '.join(chosen_cats)})[/]")
496
+ if dry_run:
497
+ console.print("[bold yellow]DRY RUN — no files will be written[/]")
498
+ console.print()
499
+
500
+ # ── Uninstall path ─────────────────────────────────────────────────────────
501
+ if args.uninstall:
502
+ console.print(f"[bold red]Uninstalling {len(skills)} skills…[/]\n")
503
+ results = run_uninstall(skills, agent_dirs)
504
+ print_uninstall_summary(results)
505
+ return
506
+
507
+ # ── Concurrent repo update ─────────────────────────────────────────────────
508
+ if args.update and not dry_run:
509
+ unique_repos = sorted(set(s.repo for s in skills))
510
+ console.print(f"[cyan]Updating {len(unique_repos)} repos concurrently (4 threads)…[/]\n")
511
+ repo_results = batch_update_repos(unique_repos, max_workers=4)
512
+ ok_count = sum(1 for ok, _ in repo_results.values() if ok)
513
+ fail_count = len(repo_results) - ok_count
514
+ console.print(
515
+ f" [green]✓ {ok_count} repos updated[/]"
516
+ + (f" [red]✗ {fail_count} failed[/]" if fail_count else "")
517
+ + "\n"
518
+ )
519
+
520
+ # ── Install ────────────────────────────────────────────────────────────────
521
+ results = run_install(skills, agent_dirs, dry_run=dry_run, update=False if args.update else False)
522
+
523
+ # ── Summary ────────────────────────────────────────────────────────────────
524
+ print_summary(results, chosen_agent_keys)
525
+
526
+
527
+ if __name__ == "__main__":
528
+ main()