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.
95 lines
3.7 KiB
PowerShell
95 lines
3.7 KiB
PowerShell
# 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)
|
|
# $env:CLAUDE_SKILLS_DIR overrides ONLY the claude target (for testing/CI).
|
|
#
|
|
# Usage: install.ps1 [-Names <name1>,<name2>] [-Prune] [-Yes]
|
|
# 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 -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.
|
|
|
|
[CmdletBinding()]
|
|
param(
|
|
[string[]]$Names = @(),
|
|
[switch]$Prune,
|
|
[switch]$Yes
|
|
)
|
|
|
|
$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.
|
|
$targets = @()
|
|
if ($env:CLAUDE_SKILLS_DIR) {
|
|
$targets += $env:CLAUDE_SKILLS_DIR
|
|
} else {
|
|
$targets += (Join-Path $env:USERPROFILE '.claude\skills')
|
|
}
|
|
$targets += (Join-Path $env:USERPROFILE '.agents\skills')
|
|
|
|
foreach ($target in $targets) {
|
|
if (-not (Test-Path $target)) {
|
|
New-Item -ItemType Directory -Force -Path $target | Out-Null
|
|
}
|
|
}
|
|
|
|
if ($Names.Count -eq 0) {
|
|
$Names = Get-ChildItem -Path $src -Directory | Sort-Object Name | Select-Object -ExpandProperty Name
|
|
}
|
|
|
|
foreach ($target in $targets) {
|
|
foreach ($name in $Names) {
|
|
$srcDir = Join-Path $src $name
|
|
$dstDir = Join-Path $target $name
|
|
if (-not (Test-Path $srcDir -PathType Container)) {
|
|
Write-Warning "skip: $name (not found in skills/)"
|
|
continue
|
|
}
|
|
if (-not (Test-Path (Join-Path $srcDir 'SKILL.md'))) {
|
|
Write-Warning "skip: $name (missing SKILL.md)"
|
|
continue
|
|
}
|
|
if (Test-Path $dstDir) {
|
|
Remove-Item -Recurse -Force $dstDir
|
|
}
|
|
Copy-Item -Recurse $srcDir $dstDir
|
|
Write-Host "installed: $name -> $dstDir"
|
|
}
|
|
}
|
|
|
|
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) {
|
|
$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)"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|