# Install skills// into BOTH agent skill dirs: # ~\.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] [-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) # -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)" } } } } }