#!/bin/bash # validator/lib/-skill--execute.sh — validate a skill directory against the agentskills.io spec _validator__skill__execute() { local FUNCTION_NAME="_validator__skill__execute" local SKILL_DIR="" # ── Parse args ────────────────────────────────────────────────────────────── while [[ $# -gt 0 ]]; do case "$1" in --skill-dir=*) SKILL_DIR="${1#*=}"; shift ;; --skill-dir) SKILL_DIR="$2"; shift 2 ;; --help|-h) cat <&2; return 2 ;; esac done # ── Validate required args ────────────────────────────────────────────────── if [[ -z "$SKILL_DIR" ]]; then echo "Error: --skill-dir is required." >&2 echo "Usage: task -t scripts/Taskfile.yml validate-shell -- --skill-dir=\"\"" >&2 return 2 fi SKILL_DIR="${SKILL_DIR%/}" # strip trailing slash # ── Counters ──────────────────────────────────────────────────────────────── local PASS=0 FAIL=0 WARN=0 _pass() { echo " ✅ $1"; ((PASS++)) || true; } _fail() { echo " ❌ $1"; ((FAIL++)) || true; } _warn() { echo " ⚠️ $1"; ((WARN++)) || true; } _info() { echo " ℹ️ $1"; } echo "" echo "Validating skill: $SKILL_DIR" echo "──────────────────────────────────────────" # ── Check: directory exists ───────────────────────────────────────────────── if [[ ! -d "$SKILL_DIR" ]]; then echo "Error: directory not found: $SKILL_DIR" >&2 return 2 fi local SKILL_FILE="$SKILL_DIR/SKILL.md" local DIR_NAME DIR_NAME="$(basename "$SKILL_DIR")" # ── Check: SKILL.md exists ────────────────────────────────────────────────── if [[ ! -f "$SKILL_FILE" ]]; then _fail "SKILL.md not found in $SKILL_DIR" echo ""; echo "Result: ❌ FAILED"; return 1 fi _pass "SKILL.md exists" # ── Check: frontmatter delimiters ─────────────────────────────────────────── local CONTENT CONTENT="$(cat "$SKILL_FILE")" if ! echo "$CONTENT" | grep -q "^---$"; then _fail "SKILL.md is missing YAML frontmatter (--- delimiters)" echo ""; echo "Result: ❌ FAILED"; return 1 fi _pass "YAML frontmatter delimiters found" # ── Extract frontmatter ───────────────────────────────────────────────────── local FRONTMATTER FRONTMATTER="$(awk '/^---$/{if(p==0){p=1;next}else{exit}} p{print}' "$SKILL_FILE")" # ── Check: name field ─────────────────────────────────────────────────────── local NAME_RAW NAME NAME_RAW="$(echo "$FRONTMATTER" | grep "^name:" | head -1 | sed 's/^name:[[:space:]]*//')" NAME="$(echo "$NAME_RAW" | tr -d "'\"" | tr -d '[:space:]')" if [[ -z "$NAME" ]]; then _fail "'name' field is missing or empty" else _pass "'name' field is present: $NAME" if [[ "$NAME" != "$DIR_NAME" ]]; then _fail "'name' ($NAME) does not match directory name ($DIR_NAME)" else _pass "'name' matches directory name" fi local NAME_LEN="${#NAME}" if [[ $NAME_LEN -gt 64 ]]; then _fail "'name' is $NAME_LEN characters (max 64)" else _pass "'name' length is $NAME_LEN (max 64)" fi if echo "$NAME" | grep -q '[^a-z0-9-]'; then _fail "'name' contains invalid characters (only a-z, 0-9, - allowed)" else _pass "'name' uses only allowed characters (a-z, 0-9, -)" fi if [[ "$NAME" == -* ]]; then _fail "'name' must not start with a hyphen" else _pass "'name' does not start with a hyphen" fi if [[ "$NAME" == *- ]]; then _fail "'name' must not end with a hyphen" else _pass "'name' does not end with a hyphen" fi if echo "$NAME" | grep -q "\-\-"; then _fail "'name' must not contain consecutive hyphens (--)" else _pass "'name' has no consecutive hyphens" fi fi # ── Check: description field ──────────────────────────────────────────────── local DESC_LINE DESC_LINE="$(echo "$FRONTMATTER" | grep "^description:" | head -1)" if [[ -z "$DESC_LINE" ]]; then _fail "'description' field is missing" else local DESC_VALUE DESC_VALUE="$(echo "$DESC_LINE" | sed 's/^description:[[:space:]]*//')" local DESC_FULL if [[ "$DESC_VALUE" == ">" || "$DESC_VALUE" == "|" || -z "$DESC_VALUE" ]]; then local DESC_BODY DESC_BODY="$(awk '/^description:/{found=1; next} found && /^ /{print; next} found && /^[^ ]/{exit}' "$SKILL_FILE" | sed 's/^ //')" DESC_FULL="$(echo "$DESC_BODY" | tr '\n' ' ' | sed 's/[[:space:]]*$//')" else DESC_FULL="$DESC_VALUE" fi if [[ -z "$DESC_FULL" ]]; then _fail "'description' is empty" else local DESC_LEN="${#DESC_FULL}" _pass "'description' is present (${DESC_LEN} chars)" if [[ $DESC_LEN -gt 1024 ]]; then _fail "'description' is $DESC_LEN characters (max 1024)" else _pass "'description' length is $DESC_LEN (max 1024)" fi fi fi # ── Check: body length ────────────────────────────────────────────────────── local BODY_LINES BODY_LINES="$(awk '/^---$/{n++} n>=2{print}' "$SKILL_FILE" | wc -l | tr -d ' ')" if [[ $BODY_LINES -gt 500 ]]; then _warn "SKILL.md body is $BODY_LINES lines (recommended max 500). Consider moving detail to references/." else _pass "SKILL.md body is $BODY_LINES lines (max 500 recommended)" fi # ── Optional directories ──────────────────────────────────────────────────── echo "" _info "Optional directories:" for dir in scripts references assets; do if [[ -d "$SKILL_DIR/$dir" ]]; then local FILE_COUNT FILE_COUNT="$(find "$SKILL_DIR/$dir" -type f | wc -l | tr -d ' ')" _info " $dir/ — found ($FILE_COUNT files)" else _info " $dir/ — not present" fi done # ── Summary ───────────────────────────────────────────────────────────────── echo "" echo "──────────────────────────────────────────" echo "Passed: $PASS | Failed: $FAIL | Warnings: $WARN" echo "" if [[ $FAIL -gt 0 ]]; then echo "Result: ❌ FAILED"; return 1 elif [[ $WARN -gt 0 ]]; then echo "Result: ✅ PASSED (with warnings)"; return 0 else echo "Result: ✅ PASSED"; return 0 fi }