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/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