From 7ca5a5ad3c296add8b2121e872228ac88b7381b1 Mon Sep 17 00:00:00 2001 From: vitya Date: Mon, 25 May 2026 08:13:26 +0300 Subject: [PATCH] feat(install): add --prune flag to install.{ps1,sh} [skip-tdd: wrapper] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For each / dir in $target that has no matching skills//, remove it. Catches stale installs after skill rename/retire (motivating case: using-synology-ops just removed but install dir lingered until manual rm). Design choices: - Combined flag (install + prune in one run), not standalone mode - Prune always scans full target — does NOT respect -Names/positional filter, since stale-cleanup is a global concern - Prints `pruning: ` per removal, no confirmation prompt - Default off — flag must be passed explicitly [skip-tdd: wrapper] — shell glue wrapping Remove-Item / rm -rf with a set-difference; verified via smoke-test on disposable target dir (fake stale dirs created, full install + prune run via env-overridden CLAUDE_SKILLS_DIR, post-state confirmed: stale dirs removed, valid skills installed, using-synology-ops absent as expected). Closes 1/3 of [install-ps1] acceptance (the --prune flag). The remaining doc .wiki/concepts/install-cross-platform.md is deferred to a follow-up commit. --- scripts/install.ps1 | 19 +++++++++++++++++-- scripts/install.sh | 29 ++++++++++++++++++++++++++--- 2 files changed, 43 insertions(+), 5 deletions(-) diff --git a/scripts/install.ps1 b/scripts/install.ps1 index 18eb065..729633e 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -1,11 +1,15 @@ # Install skills// into ~\.claude\skills\\ (or $env:CLAUDE_SKILLS_DIR) -# Usage: install.ps1 [-Names ,] (no args = all) +# Usage: install.ps1 [-Names ,] [-Prune] +# no args = install all skills/* into target +# -Prune = after install, remove target// dirs that are NOT in skills/* +# (prune always scans full target, ignores -Names filter — it's a global cleanup) # # PowerShell port of install.sh — same behavior, native cmdlets, no bash dependency. [CmdletBinding()] param( - [string[]]$Names = @() + [string[]]$Names = @(), + [switch]$Prune ) $ErrorActionPreference = 'Stop' @@ -44,3 +48,14 @@ foreach ($name in $Names) { Copy-Item -Recurse $srcDir $dstDir Write-Host "installed: $name -> $dstDir" } + +if ($Prune) { + $sourceNames = @(Get-ChildItem -Path $src -Directory | Select-Object -ExpandProperty Name) + $installedDirs = Get-ChildItem -Path $target -Directory -ErrorAction SilentlyContinue + foreach ($dir in $installedDirs) { + if ($sourceNames -notcontains $dir.Name) { + Write-Host "pruning: $($dir.Name) (not in skills/) -> $($dir.FullName)" + Remove-Item -Recurse -Force $dir.FullName + } + } +} diff --git a/scripts/install.sh b/scripts/install.sh index 4117b87..55b807c 100644 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -1,13 +1,25 @@ #!/usr/bin/env bash # Install skills// into ~/.claude/skills// (or $CLAUDE_SKILLS_DIR) -# Usage: install.sh [name...] (no args = all) +# Usage: install.sh [--prune] [name...] +# no args = install all skills/* into target +# --prune = after install, remove target// dirs that are NOT in skills/* +# (prune always scans full target, ignores name filter — global cleanup) set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" SRC="$ROOT/skills" TARGET="${CLAUDE_SKILLS_DIR:-$HOME/.claude/skills}" -if [ "$#" -eq 0 ]; then +prune=0 +positional=() +for arg in "$@"; do + case "$arg" in + --prune) prune=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=() @@ -18,7 +30,7 @@ if [ "$#" -eq 0 ]; then IFS=$'\n' names=($(printf '%s\n' "${names[@]}" | sort)) unset IFS else - names=("$@") + names=("${positional[@]}") fi mkdir -p "$TARGET" @@ -38,3 +50,14 @@ for name in "${names[@]}"; do cp -R "$src_dir" "$dst_dir" echo "installed: $name → $dst_dir" done + +if [ "$prune" -eq 1 ]; then + for d in "$TARGET"/*/; do + [ -d "$d" ] || continue + name="$(basename "$d")" + if [ ! -d "$SRC/$name" ]; then + echo "pruning: $name (not in skills/) → $d" + rm -rf "$d" + fi + done +fi