oasr 0.3.4__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.
- __init__.py +3 -0
- __main__.py +6 -0
- adapter.py +396 -0
- adapters/__init__.py +17 -0
- adapters/base.py +254 -0
- adapters/claude.py +82 -0
- adapters/codex.py +84 -0
- adapters/copilot.py +210 -0
- adapters/cursor.py +78 -0
- adapters/windsurf.py +83 -0
- cli.py +94 -0
- commands/__init__.py +6 -0
- commands/adapter.py +102 -0
- commands/add.py +302 -0
- commands/clean.py +155 -0
- commands/diff.py +180 -0
- commands/find.py +56 -0
- commands/help.py +51 -0
- commands/info.py +152 -0
- commands/list.py +110 -0
- commands/registry.py +303 -0
- commands/rm.py +128 -0
- commands/status.py +119 -0
- commands/sync.py +143 -0
- commands/update.py +417 -0
- commands/use.py +172 -0
- commands/validate.py +74 -0
- config.py +86 -0
- discovery.py +145 -0
- manifest.py +437 -0
- oasr-0.3.4.dist-info/METADATA +358 -0
- oasr-0.3.4.dist-info/RECORD +43 -0
- oasr-0.3.4.dist-info/WHEEL +4 -0
- oasr-0.3.4.dist-info/entry_points.txt +3 -0
- oasr-0.3.4.dist-info/licenses/LICENSE +187 -0
- oasr-0.3.4.dist-info/licenses/NOTICE +8 -0
- registry.py +173 -0
- remote.py +482 -0
- skillcopy/__init__.py +71 -0
- skillcopy/local.py +40 -0
- skillcopy/remote.py +98 -0
- tracking.py +181 -0
- validate.py +362 -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/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
|
commands/info.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Info command - show detailed information about a skill."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
|
|
8
|
+
from manifest import check_manifest, load_manifest
|
|
9
|
+
from registry import load_registry
|
|
10
|
+
from skillcopy.remote import is_remote_source
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def run(args: argparse.Namespace) -> int:
|
|
14
|
+
"""Show detailed information about a skill.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
args: Parsed command-line arguments
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
Exit code (0 for success, non-zero for errors)
|
|
21
|
+
"""
|
|
22
|
+
skill_name = args.skill_name
|
|
23
|
+
|
|
24
|
+
# Load registry to verify skill exists
|
|
25
|
+
entries = load_registry()
|
|
26
|
+
entry = None
|
|
27
|
+
for e in entries:
|
|
28
|
+
if e.name == skill_name:
|
|
29
|
+
entry = e
|
|
30
|
+
break
|
|
31
|
+
|
|
32
|
+
if not entry:
|
|
33
|
+
if not args.quiet:
|
|
34
|
+
print(f"Error: Skill '{skill_name}' not found", file=sys.stderr)
|
|
35
|
+
print("Try: oasr list", file=sys.stderr)
|
|
36
|
+
return 1
|
|
37
|
+
|
|
38
|
+
# Load manifest
|
|
39
|
+
manifest = load_manifest(skill_name)
|
|
40
|
+
if not manifest:
|
|
41
|
+
if not args.quiet:
|
|
42
|
+
print(f"No manifest found for: {skill_name}", file=sys.stderr)
|
|
43
|
+
return 1
|
|
44
|
+
|
|
45
|
+
# Check if remote and show progress indicator
|
|
46
|
+
is_remote = is_remote_source(manifest.source_path)
|
|
47
|
+
if is_remote and not args.quiet and not args.json:
|
|
48
|
+
platform = (
|
|
49
|
+
"GitHub"
|
|
50
|
+
if "github.com" in manifest.source_path
|
|
51
|
+
else "GitLab"
|
|
52
|
+
if "gitlab.com" in manifest.source_path
|
|
53
|
+
else "remote"
|
|
54
|
+
)
|
|
55
|
+
print(f"Checking remote skill status from {platform}...", file=sys.stderr, flush=True)
|
|
56
|
+
|
|
57
|
+
# Check status
|
|
58
|
+
status_result = check_manifest(manifest)
|
|
59
|
+
|
|
60
|
+
# Determine type
|
|
61
|
+
is_remote = is_remote_source(manifest.source_path)
|
|
62
|
+
if is_remote:
|
|
63
|
+
if "github.com" in manifest.source_path:
|
|
64
|
+
skill_type = "Remote (GitHub)"
|
|
65
|
+
elif "gitlab.com" in manifest.source_path:
|
|
66
|
+
skill_type = "Remote (GitLab)"
|
|
67
|
+
else:
|
|
68
|
+
skill_type = "Remote"
|
|
69
|
+
else:
|
|
70
|
+
skill_type = "Local"
|
|
71
|
+
|
|
72
|
+
# Format registered date
|
|
73
|
+
try:
|
|
74
|
+
reg_date = datetime.fromisoformat(manifest.registered_at.replace("Z", "+00:00"))
|
|
75
|
+
reg_date_str = reg_date.strftime("%Y-%m-%d %H:%M:%S UTC")
|
|
76
|
+
except Exception:
|
|
77
|
+
reg_date_str = manifest.registered_at
|
|
78
|
+
|
|
79
|
+
# Prepare data
|
|
80
|
+
info = {
|
|
81
|
+
"name": manifest.name,
|
|
82
|
+
"description": manifest.description,
|
|
83
|
+
"source": manifest.source_path,
|
|
84
|
+
"type": skill_type,
|
|
85
|
+
"status": status_result.status,
|
|
86
|
+
"file_count": len(manifest.files),
|
|
87
|
+
"content_hash": manifest.content_hash,
|
|
88
|
+
"registered_at": reg_date_str,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if args.files:
|
|
92
|
+
info["files"] = [{"path": f.path, "hash": f.hash} for f in manifest.files]
|
|
93
|
+
|
|
94
|
+
# Output
|
|
95
|
+
if args.json:
|
|
96
|
+
print(json.dumps(info, indent=2))
|
|
97
|
+
else:
|
|
98
|
+
# Human-readable format
|
|
99
|
+
status_icon = {
|
|
100
|
+
"valid": "✓",
|
|
101
|
+
"modified": "↻",
|
|
102
|
+
"missing": "✗",
|
|
103
|
+
}.get(status_result.status, "?")
|
|
104
|
+
|
|
105
|
+
print(f"\n[{manifest.name}]")
|
|
106
|
+
print("---")
|
|
107
|
+
print(manifest.description)
|
|
108
|
+
print("---")
|
|
109
|
+
print(f"Source: {manifest.source_path}")
|
|
110
|
+
print(f"Type: {skill_type}")
|
|
111
|
+
print(f"Status: {status_icon} {status_result.status.capitalize()}")
|
|
112
|
+
if status_result.message:
|
|
113
|
+
print(f" {status_result.message}")
|
|
114
|
+
print(f"Files: {len(manifest.files)}")
|
|
115
|
+
print(f"Hash: {manifest.content_hash[:20]}...")
|
|
116
|
+
print(f"Registered: {reg_date_str}")
|
|
117
|
+
|
|
118
|
+
if args.files and manifest.files:
|
|
119
|
+
print(f"\nFiles ({len(manifest.files)}):")
|
|
120
|
+
for file_info in manifest.files:
|
|
121
|
+
print(f" - {file_info.path}")
|
|
122
|
+
|
|
123
|
+
print()
|
|
124
|
+
|
|
125
|
+
return 0
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def register(subparsers):
|
|
129
|
+
"""Register the info command with argparse.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
subparsers: Subparsers object from argparse
|
|
133
|
+
"""
|
|
134
|
+
parser = subparsers.add_parser(
|
|
135
|
+
"info",
|
|
136
|
+
help="Show detailed information about a skill",
|
|
137
|
+
description="Display detailed information about a registered skill including "
|
|
138
|
+
"metadata, status, and optionally the list of files.",
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
parser.add_argument(
|
|
142
|
+
"skill_name",
|
|
143
|
+
help="Skill name to show information for",
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
parser.add_argument(
|
|
147
|
+
"--files",
|
|
148
|
+
action="store_true",
|
|
149
|
+
help="Show list of files in the skill",
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
parser.set_defaults(func=run)
|
commands/list.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""`asr list` command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import textwrap
|
|
9
|
+
from shutil import get_terminal_size
|
|
10
|
+
|
|
11
|
+
from registry import load_registry
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def register(subparsers) -> None:
|
|
15
|
+
p = subparsers.add_parser("list", help="List registered skills")
|
|
16
|
+
p.add_argument("--json", action="store_true", help="Output in JSON format")
|
|
17
|
+
p.add_argument("--verbose", "-v", action="store_true", help="Show full paths")
|
|
18
|
+
p.set_defaults(func=run)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _shorten_path(path: str, max_len: int = 40) -> str:
|
|
22
|
+
"""Shorten a path for display, using ~ for home directory."""
|
|
23
|
+
home = os.path.expanduser("~")
|
|
24
|
+
if path.startswith(home):
|
|
25
|
+
path = "~" + path[len(home) :]
|
|
26
|
+
|
|
27
|
+
if len(path) <= max_len:
|
|
28
|
+
return path
|
|
29
|
+
|
|
30
|
+
# Truncate middle of path
|
|
31
|
+
parts = path.split(os.sep)
|
|
32
|
+
if len(parts) <= 2:
|
|
33
|
+
return path[: max_len - 3] + "..."
|
|
34
|
+
|
|
35
|
+
# Keep first and last parts, truncate middle
|
|
36
|
+
result = parts[0] + os.sep + "..." + os.sep + parts[-1]
|
|
37
|
+
if len(result) <= max_len:
|
|
38
|
+
# Try to add more parts from the end
|
|
39
|
+
for i in range(len(parts) - 2, 0, -1):
|
|
40
|
+
candidate = parts[0] + os.sep + "..." + os.sep.join(parts[i:])
|
|
41
|
+
if len(candidate) <= max_len:
|
|
42
|
+
result = candidate
|
|
43
|
+
else:
|
|
44
|
+
break
|
|
45
|
+
return result
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _wrap_description(desc: str, width: int, indent: int = 3) -> str:
|
|
49
|
+
"""Wrap description text with proper indentation."""
|
|
50
|
+
if not desc:
|
|
51
|
+
return ""
|
|
52
|
+
prefix = " " * indent
|
|
53
|
+
wrapped = textwrap.fill(
|
|
54
|
+
desc.strip(),
|
|
55
|
+
width=max(20, width - indent),
|
|
56
|
+
initial_indent=prefix,
|
|
57
|
+
subsequent_indent=prefix,
|
|
58
|
+
)
|
|
59
|
+
return wrapped
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def run(args: argparse.Namespace) -> int:
|
|
63
|
+
entries = load_registry()
|
|
64
|
+
|
|
65
|
+
if not entries:
|
|
66
|
+
if args.json:
|
|
67
|
+
print("[]")
|
|
68
|
+
else:
|
|
69
|
+
print("No skills registered. Use 'asr add <path>' to register a skill.")
|
|
70
|
+
return 0
|
|
71
|
+
|
|
72
|
+
if args.json:
|
|
73
|
+
data = [{"name": e.name, "description": e.description, "path": e.path} for e in entries]
|
|
74
|
+
print(json.dumps(data, indent=2))
|
|
75
|
+
return 0
|
|
76
|
+
|
|
77
|
+
width = min(100, max(60, get_terminal_size((80, 20)).columns))
|
|
78
|
+
verbose = getattr(args, "verbose", False)
|
|
79
|
+
|
|
80
|
+
# Header
|
|
81
|
+
print(f"\n REGISTERED SKILLS ({len(entries)})\n")
|
|
82
|
+
|
|
83
|
+
# Calculate max name length for alignment
|
|
84
|
+
max_name = max(len(e.name) for e in entries)
|
|
85
|
+
path_width = width - max_name - 10 # Account for formatting
|
|
86
|
+
|
|
87
|
+
for e in sorted(entries, key=lambda x: x.name):
|
|
88
|
+
name = e.name
|
|
89
|
+
|
|
90
|
+
if verbose:
|
|
91
|
+
path_display = e.path
|
|
92
|
+
else:
|
|
93
|
+
path_display = _shorten_path(e.path, max(20, path_width))
|
|
94
|
+
|
|
95
|
+
# Skill header with box drawing
|
|
96
|
+
print(f" ┌─ {name}")
|
|
97
|
+
print(f" │ {path_display}")
|
|
98
|
+
|
|
99
|
+
desc = (e.description or "").strip()
|
|
100
|
+
if desc:
|
|
101
|
+
# Truncate description to one line if too long
|
|
102
|
+
max_desc = width - 6
|
|
103
|
+
if len(desc) > max_desc:
|
|
104
|
+
desc = desc[: max_desc - 3] + "..."
|
|
105
|
+
print(f" └─ {desc}")
|
|
106
|
+
else:
|
|
107
|
+
print(" └─ (no description)")
|
|
108
|
+
print()
|
|
109
|
+
|
|
110
|
+
return 0
|