221 lines
5.2 KiB
Bash
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
|