oasr 0.5.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.
Files changed (59) hide show
  1. __init__.py +3 -0
  2. __main__.py +6 -0
  3. adapter.py +396 -0
  4. adapters/__init__.py +17 -0
  5. adapters/base.py +254 -0
  6. adapters/claude.py +82 -0
  7. adapters/codex.py +84 -0
  8. adapters/copilot.py +210 -0
  9. adapters/cursor.py +78 -0
  10. adapters/windsurf.py +83 -0
  11. agents/__init__.py +25 -0
  12. agents/base.py +96 -0
  13. agents/claude.py +25 -0
  14. agents/codex.py +25 -0
  15. agents/copilot.py +25 -0
  16. agents/opencode.py +25 -0
  17. agents/registry.py +57 -0
  18. cli.py +97 -0
  19. commands/__init__.py +6 -0
  20. commands/adapter.py +102 -0
  21. commands/add.py +435 -0
  22. commands/clean.py +30 -0
  23. commands/clone.py +178 -0
  24. commands/config.py +163 -0
  25. commands/diff.py +180 -0
  26. commands/exec.py +245 -0
  27. commands/find.py +56 -0
  28. commands/help.py +51 -0
  29. commands/info.py +152 -0
  30. commands/list.py +110 -0
  31. commands/registry.py +447 -0
  32. commands/rm.py +128 -0
  33. commands/status.py +119 -0
  34. commands/sync.py +143 -0
  35. commands/update.py +417 -0
  36. commands/use.py +45 -0
  37. commands/validate.py +74 -0
  38. config/__init__.py +119 -0
  39. config/defaults.py +40 -0
  40. config/schema.py +73 -0
  41. discovery.py +145 -0
  42. manifest.py +437 -0
  43. oasr-0.5.0.dist-info/METADATA +358 -0
  44. oasr-0.5.0.dist-info/RECORD +59 -0
  45. oasr-0.5.0.dist-info/WHEEL +4 -0
  46. oasr-0.5.0.dist-info/entry_points.txt +3 -0
  47. oasr-0.5.0.dist-info/licenses/LICENSE +187 -0
  48. oasr-0.5.0.dist-info/licenses/NOTICE +8 -0
  49. policy/__init__.py +50 -0
  50. policy/defaults.py +27 -0
  51. policy/enforcement.py +98 -0
  52. policy/profile.py +185 -0
  53. registry.py +173 -0
  54. remote.py +482 -0
  55. skillcopy/__init__.py +71 -0
  56. skillcopy/local.py +40 -0
  57. skillcopy/remote.py +98 -0
  58. tracking.py +181 -0
  59. validate.py +362 -0
commands/config.py ADDED
@@ -0,0 +1,163 @@
1
+ """Config command - manage OASR configuration."""
2
+
3
+ import argparse
4
+ import sys
5
+
6
+ from agents import detect_available_agents, get_all_agent_names
7
+ from config import CONFIG_FILE, load_config, save_config
8
+
9
+
10
+ def register(subparsers: argparse._SubParsersAction) -> None:
11
+ """Register the config command."""
12
+ parser = subparsers.add_parser(
13
+ "config",
14
+ help="Manage configuration",
15
+ description="Manage OASR configuration settings",
16
+ )
17
+
18
+ config_subparsers = parser.add_subparsers(dest="config_action", help="Config actions")
19
+
20
+ # config set
21
+ set_parser = config_subparsers.add_parser(
22
+ "set",
23
+ help="Set a configuration value",
24
+ description="Set a configuration value",
25
+ )
26
+ set_parser.add_argument("key", help="Configuration key (e.g., 'agent')")
27
+ set_parser.add_argument("value", help="Configuration value")
28
+ set_parser.set_defaults(func=run_set)
29
+
30
+ # config get
31
+ get_parser = config_subparsers.add_parser(
32
+ "get",
33
+ help="Get a configuration value",
34
+ description="Get a configuration value",
35
+ )
36
+ get_parser.add_argument("key", help="Configuration key (e.g., 'agent')")
37
+ get_parser.set_defaults(func=run_get)
38
+
39
+ # config list
40
+ list_parser = config_subparsers.add_parser(
41
+ "list",
42
+ help="List all configuration",
43
+ description="List all configuration settings",
44
+ )
45
+ list_parser.set_defaults(func=run_list)
46
+
47
+ # config path
48
+ path_parser = config_subparsers.add_parser(
49
+ "path",
50
+ help="Show config file path",
51
+ description="Show configuration file path",
52
+ )
53
+ path_parser.set_defaults(func=run_path)
54
+
55
+ # Default to showing help if no subcommand
56
+ parser.set_defaults(func=lambda args: parser.print_help() or 1)
57
+
58
+
59
+ def run_set(args: argparse.Namespace) -> int:
60
+ """Set a configuration value."""
61
+ key = args.key.lower()
62
+ value = args.value
63
+
64
+ # Only support agent for now
65
+ if key == "agent":
66
+ # Validate agent name
67
+ valid_agents = get_all_agent_names()
68
+ if value not in valid_agents:
69
+ print(
70
+ f"Error: Invalid agent '{value}'. Must be one of: {', '.join(valid_agents)}",
71
+ file=sys.stderr,
72
+ )
73
+ return 1
74
+
75
+ # Load config, update, save
76
+ config = load_config(args.config if hasattr(args, "config") else None)
77
+ config["agent"]["default"] = value
78
+ save_config(config, args.config if hasattr(args, "config") else None)
79
+
80
+ # Show available vs configured
81
+ available = detect_available_agents()
82
+ if value in available:
83
+ print(f"✓ Default agent set to: {value}")
84
+ else:
85
+ print(f"✓ Default agent set to: {value}")
86
+ print(
87
+ f" Warning: '{value}' binary not found in PATH. Install it to use this agent.",
88
+ file=sys.stderr,
89
+ )
90
+
91
+ return 0
92
+ else:
93
+ print(f"Error: Unsupported config key '{key}'. Only 'agent' is supported.", file=sys.stderr)
94
+ return 1
95
+
96
+
97
+ def run_get(args: argparse.Namespace) -> int:
98
+ """Get a configuration value."""
99
+ key = args.key.lower()
100
+
101
+ config = load_config(args.config if hasattr(args, "config") else None)
102
+
103
+ if key == "agent":
104
+ agent = config["agent"].get("default")
105
+ if agent:
106
+ print(agent)
107
+ else:
108
+ print("No default agent configured", file=sys.stderr)
109
+ return 1
110
+ return 0
111
+ else:
112
+ print(f"Error: Unsupported config key '{key}'. Only 'agent' is supported.", file=sys.stderr)
113
+ return 1
114
+
115
+
116
+ def run_list(args: argparse.Namespace) -> int:
117
+ """List all configuration."""
118
+ config = load_config(args.config if hasattr(args, "config") else None)
119
+
120
+ print("Configuration:")
121
+ print()
122
+
123
+ # Agent section
124
+ print(" [agent]")
125
+ agent = config["agent"].get("default")
126
+ if agent:
127
+ available = detect_available_agents()
128
+ status = "✓" if agent in available else "✗"
129
+ print(f" default = {agent} {status}")
130
+ else:
131
+ print(" default = (not set)")
132
+ print()
133
+
134
+ # Show available agents
135
+ available = detect_available_agents()
136
+ if available:
137
+ print(f" Available agents: {', '.join(available)}")
138
+ else:
139
+ print(" Available agents: (none detected)")
140
+ print()
141
+
142
+ # Validation section
143
+ print(" [validation]")
144
+ print(f" reference_max_lines = {config['validation']['reference_max_lines']}")
145
+ print(f" strict = {config['validation']['strict']}")
146
+ print()
147
+
148
+ # Adapter section
149
+ print(" [adapter]")
150
+ print(f" default_targets = {config['adapter']['default_targets']}")
151
+ print()
152
+
153
+ return 0
154
+
155
+
156
+ def run_path(args: argparse.Namespace) -> int:
157
+ """Show config file path."""
158
+ if hasattr(args, "config") and args.config:
159
+ config_path = args.config
160
+ else:
161
+ config_path = CONFIG_FILE
162
+ print(config_path)
163
+ return 0
commands/diff.py ADDED
@@ -0,0 +1,180 @@
1
+ """`oasr diff` command - show tracked skill status."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ from manifest import load_manifest
11
+ from tracking import extract_metadata
12
+
13
+
14
+ def register(subparsers) -> None:
15
+ """Register the diff command."""
16
+ p = subparsers.add_parser("diff", help="Show status of tracked skills (copied with metadata)")
17
+ p.add_argument(
18
+ "path",
19
+ nargs="?",
20
+ type=Path,
21
+ default=Path.cwd(),
22
+ help="Path to scan for tracked skills (default: current directory)",
23
+ )
24
+ p.add_argument("--json", action="store_true", help="Output in JSON format")
25
+ p.add_argument("--quiet", action="store_true", help="Suppress info/warnings")
26
+ p.set_defaults(func=run)
27
+
28
+
29
+ def run(args: argparse.Namespace) -> int:
30
+ """Show status of tracked skills in the given path."""
31
+ scan_path = args.path.resolve()
32
+
33
+ if not scan_path.exists():
34
+ print(f"Error: Path does not exist: {scan_path}", file=sys.stderr)
35
+ return 1
36
+
37
+ # Find all SKILL.md files recursively
38
+ if not args.quiet and not args.json:
39
+ print(f"Scanning {scan_path} for tracked skills...", file=sys.stderr)
40
+
41
+ tracked_skills = []
42
+ skill_md_files = list(scan_path.rglob("SKILL.md"))
43
+
44
+ for skill_md in skill_md_files:
45
+ skill_dir = skill_md.parent
46
+ metadata = extract_metadata(skill_dir)
47
+
48
+ if metadata:
49
+ tracked_skills.append((skill_dir, metadata))
50
+
51
+ if not tracked_skills:
52
+ if args.json:
53
+ print(json.dumps({"tracked": 0, "skills": []}))
54
+ else:
55
+ print("No tracked skills found.")
56
+ return 0
57
+
58
+ # Determine status for each tracked skill
59
+ results = []
60
+ up_to_date = 0
61
+ outdated = 0
62
+ modified = 0
63
+ untracked = 0
64
+
65
+ for skill_dir, metadata in tracked_skills:
66
+ skill_name = skill_dir.name
67
+ tracked_hash = metadata.get("hash")
68
+ tracked_source = metadata.get("source")
69
+
70
+ # Validate metadata structure
71
+ if not tracked_hash or not tracked_source:
72
+ untracked += 1
73
+ results.append(
74
+ {
75
+ "name": skill_name,
76
+ "path": str(skill_dir),
77
+ "status": "error",
78
+ "message": "Corrupted metadata (missing hash or source)",
79
+ }
80
+ )
81
+ if not args.quiet and not args.json:
82
+ print(f" ✗ {skill_name}: corrupted metadata", file=sys.stderr)
83
+ continue
84
+
85
+ # Check if in registry
86
+ from registry import load_registry
87
+
88
+ try:
89
+ entries = load_registry()
90
+ except Exception as e:
91
+ if not args.quiet and not args.json:
92
+ print(f"Error loading registry: {e}", file=sys.stderr)
93
+ return 1
94
+
95
+ entry = next((e for e in entries if e.name == skill_name), None)
96
+
97
+ if entry:
98
+ try:
99
+ manifest = load_manifest(skill_name)
100
+ except Exception as e:
101
+ untracked += 1
102
+ results.append(
103
+ {"name": skill_name, "path": str(skill_dir), "status": "error", "message": f"Manifest error: {e}"}
104
+ )
105
+ if not args.quiet and not args.json:
106
+ print(f" ✗ {skill_name}: manifest error", file=sys.stderr)
107
+ continue
108
+
109
+ if manifest:
110
+ # Compare tracked hash with registry hash
111
+ if manifest.content_hash == tracked_hash:
112
+ # Up to date
113
+ status = "up-to-date"
114
+ up_to_date += 1
115
+ message = "Current"
116
+ else:
117
+ # Registry has changed
118
+ status = "outdated"
119
+ outdated += 1
120
+ message = "Registry has newer version"
121
+ else:
122
+ status = "untracked"
123
+ untracked += 1
124
+ message = "No manifest"
125
+ else:
126
+ status = "untracked"
127
+ untracked += 1
128
+ message = "Not in registry"
129
+
130
+ results.append(
131
+ {
132
+ "name": skill_name,
133
+ "path": str(skill_dir),
134
+ "status": status,
135
+ "message": message,
136
+ "tracked_hash": tracked_hash[:16] + "..." if tracked_hash else None,
137
+ "source": tracked_source,
138
+ }
139
+ )
140
+
141
+ if args.json:
142
+ print(
143
+ json.dumps(
144
+ {
145
+ "tracked": len(tracked_skills),
146
+ "up_to_date": up_to_date,
147
+ "outdated": outdated,
148
+ "modified": modified,
149
+ "untracked": untracked,
150
+ "skills": results,
151
+ },
152
+ indent=2,
153
+ )
154
+ )
155
+ else:
156
+ # Git-style output
157
+ for result in results:
158
+ if result["status"] == "up-to-date":
159
+ symbol = "✓"
160
+ elif result["status"] == "outdated":
161
+ symbol = "⚠"
162
+ elif result["status"] == "modified":
163
+ symbol = "✗"
164
+ else:
165
+ symbol = "?"
166
+
167
+ print(f"{symbol} {result['name']}: {result['status']}")
168
+ if not args.quiet:
169
+ print(f" Path: {result['path']}")
170
+ if result["message"]:
171
+ print(f" {result['message']}")
172
+
173
+ print(
174
+ f"\n{len(tracked_skills)} tracked: {up_to_date} up-to-date, {outdated} outdated, {modified} modified, {untracked} untracked"
175
+ )
176
+
177
+ if outdated > 0:
178
+ print("\nRun 'oasr sync' to update outdated skills.")
179
+
180
+ return 1 if outdated > 0 or modified > 0 else 0
commands/exec.py ADDED
@@ -0,0 +1,245 @@
1
+ """Execute a skill from the registry using an agent CLI."""
2
+
3
+ import argparse
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ import policy
8
+ from agents.registry import detect_available_agents, get_driver
9
+ from config import load_config
10
+ from registry import load_registry
11
+
12
+
13
+ def register(subparsers):
14
+ """Register the exec command."""
15
+ setup_parser(subparsers)
16
+
17
+
18
+ def setup_parser(subparsers):
19
+ """Set up the exec command parser."""
20
+ parser = subparsers.add_parser(
21
+ "exec",
22
+ help="Execute a skill from the registry",
23
+ description="Execute a skill from the registry using an agent CLI. "
24
+ "The skill is executed in the current working directory.",
25
+ )
26
+ parser.add_argument(
27
+ "skill_name",
28
+ help="Name of the skill to execute from the registry",
29
+ )
30
+ parser.add_argument(
31
+ "-p",
32
+ "--prompt",
33
+ help="Inline prompt/instructions for the agent",
34
+ )
35
+ parser.add_argument(
36
+ "-i",
37
+ "--instructions",
38
+ metavar="FILE",
39
+ help="Read prompt/instructions from a file",
40
+ )
41
+ parser.add_argument(
42
+ "-a",
43
+ "--agent",
44
+ help="Override the default agent (codex, copilot, claude, opencode)",
45
+ )
46
+ parser.add_argument(
47
+ "--profile",
48
+ help="Execution policy profile to use (default: from config)",
49
+ )
50
+ parser.add_argument(
51
+ "-y",
52
+ "--yes",
53
+ action="store_true",
54
+ help="Skip confirmation prompt for risky operations",
55
+ )
56
+ parser.add_argument(
57
+ "--confirm",
58
+ action="store_true",
59
+ help="Force confirmation prompt even for safe operations",
60
+ )
61
+ parser.set_defaults(func=run)
62
+
63
+
64
+ def run(args: argparse.Namespace) -> int:
65
+ """Execute a skill from the registry."""
66
+ # Load registry to find the skill
67
+ entries = load_registry()
68
+ entry_map = {e.name: e for e in entries}
69
+ skill_name = args.skill_name
70
+
71
+ if skill_name not in entry_map:
72
+ print(f"Error: Skill '{skill_name}' not found in registry", file=sys.stderr)
73
+ print("\nUse 'oasr registry list' to see available skills.", file=sys.stderr)
74
+ return 1
75
+
76
+ skill_entry = entry_map[skill_name]
77
+ skill_source = skill_entry.path
78
+
79
+ if not skill_source:
80
+ print(f"Error: Skill '{skill_name}' has no source configured", file=sys.stderr)
81
+ return 1
82
+
83
+ # Get the skill content - look for SKILL.md in the skill directory
84
+ skill_dir = Path(skill_source)
85
+ skill_path = skill_dir / "SKILL.md"
86
+
87
+ if not skill_path.exists():
88
+ print(f"Error: Skill file not found: {skill_path}", file=sys.stderr)
89
+ print("\nTry running 'oasr sync' to update your skills.", file=sys.stderr)
90
+ return 1
91
+
92
+ try:
93
+ skill_content = skill_path.read_text(encoding="utf-8")
94
+ except Exception as e:
95
+ print(f"Error reading skill file: {e}", file=sys.stderr)
96
+ return 1
97
+
98
+ # Get the user prompt from various sources
99
+ user_prompt = _get_user_prompt(args)
100
+ if user_prompt is None:
101
+ # Error already printed by _get_user_prompt
102
+ return 1
103
+
104
+ # Determine which agent to use
105
+ agent_name = _get_agent_name(args)
106
+ if agent_name is None:
107
+ # Error already printed by _get_agent_name
108
+ return 1
109
+
110
+ # === POLICY ENFORCEMENT ===
111
+ # Load configuration
112
+ config = load_config()
113
+
114
+ # Determine which profile to use (flag overrides config)
115
+ profile_name = args.profile if args.profile else config.get("oasr", {}).get("default_profile", "safe")
116
+
117
+ # Load the policy profile
118
+ profile = policy.load(config, profile_name)
119
+
120
+ # Detect execution context for risk assessment
121
+ stdin_used = not sys.stdin.isatty() and not args.prompt and not args.instructions
122
+ instructions_file_used = bool(args.instructions)
123
+
124
+ # Assess risk and determine if confirmation is needed
125
+ needs_confirm, reasons = policy.assess_risk(
126
+ profile=profile,
127
+ stdin_used=stdin_used,
128
+ instructions_file_used=instructions_file_used,
129
+ force_confirm=args.confirm,
130
+ )
131
+
132
+ # Require confirmation unless --yes flag is present
133
+ if needs_confirm and not args.yes:
134
+ summary = policy.summarize(profile, skill_name, agent_name)
135
+ if not policy.prompt_confirmation(summary, reasons):
136
+ return 1 # User aborted
137
+
138
+ # === END POLICY ENFORCEMENT ===
139
+
140
+ # Get the agent driver
141
+ try:
142
+ driver = get_driver(agent_name)
143
+ except ValueError as e:
144
+ print(f"Error: {e}", file=sys.stderr)
145
+ return 1
146
+
147
+ # Execute the skill
148
+ print(f"Executing skill '{skill_name}' with {agent_name}...", file=sys.stderr)
149
+ print("━" * 60, file=sys.stderr)
150
+
151
+ try:
152
+ result = driver.execute(skill_content, user_prompt)
153
+ # CompletedProcess has returncode attribute (0 = success)
154
+ # Output was already streamed to stdout since capture_output=False
155
+ return result.returncode
156
+ except Exception as e:
157
+ print(f"\nUnexpected error: {e}", file=sys.stderr)
158
+ return 1
159
+
160
+
161
+ def _get_user_prompt(args: argparse.Namespace) -> str | None:
162
+ """Get the user prompt from CLI args or stdin.
163
+
164
+ Returns None if there's an error, with error message printed to stderr.
165
+ """
166
+ # Check for conflicting options
167
+ if args.prompt and args.instructions:
168
+ print(
169
+ "Error: Cannot use both --prompt and --instructions at the same time",
170
+ file=sys.stderr,
171
+ )
172
+ return None
173
+
174
+ # Option 1: Inline prompt via -p/--prompt
175
+ if args.prompt:
176
+ return args.prompt
177
+
178
+ # Option 2: File-based instructions via -i/--instructions
179
+ if args.instructions:
180
+ instructions_path = Path(args.instructions)
181
+ if not instructions_path.exists():
182
+ print(f"Error: Instructions file not found: {args.instructions}", file=sys.stderr)
183
+ return None
184
+
185
+ try:
186
+ return instructions_path.read_text(encoding="utf-8")
187
+ except Exception as e:
188
+ print(f"Error reading instructions file: {e}", file=sys.stderr)
189
+ return None
190
+
191
+ # Option 3: Read from stdin
192
+ if not sys.stdin.isatty():
193
+ try:
194
+ return sys.stdin.read()
195
+ except Exception as e:
196
+ print(f"Error reading from stdin: {e}", file=sys.stderr)
197
+ return None
198
+
199
+ # No prompt provided
200
+ print("Error: No prompt provided", file=sys.stderr)
201
+ print("\nProvide a prompt using one of:", file=sys.stderr)
202
+ print(" -p/--prompt 'Your prompt here'", file=sys.stderr)
203
+ print(" -i/--instructions path/to/file.txt", file=sys.stderr)
204
+ print(" echo 'Your prompt' | oasr exec <skill>", file=sys.stderr)
205
+ return None
206
+
207
+
208
+ def _get_agent_name(args: argparse.Namespace) -> str | None:
209
+ """Get the agent name from CLI flag or config.
210
+
211
+ Returns None if there's an error, with error message printed to stderr.
212
+ """
213
+ # Option 1: Explicit --agent flag
214
+ if args.agent:
215
+ agent_name = args.agent.lower()
216
+ # Validate it's a known and available agent
217
+ available = detect_available_agents()
218
+ if agent_name not in available or not available[agent_name]:
219
+ print(f"Error: Agent '{agent_name}' is not available", file=sys.stderr)
220
+ print("\nAvailable agents:", file=sys.stderr)
221
+ for name in sorted(available.keys()):
222
+ status = "✓" if available[name] else "✗"
223
+ print(f" {status} {name}", file=sys.stderr)
224
+ return None
225
+ return agent_name
226
+
227
+ # Option 2: Default from config
228
+ config = load_config()
229
+ default_agent = config.get("agent", {}).get("default")
230
+
231
+ if default_agent:
232
+ return default_agent
233
+
234
+ # No agent configured
235
+ print("Error: No agent configured", file=sys.stderr)
236
+ print("\nConfigure a default agent with:", file=sys.stderr)
237
+ print(" oasr config set agent <agent-name>", file=sys.stderr)
238
+ print("\nOr specify an agent for this command:", file=sys.stderr)
239
+ print(" oasr exec --agent <agent-name> <skill>", file=sys.stderr)
240
+ print("\nAvailable agents:", file=sys.stderr)
241
+ available = detect_available_agents()
242
+ for name in sorted(available.keys()):
243
+ status = "✓" if available[name] else "✗"
244
+ print(f" {status} {name}", file=sys.stderr)
245
+ return None
commands/find.py ADDED
@@ -0,0 +1,56 @@
1
+ """`asr find` command."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ from discovery import find_skills
11
+ from registry import SkillEntry, add_skill
12
+
13
+
14
+ def register(subparsers) -> None:
15
+ p = subparsers.add_parser("find", help="Find skills recursively")
16
+ p.add_argument("root", type=Path, help="Root directory to search")
17
+ p.add_argument("--add", action="store_true", dest="add_found", help="Register found skills")
18
+ p.add_argument("--json", action="store_true", help="Output in JSON format")
19
+ p.add_argument("--quiet", action="store_true", help="Suppress info/warnings")
20
+ p.set_defaults(func=run)
21
+
22
+
23
+ def run(args: argparse.Namespace) -> int:
24
+ root = args.root.resolve()
25
+
26
+ if not root.is_dir():
27
+ print(f"Error: Not a directory: {root}", file=sys.stderr)
28
+ return 2
29
+
30
+ skills = find_skills(root)
31
+
32
+ if args.json:
33
+ data = [{"name": s.name, "description": s.description, "path": str(s.path)} for s in skills]
34
+ print(json.dumps(data, indent=2))
35
+ else:
36
+ if not skills:
37
+ print(f"No skills found under {root}")
38
+ else:
39
+ for s in skills:
40
+ print(f"{s.name:<30} {s.path}")
41
+
42
+ if args.add_found and skills:
43
+ added = 0
44
+ for s in skills:
45
+ entry = SkillEntry(
46
+ path=str(s.path),
47
+ name=s.name,
48
+ description=s.description,
49
+ )
50
+ if add_skill(entry):
51
+ added += 1
52
+
53
+ if not args.json and not args.quiet:
54
+ print(f"\nRegistered {added} new skill(s), {len(skills) - added} updated.")
55
+
56
+ return 0
commands/help.py ADDED
@@ -0,0 +1,51 @@
1
+ """`asr help` command."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+
7
+
8
+ def register(subparsers, parser_ref: argparse.ArgumentParser) -> None:
9
+ """Register the help subcommand.
10
+
11
+ Args:
12
+ subparsers: The subparsers object to add to.
13
+ parser_ref: Reference to the main parser for displaying help.
14
+ """
15
+ p = subparsers.add_parser(
16
+ "help",
17
+ help="Show help for a command",
18
+ add_help=False,
19
+ )
20
+ p.add_argument(
21
+ "command",
22
+ nargs="?",
23
+ help="Command to show help for",
24
+ )
25
+ p.set_defaults(func=lambda args: run(args, parser_ref))
26
+
27
+
28
+ def run(args: argparse.Namespace, parser: argparse.ArgumentParser) -> int:
29
+ """Show help for the specified command or general help."""
30
+ if not args.command:
31
+ parser.print_help()
32
+ return 0
33
+
34
+ # Find the subparser for the given command
35
+ subparsers_action = None
36
+ for action in parser._actions:
37
+ if isinstance(action, argparse._SubParsersAction):
38
+ subparsers_action = action
39
+ break
40
+
41
+ if subparsers_action is None:
42
+ print("Error: No commands available")
43
+ return 1
44
+
45
+ if args.command in subparsers_action.choices:
46
+ subparsers_action.choices[args.command].print_help()
47
+ return 0
48
+ else:
49
+ print(f"Unknown command: {args.command}")
50
+ print(f"\nAvailable commands: {', '.join(sorted(subparsers_action.choices.keys()))}")
51
+ return 1