Add skills

This commit is contained in:
2026-03-22 23:21:49 +02:00
parent 4cbbbae1ef
commit c09d9151ca
104 changed files with 23879 additions and 0 deletions

View File

@@ -0,0 +1,308 @@
#!/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