From 550f87d239f6fe80993e3e9399aeaddf1b6aa936 Mon Sep 17 00:00:00 2001 From: vitya Date: Thu, 20 Aug 2026 13:38:14 +0300 Subject: [PATCH] fix(scripts): prune never removes prod skills silently MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- scripts/install.ps1 | 38 +++++++++++++++++++++++++++++--------- scripts/install.sh | 27 ++++++++++++++++++++++++--- 2 files changed, 53 insertions(+), 12 deletions(-) diff --git a/scripts/install.ps1 b/scripts/install.ps1 index b7ca16f..702c0ca 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -1,19 +1,23 @@ # 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\\ (Claude Code - native, not configurable) +# ~\.agents\skills\\ (pi - native default scan path, agent-neutral namespace) # $env:CLAUDE_SKILLS_DIR overrides ONLY the claude target (for testing/CI). # -# Usage: install.ps1 [-Names ,] [-Prune] +# Usage: install.ps1 [-Names ,] [-Prune] [-Yes] # 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 -Names filter — it's a global cleanup) +# (prune always scans full target, ignores -Names filter - it's a 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. # -# PowerShell port of install.sh — same behavior, native cmdlets, no bash dependency. +# PowerShell port of install.sh - same behavior, native cmdlets, no bash dependency. [CmdletBinding()] param( [string[]]$Names = @(), - [switch]$Prune + [switch]$Prune, + [switch]$Yes ) $ErrorActionPreference = 'Stop' @@ -21,7 +25,7 @@ $ErrorActionPreference = 'Stop' $root = Split-Path -Parent $PSScriptRoot $src = Join-Path $root 'skills' -# Dual install targets — the "canon" is the git repo (skills/); both dirs are installs. +# Dual install targets - the "canon" is the git repo (skills/); both dirs are installs. $targets = @() if ($env:CLAUDE_SKILLS_DIR) { $targets += $env:CLAUDE_SKILLS_DIR @@ -62,12 +66,28 @@ foreach ($target in $targets) { if ($Prune) { $sourceNames = @(Get-ChildItem -Path $src -Directory | Select-Object -ExpandProperty Name) + $interactive = -not [Console]::IsInputRedirected foreach ($target in $targets) { $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 + $rel = $dir.Name + $loc = $dir.FullName + if ($Yes) { + Write-Warning "prune: $rel (not in skills/) - REMOVED from $loc" + Remove-Item -Recurse -Force $loc + } elseif ($interactive) { + Write-Warning "prune: $rel is in prod but has NO source in skills/ - possibly lost knowledge." + $ans = Read-Host " Remove '$rel'? [y/N]" + if ($ans -eq 'y' -or $ans -eq 'Y') { + Remove-Item -Recurse -Force $loc + Write-Host "removed: $rel" + } else { + Write-Host "kept: $rel (remove manually, or add it to skills/)" + } + } else { + Write-Warning "prune skipped: $rel (prod skill without source; non-interactive - nothing removed)" + } } } } diff --git a/scripts/install.sh b/scripts/install.sh index 304dc8f..ed04897 100644 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -4,10 +4,13 @@ # ~/.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] [name...] +# 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)" @@ -17,10 +20,12 @@ SRC="$ROOT/skills" 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 @@ -62,13 +67,29 @@ for target in "${TARGETS[@]}"; do 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 - echo "pruning: $name (not in skills/) → $d" - rm -rf "$d" + 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