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/adapter.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""`asr adapter` 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 adapters import ClaudeAdapter, CodexAdapter, CopilotAdapter, CursorAdapter, WindsurfAdapter
|
|
11
|
+
from config import load_config
|
|
12
|
+
from registry import load_registry
|
|
13
|
+
|
|
14
|
+
ADAPTERS = {
|
|
15
|
+
"cursor": CursorAdapter(),
|
|
16
|
+
"windsurf": WindsurfAdapter(),
|
|
17
|
+
"codex": CodexAdapter(),
|
|
18
|
+
"copilot": CopilotAdapter(),
|
|
19
|
+
"claude": ClaudeAdapter(),
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def register(subparsers) -> None:
|
|
24
|
+
p = subparsers.add_parser("adapter", help="Generate IDE-specific files")
|
|
25
|
+
p.add_argument("--exclude", help="Comma-separated skill names to exclude")
|
|
26
|
+
p.add_argument("--output-dir", type=Path, default=Path("."), help="Output directory")
|
|
27
|
+
p.add_argument("--copy", action="store_true", help="(Deprecated) Skills are always copied now")
|
|
28
|
+
p.add_argument("--json", action="store_true", help="Output in JSON format")
|
|
29
|
+
p.add_argument("--quiet", action="store_true", help="Suppress info/warnings")
|
|
30
|
+
p.add_argument("--config", type=Path, help="Override config file path")
|
|
31
|
+
|
|
32
|
+
adapter_subs = p.add_subparsers(dest="target", help="Target IDE")
|
|
33
|
+
|
|
34
|
+
for name in ["cursor", "windsurf", "codex", "copilot", "claude"]:
|
|
35
|
+
adapter_subs.add_parser(name, help=f"Generate {name} files")
|
|
36
|
+
|
|
37
|
+
p.set_defaults(func=run)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def run(args: argparse.Namespace) -> int:
|
|
41
|
+
config = load_config(args.config)
|
|
42
|
+
entries = load_registry()
|
|
43
|
+
|
|
44
|
+
if not entries:
|
|
45
|
+
if args.json:
|
|
46
|
+
print(json.dumps({"generated": 0, "error": "no skills registered"}))
|
|
47
|
+
else:
|
|
48
|
+
print("No skills registered. Use 'asr add <path>' first.")
|
|
49
|
+
return 1
|
|
50
|
+
|
|
51
|
+
exclude = set()
|
|
52
|
+
if args.exclude:
|
|
53
|
+
exclude = set(args.exclude.split(","))
|
|
54
|
+
|
|
55
|
+
output_dir = args.output_dir
|
|
56
|
+
|
|
57
|
+
if args.target:
|
|
58
|
+
targets = [args.target]
|
|
59
|
+
else:
|
|
60
|
+
targets = config["adapter"]["default_targets"]
|
|
61
|
+
|
|
62
|
+
total_generated = 0
|
|
63
|
+
total_removed = 0
|
|
64
|
+
results = {}
|
|
65
|
+
|
|
66
|
+
for target in targets:
|
|
67
|
+
if target not in ADAPTERS:
|
|
68
|
+
if not args.quiet:
|
|
69
|
+
print(f"Warning: Unknown adapter target: {target}", file=sys.stderr)
|
|
70
|
+
continue
|
|
71
|
+
|
|
72
|
+
adapter = ADAPTERS[target]
|
|
73
|
+
# Always copy skills now (--copy flag is deprecated but kept for backward compat)
|
|
74
|
+
generated, removed = adapter.generate_all(entries, output_dir, exclude, copy=True)
|
|
75
|
+
|
|
76
|
+
total_generated += len(generated)
|
|
77
|
+
total_removed += len(removed)
|
|
78
|
+
|
|
79
|
+
results[target] = {
|
|
80
|
+
"generated": len(generated),
|
|
81
|
+
"removed": len(removed),
|
|
82
|
+
"output_dir": str(adapter.resolve_output_dir(output_dir)),
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if args.json:
|
|
86
|
+
print(
|
|
87
|
+
json.dumps(
|
|
88
|
+
{
|
|
89
|
+
"total_generated": total_generated,
|
|
90
|
+
"total_removed": total_removed,
|
|
91
|
+
"targets": results,
|
|
92
|
+
},
|
|
93
|
+
indent=2,
|
|
94
|
+
)
|
|
95
|
+
)
|
|
96
|
+
else:
|
|
97
|
+
for target, info in results.items():
|
|
98
|
+
print(f"{target}: Generated {info['generated']} file(s) in {info['output_dir']}")
|
|
99
|
+
if info["removed"]:
|
|
100
|
+
print(f" Removed {info['removed']} stale file(s)")
|
|
101
|
+
|
|
102
|
+
return 0
|
commands/add.py
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
"""`asr add` command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import glob as globlib
|
|
7
|
+
import json
|
|
8
|
+
import shutil
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from config import load_config
|
|
13
|
+
from discovery import discover_single, find_skills
|
|
14
|
+
from registry import SkillEntry, add_skill
|
|
15
|
+
from remote import InvalidRemoteUrlError, derive_skill_name, fetch_remote_to_temp, validate_remote_url
|
|
16
|
+
from skillcopy.remote import is_remote_source
|
|
17
|
+
from validate import validate_skill
|
|
18
|
+
|
|
19
|
+
_GLOB_CHARS = set("*?[")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _looks_like_glob(value: str) -> bool:
|
|
23
|
+
return any(ch in value for ch in _GLOB_CHARS)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _expand_path_patterns(patterns: list[str]) -> list[Path]:
|
|
27
|
+
expanded: list[Path] = []
|
|
28
|
+
for raw in patterns:
|
|
29
|
+
pat = str(Path(raw).expanduser())
|
|
30
|
+
if _looks_like_glob(pat):
|
|
31
|
+
matches = globlib.glob(pat, recursive=True)
|
|
32
|
+
expanded.extend(Path(m) for m in matches)
|
|
33
|
+
else:
|
|
34
|
+
expanded.append(Path(pat))
|
|
35
|
+
return expanded
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _print_validation_result(result) -> None:
|
|
39
|
+
print(f"{result.name}")
|
|
40
|
+
if result.valid and not result.warnings:
|
|
41
|
+
print(" ✓ Valid")
|
|
42
|
+
else:
|
|
43
|
+
for msg in result.all_messages:
|
|
44
|
+
print(f" {msg}")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def register(subparsers) -> None:
|
|
48
|
+
p = subparsers.add_parser("add", help="Register a skill")
|
|
49
|
+
p.add_argument(
|
|
50
|
+
"paths",
|
|
51
|
+
nargs="+",
|
|
52
|
+
help="Path(s), URL(s), or glob pattern(s) to skill dir(s) (or root(s) for recursive)",
|
|
53
|
+
)
|
|
54
|
+
p.add_argument("-r", "--recursive", action="store_true", help="Recursively add all valid skills from path")
|
|
55
|
+
p.add_argument("--strict", action="store_true", help="Fail if validation has warnings")
|
|
56
|
+
p.add_argument("--json", action="store_true", help="Output in JSON format")
|
|
57
|
+
p.add_argument("--quiet", action="store_true", help="Suppress info/warnings")
|
|
58
|
+
p.add_argument("--config", type=Path, help="Override config file path")
|
|
59
|
+
p.set_defaults(func=run)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def run(args: argparse.Namespace) -> int:
|
|
63
|
+
config = load_config(args.config)
|
|
64
|
+
max_lines = config["validation"]["reference_max_lines"]
|
|
65
|
+
|
|
66
|
+
# Separate remote URLs from local paths
|
|
67
|
+
remote_urls = []
|
|
68
|
+
local_patterns = []
|
|
69
|
+
|
|
70
|
+
for pattern in args.paths:
|
|
71
|
+
if is_remote_source(pattern):
|
|
72
|
+
remote_urls.append(pattern)
|
|
73
|
+
else:
|
|
74
|
+
local_patterns.append(pattern)
|
|
75
|
+
|
|
76
|
+
# Expand local paths
|
|
77
|
+
expanded = []
|
|
78
|
+
if local_patterns:
|
|
79
|
+
expanded = [p.resolve() for p in _expand_path_patterns(local_patterns)]
|
|
80
|
+
|
|
81
|
+
# Check if we have anything to process
|
|
82
|
+
if not expanded and not remote_urls:
|
|
83
|
+
if args.json:
|
|
84
|
+
print(json.dumps({"added": 0, "skipped": 0, "skills": [], "error": "no paths matched"}))
|
|
85
|
+
else:
|
|
86
|
+
print("No paths matched.", file=sys.stderr)
|
|
87
|
+
return 2
|
|
88
|
+
|
|
89
|
+
# Handle recursive mode (local paths only)
|
|
90
|
+
if args.recursive:
|
|
91
|
+
if remote_urls:
|
|
92
|
+
print("Warning: --recursive flag ignored for remote URLs", file=sys.stderr)
|
|
93
|
+
exit_code = 0
|
|
94
|
+
for root in expanded:
|
|
95
|
+
code = _run_recursive(args, root, max_lines)
|
|
96
|
+
if code != 0:
|
|
97
|
+
exit_code = code
|
|
98
|
+
return exit_code
|
|
99
|
+
|
|
100
|
+
results: list[dict] = []
|
|
101
|
+
added_count = 0
|
|
102
|
+
skipped_count = 0
|
|
103
|
+
exit_code = 0
|
|
104
|
+
|
|
105
|
+
# Process remote URLs
|
|
106
|
+
for url in remote_urls:
|
|
107
|
+
# Validate URL format
|
|
108
|
+
valid, error_msg = validate_remote_url(url)
|
|
109
|
+
if not valid:
|
|
110
|
+
skipped_count += 1
|
|
111
|
+
results.append({"url": url, "added": False, "reason": f"Invalid URL: {error_msg}"})
|
|
112
|
+
exit_code = 1
|
|
113
|
+
if not args.quiet and not args.json:
|
|
114
|
+
print(f"Invalid URL: {url} - {error_msg}", file=sys.stderr)
|
|
115
|
+
continue
|
|
116
|
+
|
|
117
|
+
# Derive skill name
|
|
118
|
+
try:
|
|
119
|
+
derive_skill_name(url)
|
|
120
|
+
except InvalidRemoteUrlError as e:
|
|
121
|
+
skipped_count += 1
|
|
122
|
+
results.append({"url": url, "added": False, "reason": str(e)})
|
|
123
|
+
exit_code = 1
|
|
124
|
+
if not args.quiet and not args.json:
|
|
125
|
+
print(f"Cannot derive name from URL: {url}", file=sys.stderr)
|
|
126
|
+
continue
|
|
127
|
+
|
|
128
|
+
# Fetch to temp dir for validation
|
|
129
|
+
try:
|
|
130
|
+
if not args.quiet and not args.json:
|
|
131
|
+
# Determine platform for user feedback
|
|
132
|
+
if "github.com" in url:
|
|
133
|
+
platform = "GitHub"
|
|
134
|
+
elif "gitlab.com" in url:
|
|
135
|
+
platform = "GitLab"
|
|
136
|
+
else:
|
|
137
|
+
platform = "remote source"
|
|
138
|
+
print(f"Registering from {platform}...", file=sys.stderr)
|
|
139
|
+
|
|
140
|
+
temp_dir = fetch_remote_to_temp(url)
|
|
141
|
+
|
|
142
|
+
if not args.quiet and not args.json:
|
|
143
|
+
# Count files validated
|
|
144
|
+
file_count = sum(1 for _ in temp_dir.rglob("*") if _.is_file())
|
|
145
|
+
print(f"✓ Validated {file_count} file(s)", file=sys.stderr)
|
|
146
|
+
except Exception as e:
|
|
147
|
+
skipped_count += 1
|
|
148
|
+
results.append({"url": url, "added": False, "reason": f"Fetch failed: {e}"})
|
|
149
|
+
exit_code = 1
|
|
150
|
+
if not args.quiet and not args.json:
|
|
151
|
+
print(f"Failed to fetch {url}: {e}", file=sys.stderr)
|
|
152
|
+
continue
|
|
153
|
+
|
|
154
|
+
try:
|
|
155
|
+
# Validate fetched content (skip name match for temp directory)
|
|
156
|
+
result = validate_skill(temp_dir, reference_max_lines=max_lines, skip_name_match=True)
|
|
157
|
+
if not args.quiet and not args.json:
|
|
158
|
+
_print_validation_result(result)
|
|
159
|
+
print()
|
|
160
|
+
|
|
161
|
+
if not result.valid:
|
|
162
|
+
skipped_count += 1
|
|
163
|
+
results.append({"url": url, "added": False, "reason": "validation errors"})
|
|
164
|
+
exit_code = 1
|
|
165
|
+
continue
|
|
166
|
+
|
|
167
|
+
if args.strict and result.warnings:
|
|
168
|
+
skipped_count += 1
|
|
169
|
+
results.append({"url": url, "added": False, "reason": "validation warnings (strict mode)"})
|
|
170
|
+
exit_code = 1
|
|
171
|
+
continue
|
|
172
|
+
|
|
173
|
+
# Discover skill info from fetched content
|
|
174
|
+
discovered = discover_single(temp_dir)
|
|
175
|
+
if not discovered:
|
|
176
|
+
skipped_count += 1
|
|
177
|
+
results.append({"url": url, "added": False, "reason": "could not discover skill info"})
|
|
178
|
+
exit_code = 3
|
|
179
|
+
continue
|
|
180
|
+
|
|
181
|
+
# Create entry with URL as source_path
|
|
182
|
+
entry = SkillEntry(
|
|
183
|
+
path=url, # Store URL, not temp path
|
|
184
|
+
name=discovered.name,
|
|
185
|
+
description=discovered.description,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
is_new = add_skill(entry)
|
|
189
|
+
added_count += 1
|
|
190
|
+
results.append({"name": entry.name, "url": url, "added": True, "new": is_new})
|
|
191
|
+
|
|
192
|
+
if not args.quiet and not args.json:
|
|
193
|
+
action = "Added" if is_new else "Updated"
|
|
194
|
+
print(f"{action} remote skill: {entry.name} from {url}")
|
|
195
|
+
|
|
196
|
+
finally:
|
|
197
|
+
shutil.rmtree(temp_dir, ignore_errors=True)
|
|
198
|
+
|
|
199
|
+
# Process local paths
|
|
200
|
+
for path in expanded:
|
|
201
|
+
if not path.exists():
|
|
202
|
+
skipped_count += 1
|
|
203
|
+
results.append({"path": str(path), "added": False, "reason": "path missing"})
|
|
204
|
+
exit_code = 1
|
|
205
|
+
if not args.quiet and not args.json:
|
|
206
|
+
print(f"Not found: {path}", file=sys.stderr)
|
|
207
|
+
continue
|
|
208
|
+
|
|
209
|
+
result = validate_skill(path, reference_max_lines=max_lines)
|
|
210
|
+
if not args.quiet and not args.json:
|
|
211
|
+
_print_validation_result(result)
|
|
212
|
+
print()
|
|
213
|
+
|
|
214
|
+
if not result.valid:
|
|
215
|
+
skipped_count += 1
|
|
216
|
+
results.append({"path": str(path), "added": False, "reason": "validation errors"})
|
|
217
|
+
exit_code = 1
|
|
218
|
+
continue
|
|
219
|
+
|
|
220
|
+
if args.strict and result.warnings:
|
|
221
|
+
skipped_count += 1
|
|
222
|
+
results.append({"path": str(path), "added": False, "reason": "validation warnings (strict mode)"})
|
|
223
|
+
exit_code = 1
|
|
224
|
+
continue
|
|
225
|
+
|
|
226
|
+
discovered = discover_single(path)
|
|
227
|
+
if not discovered:
|
|
228
|
+
skipped_count += 1
|
|
229
|
+
results.append({"path": str(path), "added": False, "reason": "could not discover skill info"})
|
|
230
|
+
exit_code = 3
|
|
231
|
+
continue
|
|
232
|
+
|
|
233
|
+
entry = SkillEntry(
|
|
234
|
+
path=str(discovered.path),
|
|
235
|
+
name=discovered.name,
|
|
236
|
+
description=discovered.description,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
is_new = add_skill(entry)
|
|
240
|
+
added_count += 1
|
|
241
|
+
results.append({"name": entry.name, "path": entry.path, "added": True, "new": is_new})
|
|
242
|
+
|
|
243
|
+
if not args.quiet and not args.json:
|
|
244
|
+
action = "Added" if is_new else "Updated"
|
|
245
|
+
print(f"{action} skill: {entry.name}")
|
|
246
|
+
|
|
247
|
+
if args.json:
|
|
248
|
+
print(json.dumps({"added": added_count, "skipped": skipped_count, "skills": results}, indent=2))
|
|
249
|
+
|
|
250
|
+
return exit_code
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _run_recursive(args: argparse.Namespace, root: Path, max_lines: int) -> int:
|
|
254
|
+
if not root.is_dir():
|
|
255
|
+
print(f"Error: Not a directory: {root}", file=sys.stderr)
|
|
256
|
+
return 2
|
|
257
|
+
|
|
258
|
+
skills = find_skills(root)
|
|
259
|
+
if not skills:
|
|
260
|
+
if args.json:
|
|
261
|
+
print(json.dumps({"added": 0, "skipped": 0, "skills": []}))
|
|
262
|
+
else:
|
|
263
|
+
print(f"No skills found under {root}")
|
|
264
|
+
return 0
|
|
265
|
+
|
|
266
|
+
added_count = 0
|
|
267
|
+
skipped_count = 0
|
|
268
|
+
results = []
|
|
269
|
+
|
|
270
|
+
for s in skills:
|
|
271
|
+
result = validate_skill(s.path, reference_max_lines=max_lines)
|
|
272
|
+
|
|
273
|
+
if not result.valid:
|
|
274
|
+
skipped_count += 1
|
|
275
|
+
if not args.quiet:
|
|
276
|
+
print(f"⚠ Skipping {s.name}: validation errors", file=sys.stderr)
|
|
277
|
+
results.append({"name": s.name, "added": False, "reason": "validation errors"})
|
|
278
|
+
continue
|
|
279
|
+
|
|
280
|
+
if args.strict and result.warnings:
|
|
281
|
+
skipped_count += 1
|
|
282
|
+
if not args.quiet:
|
|
283
|
+
print(f"⚠ Skipping {s.name}: validation warnings (strict)", file=sys.stderr)
|
|
284
|
+
results.append({"name": s.name, "added": False, "reason": "validation warnings"})
|
|
285
|
+
continue
|
|
286
|
+
|
|
287
|
+
entry = SkillEntry(path=str(s.path), name=s.name, description=s.description)
|
|
288
|
+
is_new = add_skill(entry)
|
|
289
|
+
added_count += 1
|
|
290
|
+
|
|
291
|
+
if not args.quiet and not args.json:
|
|
292
|
+
action = "Added" if is_new else "Updated"
|
|
293
|
+
print(f"{action}: {s.name}")
|
|
294
|
+
|
|
295
|
+
results.append({"name": s.name, "added": True, "new": is_new})
|
|
296
|
+
|
|
297
|
+
if args.json:
|
|
298
|
+
print(json.dumps({"added": added_count, "skipped": skipped_count, "skills": results}, indent=2))
|
|
299
|
+
elif not args.quiet:
|
|
300
|
+
print(f"\n{added_count} skill(s) added, {skipped_count} skipped")
|
|
301
|
+
|
|
302
|
+
return 0
|
commands/clean.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""`asr clean` command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
|
|
8
|
+
from manifest import check_manifest, delete_manifest, list_manifests, load_manifest
|
|
9
|
+
from registry import load_registry, remove_skill
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def register(subparsers) -> None:
|
|
13
|
+
p = subparsers.add_parser("clean", help="Clean up corrupted/missing skills and orphaned artifacts")
|
|
14
|
+
p.add_argument("-y", "--yes", action="store_true", help="Skip confirmation prompt")
|
|
15
|
+
p.add_argument("--json", action="store_true", help="Output in JSON format")
|
|
16
|
+
p.add_argument("--dry-run", action="store_true", help="Show what would be cleaned without doing it")
|
|
17
|
+
p.set_defaults(func=run)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def run(args: argparse.Namespace) -> int:
|
|
21
|
+
entries = load_registry()
|
|
22
|
+
registered_names = {e.name for e in entries}
|
|
23
|
+
manifest_names = set(list_manifests())
|
|
24
|
+
|
|
25
|
+
to_remove_skills = []
|
|
26
|
+
to_remove_manifests = []
|
|
27
|
+
|
|
28
|
+
# Check for remote skills and show progress header
|
|
29
|
+
import sys
|
|
30
|
+
|
|
31
|
+
from skillcopy.remote import is_remote_source
|
|
32
|
+
|
|
33
|
+
remote_count = 0
|
|
34
|
+
for entry in entries:
|
|
35
|
+
manifest = load_manifest(entry.name)
|
|
36
|
+
if manifest and is_remote_source(manifest.source_path):
|
|
37
|
+
remote_count += 1
|
|
38
|
+
|
|
39
|
+
if remote_count > 0 and not args.json:
|
|
40
|
+
print(f"Checking {remote_count} remote skill(s)...", file=sys.stderr)
|
|
41
|
+
|
|
42
|
+
for entry in entries:
|
|
43
|
+
manifest = load_manifest(entry.name)
|
|
44
|
+
if manifest:
|
|
45
|
+
# Show progress for remote skills
|
|
46
|
+
is_remote = is_remote_source(manifest.source_path)
|
|
47
|
+
if is_remote 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" ↓ {entry.name} (checking {platform}...)", file=sys.stderr, flush=True)
|
|
56
|
+
|
|
57
|
+
status = check_manifest(manifest)
|
|
58
|
+
|
|
59
|
+
if is_remote and not args.json:
|
|
60
|
+
print(f" ✓ {entry.name} (checked)", file=sys.stderr)
|
|
61
|
+
|
|
62
|
+
if status.status == "missing":
|
|
63
|
+
to_remove_skills.append(
|
|
64
|
+
{
|
|
65
|
+
"name": entry.name,
|
|
66
|
+
"reason": "source missing",
|
|
67
|
+
"path": entry.path,
|
|
68
|
+
}
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
orphaned = manifest_names - registered_names
|
|
72
|
+
for name in orphaned:
|
|
73
|
+
to_remove_manifests.append(
|
|
74
|
+
{
|
|
75
|
+
"name": name,
|
|
76
|
+
"reason": "orphaned manifest (not in registry)",
|
|
77
|
+
}
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if not to_remove_skills and not to_remove_manifests:
|
|
81
|
+
if args.json:
|
|
82
|
+
print(json.dumps({"cleaned": 0, "message": "nothing to clean"}))
|
|
83
|
+
else:
|
|
84
|
+
print("Nothing to clean.")
|
|
85
|
+
return 0
|
|
86
|
+
|
|
87
|
+
if args.json:
|
|
88
|
+
result = {
|
|
89
|
+
"skills_to_remove": to_remove_skills,
|
|
90
|
+
"manifests_to_remove": to_remove_manifests,
|
|
91
|
+
"dry_run": args.dry_run,
|
|
92
|
+
}
|
|
93
|
+
if not args.dry_run and not args.yes:
|
|
94
|
+
result["requires_confirmation"] = True
|
|
95
|
+
print(json.dumps(result, indent=2))
|
|
96
|
+
if args.dry_run:
|
|
97
|
+
return 0
|
|
98
|
+
else:
|
|
99
|
+
print("The following will be cleaned:\n")
|
|
100
|
+
|
|
101
|
+
if to_remove_skills:
|
|
102
|
+
print("Skills with missing sources:")
|
|
103
|
+
for s in to_remove_skills:
|
|
104
|
+
print(f" ✗ {s['name']} ({s['path']})")
|
|
105
|
+
|
|
106
|
+
if to_remove_manifests:
|
|
107
|
+
print("\nOrphaned manifests:")
|
|
108
|
+
for m in to_remove_manifests:
|
|
109
|
+
print(f" ✗ {m['name']}")
|
|
110
|
+
|
|
111
|
+
print()
|
|
112
|
+
|
|
113
|
+
if args.dry_run:
|
|
114
|
+
print("(dry run - no changes made)")
|
|
115
|
+
return 0
|
|
116
|
+
|
|
117
|
+
if not args.yes and not args.json:
|
|
118
|
+
try:
|
|
119
|
+
response = input("Proceed with cleanup? [y/N] ").strip().lower()
|
|
120
|
+
if response not in ("y", "yes"):
|
|
121
|
+
print("Aborted.")
|
|
122
|
+
return 1
|
|
123
|
+
except (EOFError, KeyboardInterrupt):
|
|
124
|
+
print("\nAborted.")
|
|
125
|
+
return 1
|
|
126
|
+
|
|
127
|
+
removed_skills = []
|
|
128
|
+
removed_manifests = []
|
|
129
|
+
|
|
130
|
+
for s in to_remove_skills:
|
|
131
|
+
remove_skill(s["name"])
|
|
132
|
+
removed_skills.append(s["name"])
|
|
133
|
+
|
|
134
|
+
for m in to_remove_manifests:
|
|
135
|
+
delete_manifest(m["name"])
|
|
136
|
+
removed_manifests.append(m["name"])
|
|
137
|
+
|
|
138
|
+
if args.json:
|
|
139
|
+
print(
|
|
140
|
+
json.dumps(
|
|
141
|
+
{
|
|
142
|
+
"removed_skills": removed_skills,
|
|
143
|
+
"removed_manifests": removed_manifests,
|
|
144
|
+
},
|
|
145
|
+
indent=2,
|
|
146
|
+
)
|
|
147
|
+
)
|
|
148
|
+
else:
|
|
149
|
+
for name in removed_skills:
|
|
150
|
+
print(f"Removed skill: {name}")
|
|
151
|
+
for name in removed_manifests:
|
|
152
|
+
print(f"Removed manifest: {name}")
|
|
153
|
+
print(f"\nCleaned {len(removed_skills)} skill(s), {len(removed_manifests)} manifest(s)")
|
|
154
|
+
|
|
155
|
+
return 0
|