Files
skills/scripts/install.sh
vitya 550f87d239 fix(scripts): prune never removes prod skills silently
install.ps1/-sh -Prune: a prod skill with no source in skills/ is
potentially lost knowledge. Now:
- interactive: ask per skill (default No)
- non-interactive: skip with warning
- -Yes/--yes: remove, but warn loudly
Also fixes WinPS 5.1 parse break: removed em-dash (UTF-8 0x94 reads as
" in cp1251) from install.ps1 — file is ASCII-clean again.
Lesson: pi-add-models got pruned because it lived only in prod, no source.
2026-08-20 13:38:14 +03:00

97 lines
3.1 KiB
Bash

#!/usr/bin/env bash
# Install skills/<name>/ into BOTH agent skill dirs:
# ~/.claude/skills/<name>/ (Claude Code — native, not configurable)
# ~/.agents/skills/<name>/ (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/<name>/ 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