agent-skill-manager 0.1.0__py3-none-any.whl → 0.1.1__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.
- {agent_skill_manager-0.1.0.dist-info → agent_skill_manager-0.1.1.dist-info}/METADATA +55 -19
- agent_skill_manager-0.1.1.dist-info/RECORD +12 -0
- skill_manager/__init__.py +79 -0
- skill_manager/agents.py +137 -0
- skill_manager/cli.py +1178 -0
- skill_manager/deployment.py +260 -0
- skill_manager/github.py +177 -0
- skill_manager/metadata.py +141 -0
- skill_manager/removal.py +278 -0
- skill_manager/validation.py +83 -0
- agent_skill_manager-0.1.0.dist-info/RECORD +0 -4
- {agent_skill_manager-0.1.0.dist-info → agent_skill_manager-0.1.1.dist-info}/WHEEL +0 -0
- {agent_skill_manager-0.1.0.dist-info → agent_skill_manager-0.1.1.dist-info}/entry_points.txt +0 -0
skill_manager/cli.py
ADDED
|
@@ -0,0 +1,1178 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Skill Manager - CLI tool for managing AI agent skills.
|
|
4
|
+
|
|
5
|
+
Usage: sm <command> or skill-manager <command>
|
|
6
|
+
|
|
7
|
+
Commands:
|
|
8
|
+
download - Download skills from GitHub
|
|
9
|
+
deploy - Deploy local skills to agents
|
|
10
|
+
install - Download and deploy skills in one step
|
|
11
|
+
uninstall - Remove skills from agents (safe delete)
|
|
12
|
+
restore - Restore deleted skills from trash
|
|
13
|
+
update - Update skills from GitHub
|
|
14
|
+
update --all - Update all skills from GitHub
|
|
15
|
+
list - List installed skills and versions
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import shutil
|
|
19
|
+
import sys
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from InquirerPy import inquirer
|
|
23
|
+
from InquirerPy.base.control import Choice
|
|
24
|
+
from InquirerPy.separator import Separator
|
|
25
|
+
from rich.console import Console
|
|
26
|
+
from rich.panel import Panel
|
|
27
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
28
|
+
from rich.table import Table
|
|
29
|
+
|
|
30
|
+
from .agents import AGENTS, detect_existing_agents, get_agent_name, get_agent_path
|
|
31
|
+
from .deployment import (
|
|
32
|
+
deploy_multiple_skills,
|
|
33
|
+
deploy_skill_to_agents,
|
|
34
|
+
update_all_skills,
|
|
35
|
+
update_skill,
|
|
36
|
+
)
|
|
37
|
+
from .github import download_skill_from_github, parse_github_url
|
|
38
|
+
from .metadata import (
|
|
39
|
+
list_updatable_skills,
|
|
40
|
+
read_skill_metadata,
|
|
41
|
+
save_skill_metadata,
|
|
42
|
+
)
|
|
43
|
+
from .removal import (
|
|
44
|
+
clean_trash,
|
|
45
|
+
hard_delete_skill,
|
|
46
|
+
list_installed_skills,
|
|
47
|
+
list_trashed_skills,
|
|
48
|
+
restore_skill,
|
|
49
|
+
soft_delete_skill,
|
|
50
|
+
)
|
|
51
|
+
from .validation import (
|
|
52
|
+
get_project_root,
|
|
53
|
+
get_skill_name,
|
|
54
|
+
scan_available_skills,
|
|
55
|
+
validate_skill,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
console = Console()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def select_agents(existing_agents: dict[str, Path]) -> list[str]:
|
|
62
|
+
"""
|
|
63
|
+
Interactive prompt to select agents for deployment.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
existing_agents: Dictionary of detected agents
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
List of selected agent IDs
|
|
70
|
+
"""
|
|
71
|
+
choices = []
|
|
72
|
+
|
|
73
|
+
if existing_agents:
|
|
74
|
+
choices.append(Separator("--- Detected Agents ---"))
|
|
75
|
+
for agent_id in sorted(existing_agents.keys()):
|
|
76
|
+
info = AGENTS[agent_id]
|
|
77
|
+
choices.append(
|
|
78
|
+
Choice(
|
|
79
|
+
value=agent_id,
|
|
80
|
+
name=f"{info['name']} ({existing_agents[agent_id]})",
|
|
81
|
+
enabled=True,
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
other_agents = [aid for aid in AGENTS.keys() if aid not in existing_agents]
|
|
86
|
+
if other_agents:
|
|
87
|
+
choices.append(Separator("--- Other Available Agents ---"))
|
|
88
|
+
for agent_id in sorted(other_agents):
|
|
89
|
+
info = AGENTS[agent_id]
|
|
90
|
+
global_path = Path(info["global"]).expanduser()
|
|
91
|
+
choices.append(
|
|
92
|
+
Choice(
|
|
93
|
+
value=agent_id,
|
|
94
|
+
name=f"{info['name']} ({global_path})",
|
|
95
|
+
enabled=False,
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
selected = inquirer.checkbox(
|
|
100
|
+
message="Select agents to deploy to:",
|
|
101
|
+
choices=choices,
|
|
102
|
+
instruction="(Space to select, Enter to confirm)",
|
|
103
|
+
).execute()
|
|
104
|
+
|
|
105
|
+
return selected
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def select_deployment_type() -> str:
|
|
109
|
+
"""
|
|
110
|
+
Interactive prompt to select deployment type.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Either "global" or "project"
|
|
114
|
+
"""
|
|
115
|
+
return inquirer.select(
|
|
116
|
+
message="Select deployment location:",
|
|
117
|
+
choices=[
|
|
118
|
+
Choice(value="global", name="Global directory (recommended) - Available to all projects"),
|
|
119
|
+
Choice(value="project", name="Project directory - Current project only"),
|
|
120
|
+
],
|
|
121
|
+
default="global",
|
|
122
|
+
).execute()
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def select_skills(available_skills: list[Path]) -> list[Path]:
|
|
126
|
+
"""
|
|
127
|
+
Interactive prompt to select skills for deployment.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
available_skills: List of available skill paths
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
List of selected skill paths
|
|
134
|
+
"""
|
|
135
|
+
if not available_skills:
|
|
136
|
+
console.print("[yellow]No skills found[/yellow]")
|
|
137
|
+
return []
|
|
138
|
+
|
|
139
|
+
# Group by category
|
|
140
|
+
categories = {}
|
|
141
|
+
for skill in available_skills:
|
|
142
|
+
parts = skill.parts
|
|
143
|
+
if len(parts) > 1:
|
|
144
|
+
category = parts[0]
|
|
145
|
+
skill_name = "/".join(parts[1:])
|
|
146
|
+
else:
|
|
147
|
+
category = "Other"
|
|
148
|
+
skill_name = str(skill)
|
|
149
|
+
|
|
150
|
+
if category not in categories:
|
|
151
|
+
categories[category] = []
|
|
152
|
+
categories[category].append((skill, skill_name))
|
|
153
|
+
|
|
154
|
+
# Build choice list
|
|
155
|
+
choices = []
|
|
156
|
+
for category in sorted(categories.keys()):
|
|
157
|
+
choices.append(Separator(f"--- {category.upper()} ---"))
|
|
158
|
+
for skill, skill_name in sorted(categories[category], key=lambda x: x[1]):
|
|
159
|
+
choices.append(
|
|
160
|
+
Choice(
|
|
161
|
+
value=skill,
|
|
162
|
+
name=skill_name,
|
|
163
|
+
enabled=False,
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
selected = inquirer.checkbox(
|
|
168
|
+
message="Select skills to deploy:",
|
|
169
|
+
choices=choices,
|
|
170
|
+
instruction="(Space to select, Enter to confirm)",
|
|
171
|
+
).execute()
|
|
172
|
+
|
|
173
|
+
return selected
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def cmd_download() -> int:
|
|
177
|
+
"""
|
|
178
|
+
Download command - Download skills from GitHub.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Exit code (0 for success, non-zero for failure)
|
|
182
|
+
"""
|
|
183
|
+
console.print(
|
|
184
|
+
Panel.fit(
|
|
185
|
+
"[bold cyan]Download Skill[/bold cyan]\n"
|
|
186
|
+
"Download a skill from GitHub",
|
|
187
|
+
border_style="cyan",
|
|
188
|
+
)
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
# Get GitHub URL
|
|
192
|
+
url = inquirer.text(
|
|
193
|
+
message="Enter GitHub skill URL:",
|
|
194
|
+
validate=lambda x: len(x) > 0,
|
|
195
|
+
invalid_message="URL cannot be empty",
|
|
196
|
+
).execute()
|
|
197
|
+
|
|
198
|
+
console.print()
|
|
199
|
+
|
|
200
|
+
# Parse URL to show info
|
|
201
|
+
try:
|
|
202
|
+
owner, repo, branch, path = parse_github_url(url)
|
|
203
|
+
console.print(f"[dim]Repository: {owner}/{repo}[/dim]")
|
|
204
|
+
console.print(f"[dim]Branch: {branch}[/dim]")
|
|
205
|
+
console.print(f"[dim]Path: {path or '(root)'}[/dim]\n")
|
|
206
|
+
except ValueError as e:
|
|
207
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
208
|
+
return 1
|
|
209
|
+
|
|
210
|
+
# Ask where to save
|
|
211
|
+
save_to_local = inquirer.confirm(
|
|
212
|
+
message="Save to local skills/ directory?",
|
|
213
|
+
default=True,
|
|
214
|
+
).execute()
|
|
215
|
+
|
|
216
|
+
console.print()
|
|
217
|
+
|
|
218
|
+
# Determine destination
|
|
219
|
+
if save_to_local:
|
|
220
|
+
project_root = get_project_root()
|
|
221
|
+
skills_dir = project_root / "skills"
|
|
222
|
+
|
|
223
|
+
# Ask for category
|
|
224
|
+
category = inquirer.text(
|
|
225
|
+
message="Enter category name (leave empty for root):",
|
|
226
|
+
default="",
|
|
227
|
+
).execute()
|
|
228
|
+
|
|
229
|
+
if category:
|
|
230
|
+
dest_dir = skills_dir / category
|
|
231
|
+
else:
|
|
232
|
+
dest_dir = skills_dir
|
|
233
|
+
else:
|
|
234
|
+
dest_dir = Path.cwd() / ".tmp_skills"
|
|
235
|
+
|
|
236
|
+
# Download
|
|
237
|
+
try:
|
|
238
|
+
with Progress(
|
|
239
|
+
SpinnerColumn(),
|
|
240
|
+
TextColumn("[progress.description]{task.description}"),
|
|
241
|
+
console=console,
|
|
242
|
+
) as progress:
|
|
243
|
+
task = progress.add_task("Downloading skill...", total=None)
|
|
244
|
+
skill_dir, metadata = download_skill_from_github(url, dest_dir)
|
|
245
|
+
progress.update(task, completed=True)
|
|
246
|
+
|
|
247
|
+
# Save metadata
|
|
248
|
+
save_skill_metadata(
|
|
249
|
+
skill_dir,
|
|
250
|
+
url,
|
|
251
|
+
metadata["owner"],
|
|
252
|
+
metadata["repo"],
|
|
253
|
+
metadata["branch"],
|
|
254
|
+
metadata["path"],
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
# Validate
|
|
258
|
+
if not validate_skill(skill_dir):
|
|
259
|
+
console.print(
|
|
260
|
+
f"[yellow]Warning: {skill_dir} does not contain SKILL.md, may not be a valid skill[/yellow]"
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
console.print(f"[green]✓[/green] Skill downloaded to: {skill_dir}\n")
|
|
264
|
+
return 0
|
|
265
|
+
|
|
266
|
+
except Exception as e:
|
|
267
|
+
console.print(f"[red]Download failed: {e}[/red]")
|
|
268
|
+
return 1
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def cmd_deploy() -> int:
|
|
272
|
+
"""
|
|
273
|
+
Deploy command - Deploy local skills to agents.
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
Exit code (0 for success, non-zero for failure)
|
|
277
|
+
"""
|
|
278
|
+
console.print(
|
|
279
|
+
Panel.fit(
|
|
280
|
+
"[bold cyan]Deploy Skills[/bold cyan]\n"
|
|
281
|
+
"Deploy local skills to AI agents",
|
|
282
|
+
border_style="cyan",
|
|
283
|
+
)
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
# Get project root and skills directory
|
|
287
|
+
project_root = get_project_root()
|
|
288
|
+
skills_dir = project_root / "skills"
|
|
289
|
+
|
|
290
|
+
console.print(f"\n[dim]Project directory: {project_root}[/dim]")
|
|
291
|
+
console.print(f"[dim]Skills directory: {skills_dir}[/dim]\n")
|
|
292
|
+
|
|
293
|
+
# Scan for skills
|
|
294
|
+
with Progress(
|
|
295
|
+
SpinnerColumn(),
|
|
296
|
+
TextColumn("[progress.description]{task.description}"),
|
|
297
|
+
console=console,
|
|
298
|
+
) as progress:
|
|
299
|
+
task = progress.add_task("Scanning for skills...", total=None)
|
|
300
|
+
available_skills = scan_available_skills(skills_dir)
|
|
301
|
+
progress.update(task, completed=True)
|
|
302
|
+
|
|
303
|
+
if not available_skills:
|
|
304
|
+
console.print("[red]No skills found. Exiting.[/red]")
|
|
305
|
+
return 1
|
|
306
|
+
|
|
307
|
+
console.print(f"[green]✓[/green] Found {len(available_skills)} skills\n")
|
|
308
|
+
|
|
309
|
+
# Detect agents
|
|
310
|
+
existing_agents = detect_existing_agents()
|
|
311
|
+
if existing_agents:
|
|
312
|
+
console.print(
|
|
313
|
+
f"[green]✓[/green] Detected {len(existing_agents)} installed agents\n"
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
# Select deployment type
|
|
317
|
+
deployment_type = select_deployment_type()
|
|
318
|
+
console.print()
|
|
319
|
+
|
|
320
|
+
# Select agents
|
|
321
|
+
selected_agents = select_agents(existing_agents)
|
|
322
|
+
if not selected_agents:
|
|
323
|
+
console.print("[yellow]No agents selected. Exiting.[/yellow]")
|
|
324
|
+
return 0
|
|
325
|
+
|
|
326
|
+
console.print()
|
|
327
|
+
|
|
328
|
+
# Select skills
|
|
329
|
+
selected_skills = select_skills(available_skills)
|
|
330
|
+
if not selected_skills:
|
|
331
|
+
console.print("[yellow]No skills selected. Exiting.[/yellow]")
|
|
332
|
+
return 0
|
|
333
|
+
|
|
334
|
+
console.print()
|
|
335
|
+
|
|
336
|
+
# Show summary
|
|
337
|
+
table = Table(title="Deployment Plan", show_header=True, header_style="bold magenta")
|
|
338
|
+
table.add_column("Agent", style="cyan", no_wrap=True)
|
|
339
|
+
table.add_column("Target Path", style="green")
|
|
340
|
+
table.add_column("Skills Count", justify="right", style="yellow")
|
|
341
|
+
|
|
342
|
+
for agent_id in selected_agents:
|
|
343
|
+
target_path = get_agent_path(agent_id, deployment_type, project_root)
|
|
344
|
+
table.add_row(
|
|
345
|
+
get_agent_name(agent_id),
|
|
346
|
+
str(target_path),
|
|
347
|
+
str(len(selected_skills)),
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
console.print(table)
|
|
351
|
+
console.print(f"\nWill deploy {len(selected_skills)} skills:")
|
|
352
|
+
for skill in selected_skills:
|
|
353
|
+
console.print(f" • {skill}")
|
|
354
|
+
|
|
355
|
+
console.print()
|
|
356
|
+
|
|
357
|
+
# Confirm
|
|
358
|
+
confirm = inquirer.confirm(
|
|
359
|
+
message="Confirm deployment?",
|
|
360
|
+
default=True,
|
|
361
|
+
).execute()
|
|
362
|
+
|
|
363
|
+
if not confirm:
|
|
364
|
+
console.print("[yellow]Deployment cancelled.[/yellow]")
|
|
365
|
+
return 0
|
|
366
|
+
|
|
367
|
+
console.print()
|
|
368
|
+
|
|
369
|
+
# Deploy
|
|
370
|
+
with Progress(
|
|
371
|
+
SpinnerColumn(),
|
|
372
|
+
TextColumn("[progress.description]{task.description}"),
|
|
373
|
+
console=console,
|
|
374
|
+
) as progress:
|
|
375
|
+
task = progress.add_task("Deploying skills...", total=len(selected_agents) * len(selected_skills))
|
|
376
|
+
|
|
377
|
+
def progress_callback(agent_id, skill_path):
|
|
378
|
+
progress.advance(task)
|
|
379
|
+
|
|
380
|
+
total_deployed, total_failed = deploy_multiple_skills(
|
|
381
|
+
selected_skills,
|
|
382
|
+
skills_dir,
|
|
383
|
+
selected_agents,
|
|
384
|
+
deployment_type,
|
|
385
|
+
project_root,
|
|
386
|
+
progress_callback,
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
# Show results
|
|
390
|
+
console.print()
|
|
391
|
+
if total_failed == 0:
|
|
392
|
+
console.print(
|
|
393
|
+
Panel.fit(
|
|
394
|
+
f"[bold green]✓ Deployment successful![/bold green]\n\n"
|
|
395
|
+
f"Deployed {total_deployed} skills to {len(selected_agents)} agents",
|
|
396
|
+
border_style="green",
|
|
397
|
+
)
|
|
398
|
+
)
|
|
399
|
+
else:
|
|
400
|
+
console.print(
|
|
401
|
+
Panel.fit(
|
|
402
|
+
f"[bold yellow]⚠ Deployment completed with errors[/bold yellow]\n\n"
|
|
403
|
+
f"Success: {total_deployed} | Failed: {total_failed}",
|
|
404
|
+
border_style="yellow",
|
|
405
|
+
)
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
return 0 if total_failed == 0 else 1
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def cmd_install() -> int:
|
|
412
|
+
"""
|
|
413
|
+
Install command - Download and deploy skills in one step.
|
|
414
|
+
|
|
415
|
+
Returns:
|
|
416
|
+
Exit code (0 for success, non-zero for failure)
|
|
417
|
+
"""
|
|
418
|
+
console.print(
|
|
419
|
+
Panel.fit(
|
|
420
|
+
"[bold cyan]Install Skill[/bold cyan]\n"
|
|
421
|
+
"Download and deploy a skill from GitHub",
|
|
422
|
+
border_style="cyan",
|
|
423
|
+
)
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
# Get GitHub URL
|
|
427
|
+
url = inquirer.text(
|
|
428
|
+
message="Enter GitHub skill URL:",
|
|
429
|
+
validate=lambda x: len(x) > 0,
|
|
430
|
+
invalid_message="URL cannot be empty",
|
|
431
|
+
).execute()
|
|
432
|
+
|
|
433
|
+
console.print()
|
|
434
|
+
|
|
435
|
+
# Parse URL to show info
|
|
436
|
+
try:
|
|
437
|
+
owner, repo, branch, path = parse_github_url(url)
|
|
438
|
+
console.print(f"[dim]Repository: {owner}/{repo}[/dim]")
|
|
439
|
+
console.print(f"[dim]Branch: {branch}[/dim]")
|
|
440
|
+
console.print(f"[dim]Path: {path or '(root)'}[/dim]\n")
|
|
441
|
+
except ValueError as e:
|
|
442
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
443
|
+
return 1
|
|
444
|
+
|
|
445
|
+
# Ask whether to save locally
|
|
446
|
+
save_to_local = inquirer.confirm(
|
|
447
|
+
message="Save to local skills/ directory?",
|
|
448
|
+
default=True,
|
|
449
|
+
).execute()
|
|
450
|
+
|
|
451
|
+
console.print()
|
|
452
|
+
|
|
453
|
+
# Determine destination
|
|
454
|
+
if save_to_local:
|
|
455
|
+
project_root = get_project_root()
|
|
456
|
+
skills_dir = project_root / "skills"
|
|
457
|
+
|
|
458
|
+
# Ask for category
|
|
459
|
+
category = inquirer.text(
|
|
460
|
+
message="Enter category name (leave empty for root):",
|
|
461
|
+
default="",
|
|
462
|
+
).execute()
|
|
463
|
+
|
|
464
|
+
if category:
|
|
465
|
+
dest_dir = skills_dir / category
|
|
466
|
+
else:
|
|
467
|
+
dest_dir = skills_dir
|
|
468
|
+
else:
|
|
469
|
+
dest_dir = Path.cwd() / ".tmp_skills"
|
|
470
|
+
|
|
471
|
+
# Download skill
|
|
472
|
+
try:
|
|
473
|
+
with Progress(
|
|
474
|
+
SpinnerColumn(),
|
|
475
|
+
TextColumn("[progress.description]{task.description}"),
|
|
476
|
+
console=console,
|
|
477
|
+
) as progress:
|
|
478
|
+
task = progress.add_task("Downloading skill...", total=None)
|
|
479
|
+
skill_dir, metadata = download_skill_from_github(url, dest_dir)
|
|
480
|
+
progress.update(task, completed=True)
|
|
481
|
+
|
|
482
|
+
# Save metadata
|
|
483
|
+
save_skill_metadata(
|
|
484
|
+
skill_dir,
|
|
485
|
+
url,
|
|
486
|
+
metadata["owner"],
|
|
487
|
+
metadata["repo"],
|
|
488
|
+
metadata["branch"],
|
|
489
|
+
metadata["path"],
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
# Validate
|
|
493
|
+
if not validate_skill(skill_dir):
|
|
494
|
+
console.print(
|
|
495
|
+
f"[yellow]Warning: {skill_dir} does not contain SKILL.md, may not be a valid skill[/yellow]"
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
console.print(f"[green]✓[/green] Skill downloaded to: {skill_dir}\n")
|
|
499
|
+
|
|
500
|
+
except Exception as e:
|
|
501
|
+
console.print(f"[red]Download failed: {e}[/red]")
|
|
502
|
+
return 1
|
|
503
|
+
|
|
504
|
+
# Ask whether to deploy
|
|
505
|
+
should_deploy = inquirer.confirm(
|
|
506
|
+
message="Deploy to AI agents?",
|
|
507
|
+
default=True,
|
|
508
|
+
).execute()
|
|
509
|
+
|
|
510
|
+
if not should_deploy:
|
|
511
|
+
console.print("[yellow]Skipping deployment.[/yellow]")
|
|
512
|
+
return 0
|
|
513
|
+
|
|
514
|
+
console.print()
|
|
515
|
+
|
|
516
|
+
# Detect agents
|
|
517
|
+
existing_agents = detect_existing_agents()
|
|
518
|
+
if existing_agents:
|
|
519
|
+
console.print(
|
|
520
|
+
f"[green]✓[/green] Detected {len(existing_agents)} installed agents\n"
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
# Select deployment type
|
|
524
|
+
deployment_type = select_deployment_type()
|
|
525
|
+
console.print()
|
|
526
|
+
|
|
527
|
+
# Select agents
|
|
528
|
+
selected_agents = select_agents(existing_agents)
|
|
529
|
+
if not selected_agents:
|
|
530
|
+
console.print("[yellow]No agents selected.[/yellow]")
|
|
531
|
+
return 0
|
|
532
|
+
|
|
533
|
+
console.print()
|
|
534
|
+
|
|
535
|
+
# Deploy
|
|
536
|
+
success_count, fail_count = deploy_skill_to_agents(
|
|
537
|
+
skill_dir, selected_agents, deployment_type, get_project_root() if deployment_type == "project" else None
|
|
538
|
+
)
|
|
539
|
+
|
|
540
|
+
# Clean up temporary files
|
|
541
|
+
if not save_to_local and skill_dir.parent.name == ".tmp_skills":
|
|
542
|
+
shutil.rmtree(skill_dir.parent, ignore_errors=True)
|
|
543
|
+
|
|
544
|
+
console.print()
|
|
545
|
+
|
|
546
|
+
# Show results
|
|
547
|
+
if fail_count == 0:
|
|
548
|
+
console.print(
|
|
549
|
+
Panel.fit(
|
|
550
|
+
f"[bold green]✓ Installation successful![/bold green]\n\n"
|
|
551
|
+
f"Skill: {get_skill_name(skill_dir)}\n"
|
|
552
|
+
f"Deployed to {success_count} agents",
|
|
553
|
+
border_style="green",
|
|
554
|
+
)
|
|
555
|
+
)
|
|
556
|
+
else:
|
|
557
|
+
console.print(
|
|
558
|
+
Panel.fit(
|
|
559
|
+
f"[bold yellow]⚠ Installation completed with errors[/bold yellow]\n\n"
|
|
560
|
+
f"Success: {success_count} | Failed: {fail_count}",
|
|
561
|
+
border_style="yellow",
|
|
562
|
+
)
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
return 0 if fail_count == 0 else 1
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def cmd_uninstall() -> int:
|
|
569
|
+
"""
|
|
570
|
+
Uninstall command - Remove skills from agents.
|
|
571
|
+
|
|
572
|
+
Returns:
|
|
573
|
+
Exit code (0 for success, non-zero for failure)
|
|
574
|
+
"""
|
|
575
|
+
console.print(
|
|
576
|
+
Panel.fit(
|
|
577
|
+
"[bold cyan]Uninstall Skills[/bold cyan]\n"
|
|
578
|
+
"Remove skills from AI agents",
|
|
579
|
+
border_style="cyan",
|
|
580
|
+
)
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
# Detect agents
|
|
584
|
+
existing_agents = detect_existing_agents()
|
|
585
|
+
if not existing_agents:
|
|
586
|
+
console.print("[red]No agents detected. Exiting.[/red]")
|
|
587
|
+
return 1
|
|
588
|
+
|
|
589
|
+
console.print(f"\n[green]✓[/green] Detected {len(existing_agents)} installed agents\n")
|
|
590
|
+
|
|
591
|
+
# Select deployment type
|
|
592
|
+
deployment_type = select_deployment_type()
|
|
593
|
+
console.print()
|
|
594
|
+
|
|
595
|
+
# Select agents
|
|
596
|
+
selected_agents = select_agents(existing_agents)
|
|
597
|
+
if not selected_agents:
|
|
598
|
+
console.print("[yellow]No agents selected. Exiting.[/yellow]")
|
|
599
|
+
return 0
|
|
600
|
+
|
|
601
|
+
console.print()
|
|
602
|
+
|
|
603
|
+
# For each selected agent, list installed skills
|
|
604
|
+
all_skills = {}
|
|
605
|
+
for agent_id in selected_agents:
|
|
606
|
+
project_root = get_project_root() if deployment_type == "project" else None
|
|
607
|
+
skills = list_installed_skills(agent_id, deployment_type, project_root)
|
|
608
|
+
if skills:
|
|
609
|
+
all_skills[agent_id] = skills
|
|
610
|
+
|
|
611
|
+
if not all_skills:
|
|
612
|
+
console.print("[yellow]No skills found in selected agents.[/yellow]")
|
|
613
|
+
return 0
|
|
614
|
+
|
|
615
|
+
# Build skill selection list grouped by agent
|
|
616
|
+
choices = []
|
|
617
|
+
for agent_id in sorted(all_skills.keys()):
|
|
618
|
+
agent_name = get_agent_name(agent_id)
|
|
619
|
+
choices.append(Separator(f"--- {agent_name} ---"))
|
|
620
|
+
for skill in all_skills[agent_id]:
|
|
621
|
+
choices.append(
|
|
622
|
+
Choice(
|
|
623
|
+
value=(agent_id, skill),
|
|
624
|
+
name=skill,
|
|
625
|
+
enabled=False,
|
|
626
|
+
)
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
selected_to_remove = inquirer.checkbox(
|
|
630
|
+
message="Select skills to uninstall:",
|
|
631
|
+
choices=choices,
|
|
632
|
+
instruction="(Space to select, Enter to confirm)",
|
|
633
|
+
).execute()
|
|
634
|
+
|
|
635
|
+
if not selected_to_remove:
|
|
636
|
+
console.print("[yellow]No skills selected. Exiting.[/yellow]")
|
|
637
|
+
return 0
|
|
638
|
+
|
|
639
|
+
console.print()
|
|
640
|
+
|
|
641
|
+
# Ask for deletion type
|
|
642
|
+
deletion_type = inquirer.select(
|
|
643
|
+
message="Select deletion type:",
|
|
644
|
+
choices=[
|
|
645
|
+
Choice(value="soft", name="Safe delete (move to trash) - Can be restored later"),
|
|
646
|
+
Choice(value="hard", name="Hard delete (permanent) - Cannot be restored"),
|
|
647
|
+
],
|
|
648
|
+
default="soft",
|
|
649
|
+
).execute()
|
|
650
|
+
|
|
651
|
+
console.print()
|
|
652
|
+
|
|
653
|
+
# Show summary
|
|
654
|
+
console.print(f"[yellow]Will {deletion_type} delete {len(selected_to_remove)} skills:[/yellow]")
|
|
655
|
+
for agent_id, skill in selected_to_remove:
|
|
656
|
+
console.print(f" • {get_agent_name(agent_id)}: {skill}")
|
|
657
|
+
|
|
658
|
+
console.print()
|
|
659
|
+
|
|
660
|
+
# Confirm
|
|
661
|
+
confirm = inquirer.confirm(
|
|
662
|
+
message=f"Confirm {deletion_type} deletion?",
|
|
663
|
+
default=True,
|
|
664
|
+
).execute()
|
|
665
|
+
|
|
666
|
+
if not confirm:
|
|
667
|
+
console.print("[yellow]Deletion cancelled.[/yellow]")
|
|
668
|
+
return 0
|
|
669
|
+
|
|
670
|
+
console.print()
|
|
671
|
+
|
|
672
|
+
# Perform deletion
|
|
673
|
+
success_count = 0
|
|
674
|
+
fail_count = 0
|
|
675
|
+
project_root = get_project_root() if deployment_type == "project" else None
|
|
676
|
+
|
|
677
|
+
with Progress(
|
|
678
|
+
SpinnerColumn(),
|
|
679
|
+
TextColumn("[progress.description]{task.description}"),
|
|
680
|
+
console=console,
|
|
681
|
+
) as progress:
|
|
682
|
+
task = progress.add_task("Removing skills...", total=len(selected_to_remove))
|
|
683
|
+
|
|
684
|
+
for agent_id, skill in selected_to_remove:
|
|
685
|
+
if deletion_type == "soft":
|
|
686
|
+
success = soft_delete_skill(skill, agent_id, deployment_type, project_root)
|
|
687
|
+
else:
|
|
688
|
+
success = hard_delete_skill(skill, agent_id, deployment_type, project_root)
|
|
689
|
+
|
|
690
|
+
if success:
|
|
691
|
+
success_count += 1
|
|
692
|
+
else:
|
|
693
|
+
fail_count += 1
|
|
694
|
+
progress.advance(task)
|
|
695
|
+
|
|
696
|
+
# Show results
|
|
697
|
+
console.print()
|
|
698
|
+
if fail_count == 0:
|
|
699
|
+
console.print(
|
|
700
|
+
Panel.fit(
|
|
701
|
+
f"[bold green]✓ Uninstallation successful![/bold green]\n\n"
|
|
702
|
+
f"Removed {success_count} skills\n"
|
|
703
|
+
+ (f"[dim]Skills moved to trash and can be restored with 'sm restore'[/dim]" if deletion_type == "soft" else ""),
|
|
704
|
+
border_style="green",
|
|
705
|
+
)
|
|
706
|
+
)
|
|
707
|
+
else:
|
|
708
|
+
console.print(
|
|
709
|
+
Panel.fit(
|
|
710
|
+
f"[bold yellow]⚠ Uninstallation completed with errors[/bold yellow]\n\n"
|
|
711
|
+
f"Success: {success_count} | Failed: {fail_count}",
|
|
712
|
+
border_style="yellow",
|
|
713
|
+
)
|
|
714
|
+
)
|
|
715
|
+
|
|
716
|
+
return 0 if fail_count == 0 else 1
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
def cmd_restore() -> int:
|
|
720
|
+
"""
|
|
721
|
+
Restore command - Restore deleted skills from trash.
|
|
722
|
+
|
|
723
|
+
Returns:
|
|
724
|
+
Exit code (0 for success, non-zero for failure)
|
|
725
|
+
"""
|
|
726
|
+
console.print(
|
|
727
|
+
Panel.fit(
|
|
728
|
+
"[bold cyan]Restore Skills[/bold cyan]\n"
|
|
729
|
+
"Restore deleted skills from trash",
|
|
730
|
+
border_style="cyan",
|
|
731
|
+
)
|
|
732
|
+
)
|
|
733
|
+
|
|
734
|
+
# Detect agents
|
|
735
|
+
existing_agents = detect_existing_agents()
|
|
736
|
+
if not existing_agents:
|
|
737
|
+
console.print("[red]No agents detected. Exiting.[/red]")
|
|
738
|
+
return 1
|
|
739
|
+
|
|
740
|
+
console.print(f"\n[green]✓[/green] Detected {len(existing_agents)} installed agents\n")
|
|
741
|
+
|
|
742
|
+
# Select deployment type
|
|
743
|
+
deployment_type = select_deployment_type()
|
|
744
|
+
console.print()
|
|
745
|
+
|
|
746
|
+
# Select agents
|
|
747
|
+
selected_agents = select_agents(existing_agents)
|
|
748
|
+
if not selected_agents:
|
|
749
|
+
console.print("[yellow]No agents selected. Exiting.[/yellow]")
|
|
750
|
+
return 0
|
|
751
|
+
|
|
752
|
+
console.print()
|
|
753
|
+
|
|
754
|
+
# For each selected agent, list trashed skills
|
|
755
|
+
all_trashed = {}
|
|
756
|
+
for agent_id in selected_agents:
|
|
757
|
+
project_root = get_project_root() if deployment_type == "project" else None
|
|
758
|
+
trashed = list_trashed_skills(agent_id, deployment_type, project_root)
|
|
759
|
+
if trashed:
|
|
760
|
+
all_trashed[agent_id] = trashed
|
|
761
|
+
|
|
762
|
+
if not all_trashed:
|
|
763
|
+
console.print("[yellow]No skills found in trash.[/yellow]")
|
|
764
|
+
return 0
|
|
765
|
+
|
|
766
|
+
# Build skill selection list grouped by agent
|
|
767
|
+
choices = []
|
|
768
|
+
for agent_id in sorted(all_trashed.keys()):
|
|
769
|
+
agent_name = get_agent_name(agent_id)
|
|
770
|
+
choices.append(Separator(f"--- {agent_name} ---"))
|
|
771
|
+
for skill_info in all_trashed[agent_id]:
|
|
772
|
+
skill_name = skill_info["skill_name"]
|
|
773
|
+
deleted_at = skill_info["deleted_at"]
|
|
774
|
+
timestamp = skill_info["timestamp_dir"]
|
|
775
|
+
choices.append(
|
|
776
|
+
Choice(
|
|
777
|
+
value=(agent_id, skill_name, timestamp),
|
|
778
|
+
name=f"{skill_name} (deleted: {deleted_at})",
|
|
779
|
+
enabled=False,
|
|
780
|
+
)
|
|
781
|
+
)
|
|
782
|
+
|
|
783
|
+
selected_to_restore = inquirer.checkbox(
|
|
784
|
+
message="Select skills to restore:",
|
|
785
|
+
choices=choices,
|
|
786
|
+
instruction="(Space to select, Enter to confirm)",
|
|
787
|
+
).execute()
|
|
788
|
+
|
|
789
|
+
if not selected_to_restore:
|
|
790
|
+
console.print("[yellow]No skills selected. Exiting.[/yellow]")
|
|
791
|
+
return 0
|
|
792
|
+
|
|
793
|
+
console.print()
|
|
794
|
+
|
|
795
|
+
# Show summary
|
|
796
|
+
console.print(f"[yellow]Will restore {len(selected_to_restore)} skills:[/yellow]")
|
|
797
|
+
for agent_id, skill, _ in selected_to_restore:
|
|
798
|
+
console.print(f" • {get_agent_name(agent_id)}: {skill}")
|
|
799
|
+
|
|
800
|
+
console.print()
|
|
801
|
+
|
|
802
|
+
# Confirm
|
|
803
|
+
confirm = inquirer.confirm(
|
|
804
|
+
message="Confirm restoration?",
|
|
805
|
+
default=True,
|
|
806
|
+
).execute()
|
|
807
|
+
|
|
808
|
+
if not confirm:
|
|
809
|
+
console.print("[yellow]Restoration cancelled.[/yellow]")
|
|
810
|
+
return 0
|
|
811
|
+
|
|
812
|
+
console.print()
|
|
813
|
+
|
|
814
|
+
# Perform restoration
|
|
815
|
+
success_count = 0
|
|
816
|
+
fail_count = 0
|
|
817
|
+
project_root = get_project_root() if deployment_type == "project" else None
|
|
818
|
+
|
|
819
|
+
with Progress(
|
|
820
|
+
SpinnerColumn(),
|
|
821
|
+
TextColumn("[progress.description]{task.description}"),
|
|
822
|
+
console=console,
|
|
823
|
+
) as progress:
|
|
824
|
+
task = progress.add_task("Restoring skills...", total=len(selected_to_restore))
|
|
825
|
+
|
|
826
|
+
for agent_id, skill, timestamp in selected_to_restore:
|
|
827
|
+
if restore_skill(skill, timestamp, agent_id, deployment_type, project_root):
|
|
828
|
+
success_count += 1
|
|
829
|
+
else:
|
|
830
|
+
fail_count += 1
|
|
831
|
+
console.print(f"[red]Failed to restore {skill} (may already exist)[/red]")
|
|
832
|
+
progress.advance(task)
|
|
833
|
+
|
|
834
|
+
# Show results
|
|
835
|
+
console.print()
|
|
836
|
+
if fail_count == 0:
|
|
837
|
+
console.print(
|
|
838
|
+
Panel.fit(
|
|
839
|
+
f"[bold green]✓ Restoration successful![/bold green]\n\n"
|
|
840
|
+
f"Restored {success_count} skills",
|
|
841
|
+
border_style="green",
|
|
842
|
+
)
|
|
843
|
+
)
|
|
844
|
+
else:
|
|
845
|
+
console.print(
|
|
846
|
+
Panel.fit(
|
|
847
|
+
f"[bold yellow]⚠ Restoration completed with errors[/bold yellow]\n\n"
|
|
848
|
+
f"Success: {success_count} | Failed: {fail_count}",
|
|
849
|
+
border_style="yellow",
|
|
850
|
+
)
|
|
851
|
+
)
|
|
852
|
+
|
|
853
|
+
return 0 if fail_count == 0 else 1
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
def cmd_update() -> int:
|
|
857
|
+
"""
|
|
858
|
+
Update command - Update skills from GitHub.
|
|
859
|
+
|
|
860
|
+
Returns:
|
|
861
|
+
Exit code (0 for success, non-zero for failure)
|
|
862
|
+
"""
|
|
863
|
+
# Check for --all flag
|
|
864
|
+
update_all = len(sys.argv) > 2 and sys.argv[2] == "--all"
|
|
865
|
+
|
|
866
|
+
console.print(
|
|
867
|
+
Panel.fit(
|
|
868
|
+
"[bold cyan]Update Skills[/bold cyan]\n"
|
|
869
|
+
+ ("Update all skills from GitHub" if update_all else "Update selected skills from GitHub"),
|
|
870
|
+
border_style="cyan",
|
|
871
|
+
)
|
|
872
|
+
)
|
|
873
|
+
|
|
874
|
+
# Detect agents
|
|
875
|
+
existing_agents = detect_existing_agents()
|
|
876
|
+
if not existing_agents:
|
|
877
|
+
console.print("[red]No agents detected. Exiting.[/red]")
|
|
878
|
+
return 1
|
|
879
|
+
|
|
880
|
+
console.print(f"\n[green]✓[/green] Detected {len(existing_agents)} installed agents\n")
|
|
881
|
+
|
|
882
|
+
# Select deployment type
|
|
883
|
+
deployment_type = select_deployment_type()
|
|
884
|
+
console.print()
|
|
885
|
+
|
|
886
|
+
# Select agents
|
|
887
|
+
selected_agents = select_agents(existing_agents)
|
|
888
|
+
if not selected_agents:
|
|
889
|
+
console.print("[yellow]No agents selected. Exiting.[/yellow]")
|
|
890
|
+
return 0
|
|
891
|
+
|
|
892
|
+
console.print()
|
|
893
|
+
|
|
894
|
+
# Collect updatable skills from selected agents
|
|
895
|
+
all_updatable = {}
|
|
896
|
+
for agent_id in selected_agents:
|
|
897
|
+
project_root = get_project_root() if deployment_type == "project" else None
|
|
898
|
+
agent_path = get_agent_path(agent_id, deployment_type, project_root)
|
|
899
|
+
updatable = list_updatable_skills(agent_path)
|
|
900
|
+
if updatable:
|
|
901
|
+
all_updatable[agent_id] = updatable
|
|
902
|
+
|
|
903
|
+
if not all_updatable:
|
|
904
|
+
console.print("[yellow]No updatable skills found (no GitHub metadata).[/yellow]")
|
|
905
|
+
return 0
|
|
906
|
+
|
|
907
|
+
# Select skills to update (unless --all flag)
|
|
908
|
+
if update_all:
|
|
909
|
+
skills_to_update = all_updatable
|
|
910
|
+
else:
|
|
911
|
+
# Build selection list
|
|
912
|
+
choices = []
|
|
913
|
+
for agent_id in sorted(all_updatable.keys()):
|
|
914
|
+
agent_name = get_agent_name(agent_id)
|
|
915
|
+
choices.append(Separator(f"--- {agent_name} ---"))
|
|
916
|
+
for skill_info in all_updatable[agent_id]:
|
|
917
|
+
skill_name = skill_info["skill_name"]
|
|
918
|
+
metadata = skill_info["metadata"]
|
|
919
|
+
github_url = metadata.get("github_url", "")
|
|
920
|
+
updated_at = metadata.get("updated_at", "unknown")
|
|
921
|
+
choices.append(
|
|
922
|
+
Choice(
|
|
923
|
+
value=(agent_id, skill_name),
|
|
924
|
+
name=f"{skill_name} (updated: {updated_at[:10]})",
|
|
925
|
+
enabled=False,
|
|
926
|
+
)
|
|
927
|
+
)
|
|
928
|
+
|
|
929
|
+
selected_to_update = inquirer.checkbox(
|
|
930
|
+
message="Select skills to update:",
|
|
931
|
+
choices=choices,
|
|
932
|
+
instruction="(Space to select, Enter to confirm)",
|
|
933
|
+
).execute()
|
|
934
|
+
|
|
935
|
+
if not selected_to_update:
|
|
936
|
+
console.print("[yellow]No skills selected. Exiting.[/yellow]")
|
|
937
|
+
return 0
|
|
938
|
+
|
|
939
|
+
# Convert to dict format
|
|
940
|
+
skills_to_update = {}
|
|
941
|
+
for agent_id, skill_name in selected_to_update:
|
|
942
|
+
if agent_id not in skills_to_update:
|
|
943
|
+
skills_to_update[agent_id] = []
|
|
944
|
+
skills_to_update[agent_id].append(skill_name)
|
|
945
|
+
|
|
946
|
+
console.print()
|
|
947
|
+
|
|
948
|
+
# Show summary
|
|
949
|
+
total_count = sum(len(skills) if isinstance(skills, list) else len(all_updatable[agent_id]) for agent_id, skills in skills_to_update.items())
|
|
950
|
+
console.print(f"[yellow]Will update {total_count} skills:[/yellow]")
|
|
951
|
+
for agent_id in sorted(skills_to_update.keys()):
|
|
952
|
+
agent_name = get_agent_name(agent_id)
|
|
953
|
+
if isinstance(skills_to_update[agent_id], list):
|
|
954
|
+
skill_list = skills_to_update[agent_id]
|
|
955
|
+
else:
|
|
956
|
+
skill_list = [s["skill_name"] for s in all_updatable[agent_id]]
|
|
957
|
+
for skill in skill_list:
|
|
958
|
+
console.print(f" • {agent_name}: {skill}")
|
|
959
|
+
|
|
960
|
+
console.print()
|
|
961
|
+
|
|
962
|
+
# Confirm
|
|
963
|
+
confirm = inquirer.confirm(
|
|
964
|
+
message="Confirm update?",
|
|
965
|
+
default=True,
|
|
966
|
+
).execute()
|
|
967
|
+
|
|
968
|
+
if not confirm:
|
|
969
|
+
console.print("[yellow]Update cancelled.[/yellow]")
|
|
970
|
+
return 0
|
|
971
|
+
|
|
972
|
+
console.print()
|
|
973
|
+
|
|
974
|
+
# Perform updates
|
|
975
|
+
total_success = 0
|
|
976
|
+
total_failed = 0
|
|
977
|
+
project_root = get_project_root() if deployment_type == "project" else None
|
|
978
|
+
|
|
979
|
+
with Progress(
|
|
980
|
+
SpinnerColumn(),
|
|
981
|
+
TextColumn("[progress.description]{task.description}"),
|
|
982
|
+
console=console,
|
|
983
|
+
) as progress:
|
|
984
|
+
for agent_id in skills_to_update.keys():
|
|
985
|
+
if isinstance(skills_to_update[agent_id], list):
|
|
986
|
+
# Specific skills selected
|
|
987
|
+
skill_list = skills_to_update[agent_id]
|
|
988
|
+
task = progress.add_task(f"Updating {get_agent_name(agent_id)}...", total=len(skill_list))
|
|
989
|
+
|
|
990
|
+
for skill_name in skill_list:
|
|
991
|
+
if update_skill(skill_name, agent_id, deployment_type, project_root):
|
|
992
|
+
total_success += 1
|
|
993
|
+
else:
|
|
994
|
+
total_failed += 1
|
|
995
|
+
progress.advance(task)
|
|
996
|
+
else:
|
|
997
|
+
# Update all for this agent
|
|
998
|
+
task = progress.add_task(f"Updating {get_agent_name(agent_id)}...", total=None)
|
|
999
|
+
success, failed = update_all_skills(agent_id, deployment_type, project_root)
|
|
1000
|
+
total_success += success
|
|
1001
|
+
total_failed += failed
|
|
1002
|
+
progress.update(task, completed=True)
|
|
1003
|
+
|
|
1004
|
+
# Show results
|
|
1005
|
+
console.print()
|
|
1006
|
+
if total_failed == 0:
|
|
1007
|
+
console.print(
|
|
1008
|
+
Panel.fit(
|
|
1009
|
+
f"[bold green]✓ Update successful![/bold green]\n\n"
|
|
1010
|
+
f"Updated {total_success} skills",
|
|
1011
|
+
border_style="green",
|
|
1012
|
+
)
|
|
1013
|
+
)
|
|
1014
|
+
else:
|
|
1015
|
+
console.print(
|
|
1016
|
+
Panel.fit(
|
|
1017
|
+
f"[bold yellow]⚠ Update completed with errors[/bold yellow]\n\n"
|
|
1018
|
+
f"Success: {total_success} | Failed: {total_failed}",
|
|
1019
|
+
border_style="yellow",
|
|
1020
|
+
)
|
|
1021
|
+
)
|
|
1022
|
+
|
|
1023
|
+
return 0 if total_failed == 0 else 1
|
|
1024
|
+
|
|
1025
|
+
|
|
1026
|
+
def cmd_list() -> int:
|
|
1027
|
+
"""
|
|
1028
|
+
List command - List installed skills and versions.
|
|
1029
|
+
|
|
1030
|
+
Returns:
|
|
1031
|
+
Exit code (0 for success, non-zero for failure)
|
|
1032
|
+
"""
|
|
1033
|
+
console.print(
|
|
1034
|
+
Panel.fit(
|
|
1035
|
+
"[bold cyan]List Installed Skills[/bold cyan]\n"
|
|
1036
|
+
"Show all installed skills with version information",
|
|
1037
|
+
border_style="cyan",
|
|
1038
|
+
)
|
|
1039
|
+
)
|
|
1040
|
+
|
|
1041
|
+
# Detect agents
|
|
1042
|
+
existing_agents = detect_existing_agents()
|
|
1043
|
+
if not existing_agents:
|
|
1044
|
+
console.print("\n[red]No agents detected.[/red]")
|
|
1045
|
+
return 1
|
|
1046
|
+
|
|
1047
|
+
console.print(f"\n[green]✓[/green] Detected {len(existing_agents)} installed agents\n")
|
|
1048
|
+
|
|
1049
|
+
# Select deployment type
|
|
1050
|
+
deployment_type = select_deployment_type()
|
|
1051
|
+
console.print()
|
|
1052
|
+
|
|
1053
|
+
project_root = get_project_root() if deployment_type == "project" else None
|
|
1054
|
+
|
|
1055
|
+
# Collect all skills from all agents
|
|
1056
|
+
all_skills_data = {}
|
|
1057
|
+
for agent_id in sorted(existing_agents.keys()):
|
|
1058
|
+
agent_path = get_agent_path(agent_id, deployment_type, project_root)
|
|
1059
|
+
skills = list_installed_skills(agent_id, deployment_type, project_root)
|
|
1060
|
+
|
|
1061
|
+
if skills:
|
|
1062
|
+
all_skills_data[agent_id] = []
|
|
1063
|
+
for skill_name in skills:
|
|
1064
|
+
skill_dir = agent_path / skill_name
|
|
1065
|
+
metadata = read_skill_metadata(skill_dir)
|
|
1066
|
+
|
|
1067
|
+
if metadata:
|
|
1068
|
+
# Has GitHub metadata
|
|
1069
|
+
updated_at = metadata.get("updated_at", "unknown")
|
|
1070
|
+
github_url = metadata.get("github_url", "")
|
|
1071
|
+
version_info = updated_at[:19] if updated_at != "unknown" else "unknown"
|
|
1072
|
+
source = "GitHub"
|
|
1073
|
+
else:
|
|
1074
|
+
# No metadata, use file modification time
|
|
1075
|
+
skill_md = skill_dir / "SKILL.md"
|
|
1076
|
+
if skill_md.exists():
|
|
1077
|
+
mtime = skill_md.stat().st_mtime
|
|
1078
|
+
from datetime import datetime
|
|
1079
|
+
version_info = datetime.fromtimestamp(mtime).strftime("%Y-%m-%d %H:%M:%S")
|
|
1080
|
+
else:
|
|
1081
|
+
version_info = "unknown"
|
|
1082
|
+
source = "Local"
|
|
1083
|
+
github_url = ""
|
|
1084
|
+
|
|
1085
|
+
all_skills_data[agent_id].append({
|
|
1086
|
+
"name": skill_name,
|
|
1087
|
+
"version": version_info,
|
|
1088
|
+
"source": source,
|
|
1089
|
+
"url": github_url,
|
|
1090
|
+
})
|
|
1091
|
+
|
|
1092
|
+
if not all_skills_data:
|
|
1093
|
+
console.print("[yellow]No skills found in selected agents.[/yellow]")
|
|
1094
|
+
return 0
|
|
1095
|
+
|
|
1096
|
+
# Display tables for each agent
|
|
1097
|
+
for agent_id in sorted(all_skills_data.keys()):
|
|
1098
|
+
agent_name = get_agent_name(agent_id)
|
|
1099
|
+
skills_list = all_skills_data[agent_id]
|
|
1100
|
+
|
|
1101
|
+
table = Table(
|
|
1102
|
+
title=f"{agent_name} ({len(skills_list)} skills)",
|
|
1103
|
+
show_header=True,
|
|
1104
|
+
header_style="bold cyan",
|
|
1105
|
+
)
|
|
1106
|
+
table.add_column("Skill Name", style="green", no_wrap=True)
|
|
1107
|
+
table.add_column("Version/Updated", style="yellow")
|
|
1108
|
+
table.add_column("Source", style="blue")
|
|
1109
|
+
table.add_column("GitHub URL", style="dim", overflow="fold")
|
|
1110
|
+
|
|
1111
|
+
for skill in sorted(skills_list, key=lambda x: x["name"]):
|
|
1112
|
+
table.add_row(
|
|
1113
|
+
skill["name"],
|
|
1114
|
+
skill["version"],
|
|
1115
|
+
skill["source"],
|
|
1116
|
+
skill["url"][:50] + "..." if len(skill["url"]) > 50 else skill["url"],
|
|
1117
|
+
)
|
|
1118
|
+
|
|
1119
|
+
console.print(table)
|
|
1120
|
+
console.print()
|
|
1121
|
+
|
|
1122
|
+
return 0
|
|
1123
|
+
|
|
1124
|
+
|
|
1125
|
+
def main() -> int:
|
|
1126
|
+
"""
|
|
1127
|
+
Main CLI entry point.
|
|
1128
|
+
|
|
1129
|
+
Returns:
|
|
1130
|
+
Exit code
|
|
1131
|
+
"""
|
|
1132
|
+
if len(sys.argv) < 2:
|
|
1133
|
+
console.print(
|
|
1134
|
+
Panel.fit(
|
|
1135
|
+
"[bold cyan]Skill Manager[/bold cyan]\n\n"
|
|
1136
|
+
"Usage:\n"
|
|
1137
|
+
" sm download - Download a skill from GitHub\n"
|
|
1138
|
+
" sm deploy - Deploy local skills to agents\n"
|
|
1139
|
+
" sm install - Download and deploy in one step\n"
|
|
1140
|
+
" sm uninstall - Remove skills from agents (safe delete)\n"
|
|
1141
|
+
" sm restore - Restore deleted skills from trash\n"
|
|
1142
|
+
" sm update - Update skills from GitHub\n"
|
|
1143
|
+
" sm update --all - Update all skills from GitHub\n"
|
|
1144
|
+
" sm list - List installed skills and versions\n\n"
|
|
1145
|
+
"[dim]Note: You can also use 'skill-manager' instead of 'sm'[/dim]",
|
|
1146
|
+
border_style="cyan",
|
|
1147
|
+
)
|
|
1148
|
+
)
|
|
1149
|
+
return 1
|
|
1150
|
+
|
|
1151
|
+
command = sys.argv[1]
|
|
1152
|
+
|
|
1153
|
+
try:
|
|
1154
|
+
if command == "download":
|
|
1155
|
+
return cmd_download()
|
|
1156
|
+
elif command == "deploy":
|
|
1157
|
+
return cmd_deploy()
|
|
1158
|
+
elif command == "install":
|
|
1159
|
+
return cmd_install()
|
|
1160
|
+
elif command == "uninstall":
|
|
1161
|
+
return cmd_uninstall()
|
|
1162
|
+
elif command == "restore":
|
|
1163
|
+
return cmd_restore()
|
|
1164
|
+
elif command == "update":
|
|
1165
|
+
return cmd_update()
|
|
1166
|
+
elif command == "list":
|
|
1167
|
+
return cmd_list()
|
|
1168
|
+
else:
|
|
1169
|
+
console.print(f"[red]Unknown command: {command}[/red]")
|
|
1170
|
+
console.print("Available commands: download, deploy, install, uninstall, restore, update, list")
|
|
1171
|
+
return 1
|
|
1172
|
+
except KeyboardInterrupt:
|
|
1173
|
+
console.print("\n[yellow]Cancelled[/yellow]")
|
|
1174
|
+
return 130
|
|
1175
|
+
|
|
1176
|
+
|
|
1177
|
+
if __name__ == "__main__":
|
|
1178
|
+
sys.exit(main())
|