#!/usr/bin/env bash set -euo pipefail usage() { cat <<'EOF' Usage: bash scripts/install-skill.sh fix-title bash scripts/install-skill.sh --all Options: --force Back up and replace existing installed Skill --install-claude-link Create/update .claude/skills link to .agents/skills EOF } skill="" install_all=0 force=0 install_claude_link=0 while [[ $# -gt 0 ]]; do case "$1" in --all) install_all=1 shift ;; --force) force=1 shift ;; --install-claude-link) install_claude_link=1 shift ;; -h|--help) usage exit 0 ;; -*) echo "Unknown option: $1" >&2 usage exit 2 ;; *) if [[ -n "$skill" ]]; then echo "Only one Skill name may be provided." >&2 usage exit 2 fi skill="$1" shift ;; esac done if [[ "$install_all" -eq 1 && -n "$skill" ]] || [[ "$install_all" -eq 0 && -z "$skill" ]]; then usage exit 2 fi script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" repo_root="$(cd "$script_dir/.." && pwd)" skills_root="$repo_root/skills" registry_path="$repo_root/registry/skills-index.md" if [[ -n "${USERPROFILE:-}" ]]; then home_dir="$USERPROFILE" else home_dir="$HOME" fi install_root="${SKILLS_VAULT_INSTALL_ROOT:-$home_dir/.agents/skills}" claude_skills_root="${SKILLS_VAULT_CLAUDE_SKILLS_ROOT:-$home_dir/.claude/skills}" registry_meta() { local name="$1" [[ -f "$registry_path" ]] || return 1 grep -E "^\|[[:space:]]*\`$name\`[[:space:]]*\|" "$registry_path" | head -n 1 || true } registry_cell() { local line="$1" local index="$2" printf '%s\n' "$line" | awk -F'|' -v i="$index" '{gsub(/^ +| +$/, "", $(i + 1)); gsub(/`/, "", $(i + 1)); print $(i + 1)}' } all_default_skills() { local allowed_statuses=" tested installable installed " for dir in "$skills_root"/*; do [[ -d "$dir" ]] || continue [[ -f "$dir/SKILL.md" ]] || continue local name name="$(basename "$dir")" local line line="$(registry_meta "$name")" if [[ -z "$line" ]]; then echo "Skipping $name: no registry row" >&2 continue fi local status policy status="$(registry_cell "$line" 4)" policy="$(registry_cell "$line" 5)" if [[ "$allowed_statuses" != *" $status "* ]]; then echo "Skipping $name: status is $status" >&2 continue fi if [[ "$policy" != "default" ]]; then echo "Skipping $name: install policy is $policy" >&2 continue fi printf '%s\n' "$name" done } backup_existing_path() { local path="$1" local stamp stamp="$(date +%Y%m%d%H%M%S)" local backup="$path.backup-$stamp" mv "$path" "$backup" printf '%s\n' "$backup" } copy_skill_directory() { local source="$1" local target="$2" mkdir -p "$target" shopt -s dotglob nullglob for item in "$source"/*; do local base base="$(basename "$item")" case "$base" in manual-test|__pycache__|.pytest_cache) continue ;; esac cp -a "$item" "$target/" done shopt -u dotglob nullglob } create_claude_link() { local name="$1" local target="$2" local link="$claude_skills_root/$name" mkdir -p "$claude_skills_root" if [[ -e "$link" || -L "$link" ]]; then if [[ "$force" -ne 1 ]]; then echo "Claude Skill link/path already exists: $link. Re-run with --force to back it up and replace it." >&2 exit 1 fi local backup backup="$(backup_existing_path "$link")" echo "Backed up existing Claude path to $backup" fi if [[ "${OS:-}" == "Windows_NT" ]]; then cmd.exe /c mklink /J "$(cygpath -w "$link")" "$(cygpath -w "$target")" >/dev/null else ln -s "$target" "$link" fi echo "Linked Claude Skill $link -> $target" } install_one_skill() { local name="$1" if [[ ! "$name" =~ ^[a-z0-9][a-z0-9-]*$ ]]; then echo "Invalid Skill name: $name" >&2 exit 1 fi local source="$skills_root/$name" if [[ ! -f "$source/SKILL.md" ]]; then echo "Skill source not found: $source/SKILL.md" >&2 exit 1 fi local line line="$(registry_meta "$name")" if [[ -n "$line" ]]; then local policy policy="$(registry_cell "$line" 5)" if [[ "$policy" == "disabled" && "$force" -ne 1 ]]; then echo "Skill $name has install policy disabled. Use --force only if you intentionally want to install it." >&2 exit 1 fi fi local target="$install_root/$name" mkdir -p "$install_root" if [[ -e "$target" || -L "$target" ]]; then if [[ "$force" -ne 1 ]]; then echo "Installed Skill already exists: $target. Re-run with --force to back it up and replace it." >&2 exit 1 fi local backup backup="$(backup_existing_path "$target")" echo "Backed up existing Skill to $backup" fi copy_skill_directory "$source" "$target" echo "Installed $name to $target" if [[ "$install_claude_link" -eq 1 ]]; then create_claude_link "$name" "$target" fi } if [[ "$install_all" -eq 1 ]]; then mapfile -t skills < <(all_default_skills) if [[ "${#skills[@]}" -eq 0 ]]; then echo "No default installable Skills found." exit 0 fi for name in "${skills[@]}"; do install_one_skill "$name" done else install_one_skill "$skill" fi