Basic validation of a skill
(skill_path)
| 10 | from pathlib import Path |
| 11 | |
| 12 | def validate_skill(skill_path): |
| 13 | """Basic validation of a skill""" |
| 14 | skill_path = Path(skill_path) |
| 15 | |
| 16 | # Check SKILL.md exists |
| 17 | skill_md = skill_path / 'SKILL.md' |
| 18 | if not skill_md.exists(): |
| 19 | return False, "SKILL.md not found" |
| 20 | |
| 21 | # Read and validate frontmatter |
| 22 | content = skill_md.read_text() |
| 23 | if not content.startswith('---'): |
| 24 | return False, "No YAML frontmatter found" |
| 25 | |
| 26 | # Extract frontmatter |
| 27 | match = re.match(r'^---\n(.*?)\n---', content, re.DOTALL) |
| 28 | if not match: |
| 29 | return False, "Invalid frontmatter format" |
| 30 | |
| 31 | frontmatter_text = match.group(1) |
| 32 | |
| 33 | # Parse YAML frontmatter |
| 34 | try: |
| 35 | frontmatter = yaml.safe_load(frontmatter_text) |
| 36 | if not isinstance(frontmatter, dict): |
| 37 | return False, "Frontmatter must be a YAML dictionary" |
| 38 | except yaml.YAMLError as e: |
| 39 | return False, f"Invalid YAML in frontmatter: {e}" |
| 40 | |
| 41 | # Define allowed properties |
| 42 | ALLOWED_PROPERTIES = {'name', 'description', 'license', 'allowed-tools', 'metadata'} |
| 43 | |
| 44 | # Check for unexpected properties (excluding nested keys under metadata) |
| 45 | unexpected_keys = set(frontmatter.keys()) - ALLOWED_PROPERTIES |
| 46 | if unexpected_keys: |
| 47 | return False, ( |
| 48 | f"Unexpected key(s) in SKILL.md frontmatter: {', '.join(sorted(unexpected_keys))}. " |
| 49 | f"Allowed properties are: {', '.join(sorted(ALLOWED_PROPERTIES))}" |
| 50 | ) |
| 51 | |
| 52 | # Check required fields |
| 53 | if 'name' not in frontmatter: |
| 54 | return False, "Missing 'name' in frontmatter" |
| 55 | if 'description' not in frontmatter: |
| 56 | return False, "Missing 'description' in frontmatter" |
| 57 | |
| 58 | # Extract name for validation |
| 59 | name = frontmatter.get('name', '') |
| 60 | if not isinstance(name, str): |
| 61 | return False, f"Name must be a string, got {type(name).__name__}" |
| 62 | name = name.strip() |
| 63 | if name: |
| 64 | # Check naming convention (hyphen-case: lowercase with hyphens) |
| 65 | if not re.match(r'^[a-z0-9-]+$', name): |
| 66 | return False, f"Name '{name}' should be hyphen-case (lowercase letters, digits, and hyphens only)" |
| 67 | if name.startswith('-') or name.endswith('-') or '--' in name: |
| 68 | return False, f"Name '{name}' cannot start/end with hyphen or contain consecutive hyphens" |
| 69 | # Check name length (max 64 characters per spec) |
no test coverage detected