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.
- skill_blast/__init__.py +4 -0
- skill_blast/__main__.py +5 -0
- skill_blast/agents.py +108 -0
- skill_blast/cli.py +528 -0
- skill_blast/installer.py +274 -0
- skill_blast/skills.py +266 -0
- skill_blast-1.0.0.dist-info/METADATA +398 -0
- skill_blast-1.0.0.dist-info/RECORD +11 -0
- skill_blast-1.0.0.dist-info/WHEEL +4 -0
- skill_blast-1.0.0.dist-info/entry_points.txt +2 -0
- skill_blast-1.0.0.dist-info/licenses/LICENSE +21 -0
skill_blast/__init__.py
ADDED
skill_blast/__main__.py
ADDED
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()
|