agent-skill-manager 0.1.3__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
@@ -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,85 @@ 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("--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
+
61
149
  def select_agents(existing_agents: dict[str, Path]) -> list[str]:
62
150
  """
63
151
  Interactive prompt to select agents for deployment.
@@ -182,8 +270,7 @@ def cmd_download() -> int:
182
270
  """
183
271
  console.print(
184
272
  Panel.fit(
185
- "[bold cyan]Download Skill[/bold cyan]\n"
186
- "Download a skill from GitHub",
273
+ "[bold cyan]Download Skill[/bold cyan]\nDownload a skill from GitHub",
187
274
  border_style="cyan",
188
275
  )
189
276
  )
@@ -256,9 +343,7 @@ def cmd_download() -> int:
256
343
 
257
344
  # Validate
258
345
  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
- )
346
+ console.print(f"[yellow]Warning: {skill_dir} does not contain SKILL.md, may not be a valid skill[/yellow]")
262
347
 
263
348
  console.print(f"[green]✓[/green] Skill downloaded to: {skill_dir}\n")
264
349
  return 0
@@ -277,8 +362,7 @@ def cmd_deploy() -> int:
277
362
  """
278
363
  console.print(
279
364
  Panel.fit(
280
- "[bold cyan]Deploy Skills[/bold cyan]\n"
281
- "Deploy local skills to AI agents",
365
+ "[bold cyan]Deploy Skills[/bold cyan]\nDeploy local skills to AI agents",
282
366
  border_style="cyan",
283
367
  )
284
368
  )
@@ -309,9 +393,7 @@ def cmd_deploy() -> int:
309
393
  # Detect agents
310
394
  existing_agents = detect_existing_agents()
311
395
  if existing_agents:
312
- console.print(
313
- f"[green]✓[/green] Detected {len(existing_agents)} installed agents\n"
314
- )
396
+ console.print(f"[green]✓[/green] Detected {len(existing_agents)} installed agents\n")
315
397
 
316
398
  # Select deployment type
317
399
  deployment_type = select_deployment_type()
@@ -417,8 +499,7 @@ def cmd_install() -> int:
417
499
  """
418
500
  console.print(
419
501
  Panel.fit(
420
- "[bold cyan]Install Skill[/bold cyan]\n"
421
- "Download and deploy a skill from GitHub",
502
+ "[bold cyan]Install Skill[/bold cyan]\nDownload and deploy a skill from GitHub",
422
503
  border_style="cyan",
423
504
  )
424
505
  )
@@ -491,9 +572,7 @@ def cmd_install() -> int:
491
572
 
492
573
  # Validate
493
574
  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
- )
575
+ console.print(f"[yellow]Warning: {skill_dir} does not contain SKILL.md, may not be a valid skill[/yellow]")
497
576
 
498
577
  console.print(f"[green]✓[/green] Skill downloaded to: {skill_dir}\n")
499
578
 
@@ -516,9 +595,7 @@ def cmd_install() -> int:
516
595
  # Detect agents
517
596
  existing_agents = detect_existing_agents()
518
597
  if existing_agents:
519
- console.print(
520
- f"[green]✓[/green] Detected {len(existing_agents)} installed agents\n"
521
- )
598
+ console.print(f"[green]✓[/green] Detected {len(existing_agents)} installed agents\n")
522
599
 
523
600
  # Select deployment type
524
601
  deployment_type = select_deployment_type()
@@ -574,8 +651,7 @@ def cmd_uninstall() -> int:
574
651
  """
575
652
  console.print(
576
653
  Panel.fit(
577
- "[bold cyan]Uninstall Skills[/bold cyan]\n"
578
- "Remove skills from AI agents",
654
+ "[bold cyan]Uninstall Skills[/bold cyan]\nRemove skills from AI agents",
579
655
  border_style="cyan",
580
656
  )
581
657
  )
@@ -725,8 +801,7 @@ def cmd_restore() -> int:
725
801
  """
726
802
  console.print(
727
803
  Panel.fit(
728
- "[bold cyan]Restore Skills[/bold cyan]\n"
729
- "Restore deleted skills from trash",
804
+ "[bold cyan]Restore Skills[/bold cyan]\nRestore deleted skills from trash",
730
805
  border_style="cyan",
731
806
  )
732
807
  )
@@ -836,8 +911,7 @@ def cmd_restore() -> int:
836
911
  if fail_count == 0:
837
912
  console.print(
838
913
  Panel.fit(
839
- f"[bold green]✓ Restoration successful![/bold green]\n\n"
840
- f"Restored {success_count} skills",
914
+ f"[bold green]✓ Restoration successful![/bold green]\n\nRestored {success_count} skills",
841
915
  border_style="green",
842
916
  )
843
917
  )
@@ -916,7 +990,6 @@ def cmd_update() -> int:
916
990
  for skill_info in all_updatable[agent_id]:
917
991
  skill_name = skill_info["skill_name"]
918
992
  metadata = skill_info["metadata"]
919
- github_url = metadata.get("github_url", "")
920
993
  updated_at = metadata.get("updated_at", "unknown")
921
994
  choices.append(
922
995
  Choice(
@@ -946,7 +1019,10 @@ def cmd_update() -> int:
946
1019
  console.print()
947
1020
 
948
1021
  # 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())
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
+ )
950
1026
  console.print(f"[yellow]Will update {total_count} skills:[/yellow]")
951
1027
  for agent_id in sorted(skills_to_update.keys()):
952
1028
  agent_name = get_agent_name(agent_id)
@@ -1006,8 +1082,7 @@ def cmd_update() -> int:
1006
1082
  if total_failed == 0:
1007
1083
  console.print(
1008
1084
  Panel.fit(
1009
- f"[bold green]✓ Update successful![/bold green]\n\n"
1010
- f"Updated {total_success} skills",
1085
+ f"[bold green]✓ Update successful![/bold green]\n\nUpdated {total_success} skills",
1011
1086
  border_style="green",
1012
1087
  )
1013
1088
  )
@@ -1032,8 +1107,7 @@ def cmd_list() -> int:
1032
1107
  """
1033
1108
  console.print(
1034
1109
  Panel.fit(
1035
- "[bold cyan]List Installed Skills[/bold cyan]\n"
1036
- "Show all installed skills with version information",
1110
+ "[bold cyan]List Installed Skills[/bold cyan]\nShow all installed skills with version information",
1037
1111
  border_style="cyan",
1038
1112
  )
1039
1113
  )
@@ -1076,18 +1150,21 @@ def cmd_list() -> int:
1076
1150
  if skill_md.exists():
1077
1151
  mtime = skill_md.stat().st_mtime
1078
1152
  from datetime import datetime
1153
+
1079
1154
  version_info = datetime.fromtimestamp(mtime).strftime("%Y-%m-%d %H:%M:%S")
1080
1155
  else:
1081
1156
  version_info = "unknown"
1082
1157
  source = "Local"
1083
1158
  github_url = ""
1084
1159
 
1085
- all_skills_data[agent_id].append({
1086
- "name": skill_name,
1087
- "version": version_info,
1088
- "source": source,
1089
- "url": github_url,
1090
- })
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
+ )
1091
1168
 
1092
1169
  if not all_skills_data:
1093
1170
  console.print("[yellow]No skills found in selected agents.[/yellow]")
@@ -1122,6 +1199,416 @@ def cmd_list() -> int:
1122
1199
  return 0
1123
1200
 
1124
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
+
1125
1612
  def main() -> int:
1126
1613
  """
1127
1614
  Main CLI entry point.
@@ -1129,51 +1616,59 @@ def main() -> int:
1129
1616
  Returns:
1130
1617
  Exit code
1131
1618
  """
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
1619
+ parser = create_parser()
1136
1620
 
1621
+ # Handle no arguments
1137
1622
  if len(sys.argv) < 2:
1138
1623
  console.print(
1139
1624
  Panel.fit(
1140
1625
  f"[bold cyan]Skill Manager[/bold cyan] [dim]v{__version__}[/dim]\n\n"
1141
1626
  "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]",
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]",
1152
1644
  border_style="cyan",
1153
1645
  )
1154
1646
  )
1155
1647
  return 1
1156
1648
 
1157
- command = sys.argv[1]
1649
+ args = parser.parse_args()
1158
1650
 
1159
1651
  try:
1160
- if command == "download":
1161
- return cmd_download()
1162
- elif command == "deploy":
1652
+ if args.command == "download":
1653
+ return cmd_download_cli(args)
1654
+ elif args.command == "deploy":
1163
1655
  return cmd_deploy()
1164
- elif command == "install":
1165
- return cmd_install()
1166
- 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":
1167
1661
  return cmd_uninstall()
1168
- elif command == "restore":
1662
+ elif args.command == "restore":
1169
1663
  return cmd_restore()
1170
- elif command == "update":
1664
+ elif args.command == "update":
1171
1665
  return cmd_update()
1172
- elif command == "list":
1666
+ elif args.command == "list":
1173
1667
  return cmd_list()
1668
+ elif args.command == "agents":
1669
+ return cmd_agents()
1174
1670
  else:
1175
- console.print(f"[red]Unknown command: {command}[/red]")
1176
- console.print("Available commands: download, deploy, install, uninstall, restore, update, list, --version")
1671
+ parser.print_help()
1177
1672
  return 1
1178
1673
  except KeyboardInterrupt:
1179
1674
  console.print("\n[yellow]Cancelled[/yellow]")