skills-vault/scripts/install-skill.sh

221 lines
5.2 KiB
Bash

#!/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