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