param( [string]$Skill, [switch]$All, [switch]$Force, [switch]$InstallClaudeLink, [string]$InstallRoot = "$HOME\.agents\skills", [string]$ClaudeSkillsRoot = "$HOME\.claude\skills" ) $ErrorActionPreference = "Stop" function Show-Usage { Write-Host "Usage:" Write-Host " powershell -ExecutionPolicy Bypass -File .\scripts\install-skill.ps1 -Skill fix-title" Write-Host " powershell -ExecutionPolicy Bypass -File .\scripts\install-skill.ps1 -All" Write-Host "" Write-Host "Options:" Write-Host " -Force Back up and replace existing installed Skill" Write-Host " -InstallClaudeLink Create/update .claude\skills link to .agents\skills" } if (($Skill -and $All) -or (-not $Skill -and -not $All)) { Show-Usage exit 2 } $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path $RepoRoot = Resolve-Path (Join-Path $ScriptDir "..") $SkillsRoot = Join-Path $RepoRoot "skills" $RegistryPath = Join-Path $RepoRoot "registry\skills-index.md" function Get-RegistryMeta { param([string]$Name) if (-not (Test-Path -LiteralPath $RegistryPath)) { return $null } $escaped = [regex]::Escape("`$Name") $line = Get-Content -LiteralPath $RegistryPath | Where-Object { $_ -match "^\|\s*``$([regex]::Escape($Name))``\s*\|" } | Select-Object -First 1 if (-not $line) { return $null } $parts = $line.Trim() -split "\|" $cells = @() foreach ($part in $parts) { $cell = $part.Trim() if ($cell.Length -gt 0) { $cells += $cell } } if ($cells.Count -lt 7) { return $null } return [PSCustomObject]@{ Skill = $cells[0].Trim("``") Purpose = $cells[1] Source = $cells[2] Status = $cells[3].Trim("``") InstallPolicy = $cells[4].Trim("``") InstalledPath = $cells[5] CcpeRegistration = $cells[6] } } function Get-AllDefaultSkills { $allowedStatuses = @("tested", "installable", "installed") $skills = @() Get-ChildItem -LiteralPath $SkillsRoot -Directory | ForEach-Object { $skillName = $_.Name $skillFile = Join-Path $_.FullName "SKILL.md" if (-not (Test-Path -LiteralPath $skillFile)) { return } $meta = Get-RegistryMeta -Name $skillName if (-not $meta) { Write-Host "Skipping ${skillName}: no registry row" return } if ($allowedStatuses -notcontains $meta.Status) { Write-Host "Skipping ${skillName}: status is $($meta.Status)" return } if ($meta.InstallPolicy -ne "default") { Write-Host "Skipping ${skillName}: install policy is $($meta.InstallPolicy)" return } $skills += $skillName } return $skills } function Copy-SkillDirectory { param( [string]$Name, [string]$Source, [string]$Target ) New-Item -ItemType Directory -Force -Path (Split-Path -Parent $Target) | Out-Null New-Item -ItemType Directory -Force -Path $Target | Out-Null Get-ChildItem -Force -LiteralPath $Source | Where-Object { $_.Name -notin @("manual-test", "__pycache__", ".pytest_cache") } | ForEach-Object { Copy-Item -LiteralPath $_.FullName -Destination $Target -Recurse -Force } } function Backup-ExistingPath { param([string]$Path) $stamp = Get-Date -Format "yyyyMMddHHmmss" $backup = "$Path.backup-$stamp" Move-Item -LiteralPath $Path -Destination $backup return $backup } function Install-OneSkill { param([string]$Name) if ($Name -notmatch "^[a-z0-9][a-z0-9-]*$") { throw "Invalid Skill name: $Name" } $source = Join-Path $SkillsRoot $Name $skillFile = Join-Path $source "SKILL.md" if (-not (Test-Path -LiteralPath $skillFile)) { throw "Skill source not found: $skillFile" } $meta = Get-RegistryMeta -Name $Name if ($meta -and $meta.InstallPolicy -eq "disabled" -and -not $Force) { throw "Skill $Name has install policy disabled. Use -Force only if you intentionally want to install it." } $target = Join-Path $InstallRoot $Name if (Test-Path -LiteralPath $target) { if (-not $Force) { throw "Installed Skill already exists: $target. Re-run with -Force to back it up and replace it." } $backup = Backup-ExistingPath -Path $target Write-Host "Backed up existing Skill to $backup" } Copy-SkillDirectory -Name $Name -Source $source -Target $target Write-Host "Installed $Name to $target" if ($InstallClaudeLink) { New-Item -ItemType Directory -Force -Path $ClaudeSkillsRoot | Out-Null $claudeTarget = Join-Path $ClaudeSkillsRoot $Name if (Test-Path -LiteralPath $claudeTarget) { if (-not $Force) { throw "Claude Skill link/path already exists: $claudeTarget. Re-run with -Force to back it up and replace it." } $backup = Backup-ExistingPath -Path $claudeTarget Write-Host "Backed up existing Claude path to $backup" } New-Item -ItemType Junction -Path $claudeTarget -Target $target | Out-Null Write-Host "Linked Claude Skill $claudeTarget -> $target" } } if ($All) { $skillNames = Get-AllDefaultSkills if ($skillNames.Count -eq 0) { Write-Host "No default installable Skills found." exit 0 } foreach ($name in $skillNames) { Install-OneSkill -Name $name } } else { Install-OneSkill -Name $Skill }