| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210 |
- #!/bin/bash
- # Validate skill trigger keywords
- # Ensures descriptions contain advertised triggers and follows naming conventions
- set -euo pipefail
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
- SKILLS_DIR="$SCRIPT_DIR/../../skills"
- # Colors
- RED='\033[0;31m'
- GREEN='\033[0;32m'
- YELLOW='\033[1;33m'
- BLUE='\033[0;34m'
- NC='\033[0m'
- PASSED=0
- FAILED=0
- WARNINGS=0
- pass() { ((PASSED++)); echo -e "${GREEN}✓${NC} $1"; }
- fail() { ((FAILED++)); echo -e "${RED}✗${NC} $1: $2"; }
- warn() { ((WARNINGS++)); echo -e "${YELLOW}!${NC} $1: $2"; }
- # Extract frontmatter field from SKILL.md
- get_frontmatter() {
- local file="$1"
- local field="$2"
- # Extract value between --- markers (|| true to prevent set -e failure)
- sed -n '/^---$/,/^---$/p' "$file" | grep "^${field}:" | sed "s/^${field}: *//" | sed 's/^"//' | sed 's/"$//' || true
- }
- # Validate skill name format
- validate_name() {
- local skill_dir="$1"
- local name="$2"
- local dirname
- dirname=$(basename "$skill_dir")
- # Check name matches directory
- if [[ "$name" != "$dirname" ]]; then
- fail "$dirname" "name '$name' doesn't match directory"
- return 1
- fi
- # Check format: lowercase, numbers, hyphens only
- if [[ ! "$name" =~ ^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$ ]]; then
- fail "$dirname" "name must be lowercase alphanumeric with hyphens"
- return 1
- fi
- # Check length
- if [[ ${#name} -gt 64 ]]; then
- fail "$dirname" "name exceeds 64 characters"
- return 1
- fi
- return 0
- }
- # Validate description has triggers
- validate_description() {
- local skill_dir="$1"
- local description="$2"
- local dirname
- dirname=$(basename "$skill_dir")
- # Check non-empty
- if [[ -z "$description" ]]; then
- fail "$dirname" "description is empty"
- return 1
- fi
- # Check length
- if [[ ${#description} -gt 1024 ]]; then
- fail "$dirname" "description exceeds 1024 characters"
- return 1
- fi
- # Check for trigger keywords
- if [[ "$description" != *"Triggers on"* && "$description" != *"triggers on"* && "$description" != *"Auto-activates"* ]]; then
- warn "$dirname" "no 'Triggers on:' section in description"
- fi
- return 0
- }
- # Extract and validate trigger keywords
- validate_triggers() {
- local skill_dir="$1"
- local description="$2"
- local dirname
- dirname=$(basename "$skill_dir")
- # Extract triggers after "Triggers on" using sed (macOS compatible)
- local triggers=""
- if [[ "$description" == *"Triggers on:"* ]]; then
- triggers=$(echo "$description" | sed -n 's/.*Triggers on:[[:space:]]*//p')
- elif [[ "$description" == *"Triggers on "* ]]; then
- # Handle "Triggers on X, Y, Z" without colon
- triggers=$(echo "$description" | sed -n 's/.*Triggers on[[:space:]]*//p')
- elif [[ "$description" == *"triggers on:"* ]]; then
- triggers=$(echo "$description" | sed -n 's/.*triggers on:[[:space:]]*//p')
- elif [[ "$description" == *"Auto-activates"* ]]; then
- triggers=$(echo "$description" | sed -n 's/.*Auto-activates[[:space:]]*//p')
- fi
- if [[ -n "$triggers" ]]; then
- # Count trigger keywords (comma separated)
- local count
- count=$(echo "$triggers" | tr ',' '\n' | wc -l | tr -d ' ')
- if [[ $count -lt 3 ]]; then
- warn "$dirname" "only $count trigger keywords (recommend 5+)"
- else
- pass "$dirname: $count trigger keywords"
- fi
- fi
- }
- # Validate required CLI tools are documented
- validate_compatibility() {
- local skill_dir="$1"
- local skill_file="$skill_dir/SKILL.md"
- local dirname
- dirname=$(basename "$skill_dir")
- local compat
- compat=$(get_frontmatter "$skill_file" "compatibility")
- local content
- content=$(cat "$skill_file")
- # Check if skill references CLI tools
- local needs_tools=false
- if [[ "$content" == *"brew install"* || "$content" == *"npm install"* ]]; then
- needs_tools=true
- fi
- if [[ "$needs_tools" == true && -z "$compat" ]]; then
- warn "$dirname" "references CLI tools but no compatibility field"
- fi
- }
- # Validate allowed-tools field
- validate_allowed_tools() {
- local skill_dir="$1"
- local skill_file="$skill_dir/SKILL.md"
- local dirname
- dirname=$(basename "$skill_dir")
- local tools
- tools=$(get_frontmatter "$skill_file" "allowed-tools")
- if [[ -z "$tools" ]]; then
- warn "$dirname" "no allowed-tools field"
- fi
- }
- # Main validation
- validate_skill() {
- local skill_dir="$1"
- local skill_file="$skill_dir/SKILL.md"
- if [[ ! -f "$skill_file" ]]; then
- fail "$(basename "$skill_dir")" "SKILL.md not found"
- return
- fi
- local name description
- name=$(get_frontmatter "$skill_file" "name")
- description=$(get_frontmatter "$skill_file" "description")
- validate_name "$skill_dir" "$name" || true
- validate_description "$skill_dir" "$description" || true
- validate_triggers "$skill_dir" "$description"
- validate_compatibility "$skill_dir"
- validate_allowed_tools "$skill_dir"
- }
- # === Main ===
- main() {
- echo "=== Skill Trigger Validation ==="
- echo ""
- local skill_count=0
- for skill_dir in "$SKILLS_DIR"/*/; do
- if [[ -d "$skill_dir" ]]; then
- ((skill_count++))
- echo -e "${BLUE}--- $(basename "$skill_dir") ---${NC}"
- validate_skill "$skill_dir"
- echo ""
- fi
- done
- echo "=== Summary ==="
- echo "Skills validated: $skill_count"
- echo -e "Passed: ${GREEN}$PASSED${NC}"
- echo -e "Failed: ${RED}$FAILED${NC}"
- echo -e "Warnings: ${YELLOW}$WARNINGS${NC}"
- [[ $FAILED -eq 0 ]]
- }
- main "$@"
|