agent-skill-manager 0.1.2__py3-none-any.whl → 0.2.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_manager/cli.py CHANGED
@@ -13,8 +13,10 @@ Commands:
13
13
  update - Update skills from GitHub
14
14
  update --all - Update all skills from GitHub
15
15
  list - List installed skills and versions
16
+ discover - Discover skills in a GitHub repository
16
17
  """
17
18
 
19
+ import argparse
18
20
  import shutil
19
21
  import sys
20
22
  from pathlib import Path
@@ -27,14 +29,22 @@ from rich.panel import Panel
27
29
  from rich.progress import Progress, SpinnerColumn, TextColumn
28
30
  from rich.table import Table
29
31
 
30
- from .agents import AGENTS, detect_existing_agents, get_agent_name, get_agent_path
32
+ from . import __version__
33
+ from .agents import AGENTS, detect_existing_agents, get_agent_name, get_agent_path, supports_global_deployment
31
34
  from .deployment import (
32
35
  deploy_multiple_skills,
33
36
  deploy_skill_to_agents,
37
+ is_symlink_supported,
34
38
  update_all_skills,
35
39
  update_skill,
36
40
  )
37
- from .github import download_skill_from_github, parse_github_url
41
+ from .github import (
42
+ discover_skills_in_repo,
43
+ download_multiple_skills,
44
+ download_skill_from_github,
45
+ get_system_temp_dir,
46
+ parse_github_url,
47
+ )
38
48
  from .metadata import (
39
49
  list_updatable_skills,
40
50
  read_skill_metadata,
@@ -57,6 +67,85 @@ from .validation import (
57
67
  console = Console()
58
68
 
59
69
 
70
+ def create_parser() -> argparse.ArgumentParser:
71
+ """Create the argument parser with all subcommands."""
72
+ parser = argparse.ArgumentParser(
73
+ prog="sm",
74
+ description="CLI tool for managing AI agent skills",
75
+ formatter_class=argparse.RawDescriptionHelpFormatter,
76
+ )
77
+ parser.add_argument("-v", "--version", action="version", version=f"%(prog)s {__version__}")
78
+
79
+ subparsers = parser.add_subparsers(dest="command", help="Available commands")
80
+
81
+ # Install command
82
+ install_parser = subparsers.add_parser("install", help="Download and deploy skills from GitHub")
83
+ install_parser.add_argument("url", nargs="?", help="GitHub URL of the skill or repository")
84
+ install_parser.add_argument(
85
+ "-a", "--agent", action="append", dest="agents", help="Target agent(s) (can be specified multiple times)"
86
+ )
87
+ install_parser.add_argument(
88
+ "-t", "--type", choices=["global", "project"], default="global", help="Deployment type (default: global)"
89
+ )
90
+ install_parser.add_argument("-d", "--dest", type=Path, help="Destination directory for downloaded skills")
91
+ install_parser.add_argument("--symlink", action="store_true", help="Use symlinks instead of copying")
92
+ install_parser.add_argument("--discover", action="store_true", help="Discover and install all skills in a repository")
93
+ install_parser.add_argument("--no-deploy", action="store_true", help="Download only, do not deploy")
94
+ install_parser.add_argument("-y", "--yes", action="store_true", help="Skip confirmation prompts")
95
+
96
+ # Download command
97
+ download_parser = subparsers.add_parser("download", help="Download skills from GitHub")
98
+ download_parser.add_argument("url", nargs="?", help="GitHub URL of the skill or repository")
99
+ download_parser.add_argument("-d", "--dest", type=Path, help="Destination directory")
100
+ download_parser.add_argument("--discover", action="store_true", help="Discover all skills in a repository")
101
+ download_parser.add_argument("-y", "--yes", action="store_true", help="Skip confirmation prompts")
102
+
103
+ # Deploy command
104
+ deploy_parser = subparsers.add_parser("deploy", help="Deploy local skills to agents")
105
+ deploy_parser.add_argument("skills", nargs="*", help="Skill names or paths to deploy")
106
+ deploy_parser.add_argument("-a", "--agent", action="append", dest="agents", help="Target agent(s)")
107
+ deploy_parser.add_argument("-t", "--type", choices=["global", "project"], default="global", help="Deployment type")
108
+ deploy_parser.add_argument("--symlink", action="store_true", help="Use symlinks instead of copying")
109
+ deploy_parser.add_argument("-y", "--yes", action="store_true", help="Skip confirmation prompts")
110
+
111
+ # Discover command
112
+ discover_parser = subparsers.add_parser("discover", help="Discover skills in a GitHub repository")
113
+ discover_parser.add_argument("url", nargs="?", help="GitHub URL to scan")
114
+
115
+ # Uninstall command
116
+ uninstall_parser = subparsers.add_parser("uninstall", help="Remove skills from agents")
117
+ uninstall_parser.add_argument("skills", nargs="*", help="Skill names to uninstall")
118
+ uninstall_parser.add_argument("-a", "--agent", action="append", dest="agents", help="Target agent(s)")
119
+ uninstall_parser.add_argument("-t", "--type", choices=["global", "project"], default="global", help="Deployment type")
120
+ uninstall_parser.add_argument("--hard", action="store_true", help="Permanently delete (no trash)")
121
+ uninstall_parser.add_argument("-y", "--yes", action="store_true", help="Skip confirmation prompts")
122
+
123
+ # Restore command
124
+ restore_parser = subparsers.add_parser("restore", help="Restore deleted skills from trash")
125
+ restore_parser.add_argument("skills", nargs="*", help="Skill names to restore")
126
+ restore_parser.add_argument("-a", "--agent", action="append", dest="agents", help="Target agent(s)")
127
+ restore_parser.add_argument("-t", "--type", choices=["global", "project"], default="global", help="Deployment type")
128
+ restore_parser.add_argument("-y", "--yes", action="store_true", help="Skip confirmation prompts")
129
+
130
+ # Update command
131
+ update_parser = subparsers.add_parser("update", help="Update skills from GitHub")
132
+ update_parser.add_argument("skills", nargs="*", help="Skill names to update")
133
+ update_parser.add_argument("-a", "--agent", action="append", dest="agents", help="Target agent(s)")
134
+ update_parser.add_argument("-t", "--type", choices=["global", "project"], default="global", help="Deployment type")
135
+ update_parser.add_argument("--all", action="store_true", help="Update all skills")
136
+ update_parser.add_argument("-y", "--yes", action="store_true", help="Skip confirmation prompts")
137
+
138
+ # List command
139
+ list_parser = subparsers.add_parser("list", help="List installed skills")
140
+ list_parser.add_argument("-a", "--agent", action="append", dest="agents", help="Target agent(s)")
141
+ list_parser.add_argument("-t", "--type", choices=["global", "project"], default="global", help="Deployment type")
142
+
143
+ # Agents command (list available agents)
144
+ subparsers.add_parser("agents", help="List all supported agents")
145
+
146
+ return parser
147
+
148
+
60
149
  def select_agents(existing_agents: dict[str, Path]) -> list[str]:
61
150
  """
62
151
  Interactive prompt to select agents for deployment.
@@ -181,8 +270,7 @@ def cmd_download() -> int:
181
270
  """
182
271
  console.print(
183
272
  Panel.fit(
184
- "[bold cyan]Download Skill[/bold cyan]\n"
185
- "Download a skill from GitHub",
273
+ "[bold cyan]Download Skill[/bold cyan]\nDownload a skill from GitHub",
186
274
  border_style="cyan",
187
275
  )
188
276
  )
@@ -255,9 +343,7 @@ def cmd_download() -> int:
255
343
 
256
344
  # Validate
257
345
  if not validate_skill(skill_dir):
258
- console.print(
259
- f"[yellow]Warning: {skill_dir} does not contain SKILL.md, may not be a valid skill[/yellow]"
260
- )
346
+ console.print(f"[yellow]Warning: {skill_dir} does not contain SKILL.md, may not be a valid skill[/yellow]")
261
347
 
262
348
  console.print(f"[green]✓[/green] Skill downloaded to: {skill_dir}\n")
263
349
  return 0
@@ -276,8 +362,7 @@ def cmd_deploy() -> int:
276
362
  """
277
363
  console.print(
278
364
  Panel.fit(
279
- "[bold cyan]Deploy Skills[/bold cyan]\n"
280
- "Deploy local skills to AI agents",
365
+ "[bold cyan]Deploy Skills[/bold cyan]\nDeploy local skills to AI agents",
281
366
  border_style="cyan",
282
367
  )
283
368
  )
@@ -308,9 +393,7 @@ def cmd_deploy() -> int:
308
393
  # Detect agents
309
394
  existing_agents = detect_existing_agents()
310
395
  if existing_agents:
311
- console.print(
312
- f"[green]✓[/green] Detected {len(existing_agents)} installed agents\n"
313
- )
396
+ console.print(f"[green]✓[/green] Detected {len(existing_agents)} installed agents\n")
314
397
 
315
398
  # Select deployment type
316
399
  deployment_type = select_deployment_type()
@@ -416,8 +499,7 @@ def cmd_install() -> int:
416
499
  """
417
500
  console.print(
418
501
  Panel.fit(
419
- "[bold cyan]Install Skill[/bold cyan]\n"
420
- "Download and deploy a skill from GitHub",
502
+ "[bold cyan]Install Skill[/bold cyan]\nDownload and deploy a skill from GitHub",
421
503
  border_style="cyan",
422
504
  )
423
505
  )
@@ -490,9 +572,7 @@ def cmd_install() -> int:
490
572
 
491
573
  # Validate
492
574
  if not validate_skill(skill_dir):
493
- console.print(
494
- f"[yellow]Warning: {skill_dir} does not contain SKILL.md, may not be a valid skill[/yellow]"
495
- )
575
+ console.print(f"[yellow]Warning: {skill_dir} does not contain SKILL.md, may not be a valid skill[/yellow]")
496
576
 
497
577
  console.print(f"[green]✓[/green] Skill downloaded to: {skill_dir}\n")
498
578
 
@@ -515,9 +595,7 @@ def cmd_install() -> int:
515
595
  # Detect agents
516
596
  existing_agents = detect_existing_agents()
517
597
  if existing_agents:
518
- console.print(
519
- f"[green]✓[/green] Detected {len(existing_agents)} installed agents\n"
520
- )
598
+ console.print(f"[green]✓[/green] Detected {len(existing_agents)} installed agents\n")
521
599
 
522
600
  # Select deployment type
523
601
  deployment_type = select_deployment_type()
@@ -573,8 +651,7 @@ def cmd_uninstall() -> int:
573
651
  """
574
652
  console.print(
575
653
  Panel.fit(
576
- "[bold cyan]Uninstall Skills[/bold cyan]\n"
577
- "Remove skills from AI agents",
654
+ "[bold cyan]Uninstall Skills[/bold cyan]\nRemove skills from AI agents",
578
655
  border_style="cyan",
579
656
  )
580
657
  )
@@ -724,8 +801,7 @@ def cmd_restore() -> int:
724
801
  """
725
802
  console.print(
726
803
  Panel.fit(
727
- "[bold cyan]Restore Skills[/bold cyan]\n"
728
- "Restore deleted skills from trash",
804
+ "[bold cyan]Restore Skills[/bold cyan]\nRestore deleted skills from trash",
729
805
  border_style="cyan",
730
806
  )
731
807
  )
@@ -835,8 +911,7 @@ def cmd_restore() -> int:
835
911
  if fail_count == 0:
836
912
  console.print(
837
913
  Panel.fit(
838
- f"[bold green]✓ Restoration successful![/bold green]\n\n"
839
- f"Restored {success_count} skills",
914
+ f"[bold green]✓ Restoration successful![/bold green]\n\nRestored {success_count} skills",
840
915
  border_style="green",
841
916
  )
842
917
  )
@@ -915,7 +990,6 @@ def cmd_update() -> int:
915
990
  for skill_info in all_updatable[agent_id]:
916
991
  skill_name = skill_info["skill_name"]
917
992
  metadata = skill_info["metadata"]
918
- github_url = metadata.get("github_url", "")
919
993
  updated_at = metadata.get("updated_at", "unknown")
920
994
  choices.append(
921
995
  Choice(
@@ -945,7 +1019,10 @@ def cmd_update() -> int:
945
1019
  console.print()
946
1020
 
947
1021
  # Show summary
948
- total_count = sum(len(skills) if isinstance(skills, list) else len(all_updatable[agent_id]) for agent_id, skills in skills_to_update.items())
1022
+ total_count = sum(
1023
+ len(skills) if isinstance(skills, list) else len(all_updatable[agent_id])
1024
+ for agent_id, skills in skills_to_update.items()
1025
+ )
949
1026
  console.print(f"[yellow]Will update {total_count} skills:[/yellow]")
950
1027
  for agent_id in sorted(skills_to_update.keys()):
951
1028
  agent_name = get_agent_name(agent_id)
@@ -1005,8 +1082,7 @@ def cmd_update() -> int:
1005
1082
  if total_failed == 0:
1006
1083
  console.print(
1007
1084
  Panel.fit(
1008
- f"[bold green]✓ Update successful![/bold green]\n\n"
1009
- f"Updated {total_success} skills",
1085
+ f"[bold green]✓ Update successful![/bold green]\n\nUpdated {total_success} skills",
1010
1086
  border_style="green",
1011
1087
  )
1012
1088
  )
@@ -1031,8 +1107,7 @@ def cmd_list() -> int:
1031
1107
  """
1032
1108
  console.print(
1033
1109
  Panel.fit(
1034
- "[bold cyan]List Installed Skills[/bold cyan]\n"
1035
- "Show all installed skills with version information",
1110
+ "[bold cyan]List Installed Skills[/bold cyan]\nShow all installed skills with version information",
1036
1111
  border_style="cyan",
1037
1112
  )
1038
1113
  )
@@ -1075,18 +1150,21 @@ def cmd_list() -> int:
1075
1150
  if skill_md.exists():
1076
1151
  mtime = skill_md.stat().st_mtime
1077
1152
  from datetime import datetime
1153
+
1078
1154
  version_info = datetime.fromtimestamp(mtime).strftime("%Y-%m-%d %H:%M:%S")
1079
1155
  else:
1080
1156
  version_info = "unknown"
1081
1157
  source = "Local"
1082
1158
  github_url = ""
1083
1159
 
1084
- all_skills_data[agent_id].append({
1085
- "name": skill_name,
1086
- "version": version_info,
1087
- "source": source,
1088
- "url": github_url,
1089
- })
1160
+ all_skills_data[agent_id].append(
1161
+ {
1162
+ "name": skill_name,
1163
+ "version": version_info,
1164
+ "source": source,
1165
+ "url": github_url,
1166
+ }
1167
+ )
1090
1168
 
1091
1169
  if not all_skills_data:
1092
1170
  console.print("[yellow]No skills found in selected agents.[/yellow]")
@@ -1121,6 +1199,416 @@ def cmd_list() -> int:
1121
1199
  return 0
1122
1200
 
1123
1201
 
1202
+ def cmd_discover(args: argparse.Namespace) -> int:
1203
+ """
1204
+ Discover command - Discover skills in a GitHub repository.
1205
+
1206
+ Args:
1207
+ args: Parsed command line arguments
1208
+
1209
+ Returns:
1210
+ Exit code (0 for success, non-zero for failure)
1211
+ """
1212
+ console.print(
1213
+ Panel.fit(
1214
+ "[bold cyan]Discover Skills[/bold cyan]\nScan a GitHub repository for skills",
1215
+ border_style="cyan",
1216
+ )
1217
+ )
1218
+
1219
+ # Get URL
1220
+ url = args.url
1221
+ if not url:
1222
+ url = inquirer.text(
1223
+ message="Enter GitHub URL to scan:",
1224
+ validate=lambda x: len(x) > 0,
1225
+ invalid_message="URL cannot be empty",
1226
+ ).execute()
1227
+
1228
+ console.print()
1229
+
1230
+ # Parse URL to show info
1231
+ try:
1232
+ owner, repo, branch, path = parse_github_url(url)
1233
+ console.print(f"[dim]Repository: {owner}/{repo}[/dim]")
1234
+ console.print(f"[dim]Branch: {branch}[/dim]")
1235
+ console.print(f"[dim]Path: {path or '(root)'}[/dim]\n")
1236
+ except ValueError as e:
1237
+ console.print(f"[red]Error: {e}[/red]")
1238
+ return 1
1239
+
1240
+ # Discover skills
1241
+ try:
1242
+ with Progress(
1243
+ SpinnerColumn(),
1244
+ TextColumn("[progress.description]{task.description}"),
1245
+ console=console,
1246
+ ) as progress:
1247
+ task = progress.add_task("Scanning repository for skills...", total=None)
1248
+ skills = discover_skills_in_repo(url)
1249
+ progress.update(task, completed=True)
1250
+
1251
+ if not skills:
1252
+ console.print("[yellow]No skills found in the repository.[/yellow]")
1253
+ return 0
1254
+
1255
+ # Display found skills
1256
+ table = Table(title=f"Found {len(skills)} Skills", show_header=True, header_style="bold cyan")
1257
+ table.add_column("Name", style="green", no_wrap=True)
1258
+ table.add_column("Path", style="yellow")
1259
+ table.add_column("URL", style="dim", overflow="fold")
1260
+
1261
+ for skill in sorted(skills, key=lambda x: x["name"]):
1262
+ table.add_row(
1263
+ skill["name"],
1264
+ skill["path"],
1265
+ skill["url"][:60] + "..." if len(skill["url"]) > 60 else skill["url"],
1266
+ )
1267
+
1268
+ console.print(table)
1269
+ console.print()
1270
+
1271
+ console.print("[dim]Use 'sm install <url> --discover' to install all found skills[/dim]")
1272
+ return 0
1273
+
1274
+ except Exception as e:
1275
+ console.print(f"[red]Discovery failed: {e}[/red]")
1276
+ return 1
1277
+
1278
+
1279
+ def cmd_agents() -> int:
1280
+ """
1281
+ Agents command - List all supported agents.
1282
+
1283
+ Returns:
1284
+ Exit code (0 for success)
1285
+ """
1286
+ console.print(
1287
+ Panel.fit(
1288
+ "[bold cyan]Supported Agents[/bold cyan]\nAll AI agents supported by skill-manager",
1289
+ border_style="cyan",
1290
+ )
1291
+ )
1292
+
1293
+ table = Table(show_header=True, header_style="bold cyan")
1294
+ table.add_column("Agent ID", style="green", no_wrap=True)
1295
+ table.add_column("Name", style="yellow")
1296
+ table.add_column("Project Path", style="dim")
1297
+ table.add_column("Global Path", style="dim")
1298
+
1299
+ for agent_id in sorted(AGENTS.keys()):
1300
+ info = AGENTS[agent_id]
1301
+ global_path = info.get("global") or "N/A (project-only)"
1302
+ table.add_row(
1303
+ agent_id,
1304
+ info["name"],
1305
+ info["project"],
1306
+ global_path,
1307
+ )
1308
+
1309
+ console.print(table)
1310
+ console.print(f"\n[dim]Total: {len(AGENTS)} agents supported[/dim]")
1311
+ return 0
1312
+
1313
+
1314
+ def cmd_install_cli(args: argparse.Namespace) -> int:
1315
+ """
1316
+ Install command with CLI arguments - Download and deploy skills.
1317
+
1318
+ Args:
1319
+ args: Parsed command line arguments
1320
+
1321
+ Returns:
1322
+ Exit code (0 for success, non-zero for failure)
1323
+ """
1324
+ # If no URL provided, fall back to interactive mode
1325
+ if not args.url:
1326
+ return cmd_install()
1327
+
1328
+ console.print(
1329
+ Panel.fit(
1330
+ "[bold cyan]Install Skill[/bold cyan]\nDownload and deploy a skill from GitHub",
1331
+ border_style="cyan",
1332
+ )
1333
+ )
1334
+
1335
+ url = args.url
1336
+ deployment_type = args.type
1337
+ use_symlink = args.symlink
1338
+ skip_deploy = args.no_deploy
1339
+ # auto_confirm = args.yes # Reserved for future use
1340
+
1341
+ # Parse URL to show info
1342
+ try:
1343
+ owner, repo, branch, path = parse_github_url(url)
1344
+ console.print(f"[dim]Repository: {owner}/{repo}[/dim]")
1345
+ console.print(f"[dim]Branch: {branch}[/dim]")
1346
+ console.print(f"[dim]Path: {path or '(root)'}[/dim]\n")
1347
+ except ValueError as e:
1348
+ console.print(f"[red]Error: {e}[/red]")
1349
+ return 1
1350
+
1351
+ # Determine destination directory
1352
+ if args.dest:
1353
+ dest_dir = args.dest
1354
+ else:
1355
+ dest_dir = get_system_temp_dir()
1356
+
1357
+ dest_dir.mkdir(parents=True, exist_ok=True)
1358
+ console.print(f"[dim]Download location: {dest_dir}[/dim]\n")
1359
+
1360
+ # Check symlink support
1361
+ if use_symlink and not is_symlink_supported():
1362
+ console.print("[yellow]Warning: Symlinks not supported on this system, will copy instead[/yellow]")
1363
+ use_symlink = False
1364
+
1365
+ # Discover skills if requested
1366
+ if args.discover:
1367
+ with Progress(
1368
+ SpinnerColumn(),
1369
+ TextColumn("[progress.description]{task.description}"),
1370
+ console=console,
1371
+ ) as progress:
1372
+ task = progress.add_task("Discovering skills...", total=None)
1373
+ skills_info = discover_skills_in_repo(url)
1374
+ progress.update(task, completed=True)
1375
+
1376
+ if not skills_info:
1377
+ console.print("[yellow]No skills found in the repository.[/yellow]")
1378
+ return 0
1379
+
1380
+ console.print(f"[green]✓[/green] Found {len(skills_info)} skills\n")
1381
+
1382
+ # Download all skills
1383
+ with Progress(
1384
+ SpinnerColumn(),
1385
+ TextColumn("[progress.description]{task.description}"),
1386
+ console=console,
1387
+ ) as progress:
1388
+ task = progress.add_task("Downloading skills...", total=None)
1389
+ downloaded = download_multiple_skills(skills_info, dest_dir)
1390
+ progress.update(task, completed=True)
1391
+
1392
+ console.print(f"[green]✓[/green] Downloaded {len(downloaded)} skills\n")
1393
+
1394
+ # Save metadata for each
1395
+ for skill_dir, metadata in downloaded:
1396
+ save_skill_metadata(
1397
+ skill_dir,
1398
+ metadata["url"],
1399
+ metadata["owner"],
1400
+ metadata["repo"],
1401
+ metadata["branch"],
1402
+ metadata["path"],
1403
+ )
1404
+ else:
1405
+ # Download single skill
1406
+ try:
1407
+ with Progress(
1408
+ SpinnerColumn(),
1409
+ TextColumn("[progress.description]{task.description}"),
1410
+ console=console,
1411
+ ) as progress:
1412
+ task = progress.add_task("Downloading skill...", total=None)
1413
+ skill_dir, metadata = download_skill_from_github(url, dest_dir)
1414
+ progress.update(task, completed=True)
1415
+
1416
+ save_skill_metadata(
1417
+ skill_dir,
1418
+ url,
1419
+ metadata["owner"],
1420
+ metadata["repo"],
1421
+ metadata["branch"],
1422
+ metadata["path"],
1423
+ )
1424
+
1425
+ if not validate_skill(skill_dir):
1426
+ console.print(f"[yellow]Warning: {skill_dir} does not contain SKILL.md, may not be a valid skill[/yellow]")
1427
+
1428
+ console.print(f"[green]✓[/green] Skill downloaded to: {skill_dir}\n")
1429
+ downloaded = [(skill_dir, metadata)]
1430
+
1431
+ except Exception as e:
1432
+ console.print(f"[red]Download failed: {e}[/red]")
1433
+ return 1
1434
+
1435
+ if skip_deploy:
1436
+ console.print("[dim]Skipping deployment (--no-deploy)[/dim]")
1437
+ return 0
1438
+
1439
+ # Determine agents
1440
+ if args.agents:
1441
+ selected_agents = args.agents
1442
+ # Validate agents
1443
+ for agent_id in selected_agents:
1444
+ if agent_id not in AGENTS:
1445
+ console.print(f"[red]Unknown agent: {agent_id}[/red]")
1446
+ console.print(f"[dim]Available agents: {', '.join(sorted(AGENTS.keys()))}[/dim]")
1447
+ return 1
1448
+ if deployment_type == "global" and not supports_global_deployment(agent_id):
1449
+ console.print(f"[red]Agent '{agent_id}' does not support global deployment[/red]")
1450
+ return 1
1451
+ else:
1452
+ # Interactive selection
1453
+ existing_agents = detect_existing_agents()
1454
+ if existing_agents:
1455
+ console.print(f"[green]✓[/green] Detected {len(existing_agents)} installed agents\n")
1456
+ selected_agents = select_agents(existing_agents)
1457
+ if not selected_agents:
1458
+ console.print("[yellow]No agents selected.[/yellow]")
1459
+ return 0
1460
+
1461
+ console.print()
1462
+
1463
+ # Deploy
1464
+ project_root = get_project_root() if deployment_type == "project" else None
1465
+ total_success = 0
1466
+ total_fail = 0
1467
+
1468
+ for skill_dir, _ in downloaded:
1469
+ success_count, fail_count = deploy_skill_to_agents(
1470
+ skill_dir, selected_agents, deployment_type, project_root, use_symlink
1471
+ )
1472
+ total_success += success_count
1473
+ total_fail += fail_count
1474
+
1475
+ # Show results
1476
+ console.print()
1477
+ if total_fail == 0:
1478
+ link_type = "symlinked" if use_symlink else "deployed"
1479
+ console.print(
1480
+ Panel.fit(
1481
+ f"[bold green]✓ Installation successful![/bold green]\n\nSkills {link_type} to {total_success} agent(s)",
1482
+ border_style="green",
1483
+ )
1484
+ )
1485
+ else:
1486
+ console.print(
1487
+ Panel.fit(
1488
+ f"[bold yellow]⚠ Installation completed with errors[/bold yellow]\n\n"
1489
+ f"Success: {total_success} | Failed: {total_fail}",
1490
+ border_style="yellow",
1491
+ )
1492
+ )
1493
+
1494
+ return 0 if total_fail == 0 else 1
1495
+
1496
+
1497
+ def cmd_download_cli(args: argparse.Namespace) -> int:
1498
+ """
1499
+ Download command with CLI arguments.
1500
+
1501
+ Args:
1502
+ args: Parsed command line arguments
1503
+
1504
+ Returns:
1505
+ Exit code (0 for success, non-zero for failure)
1506
+ """
1507
+ # If no URL provided, fall back to interactive mode
1508
+ if not args.url:
1509
+ return cmd_download()
1510
+
1511
+ console.print(
1512
+ Panel.fit(
1513
+ "[bold cyan]Download Skill[/bold cyan]\nDownload a skill from GitHub",
1514
+ border_style="cyan",
1515
+ )
1516
+ )
1517
+
1518
+ url = args.url
1519
+
1520
+ # Parse URL to show info
1521
+ try:
1522
+ owner, repo, branch, path = parse_github_url(url)
1523
+ console.print(f"[dim]Repository: {owner}/{repo}[/dim]")
1524
+ console.print(f"[dim]Branch: {branch}[/dim]")
1525
+ console.print(f"[dim]Path: {path or '(root)'}[/dim]\n")
1526
+ except ValueError as e:
1527
+ console.print(f"[red]Error: {e}[/red]")
1528
+ return 1
1529
+
1530
+ # Determine destination directory
1531
+ if args.dest:
1532
+ dest_dir = args.dest
1533
+ else:
1534
+ dest_dir = get_system_temp_dir()
1535
+
1536
+ dest_dir.mkdir(parents=True, exist_ok=True)
1537
+ console.print(f"[dim]Download location: {dest_dir}[/dim]\n")
1538
+
1539
+ # Discover skills if requested
1540
+ if args.discover:
1541
+ with Progress(
1542
+ SpinnerColumn(),
1543
+ TextColumn("[progress.description]{task.description}"),
1544
+ console=console,
1545
+ ) as progress:
1546
+ task = progress.add_task("Discovering skills...", total=None)
1547
+ skills_info = discover_skills_in_repo(url)
1548
+ progress.update(task, completed=True)
1549
+
1550
+ if not skills_info:
1551
+ console.print("[yellow]No skills found in the repository.[/yellow]")
1552
+ return 0
1553
+
1554
+ console.print(f"[green]✓[/green] Found {len(skills_info)} skills\n")
1555
+
1556
+ # Download all skills
1557
+ with Progress(
1558
+ SpinnerColumn(),
1559
+ TextColumn("[progress.description]{task.description}"),
1560
+ console=console,
1561
+ ) as progress:
1562
+ task = progress.add_task("Downloading skills...", total=None)
1563
+ downloaded = download_multiple_skills(skills_info, dest_dir)
1564
+ progress.update(task, completed=True)
1565
+
1566
+ console.print(f"[green]✓[/green] Downloaded {len(downloaded)} skills to {dest_dir}\n")
1567
+
1568
+ # Save metadata for each
1569
+ for skill_dir, metadata in downloaded:
1570
+ save_skill_metadata(
1571
+ skill_dir,
1572
+ metadata["url"],
1573
+ metadata["owner"],
1574
+ metadata["repo"],
1575
+ metadata["branch"],
1576
+ metadata["path"],
1577
+ )
1578
+
1579
+ return 0
1580
+ else:
1581
+ # Download single skill
1582
+ try:
1583
+ with Progress(
1584
+ SpinnerColumn(),
1585
+ TextColumn("[progress.description]{task.description}"),
1586
+ console=console,
1587
+ ) as progress:
1588
+ task = progress.add_task("Downloading skill...", total=None)
1589
+ skill_dir, metadata = download_skill_from_github(url, dest_dir)
1590
+ progress.update(task, completed=True)
1591
+
1592
+ save_skill_metadata(
1593
+ skill_dir,
1594
+ url,
1595
+ metadata["owner"],
1596
+ metadata["repo"],
1597
+ metadata["branch"],
1598
+ metadata["path"],
1599
+ )
1600
+
1601
+ if not validate_skill(skill_dir):
1602
+ console.print(f"[yellow]Warning: {skill_dir} does not contain SKILL.md, may not be a valid skill[/yellow]")
1603
+
1604
+ console.print(f"[green]✓[/green] Skill downloaded to: {skill_dir}\n")
1605
+ return 0
1606
+
1607
+ except Exception as e:
1608
+ console.print(f"[red]Download failed: {e}[/red]")
1609
+ return 1
1610
+
1611
+
1124
1612
  def main() -> int:
1125
1613
  """
1126
1614
  Main CLI entry point.
@@ -1128,45 +1616,59 @@ def main() -> int:
1128
1616
  Returns:
1129
1617
  Exit code
1130
1618
  """
1619
+ parser = create_parser()
1620
+
1621
+ # Handle no arguments
1131
1622
  if len(sys.argv) < 2:
1132
1623
  console.print(
1133
1624
  Panel.fit(
1134
- "[bold cyan]Skill Manager[/bold cyan]\n\n"
1625
+ f"[bold cyan]Skill Manager[/bold cyan] [dim]v{__version__}[/dim]\n\n"
1135
1626
  "Usage:\n"
1136
- " sm download - Download a skill from GitHub\n"
1137
- " sm deploy - Deploy local skills to agents\n"
1138
- " sm install - Download and deploy in one step\n"
1139
- " sm uninstall - Remove skills from agents (safe delete)\n"
1140
- " sm restore - Restore deleted skills from trash\n"
1141
- " sm update - Update skills from GitHub\n"
1142
- " sm update --all - Update all skills from GitHub\n"
1143
- " sm list - List installed skills and versions\n\n"
1144
- "[dim]Note: You can also use 'skill-manager' instead of 'sm'[/dim]",
1627
+ " sm install [url] - Download and deploy skills\n"
1628
+ " sm download [url] - Download a skill from GitHub\n"
1629
+ " sm deploy - Deploy local skills to agents\n"
1630
+ " sm discover [url] - Discover skills in a repository\n"
1631
+ " sm uninstall - Remove skills from agents\n"
1632
+ " sm restore - Restore deleted skills from trash\n"
1633
+ " sm update [--all] - Update skills from GitHub\n"
1634
+ " sm list - List installed skills\n"
1635
+ " sm agents - List all supported agents\n"
1636
+ " sm --version - Show version information\n\n"
1637
+ "[bold]CLI Options:[/bold]\n"
1638
+ " -a, --agent AGENT - Target agent(s)\n"
1639
+ " -t, --type TYPE - Deployment type (global/project)\n"
1640
+ " --symlink - Use symlinks instead of copying\n"
1641
+ " --discover - Discover all skills in repository\n"
1642
+ " -y, --yes - Skip confirmation prompts\n\n"
1643
+ "[dim]Example: sm install https://github.com/cloudflare/skills --discover -a windsurf --symlink[/dim]",
1145
1644
  border_style="cyan",
1146
1645
  )
1147
1646
  )
1148
1647
  return 1
1149
1648
 
1150
- command = sys.argv[1]
1649
+ args = parser.parse_args()
1151
1650
 
1152
1651
  try:
1153
- if command == "download":
1154
- return cmd_download()
1155
- elif command == "deploy":
1652
+ if args.command == "download":
1653
+ return cmd_download_cli(args)
1654
+ elif args.command == "deploy":
1156
1655
  return cmd_deploy()
1157
- elif command == "install":
1158
- return cmd_install()
1159
- elif command == "uninstall":
1656
+ elif args.command == "install":
1657
+ return cmd_install_cli(args)
1658
+ elif args.command == "discover":
1659
+ return cmd_discover(args)
1660
+ elif args.command == "uninstall":
1160
1661
  return cmd_uninstall()
1161
- elif command == "restore":
1662
+ elif args.command == "restore":
1162
1663
  return cmd_restore()
1163
- elif command == "update":
1664
+ elif args.command == "update":
1164
1665
  return cmd_update()
1165
- elif command == "list":
1666
+ elif args.command == "list":
1166
1667
  return cmd_list()
1668
+ elif args.command == "agents":
1669
+ return cmd_agents()
1167
1670
  else:
1168
- console.print(f"[red]Unknown command: {command}[/red]")
1169
- console.print("Available commands: download, deploy, install, uninstall, restore, update, list")
1671
+ parser.print_help()
1170
1672
  return 1
1171
1673
  except KeyboardInterrupt:
1172
1674
  console.print("\n[yellow]Cancelled[/yellow]")