#!/usr/bin/env bash # Install skills// into BOTH agent skill dirs: # ~/.claude/skills// (Claude Code — native, not configurable) # ~/.agents/skills// (pi — native default scan path, agent-neutral namespace) # $CLAUDE_SKILLS_DIR overrides ONLY the claude target (for testing/CI). # # Usage: install.sh [--prune] [--yes] [name...] # no args = install all skills/* into both targets # --prune = after install, remove target// dirs that are NOT in skills/* # (prune always scans full target, ignores name filter — global cleanup) # --yes = with --prune: remove prod-only skills without asking (still warns loudly). # Without --yes, prune asks per skill (default No) and NEVER removes silently: # a prod skill with no source in skills/ is potentially lost knowledge. set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" SRC="$ROOT/skills" # Dual install targets — the "canon" is the git repo (skills/); both dirs are installs. TARGETS=("${CLAUDE_SKILLS_DIR:-$HOME/.claude/skills}" "$HOME/.agents/skills") prune=0 yes=0 positional=() for arg in "$@"; do case "$arg" in --prune) prune=1 ;; --yes) yes=1 ;; *) positional+=("$arg") ;; esac done if [ "${#positional[@]}" -eq 0 ]; then # Portable across bash 3.2 (stock macOS) and bash 4+ (Linux, git-bash): # avoid `mapfile` (bash 4+) and `find -printf` (GNU find only). names=() for d in "$SRC"/*/; do [ -d "$d" ] || continue names+=("$(basename "$d")") done IFS=$'\n' names=($(printf '%s\n' "${names[@]}" | sort)) unset IFS else names=("${positional[@]}") fi for target in "${TARGETS[@]}"; do mkdir -p "$target" done for target in "${TARGETS[@]}"; do for name in "${names[@]}"; do src_dir="$SRC/$name" dst_dir="$target/$name" if [ ! -d "$src_dir" ]; then echo "skip: $name (not found in skills/)" >&2 continue fi if [ ! -f "$src_dir/SKILL.md" ]; then echo "skip: $name (missing SKILL.md)" >&2 continue fi rm -rf "$dst_dir" cp -R "$src_dir" "$dst_dir" echo "installed: $name → $dst_dir" done done if [ "$prune" -eq 1 ]; then interactive=1 if [ ! -t 0 ]; then interactive=0; fi for target in "${TARGETS[@]}"; do for d in "$target"/*/; do [ -d "$d" ] || continue name="$(basename "$d")" if [ ! -d "$SRC/$name" ]; then if [ "$yes" -eq 1 ]; then echo "WARN: prune: $name (not in skills/) — REMOVED from $d" >&2 rm -rf "$d" elif [ "$interactive" -eq 1 ]; then echo "WARN: prune: $name is in prod but has NO source in skills/ — possibly lost knowledge." >&2 printf ' Remove "%s"? [y/N] ' "$name" read -r ans if [ "$ans" = "y" ] || [ "$ans" = "Y" ]; then rm -rf "$d" echo "removed: $name" else echo "kept: $name (remove manually, or add it to skills/)" fi else echo "WARN: prune skipped: $name (prod skill without source; non-interactive — nothing removed)" >&2 fi fi done done fi