feat(install): add --prune flag to install.{ps1,sh} [skip-tdd: wrapper]
For each <name>/ dir in $target that has no matching skills/<name>/, 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: <name>` 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.
This commit is contained in:
@@ -1,11 +1,15 @@
|
|||||||
# Install skills/<name>/ into ~\.claude\skills\<name>\ (or $env:CLAUDE_SKILLS_DIR)
|
# Install skills/<name>/ into ~\.claude\skills\<name>\ (or $env:CLAUDE_SKILLS_DIR)
|
||||||
# Usage: install.ps1 [-Names <name1>,<name2>] (no args = all)
|
# Usage: install.ps1 [-Names <name1>,<name2>] [-Prune]
|
||||||
|
# no args = install all skills/* into target
|
||||||
|
# -Prune = after install, remove target/<name>/ 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.
|
# PowerShell port of install.sh — same behavior, native cmdlets, no bash dependency.
|
||||||
|
|
||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param(
|
param(
|
||||||
[string[]]$Names = @()
|
[string[]]$Names = @(),
|
||||||
|
[switch]$Prune
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
@@ -44,3 +48,14 @@ foreach ($name in $Names) {
|
|||||||
Copy-Item -Recurse $srcDir $dstDir
|
Copy-Item -Recurse $srcDir $dstDir
|
||||||
Write-Host "installed: $name -> $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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,13 +1,25 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Install skills/<name>/ into ~/.claude/skills/<name>/ (or $CLAUDE_SKILLS_DIR)
|
# Install skills/<name>/ into ~/.claude/skills/<name>/ (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/<name>/ dirs that are NOT in skills/*
|
||||||
|
# (prune always scans full target, ignores name filter — global cleanup)
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
SRC="$ROOT/skills"
|
SRC="$ROOT/skills"
|
||||||
TARGET="${CLAUDE_SKILLS_DIR:-$HOME/.claude/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):
|
# Portable across bash 3.2 (stock macOS) and bash 4+ (Linux, git-bash):
|
||||||
# avoid `mapfile` (bash 4+) and `find -printf` (GNU find only).
|
# avoid `mapfile` (bash 4+) and `find -printf` (GNU find only).
|
||||||
names=()
|
names=()
|
||||||
@@ -18,7 +30,7 @@ if [ "$#" -eq 0 ]; then
|
|||||||
IFS=$'\n' names=($(printf '%s\n' "${names[@]}" | sort))
|
IFS=$'\n' names=($(printf '%s\n' "${names[@]}" | sort))
|
||||||
unset IFS
|
unset IFS
|
||||||
else
|
else
|
||||||
names=("$@")
|
names=("${positional[@]}")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mkdir -p "$TARGET"
|
mkdir -p "$TARGET"
|
||||||
@@ -38,3 +50,14 @@ for name in "${names[@]}"; do
|
|||||||
cp -R "$src_dir" "$dst_dir"
|
cp -R "$src_dir" "$dst_dir"
|
||||||
echo "installed: $name → $dst_dir"
|
echo "installed: $name → $dst_dir"
|
||||||
done
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user