Files
oleg-lukasonokandClaude Fable 5 6042272ccc Add skill-manager skill (migrated from skill-creator) with deploy action
- Skill content moved from ~/.agents/skills/skill-creator, renamed to skill-manager
- New deploy CLI action: installs any skill via absolute-path symlinks (or copies)
  into $HOME/.agents/skills, $HOME/.claude/skills and $HOME/.cline/skills
- Taskfile + shell module wrappers (task deploy / cli:deploy)
- SKILL.md: deploy docs, origin-repository/origin-path metadata

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 22:32:42 +03:00

213 lines
8.3 KiB
Bash
Executable File
Raw Permalink 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
# 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 <<EOF
Usage: task -t scripts/Taskfile.yml validate-shell -- --skill-dir="<path>"
Validate an Agent Skill directory against the agentskills.io specification.
Options:
--skill-dir PATH Path to the skill directory to validate (required)
Checks:
- SKILL.md exists
- YAML frontmatter is present (--- delimiters)
- 'name' field is present and valid (a-z, 0-9, hyphens only)
- 'name' matches the directory name
- 'name' is <= 64 characters, no leading/trailing/consecutive hyphens
- 'description' field is present and <= 1024 characters
- SKILL.md body is <= 500 lines (warning if exceeded)
- Optional directories: scripts/, references/, assets/
Exit codes:
0 All checks passed
1 One or more checks failed
2 Usage error
Examples:
task -t scripts/Taskfile.yml validate-shell -- --skill-dir=".agents/skills/my-skill"
EOF
return 0
;;
*) echo "Error: unknown argument: $1" >&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=\"<path>\"" >&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
}