115 lines
4.2 KiB
Python
Executable File
115 lines
4.2 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Validate overall plugin structure and organization.
|
|
|
|
Checks:
|
|
- skills/ directory exists if skills are mentioned
|
|
- agents/, commands/, hooks/ directories follow naming conventions
|
|
- No invalid file/directory names
|
|
- Required metadata files present
|
|
"""
|
|
|
|
import os
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
|
|
def validate_plugin_structure():
|
|
"""Check plugin directory structure and naming."""
|
|
errors = []
|
|
plugin_root = Path(os.environ.get("CLAUDE_PLUGIN_ROOT", "."))
|
|
|
|
# Check for valid component directories
|
|
valid_dirs = {"skills", "agents", "commands", "hooks", ".claude-plugin"}
|
|
|
|
for item in plugin_root.iterdir():
|
|
if item.is_dir() and item.name.startswith("."):
|
|
continue # Skip hidden dirs
|
|
|
|
if item.is_dir() and item.name not in valid_dirs:
|
|
# Check if it's a generated dir like __pycache__
|
|
if item.name.startswith("__"):
|
|
continue
|
|
# Check if it's a valid plugin component
|
|
if not re.match(r"^[a-z0-9_-]+$", item.name):
|
|
errors.append(f"Invalid directory name: {item.name} (use lowercase, hyphens, underscores)")
|
|
|
|
# Check skills structure if skills directory exists
|
|
skills_dir = plugin_root / "skills"
|
|
if skills_dir.exists():
|
|
for skill_path in skills_dir.iterdir():
|
|
if not skill_path.is_dir():
|
|
continue
|
|
|
|
# Check SKILL.md exists
|
|
skill_md = skill_path / "SKILL.md"
|
|
if not skill_md.exists():
|
|
errors.append(f"skills/{skill_path.name}/: Missing SKILL.md file")
|
|
|
|
# Check skill directory name format
|
|
if not re.match(r"^[a-z0-9-]+$", skill_path.name):
|
|
errors.append(f"skills/{skill_path.name}/: Invalid directory name (use lowercase with hyphens only)")
|
|
|
|
# Check agents directory if exists
|
|
agents_dir = plugin_root / "agents"
|
|
if agents_dir.exists():
|
|
for agent_file in agents_dir.iterdir():
|
|
if agent_file.is_file() and agent_file.suffix == ".md":
|
|
# Agent files should use kebab-case
|
|
name = agent_file.stem
|
|
if not re.match(r"^[a-z0-9-]+$", name):
|
|
errors.append(
|
|
f"agents/{agent_file.name}: Invalid agent name (use kebab-case: lowercase with hyphens)"
|
|
)
|
|
|
|
# Check commands directory if exists
|
|
commands_dir = plugin_root / "commands"
|
|
if commands_dir.exists():
|
|
for cmd_file in commands_dir.iterdir():
|
|
if cmd_file.is_file() and cmd_file.suffix == ".md":
|
|
# Command files should use kebab-case
|
|
name = cmd_file.stem
|
|
if not re.match(r"^[a-z0-9-]+$", name):
|
|
errors.append(
|
|
f"commands/{cmd_file.name}: Invalid command name (use kebab-case: lowercase with hyphens)"
|
|
)
|
|
|
|
# Check hooks directory if exists
|
|
hooks_dir = plugin_root / "hooks"
|
|
if hooks_dir.exists():
|
|
hooks_json = hooks_dir / "hooks.json"
|
|
if not hooks_json.exists():
|
|
errors.append("hooks/: Missing hooks.json file")
|
|
|
|
# Check scripts directory
|
|
scripts_dir = hooks_dir / "scripts"
|
|
if scripts_dir.exists():
|
|
for script in scripts_dir.iterdir():
|
|
if script.is_file():
|
|
# Scripts should be executable
|
|
if not os.access(script, os.X_OK):
|
|
# Note: We don't error here, just validate naming
|
|
pass
|
|
|
|
# Check script naming
|
|
if script.suffix in {".py", ".sh"}:
|
|
name = script.stem
|
|
if not re.match(r"^[a-z0-9_]+$", name):
|
|
errors.append(
|
|
f"hooks/scripts/{script.name}: Invalid script name "
|
|
"(use snake_case: lowercase with underscores)"
|
|
)
|
|
|
|
if errors:
|
|
print("❌ Plugin Structure Validation Failed:")
|
|
for error in errors:
|
|
print(f" • {error}")
|
|
return 1
|
|
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(validate_plugin_structure())
|