185 lines
5.5 KiB
PowerShell
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
|
|
}
|