extensions/specrew-speckit/scripts/deploy-speckit-extension.ps1

[CmdletBinding()]
param(
    [Parameter(Mandatory = $true)]
    [string]$ProjectPath,

    [switch]$DryRun,
    [switch]$RefreshExisting,
    [switch]$PassThru
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

$sharedGovernancePath = Join-Path $PSScriptRoot 'shared-governance.ps1'
if (-not (Test-Path -LiteralPath $sharedGovernancePath -PathType Leaf)) {
    throw "Missing shared governance helper '$sharedGovernancePath'."
}
. $sharedGovernancePath

function Add-DeploymentAction {
    param(
        [AllowEmptyCollection()]
        [Parameter(Mandatory = $true)]
        [System.Collections.ArrayList]$Actions,

        [Parameter(Mandatory = $true)]
        [string]$Action,

        [Parameter(Mandatory = $true)]
        [string]$Path
    )

    $null = $Actions.Add([pscustomobject]@{
            Action = $Action
            Path   = $Path
        })
}

function Ensure-Directory {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Path,

        [AllowEmptyCollection()]
        [Parameter(Mandatory = $true)]
        [System.Collections.ArrayList]$Actions
    )

    if (Test-Path -LiteralPath $Path) {
        Add-DeploymentAction -Actions $Actions -Action 'preserved-directory' -Path $Path
        return
    }

    Add-DeploymentAction -Actions $Actions -Action 'created-directory' -Path $Path
    if (-not $DryRun) {
        New-Item -ItemType Directory -Path $Path -Force | Out-Null
    }
}

function Copy-MissingItem {
    param(
        [Parameter(Mandatory = $true)]
        [string]$SourcePath,

        [Parameter(Mandatory = $true)]
        [string]$TargetPath,

        [AllowEmptyCollection()]
        [Parameter(Mandatory = $true)]
        [System.Collections.ArrayList]$Actions
    )

    # -Force needed so Get-Item finds hidden source items (e.g., .gitkeep)
    # on Linux PowerShell. The recursive Get-ChildItem below uses -Force to
    # enumerate hidden files; without -Force here, Get-Item then fails to
    # re-open those exact children, breaking `specrew update` on Linux.
    $sourceItem = Get-Item -LiteralPath $SourcePath -Force
    if ($sourceItem.PSIsContainer) {
        Ensure-Directory -Path $TargetPath -Actions $Actions
        $children = @(Get-ChildItem -LiteralPath $SourcePath -Force)
        foreach ($child in $children) {
            Copy-MissingItem -SourcePath $child.FullName -TargetPath (Join-Path $TargetPath $child.Name) -Actions $Actions
        }

        return
    }

    if (Test-Path -LiteralPath $TargetPath) {
        if (-not $RefreshExisting) {
            Add-DeploymentAction -Actions $Actions -Action 'preserved' -Path $TargetPath
            return
        }

        $sourceContent = Get-Content -LiteralPath $SourcePath -Raw
        $targetContent = Get-Content -LiteralPath $TargetPath -Raw
        if ($sourceContent -eq $targetContent) {
            Add-DeploymentAction -Actions $Actions -Action 'preserved' -Path $TargetPath
            return
        }

        Add-DeploymentAction -Actions $Actions -Action $(if ($DryRun) { 'would-update' } else { 'updated' }) -Path $TargetPath
        if (-not $DryRun) {
            $parent = Split-Path -Parent $TargetPath
            if (-not (Test-Path -LiteralPath $parent)) {
                New-Item -ItemType Directory -Path $parent -Force | Out-Null
            }

            Copy-Item -LiteralPath $SourcePath -Destination $TargetPath -Force
        }
        return
    }

    Add-DeploymentAction -Actions $Actions -Action 'created' -Path $TargetPath
    if (-not $DryRun) {
        $parent = Split-Path -Parent $TargetPath
        if (-not (Test-Path -LiteralPath $parent)) {
            New-Item -ItemType Directory -Path $parent -Force | Out-Null
        }

        Copy-Item -LiteralPath $SourcePath -Destination $TargetPath -Force
    }
}

function Get-ExtensionVersion {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ManifestPath
    )

    $manifestContent = Get-Content -LiteralPath $ManifestPath -Raw
    $versionMatch = [regex]::Match($manifestContent, '(?m)^\s*version:\s*"?(?<version>[^"\r\n]+)')
    if (-not $versionMatch.Success) {
        throw "Could not determine Specrew extension version from '$ManifestPath'."
    }

    return $versionMatch.Groups['version'].Value.Trim()
}

function Ensure-ExtensionRegistration {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ManifestPath,

        [Parameter(Mandatory = $true)]
        [string]$ExtensionName,

        [Parameter(Mandatory = $true)]
        [string]$ExtensionVersion,

        [AllowEmptyCollection()]
        [Parameter(Mandatory = $true)]
        [System.Collections.ArrayList]$Actions
    )

    $entryLines = @(
        (' - name: {0}' -f $ExtensionName),
        (' version: {0}' -f $ExtensionVersion),
        ' enabled: true',
        ' source: local',
        ' path: .specify/extensions/specrew-speckit'
    )

    if (-not (Test-Path -LiteralPath $ManifestPath)) {
        Add-DeploymentAction -Actions $Actions -Action 'created' -Path $ManifestPath
        if (-not $DryRun) {
            $newContent = @(
                'installed:'
                $entryLines
                'settings:'
                ' auto_execute_hooks: true'
                'hooks: {}'
                ''
            ) -join [Environment]::NewLine
            [System.IO.File]::WriteAllText($ManifestPath, $newContent, [System.Text.UTF8Encoding]::new($false))
        }

        return
    }

    $lines = [System.Collections.Generic.List[string]]::new()
    $lines.AddRange([string[]](Get-Content -LiteralPath $ManifestPath))

    $existingEntryStart = -1
    for ($index = 0; $index -lt $lines.Count; $index++) {
        if ($lines[$index] -match '^\s*-\s*name:\s*"?specrew-speckit"?\s*$') {
            $existingEntryStart = $index
            break
        }
    }

    if ($existingEntryStart -ge 0) {
        $existingEntryEnd = $existingEntryStart + 1
        while ($existingEntryEnd -lt $lines.Count) {
            $currentLine = $lines[$existingEntryEnd]
            if (-not [string]::IsNullOrWhiteSpace($currentLine) -and $currentLine -notmatch '^\s{4,}') {
                break
            }

            $existingEntryEnd++
        }

        $existingEntryLines = @()
        for ($index = $existingEntryStart; $index -lt $existingEntryEnd; $index++) {
            $existingEntryLines += $lines[$index]
        }

        if ($existingEntryLines.Count -eq $entryLines.Count) {
            $matchesDesiredEntry = $true
            for ($index = 0; $index -lt $entryLines.Count; $index++) {
                if ($existingEntryLines[$index] -ne $entryLines[$index]) {
                    $matchesDesiredEntry = $false
                    break
                }
            }

            if ($matchesDesiredEntry) {
                Add-DeploymentAction -Actions $Actions -Action 'preserved-registration' -Path $ManifestPath
                return
            }
        }

        for ($index = $existingEntryEnd - 1; $index -ge $existingEntryStart; $index--) {
            $lines.RemoveAt($index)
        }

        for ($offset = 0; $offset -lt $entryLines.Count; $offset++) {
            $lines.Insert($existingEntryStart + $offset, $entryLines[$offset])
        }

        Add-DeploymentAction -Actions $Actions -Action 'updated-registration' -Path $ManifestPath
        if (-not $DryRun) {
            $content = ($lines -join [Environment]::NewLine)
            if (-not $content.EndsWith([Environment]::NewLine)) {
                $content += [Environment]::NewLine
            }

            [System.IO.File]::WriteAllText($ManifestPath, $content, [System.Text.UTF8Encoding]::new($false))
        }

        return
    }

    $installedIndex = -1
    for ($index = 0; $index -lt $lines.Count; $index++) {
        if ($lines[$index] -match '^\s*installed:\s*(\[\s*\])?\s*$') {
            $installedIndex = $index
            break
        }
    }

    if ($installedIndex -lt 0) {
        $newLines = [System.Collections.Generic.List[string]]::new()
        $newLines.Add('installed:')
        foreach ($entryLine in $entryLines) {
            $newLines.Add($entryLine)
        }

        if ($lines.Count -gt 0 -and -not [string]::IsNullOrWhiteSpace($lines[0])) {
            $newLines.Add('')
        }

        $newLines.AddRange($lines)
        $lines = $newLines
    }
    else {
        if ($lines[$installedIndex] -match '^\s*installed:\s*\[\s*\]\s*$') {
            $lines[$installedIndex] = 'installed:'
            $insertIndex = $installedIndex + 1
        }
        else {
            $insertIndex = $installedIndex + 1
            while ($insertIndex -lt $lines.Count -and ($lines[$insertIndex] -match '^\s+' -or [string]::IsNullOrWhiteSpace($lines[$insertIndex]))) {
                $insertIndex++
            }
        }

        for ($offset = 0; $offset -lt $entryLines.Count; $offset++) {
            $lines.Insert($insertIndex + $offset, $entryLines[$offset])
        }
    }

    Add-DeploymentAction -Actions $Actions -Action 'updated-registration' -Path $ManifestPath
    if (-not $DryRun) {
        $content = ($lines -join [Environment]::NewLine)
        if (-not $content.EndsWith([Environment]::NewLine)) {
            $content += [Environment]::NewLine
        }

        [System.IO.File]::WriteAllText($ManifestPath, $content, [System.Text.UTF8Encoding]::new($false))
    }
}

$resolvedProjectPath = Resolve-ProjectPath -Path $ProjectPath
$extensionRoot = Split-Path -Parent $PSScriptRoot
$targetSpecifyRoot = Join-Path $resolvedProjectPath '.specify'
$targetExtensionRoot = Join-Path $targetSpecifyRoot 'extensions\specrew-speckit'
$targetExtensionsManifest = Join-Path $targetSpecifyRoot 'extensions.yml'
$actions = [System.Collections.ArrayList]::new()

if (-not (Test-Path -LiteralPath $targetSpecifyRoot) -and -not $DryRun) {
    throw "Spec Kit must be initialized before deploying the Specrew extension. Missing '$targetSpecifyRoot'."
}

$extensionVersion = Get-ExtensionVersion -ManifestPath (Join-Path $extensionRoot 'extension.yml')

if ($DryRun -and -not (Test-Path -LiteralPath $targetSpecifyRoot)) {
    Add-DeploymentAction -Actions $actions -Action 'would-create-directory' -Path $targetSpecifyRoot
}

Ensure-Directory -Path (Join-Path $targetSpecifyRoot 'extensions') -Actions $actions
Ensure-Directory -Path $targetExtensionRoot -Actions $actions

$itemsToCopy = @('commands', 'extension.yml', 'README.md', 'hooks', 'scripts', 'templates', 'squad-templates')
foreach ($item in $itemsToCopy) {
    Copy-MissingItem -SourcePath (Join-Path $extensionRoot $item) -TargetPath (Join-Path $targetExtensionRoot $item) -Actions $actions
}

Ensure-ExtensionRegistration -ManifestPath $targetExtensionsManifest -ExtensionName 'specrew-speckit' -ExtensionVersion $extensionVersion -Actions $actions

if ($PassThru) {
    foreach ($action in $actions) {
        $action
    }
    return
}

$actions | Select-Object Action, Path | Format-Table -AutoSize
Write-Host ("Spec Kit extension deployment {0} for {1}" -f ($(if ($DryRun) { 'previewed' } else { 'completed' }), $resolvedProjectPath)) -ForegroundColor Green
exit 0