scripts/internal/refocus-deploy-integration.ps1

# Feature 171 T017 (FR-014/FR-018): refocus deploy integration for init/update.
# Dot-sourced by scripts/specrew-update.ps1 and scripts/specrew-init.ps1.
#
# Two responsibilities:
# 1. Catalog managed-with-overlay merge: the speckit-extension deploy mirrors
# refocus-scopes.json WHOLESALE, which would clobber user keys. Capture the
# user-owned keys BEFORE the deploy (per-trigger `enabled` flags + provider
# rows whose id is not canonical) and re-apply them AFTER. Fail SAFE: an
# unreadable pre-existing catalog aborts the overlay (capture returns the
# abort marker) and the re-apply leaves the freshly-deployed canonical file
# untouched rather than guessing.
# 2. Hook deployment wiring: invoke deploy-refocus-hooks.ps1 per host — claude
# when the project carries .claude/, codex/copilot/cursor when the host
# binary is on PATH. The deploy script itself respects recorded opt-outs
# (no silent re-enable; the update-never-flips-disables principle).

function Get-RefocusCatalogOverlay {
    param([Parameter(Mandatory = $true)][string]$ProjectPath)
    $catalogPath = Join-Path $ProjectPath '.specify/extensions/specrew-speckit/refocus-scopes.json'
    if (-not (Test-Path -LiteralPath $catalogPath -PathType Leaf)) {
        return [pscustomobject]@{ Present = $false; Aborted = $false; TriggerEnabled = @{}; UserProviders = @() }
    }
    try {
        $catalog = Get-Content -LiteralPath $catalogPath -Raw -Encoding UTF8 | ConvertFrom-Json
    }
    catch {
        # Fail SAFE: unreadable catalog -> no overlay (the deploy will restore a
        # clean canonical file; we never merge into something we cannot parse).
        return [pscustomobject]@{ Present = $true; Aborted = $true; TriggerEnabled = @{}; UserProviders = @() }
    }
    $triggerEnabled = @{}
    if ($catalog.PSObject.Properties['triggers'] -and $null -ne $catalog.triggers) {
        foreach ($prop in $catalog.triggers.PSObject.Properties) {
            if ($prop.Value.PSObject.Properties['enabled']) {
                $triggerEnabled[$prop.Name] = [bool]$prop.Value.enabled
            }
        }
    }
    $userProviders = @()
    if ($catalog.PSObject.Properties['providers'] -and $null -ne $catalog.providers) {
        $userProviders = @($catalog.providers | Where-Object { [string]$_.id -ne 'refocus' })
    }
    return [pscustomobject]@{ Present = $true; Aborted = $false; TriggerEnabled = $triggerEnabled; UserProviders = $userProviders }
}

function Set-RefocusCatalogOverlay {
    param(
        [Parameter(Mandatory = $true)][string]$ProjectPath,
        [Parameter(Mandatory = $true)]$Overlay
    )
    if (-not $Overlay.Present -or $Overlay.Aborted) { return $false }
    if (($Overlay.TriggerEnabled.Count -eq 0) -and (@($Overlay.UserProviders).Count -eq 0)) { return $false }
    $catalogPath = Join-Path $ProjectPath '.specify/extensions/specrew-speckit/refocus-scopes.json'
    if (-not (Test-Path -LiteralPath $catalogPath -PathType Leaf)) { return $false }
    try {
        $catalog = Get-Content -LiteralPath $catalogPath -Raw -Encoding UTF8 | ConvertFrom-Json
    }
    catch { return $false }   # fail safe: never merge into an unparsable file

    $changed = $false
    if ($catalog.PSObject.Properties['triggers'] -and $null -ne $catalog.triggers) {
        foreach ($key in $Overlay.TriggerEnabled.Keys) {
            $trigger = $catalog.triggers.PSObject.Properties[$key]
            if ($null -ne $trigger -and $trigger.Value.PSObject.Properties['enabled'] -and ([bool]$trigger.Value.enabled) -ne $Overlay.TriggerEnabled[$key]) {
                $trigger.Value.PSObject.Properties['enabled'].Value = $Overlay.TriggerEnabled[$key]
                $changed = $true
            }
        }
    }
    if (@($Overlay.UserProviders).Count -gt 0 -and $catalog.PSObject.Properties['providers']) {
        # Dup-ID guard (PR #2152 review): if a newer canonical catalog now ships a
        # provider whose id matches a captured user row, do NOT re-append it — that would
        # duplicate the id and run the provider twice. Only restore user rows whose id is
        # absent from the freshly-deployed canonical set.
        $canonical = @($catalog.providers)
        $canonicalIds = @($canonical | ForEach-Object { [string]$_.id })
        $newUserProviders = @($Overlay.UserProviders | Where-Object { $canonicalIds -notcontains [string]$_.id })
        if (@($newUserProviders).Count -gt 0) {
            $catalog.PSObject.Properties['providers'].Value = @($canonical + $newUserProviders)
            $changed = $true
        }
    }
    if ($changed) {
        [System.IO.File]::WriteAllText($catalogPath, ($catalog | ConvertTo-Json -Depth 16), [System.Text.UTF8Encoding]::new($false))
    }
    return $changed
}

function Invoke-RefocusHookDeployment {
    param(
        [Parameter(Mandatory = $true)][string]$ProjectPath,
        [Parameter(Mandatory = $true)][string]$DeployScriptPath,
        # Hermetic-test seam (PR #2152 review): deploy-refocus-hooks.ps1 already supports
        # -UserHomeOverride; expose it here so e2e/consumer test runners can keep codex/
        # copilot/cursor writes out of the REAL user home. Unset in production (no behavior
        # change — the deploy script defaults to the real home).
        [string]$UserHomeOverride
    )
    $actions = New-Object System.Collections.Generic.List[object]
    if (-not (Test-Path -LiteralPath $DeployScriptPath -PathType Leaf)) { return $actions.ToArray() }

    $hostTargets = New-Object System.Collections.Generic.List[string]
    if (Test-Path -LiteralPath (Join-Path $ProjectPath '.claude') -PathType Container) { $hostTargets.Add('claude') | Out-Null }
    foreach ($pair in @(@('codex', 'codex'), @('copilot', 'copilot'), @('cursor', 'cursor-agent'))) {
        if ($null -ne (Get-Command $pair[1] -ErrorAction SilentlyContinue)) { $hostTargets.Add($pair[0]) | Out-Null }
    }

    foreach ($hostKind in $hostTargets) {
        try {
            $deployArgs = @{ ProjectPath = $ProjectPath; HostKind = $hostKind }
            if (-not [string]::IsNullOrWhiteSpace($UserHomeOverride)) { $deployArgs['UserHomeOverride'] = $UserHomeOverride }
            $output = @(& $DeployScriptPath @deployArgs 2>&1 | ForEach-Object { [string]$_ })
            $actions.Add([pscustomobject]@{ HostKind = $hostKind; Action = 'refocus-hooks'; Detail = ($output -join ' ') }) | Out-Null
        }
        catch {
            # Fail open: hook deployment problems never fail init/update.
            $actions.Add([pscustomobject]@{ HostKind = $hostKind; Action = 'refocus-hooks-failed'; Detail = $_.Exception.Message }) | Out-Null
        }
    }
    return $actions.ToArray()
}