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.
- __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
- agents/__init__.py +25 -0
- agents/base.py +96 -0
- agents/claude.py +25 -0
- agents/codex.py +25 -0
- agents/copilot.py +25 -0
- agents/opencode.py +25 -0
- agents/registry.py +57 -0
- cli.py +97 -0
- commands/__init__.py +6 -0
- commands/adapter.py +102 -0
- commands/add.py +435 -0
- commands/clean.py +30 -0
- commands/clone.py +178 -0
- commands/config.py +163 -0
- commands/diff.py +180 -0
- commands/exec.py +245 -0
- commands/find.py +56 -0
- commands/help.py +51 -0
- commands/info.py +152 -0
- commands/list.py +110 -0
- commands/registry.py +447 -0
- commands/rm.py +128 -0
- commands/status.py +119 -0
- commands/sync.py +143 -0
- commands/update.py +417 -0
- commands/use.py +45 -0
- commands/validate.py +74 -0
- config/__init__.py +119 -0
- config/defaults.py +40 -0
- config/schema.py +73 -0
- discovery.py +145 -0
- manifest.py +437 -0
- oasr-0.5.0.dist-info/METADATA +358 -0
- oasr-0.5.0.dist-info/RECORD +59 -0
- oasr-0.5.0.dist-info/WHEEL +4 -0
- oasr-0.5.0.dist-info/entry_points.txt +3 -0
- oasr-0.5.0.dist-info/licenses/LICENSE +187 -0
- oasr-0.5.0.dist-info/licenses/NOTICE +8 -0
- policy/__init__.py +50 -0
- policy/defaults.py +27 -0
- policy/enforcement.py +98 -0
- policy/profile.py +185 -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/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
|