2020
2121Run from the repo root:
2222
23- ./scripts/check.sh # used by CI ; thin wrapper
24- uv run scripts/validate_skills.py # ad-hoc
23+ ./scripts/check.sh # used locally ; thin wrapper
24+ uv run scripts/validate_skills.py # validate every skill + manifest
2525 uv run scripts/validate_skills.py --skills-dir skills
26+ uv run scripts/validate_skills.py --list # print skill names as JSON
27+ uv run scripts/validate_skills.py --skill rocm-doctor # one skill only
28+ uv run scripts/validate_skills.py --marketplace-only # manifest only
2629
27- Exits non-zero if any skill fails validation.
30+ The `--list` / `--skill` options let CI validate each skill in its own job
31+ (see .github/workflows/validate.yml) so a single bad skill doesn't mask the
32+ status of the others.
33+
34+ Exits non-zero if any validated skill (or the marketplace check) fails.
2835"""
2936
3037from __future__ import annotations
@@ -227,6 +234,53 @@ def validate_claude_marketplace(skill_dirs: list[Path]) -> list[str]:
227234 return errors
228235
229236
237+ def _print_report (report : SkillReport ) -> int :
238+ """Print a single skill report and return its error count."""
239+ status = "OK " if not report .errors else "FAIL"
240+ print (f"[{ status } ] { report .skill } " )
241+ for err in report .errors :
242+ print (f" { err } " )
243+ return len (report .errors )
244+
245+
246+ def list_skills (skills_dir : Path ) -> int :
247+ """Print discovered skill names as a compact JSON array (for CI matrices)."""
248+ skills = discover_skills (skills_dir )
249+ if not skills :
250+ print (f"No skills found under { skills_dir } " , file = sys .stderr )
251+ return 1
252+ print (json .dumps ([p .name for p in skills ], separators = ("," , ":" )))
253+ return 0
254+
255+
256+ def run_single (skills_dir : Path , name : str ) -> int :
257+ """Validate a single skill directory by name (no marketplace cross-check)."""
258+ skill_dir = skills_dir / name
259+ if not skill_dir .is_dir ():
260+ print (f"No such skill directory: { skill_dir } " , file = sys .stderr )
261+ return 1
262+
263+ errors = _print_report (validate_skill (skill_dir ))
264+ print (f"\n Summary: { errors } error(s) in skill `{ name } `" )
265+ return 0 if errors == 0 else 1
266+
267+
268+ def run_marketplace (skills_dir : Path ) -> int :
269+ """Validate only that marketplace.json is in sync with skills on disk."""
270+ skills = discover_skills (skills_dir )
271+ if not skills :
272+ print (f"No skills found under { skills_dir } " , file = sys .stderr )
273+ return 1
274+
275+ marketplace_errors = validate_claude_marketplace (skills )
276+ status = "OK " if not marketplace_errors else "FAIL"
277+ print (f"[{ status } ] .claude-plugin/marketplace.json" )
278+ for err in marketplace_errors :
279+ print (f" { err } " )
280+ print (f"\n Summary: { len (marketplace_errors )} error(s) in marketplace manifest" )
281+ return 0 if not marketplace_errors else 1
282+
283+
230284def run (skills_dir : Path ) -> int :
231285 skills = discover_skills (skills_dir )
232286 if not skills :
@@ -236,12 +290,7 @@ def run(skills_dir: Path) -> int:
236290 print (f"Validating { len (skills )} skill(s) in { skills_dir } \n " )
237291 total_errors = 0
238292 for skill_dir in skills :
239- report = validate_skill (skill_dir )
240- status = "OK " if not report .errors else "FAIL"
241- print (f"[{ status } ] { report .skill } " )
242- for err in report .errors :
243- print (f" { err } " )
244- total_errors += len (report .errors )
293+ total_errors += _print_report (validate_skill (skill_dir ))
245294
246295 marketplace_errors = validate_claude_marketplace (skills )
247296 marketplace_status = "OK " if not marketplace_errors else "FAIL"
@@ -264,8 +313,33 @@ def main(argv: list[str] | None = None) -> int:
264313 default = DEFAULT_SKILLS_DIR ,
265314 help = f"Directory containing skill folders (default: { DEFAULT_SKILLS_DIR } )." ,
266315 )
316+ group = parser .add_mutually_exclusive_group ()
317+ group .add_argument (
318+ "--list" ,
319+ action = "store_true" ,
320+ help = "Print discovered skill names as a JSON array and exit." ,
321+ )
322+ group .add_argument (
323+ "--skill" ,
324+ metavar = "NAME" ,
325+ help = "Validate only the named skill directory (skips the marketplace "
326+ "cross-check, which is repo-wide)." ,
327+ )
328+ group .add_argument (
329+ "--marketplace-only" ,
330+ action = "store_true" ,
331+ help = "Only validate that marketplace.json is in sync with skills/." ,
332+ )
267333 args = parser .parse_args (argv )
268- return run (args .skills_dir .resolve ())
334+ skills_dir = args .skills_dir .resolve ()
335+
336+ if args .list :
337+ return list_skills (skills_dir )
338+ if args .skill :
339+ return run_single (skills_dir , args .skill )
340+ if args .marketplace_only :
341+ return run_marketplace (skills_dir )
342+ return run (skills_dir )
269343
270344
271345if __name__ == "__main__" :
0 commit comments