-
Notifications
You must be signed in to change notification settings - Fork 71
Expand file tree
/
Copy path.skillsaw-custom.py
More file actions
executable file
·335 lines (282 loc) · 12.3 KB
/
Copy path.skillsaw-custom.py
File metadata and controls
executable file
·335 lines (282 loc) · 12.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
"""
Custom skillsaw rules for AIPCC AI helpers
"""
import subprocess
from typing import List
try:
from src.context import RepositoryContext
from src.rule import Rule, RuleViolation, Severity
except ImportError:
from skillsaw import RepositoryContext, Rule, RuleViolation, Severity
class PluginsDocUpToDateRule(Rule):
"""Check that docs/data.json and claude-settings.json are up-to-date."""
@property
def rule_id(self) -> str:
return "plugins-doc-up-to-date"
@property
def description(self) -> str:
return (
"docs/data.json and images/claude/claude-settings.json must be"
" up-to-date with plugin metadata. Run 'make update' to regenerate."
)
def default_severity(self) -> Severity:
return Severity.ERROR
def check(self, context: RepositoryContext) -> List[RuleViolation]:
violations = []
# Only check marketplace repos
if not context.has_marketplace():
return violations
data_json_path = context.root_path / "docs" / "data.json"
claude_settings_path = context.root_path / "images" / "claude" / "claude-settings.json"
# Check if we have categories.yaml as the source of truth
categories_yaml_path = context.root_path / "categories.yaml"
if not categories_yaml_path.exists():
return violations
try:
# Read current content of files to check
original_data_json = data_json_path.read_text() if data_json_path.exists() else None
original_claude_settings = (
claude_settings_path.read_text() if claude_settings_path.exists() else None
)
# Run build-website.py if it exists
website_script_path = context.root_path / "scripts" / "build-website.py"
if website_script_path.exists():
result = subprocess.run(
["python3", str(website_script_path)],
cwd=str(context.root_path),
capture_output=True,
text=True,
timeout=30,
)
if result.returncode != 0:
violations.append(
self.violation(
f"build-website.py failed: {result.stderr}",
file_path=data_json_path
if data_json_path.exists()
else categories_yaml_path,
)
)
return violations
# Run update_claude_settings.py if it exists
claude_settings_script_path = (
context.root_path / "scripts" / "update_claude_settings.py"
)
if claude_settings_script_path.exists():
result = subprocess.run(
["python3", str(claude_settings_script_path)],
cwd=str(context.root_path),
capture_output=True,
text=True,
timeout=30,
)
if result.returncode != 0:
violations.append(
self.violation(
f"update_claude_settings.py failed: {result.stderr}",
file_path=claude_settings_path
if claude_settings_path.exists()
else categories_yaml_path,
)
)
return violations
# Check if generated files changed
# Check if docs/data.json changed
if data_json_path.exists():
generated_data_json = data_json_path.read_text()
if original_data_json != generated_data_json:
# Restore original content
if original_data_json is not None:
data_json_path.write_text(original_data_json)
violations.append(
self.violation(
"docs/data.json is out of sync with plugin"
" metadata. Run 'make update' to update.",
file_path=data_json_path,
)
)
# Check if images/claude-settings.json changed
if claude_settings_path.exists():
generated_claude_settings = claude_settings_path.read_text()
if original_claude_settings != generated_claude_settings:
# Restore original content
if original_claude_settings is not None:
claude_settings_path.write_text(original_claude_settings)
violations.append(
self.violation(
"images/claude/claude-settings.json is out of"
" sync with plugin metadata."
" Run 'make update' to update.",
file_path=claude_settings_path,
)
)
except subprocess.TimeoutExpired:
violations.append(
self.violation("'make update' timed out", file_path=categories_yaml_path)
)
except Exception as e:
violations.append(
self.violation(
f"Error checking files up-to-date status: {e}",
file_path=categories_yaml_path,
)
)
return violations
class MarketplacePluginsUpToDateRule(Rule):
"""Check that .claude-plugin/marketplace.json includes all available plugins"""
@property
def rule_id(self) -> str:
return "marketplace-plugins-up-to-date"
@property
def description(self) -> str:
return ".claude-plugin/marketplace.json must include all available plugins"
def default_severity(self) -> Severity:
return Severity.ERROR
def check(self, context: RepositoryContext) -> List[RuleViolation]:
violations = []
# Only check repos with Claude plugins
marketplace_path = context.root_path / ".claude-plugin" / "marketplace.json"
if not marketplace_path.exists():
return violations
plugins_dir = context.root_path / "claude-plugins"
if not plugins_dir.exists():
return violations
try:
import json
# Read marketplace.json
with open(marketplace_path, "r") as f:
marketplace_data = json.load(f)
if "plugins" not in marketplace_data:
violations.append(
self.violation(
"marketplace.json is missing 'plugins' field",
file_path=marketplace_path,
)
)
return violations
# Get available plugin directories
available_plugins = []
for plugin_dir in plugins_dir.iterdir():
if plugin_dir.is_dir():
available_plugins.append(plugin_dir.name)
# Get plugins listed in marketplace.json
marketplace_plugins = {}
for plugin in marketplace_data["plugins"]:
name = plugin.get("name")
source = plugin.get("source")
if name:
marketplace_plugins[name] = source
# Check for missing plugins
missing_plugins = set(available_plugins) - set(marketplace_plugins.keys())
if missing_plugins:
violations.append(
self.violation(
"marketplace.json is missing plugins:"
f" {', '.join(sorted(missing_plugins))}",
file_path=marketplace_path,
)
)
# Check source paths are correct
for plugin_name, source_path in marketplace_plugins.items():
if plugin_name in available_plugins:
expected_source = f"./claude-plugins/{plugin_name}"
if source_path != expected_source:
violations.append(
self.violation(
f"Plugin '{plugin_name}' source path should"
f" be '{expected_source}',"
f" got '{source_path}'",
file_path=marketplace_path,
)
)
except json.JSONDecodeError as e:
violations.append(
self.violation(f"Invalid JSON in marketplace.json: {e}", file_path=marketplace_path)
)
except Exception as e:
violations.append(
self.violation(f"Error checking marketplace.json: {e}", file_path=marketplace_path)
)
return violations
class CategoriesYamlValidationRule(Rule):
"""Validate categories.yaml structure, tool consistency, and prevent duplicates"""
@property
def rule_id(self) -> str:
return "tools-yaml-validation"
@property
def description(self) -> str:
return (
"categories.yaml must have valid structure, consistent"
" categories, proper tool definitions, and no duplicate"
" tool names across types"
)
def default_severity(self) -> Severity:
return Severity.ERROR
def check(self, context: RepositoryContext) -> List[RuleViolation]:
violations = []
# Check if categories.yaml exists
categories_yaml_path = context.root_path / "categories.yaml"
if not categories_yaml_path.exists():
if context.has_marketplace():
violations.append(
self.violation(
"categories.yaml is required for marketplace repos",
file_path=categories_yaml_path,
)
)
return violations
# Run the validation script
validation_script_path = context.root_path / "scripts" / "validate_tools.py"
if not validation_script_path.exists():
violations.append(
self.violation(
"scripts/validate_tools.py not found but categories.yaml exists",
file_path=categories_yaml_path,
)
)
return violations
try:
result = subprocess.run(
["python3", str(validation_script_path), str(categories_yaml_path)],
cwd=str(context.root_path),
capture_output=True,
text=True,
timeout=30,
)
if result.returncode != 0:
output = "\n".join(part for part in [result.stdout, result.stderr] if part)
# Parse the validation errors from the script output
error_lines = output.strip().split("\n")
# Find the lines that contain actual errors (start with ✗)
for line in error_lines:
if line.strip().startswith("✗"):
error_msg = line.strip()[2:].strip() # Remove ✗ prefix
violations.append(
self.violation(
f"categories.yaml validation error: {error_msg}",
file_path=categories_yaml_path,
)
)
# If no specific errors found, use the full output
if not violations:
violations.append(
self.violation(
f"categories.yaml validation failed: {output.strip()}",
file_path=categories_yaml_path,
)
)
except subprocess.TimeoutExpired:
violations.append(
self.violation(
"categories.yaml validation script timed out",
file_path=categories_yaml_path,
)
)
except Exception as e:
violations.append(
self.violation(
f"Error running categories.yaml validation: {e}",
file_path=categories_yaml_path,
)
)
return violations