private/95-skills.ps1

# Install the UI4ME high-level PowerShell coding-agent skill without Python.
# Never overwrite modified/unowned skills without explicit -Force.

function Get-UiSkillHashes {
    param([Parameter(Mandatory)][string]$Path)
    $root = [IO.Path]::GetFullPath($Path).TrimEnd([char]'\', [char]'/')
    $result = @{}
    foreach ($entry in @(Get-ChildItem -LiteralPath $root -Recurse -Force)) {
        if (([int]$entry.Attributes -band [int][IO.FileAttributes]::ReparsePoint) -ne 0) {
            throw "Refusing symlink/reparse point in UI4ME skill: $($entry.FullName)"
        }
        if ($entry.PSIsContainer -or $entry.Name -eq '.ui4me-ui-managed.json') { continue }
        $key = $entry.FullName.Substring($root.Length + 1).Replace('\', '/')
        $result[$key] = (Get-FileHash -LiteralPath $entry.FullName -Algorithm SHA256).Hash.ToLowerInvariant()
    }
    if (-not $result.ContainsKey('SKILL.md')) { throw 'UI4ME skill is missing SKILL.md' }
    return $result
}

function Get-UiSkillState {
    param([Parameter(Mandatory)][string]$Path)
    if (-not (Test-Path -LiteralPath $Path)) { return 'missing' }
    $item = Get-Item -LiteralPath $Path -Force
    if (-not $item.PSIsContainer -or (([int]$item.Attributes -band [int][IO.FileAttributes]::ReparsePoint) -ne 0)) {
        return 'unmanaged'
    }
    $marker = Join-Path $Path '.ui4me-ui-managed.json'
    if (-not (Test-Path -LiteralPath $marker -PathType Leaf)) { return 'unmanaged' }
    try {
        $entry = Get-Item -LiteralPath $marker -Force  # -Force: dot-files are hidden on Linux/macOS
        if (([int]$entry.Attributes -band [int][IO.FileAttributes]::ReparsePoint) -ne 0) { return 'unmanaged' }
        $data = Get-Content -LiteralPath $marker -Raw -Encoding UTF8 | ConvertFrom-Json -ErrorAction Stop
        if ($data.name -ne 'ui4me-ui-powershell') { return 'unmanaged' }
        $files = Get-UiSkillHashes $Path
        $expected = @($data.files.PSObject.Properties)
        if ($expected.Count -ne $files.Count) { return 'modified' }
        foreach ($member in $expected) {
            if (-not $files.ContainsKey($member.Name) -or $files[$member.Name] -ne [string]$member.Value) { return 'modified' }
        }
        return 'installed'
    } catch { return 'unmanaged' }
}

function Install-UI4MEUiSkill {
    <#
    .SYNOPSIS
    Install, inspect, update or remove the UI4ME high-level PowerShell agent skill.
    .EXAMPLE
    Install-UI4MEUiSkill -Agents all -Scope user
    .EXAMPLE
    Install-UI4MEUiSkill -Action status -Agents claude,codex -Scope project -ProjectPath .
    #>

    [CmdletBinding()]
    param([ValidateSet('install', 'status', 'uninstall')][string]$Action = 'install',
          [string[]]$Agents = @('all'), [ValidateSet('user', 'project')][string]$Scope = 'user',
          [string]$ProjectPath, [switch]$Force, [switch]$DryRun)
    if ($Scope -eq 'project' -and -not $ProjectPath) { throw '-ProjectPath is required with -Scope project' }
    if ($Scope -eq 'user' -and $ProjectPath) { throw '-ProjectPath only applies to -Scope project' }
    $names = @('claude','codex','opencode','copilot','cursor','gemini')
    $choices = @($Agents | ForEach-Object { $_.Split(',') } | ForEach-Object { $_.Trim().ToLowerInvariant() } |
        Where-Object { $_ } | Select-Object -Unique)
    $targets = if ($choices -contains 'all') { $names } else { $choices }
    if (-not $targets) { throw 'Select at least one coding agent' }
    foreach ($name in $targets) { if ($name -notin $names) { throw "Unknown agent '$name'" } }
    $mapping = @{ claude = '.claude/skills'; codex = '.agents/skills'; opencode = '.config/opencode/skills'
        copilot = '.copilot/skills'; cursor = '.cursor/skills'; gemini = '.gemini/skills' }
    if ($Scope -eq 'project') {
        $base = (Resolve-Path -LiteralPath $ProjectPath -ErrorAction Stop).Path
        $mapping.opencode = '.opencode/skills'; $mapping.copilot = '.github/skills'
    } else {
        $base = [Environment]::GetFolderPath('UserProfile')
        if (-not $base) { $base = $HOME }
    }
    $source = Join-Path $script:ModuleRoot 'skills/ui4me-ui-powershell'
    if ($Action -eq 'install' -and -not $DryRun -and -not (Test-Path -LiteralPath (Join-Path $source 'SKILL.md') -PathType Leaf)) {
        throw 'UI4ME PowerShell skill missing from the module. Reinstall UI4ME.'
    }
    foreach ($agent in $targets) {
        $target = [IO.Path]::GetFullPath((Join-Path (Join-Path $base $mapping[$agent]) 'ui4me-ui-powershell'))
        $parent = Split-Path -Parent $target
        $ancestor = $target
        while ($ancestor) {
            if (Test-Path -LiteralPath $ancestor) {
                $node = Get-Item -LiteralPath $ancestor -Force
                if (([int]$node.Attributes -band [int][IO.FileAttributes]::ReparsePoint) -ne 0) {
                    throw "Refusing linked UI4ME skill destination: $ancestor"
                }
            }
            $next = Split-Path -Parent $ancestor
            if (-not $next -or $next -eq $ancestor) { break }
            $ancestor = $next
        }
        $state = Get-UiSkillState $target
        if ($Action -eq 'status' -or $DryRun) {
            [pscustomobject]@{ Agent = $agent; Scope = $Scope; State = $state; Path = $target
                Action = $(if ($DryRun) { "would-$Action" } else { 'status' }) }
            continue
        }
        if ($Action -eq 'uninstall') {
            if ($state -eq 'missing') { continue }
            if ($state -ne 'installed' -and -not $Force) { throw "Refusing to remove $state skill at $target; use -Force" }
            Remove-Item -LiteralPath $target -Recurse -Force
            [pscustomobject]@{ Agent = $agent; Scope = $Scope; Path = $target; Action = 'removed' }
            continue
        }
        if ($state -in @('modified', 'unmanaged') -and -not $Force) {
            throw "Refusing to replace $state skill at $target; use -Force"
        }
        [void](New-Item -ItemType Directory -Path $parent -Force)
        $stage = Join-Path $parent ('.ui4me-ui-new-' + [guid]::NewGuid().ToString('N'))
        $backup = Join-Path $parent ('.ui4me-ui-old-' + [guid]::NewGuid().ToString('N'))
        try {
            Copy-Item -LiteralPath $source -Destination $stage -Recurse -ErrorAction Stop
            $files = Get-UiSkillHashes $stage
            $record = @{ name = 'ui4me-ui-powershell'; files = $files; version = '2.1.0' } | ConvertTo-Json -Depth 8
            [IO.File]::WriteAllText((Join-Path $stage '.ui4me-ui-managed.json'), $record,
                (New-Object Text.UTF8Encoding($false)))
            if ($state -ne 'missing') { Move-Item -LiteralPath $target -Destination $backup -ErrorAction Stop }
            try { Move-Item -LiteralPath $stage -Destination $target -ErrorAction Stop }
            catch { if (Test-Path -LiteralPath $backup) { Move-Item -LiteralPath $backup -Destination $target }; throw }
            if (Test-Path -LiteralPath $backup) { Remove-Item -LiteralPath $backup -Recurse -Force }
            [pscustomobject]@{ Agent = $agent; Scope = $Scope; Path = $target; Action = 'installed' }
        } finally {
            if (Test-Path -LiteralPath $stage) { Remove-Item -LiteralPath $stage -Recurse -Force }
        }
    }
}