Files
.dotfiles/.config/opencode/skills/.skill-builder.disabled/scripts/validate-skill.sh
2026-03-22 23:21:49 +02:00

309 lines
9.1 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/bash
#
# validate-skill.sh - Strictly validate a skill's structure and content
#
# Usage: ./validate-skill.sh <path/to/skill-directory> [options]
#
# Options:
# --strict Fail on warnings (default)
# --lenient Only fail on errors, report warnings
# --verbose Show detailed output
#
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Validation counters
ERRORS=0
WARNINGS=0
# Default mode
STRICT=true
VERBOSE=false
usage() {
echo "Usage: $0 <path/to/skill-directory> [options]"
echo ""
echo "Options:"
echo " --strict Fail on warnings (default)"
echo " --lenient Only fail on errors"
echo " --verbose Show detailed output"
echo ""
echo "Examples:"
echo " $0 ~/.config/opencode/skills/my-skill"
echo " $0 ./my-skill --verbose"
exit 1
}
# Error and warning functions
error() {
echo -e "${RED}✗ ERROR:${NC} $1"
((ERRORS++)) || true
}
warn() {
if [[ "$STRICT" == true ]]; then
echo -e "${YELLOW}✗ WARNING (treated as error in strict mode):${NC} $1"
((ERRORS++)) || true
else
echo -e "${YELLOW}⚠ WARNING:${NC} $1"
((WARNINGS++)) || true
fi
}
info() {
if [[ "$VERBOSE" == true ]]; then
echo -e "${BLUE}${NC} $1"
fi
}
success() {
echo -e "${GREEN}${NC} $1"
}
# Parse arguments
SKILL_DIR=""
while [[ $# -gt 0 ]]; do
case $1 in
--strict)
STRICT=true
shift
;;
--lenient)
STRICT=false
shift
;;
--verbose)
VERBOSE=true
shift
;;
-h|--help)
usage
;;
-*)
echo -e "${RED}Error: Unknown option $1${NC}"
usage
;;
*)
if [[ -z "$SKILL_DIR" ]]; then
SKILL_DIR="$1"
else
echo -e "${RED}Error: Multiple skill directories provided${NC}"
usage
fi
shift
;;
esac
done
# Validate skill directory argument
if [[ -z "$SKILL_DIR" ]]; then
echo -e "${RED}Error: Skill directory is required${NC}"
usage
fi
# Resolve path
SKILL_DIR="$(cd "$SKILL_DIR" 2>/dev/null && pwd)" || {
echo -e "${RED}Error: Cannot access directory: $SKILL_DIR${NC}"
exit 1
}
# Get skill name from directory
SKILL_NAME="$(basename "$SKILL_DIR")"
echo "========================================"
echo "Validating skill: $SKILL_NAME"
echo "Directory: $SKILL_DIR"
echo "========================================"
echo ""
# Check 1: Directory exists
if [[ ! -d "$SKILL_DIR" ]]; then
error "Directory does not exist: $SKILL_DIR"
exit 1
fi
success "Directory exists"
# Check 2: SKILL.md exists
SKILL_MD="$SKILL_DIR/SKILL.md"
if [[ ! -f "$SKILL_MD" ]]; then
error "SKILL.md file not found in $SKILL_DIR"
error "Skill must contain a SKILL.md file"
exit 1
fi
success "SKILL.md file exists"
# Check 3: SKILL.md starts with YAML frontmatter
if ! head -1 "$SKILL_MD" | grep -q '^---\s*$'; then
error "SKILL.md must start with YAML frontmatter (---)"
else
success "SKILL.md starts with YAML frontmatter"
fi
# Extract frontmatter content
FRONTMATTER=$(sed -n '/^---$/,/^---$/p' "$SKILL_MD" | head -n -1 | tail -n +2)
info "Extracted frontmatter"
# Check 4: Name field exists and is valid
if ! echo "$FRONTMATTER" | grep -q '^name:'; then
error "Frontmatter missing required field: 'name'"
else
FM_NAME=$(echo "$FRONTMATTER" | grep '^name:' | sed 's/^name:\s*//' | tr -d '"' | tr -d "'")
if [[ -z "$FM_NAME" ]]; then
error "'name' field is empty"
elif [[ ! "$FM_NAME" =~ ^[a-z0-9-]+$ ]]; then
error "'name' must be lowercase alphanumeric with hyphens: '$FM_NAME'"
elif [[ "$FM_NAME" != "$SKILL_NAME" ]]; then
error "'name' ($FM_NAME) does not match directory name ($SKILL_NAME)"
else
success "'name' field is valid: $FM_NAME"
fi
fi
# Check 5: Description field exists and is valid
if ! echo "$FRONTMATTER" | grep -q '^description:'; then
error "Frontmatter missing required field: 'description'"
else
FM_DESC=$(echo "$FRONTMATTER" | sed -n '/^description:/,/^\S/{/^description:/p;/^\S/q}' | sed 's/^description:\s*//' | tr -d '"' | tr -d "'" | sed ':a;N;$!ba;s/\n/ /g')
DESC_LEN=${#FM_DESC}
if [[ -z "$FM_DESC" ]]; then
error "'description' field is empty"
elif [[ $DESC_LEN -lt 20 ]]; then
error "'description' must be at least 20 characters (found $DESC_LEN)"
elif [[ $DESC_LEN -gt 1024 ]]; then
error "'description' must be at most 1024 characters (found $DESC_LEN)"
else
success "'description' field length is valid ($DESC_LEN chars)"
fi
# Check for common description issues
if echo "$FM_DESC" | grep -qi '^use this skill'; then
warn "Description uses second person ('Use this skill'). Use third person: 'This skill should be used when...'"
fi
if echo "$FM_DESC" | grep -qi '^this skill provides\|^this skill helps'; then
warn "Description is vague. Include specific trigger phrases users would say"
fi
# Check for quoted phrases (allow either single or double quotes in original)
if ! echo "$FRONTMATTER" | grep -A 5 '^description:' | grep -q '"\|\"'; then
warn "Description should include specific trigger phrases in quotes, e.g.: \"create X\", \"configure Y\""
fi
fi
# Check 6: No unknown frontmatter fields (informational)
KNOWN_FIELDS="^name:\|^description:\|^license:\|^compatibility:\|^metadata:\|^allowed-tools:"
UNKNOWN_FIELDS=$(echo "$FRONTMATTER" | grep -v '^\s*$' | grep -v "$KNOWN_FIELDS" || true)
if [[ -n "$UNKNOWN_FIELDS" ]]; then
info "Unknown frontmatter fields (will be ignored):"
echo "$UNKNOWN_FIELDS" | sed 's/^/ /'
fi
# Check 7: YAML syntax is valid
if ! echo "$FRONTMATTER" | python3 -c "import yaml, sys; yaml.safe_load(sys.stdin)" 2>/dev/null; then
# Fallback: check with basic pattern matching
if echo "$FRONTMATTER" | grep -q '^\s*:\s*$'; then
error "Invalid YAML syntax: empty key found"
fi
# Count lines with colons to detect malformed entries
MALFORMED=$(echo "$FRONTMATTER" | grep -c '^\s*[^:]*:\s*$' || true)
if [[ $MALFORMED -gt 0 ]]; then
error "Potential YAML syntax issues detected"
fi
else
success "YAML frontmatter syntax appears valid"
fi
# Check 8: SKILL.md has content after frontmatter
# Remove everything from line 1 through the second occurrence of ---
BODY_CONTENT=$(sed '0,/^---$/d;0,/^---$/d' "$SKILL_MD")
if [[ -z "$(echo "$BODY_CONTENT" | tr -d '[:space:]')" ]]; then
error "SKILL.md has no content after frontmatter"
else
success "SKILL.md has body content"
fi
# Check 9: Check for common writing style issues in body
if echo "$BODY_CONTENT" | grep -qE '^You (should|need|can|must)'; then
warn "Body uses second person ('You should...'). Prefer imperative form: 'Do this...'"
fi
if echo "$BODY_CONTENT" | grep -q '^When to Use This Skill'; then
warn "'When to Use This Skill' section in body is redundant. Include triggers in description field instead"
fi
# Check 10: Verify referenced files exist
if [[ -d "$SKILL_DIR/references" ]]; then
REF_COUNT=$(find "$SKILL_DIR/references" -type f | wc -l)
success "References directory exists with $REF_COUNT file(s)"
# Check if references are mentioned in SKILL.md
for ref_file in "$SKILL_DIR/references"/*; do
if [[ -f "$ref_file" ]]; then
ref_name=$(basename "$ref_file")
if ! grep -q "$ref_name" "$SKILL_MD"; then
warn "Reference file '$ref_name' is not mentioned in SKILL.md"
fi
fi
done
fi
if [[ -d "$SKILL_DIR/scripts" ]]; then
SCRIPT_COUNT=$(find "$SKILL_DIR/scripts" -type f | wc -l)
success "Scripts directory exists with $SCRIPT_COUNT file(s)"
# Check scripts are executable
for script in "$SKILL_DIR/scripts"/*; do
if [[ -f "$script" ]] && [[ ! -x "$script" ]]; then
warn "Script '$(basename "$script")' is not executable (run: chmod +x)"
fi
done
fi
if [[ -d "$SKILL_DIR/assets" ]]; then
ASSET_COUNT=$(find "$SKILL_DIR/assets" -type f | wc -l)
success "Assets directory exists with $ASSET_COUNT file(s)"
fi
# Check 11: File organization
if [[ -f "$SKILL_DIR/README.md" ]]; then
warn "README.md found. Skills should not include README.md files"
fi
if [[ -f "$SKILL_DIR/CHANGELOG.md" ]]; then
warn "CHANGELOG.md found. Skills should not include auxiliary documentation"
fi
echo ""
echo "========================================"
# Final report
if [[ $ERRORS -eq 0 ]]; then
echo -e "${GREEN}✓ VALIDATION PASSED${NC}"
if [[ $WARNINGS -gt 0 ]]; then
echo -e "${YELLOW} $WARNINGS warning(s) (not treated as errors)${NC}"
fi
echo ""
echo "Your skill is ready to use!"
exit 0
else
echo -e "${RED}✗ VALIDATION FAILED${NC}"
echo -e "${RED} $ERRORS error(s) found${NC}"
if [[ $WARNINGS -gt 0 ]]; then
echo -e "${YELLOW} $WARNINGS warning(s)${NC}"
fi
echo ""
echo "Fix the errors above and run validation again."
exit 1
fi