skills-vault/scripts/install-skill.ps1

185 lines
5.5 KiB
PowerShell

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
}