#!/usr/bin/env python3 """ Validate an OurDigital skill structure and content. Usage: python validate_skill.py {skill-directory} python validate_skill.py 02-ourdigital-blog """ import argparse import re from pathlib import Path class SkillValidator: def __init__(self, skill_path: Path): self.skill_path = skill_path self.errors = [] self.warnings = [] def validate(self) -> bool: """Run all validations.""" self._check_directory_structure() self._check_skill_files() self._check_frontmatter() self._check_content_length() self._check_ourdigital_triggers() return len(self.errors) == 0 def _check_directory_structure(self): """Verify required directories exist.""" required_dirs = [ "code", "desktop", ] recommended_dirs = [ "shared", "docs", ] for d in required_dirs: if not (self.skill_path / d).is_dir(): self.errors.append(f"Missing required directory: {d}/") for d in recommended_dirs: if not (self.skill_path / d).is_dir(): self.warnings.append(f"Missing recommended directory: {d}/") def _check_skill_files(self): """Verify SKILL.md files exist.""" skill_files = [ "code/SKILL.md", "desktop/SKILL.md", ] for f in skill_files: if not (self.skill_path / f).is_file(): self.errors.append(f"Missing required file: {f}") def _check_frontmatter(self): """Verify YAML frontmatter in SKILL.md files.""" for env in ["code", "desktop"]: skill_file = self.skill_path / env / "SKILL.md" if not skill_file.is_file(): continue content = skill_file.read_text() # Check for YAML frontmatter if not content.startswith("---"): self.errors.append(f"{env}/SKILL.md: Missing YAML frontmatter") continue # Check required fields required_fields = ["name", "description", "version", "author", "environment"] for field in required_fields: if f"{field}:" not in content.split("---")[1]: self.warnings.append(f"{env}/SKILL.md: Missing frontmatter field '{field}'") def _check_content_length(self): """Verify SKILL.md body is within word limit.""" for env in ["code", "desktop"]: skill_file = self.skill_path / env / "SKILL.md" if not skill_file.is_file(): continue content = skill_file.read_text() # Extract body (after frontmatter) parts = content.split("---") if len(parts) >= 3: body = "---".join(parts[2:]) word_count = len(body.split()) if word_count < 200: self.warnings.append( f"{env}/SKILL.md: Body too short ({word_count} words, recommended 800-1200)" ) elif word_count > 1500: self.warnings.append( f"{env}/SKILL.md: Body too long ({word_count} words, recommended 800-1200)" ) def _check_ourdigital_triggers(self): """Verify 'ourdigital' keyword in triggers.""" for env in ["code", "desktop"]: skill_file = self.skill_path / env / "SKILL.md" if not skill_file.is_file(): continue content = skill_file.read_text().lower() # Check for ourdigital in description/triggers if "ourdigital" not in content: self.errors.append( f"{env}/SKILL.md: Missing 'ourdigital' keyword in triggers" ) def report(self): """Print validation report.""" print(f"\nValidation Report: {self.skill_path.name}") print("=" * 50) if self.errors: print(f"\nERRORS ({len(self.errors)}):") for e in self.errors: print(f" [X] {e}") if self.warnings: print(f"\nWARNINGS ({len(self.warnings)}):") for w in self.warnings: print(f" [!] {w}") if not self.errors and not self.warnings: print("\n All checks passed!") print("\n" + "=" * 50) status = "PASS" if not self.errors else "FAIL" print(f"Status: {status}") return not self.errors def main(): parser = argparse.ArgumentParser(description="Validate an OurDigital skill") parser.add_argument("skill_dir", help="Skill directory name or path") args = parser.parse_args() # Handle relative or absolute path skill_path = Path(args.skill_dir) if not skill_path.is_absolute(): # Try relative to custom-skills directory base_path = Path(__file__).parent.parent.parent.parent skill_path = base_path / args.skill_dir if not skill_path.is_dir(): print(f"Error: Skill directory not found: {skill_path}") return 1 validator = SkillValidator(skill_path) validator.validate() success = validator.report() return 0 if success else 1 if __name__ == "__main__": exit(main())