scripts/init/dependency-install.ps1

# Dependency-install + validation helpers for specrew-init.ps1 (extracted via Proposal 108 Slice 5)
#
# Depends on: scripts/init/_utilities.ps1 (Invoke-NativeCommand*, Add-Action,
# Invoke-WithNativeCommandEncoding); scripts/init/spec-kit-deploy.ps1 (Get-SpecKitInstallArguments
# called from Install-MissingDependency on the Spec Kit branch).
#
# Functions:
# - Install-MissingDependency install Spec Kit (uv) or Squad CLI (npm)
# - Invoke-VersionValidation shell out to extensions/specrew-speckit/scripts/validate-versions.ps1
# - Get-DependencyValidationIssue translate validator results to issue list
# - Resolve-DependencyValidationIssue resolve issues + log actions
#
# Squad coupling: Install-MissingDependency hardcodes `npm install -g @bradygaster/squad-cli`
# for the Squad branch. This is the Squad-only deployment path; moves to hosts/copilot/handlers.ps1
# as part of Install-CopilotHostRuntime when Proposal 024 Slice 3 ships. Allow-listed in the
# host-coupling firewall test for now via specrew-init.ps1 wholesale entry.

Set-StrictMode -Version Latest

function Install-MissingDependency {
    param(
        [Parameter(Mandatory = $true)]
        [pscustomobject]$Dependency,

        [Parameter(Mandatory = $true)]
        [switch]$PreviewOnly
    )

    switch ($Dependency.Platform) {
        'Spec Kit' {
            if (-not (Get-Command uv -ErrorAction SilentlyContinue)) {
                throw "Spec Kit is missing and 'uv' is not available to install it."
            }

            $command = 'uv'
            $arguments = Get-SpecKitInstallArguments -Version $Dependency.MinimumVersion -ForceInstall $false
        }
        'Squad' {
            if (-not (Get-Command npm -ErrorAction SilentlyContinue)) {
                throw "Squad is missing and 'npm' is not available to install it."
            }

            $command = 'npm'
            $arguments = @('install', '-g', ('@bradygaster/squad-cli@{0}' -f $Dependency.MinimumVersion))
        }
        default {
            throw "Unsupported dependency platform '$($Dependency.Platform)'."
        }
    }

    if ($PreviewOnly) {
        Write-Host ("[dry-run] {0} {1}" -f $command, ($arguments -join ' ')) -ForegroundColor Yellow
        return
    }

    & $command @arguments
    if ((Get-NativeExitCode) -ne 0) {
        throw ("Failed to install {0}." -f $Dependency.Platform)
    }
}

function Invoke-VersionValidation {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ScriptPath,

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

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

    try {
        return @(& $ScriptPath -MinimumSpecKitVersion $MinimumSpecKitVersion -MinimumSquadVersion $MinimumSquadVersion -PassThru)
    }
    catch {
        Write-Error ("Dependency validation failed unexpectedly. Re-run '{0}' directly for details. {1}" -f $ScriptPath, $_.Exception.Message)
        exit 4
    }
}

function Get-DependencyValidationIssue {
    param(
        [Parameter(Mandatory = $true)]
        [pscustomobject[]]$Results,

        [Parameter(Mandatory = $true)]
        [bool]$IncludeMissing,

        [Parameter(Mandatory = $true)]
        [bool]$AfterInstallAttempt
    )

    $failures = [System.Collections.ArrayList]::new()

    if ($IncludeMissing) {
        foreach ($dependency in @($Results | Where-Object { -not $_.IsInstalled })) {
            $message = if ($AfterInstallAttempt) {
                "{0} is still not installed after the installation attempt. Run '{1}' to install it, then re-run specrew init." -f $dependency.Platform, $dependency.SuggestedInstall
            }
            else {
                "{0} is not installed. Run '{1}'." -f $dependency.Platform, $dependency.SuggestedInstall
            }

            $null = $failures.Add([pscustomobject]@{
                    ExitCode = 4
                    Message  = $message
                    Outcome  = ("{0}: missing ({1})" -f $dependency.Platform, $dependency.SuggestedInstall)
                })
        }
    }

    foreach ($dependency in @($Results | Where-Object { $_.IsInstalled -and -not $_.IsOperational })) {
        $failureDetail = if ($dependency.ProbeError) { $dependency.ProbeError } elseif ($dependency.ValidationError) { $dependency.ValidationError } else { 'the command did not complete successfully' }
        $message = "{0} is installed but the '{1}' command is not healthy ({2}). Run '{3}' to repair it, then re-run specrew init." -f $dependency.Platform, $dependency.CommandName, $failureDetail, $dependency.SuggestedRepair

        $null = $failures.Add([pscustomobject]@{
                ExitCode = 1
                Message  = $message
                Outcome  = ("{0}: requires repair ({1})" -f $dependency.Platform, $dependency.SuggestedRepair)
            })
    }

    foreach ($dependency in @($Results | Where-Object { $_.IsInstalled -and -not $_.IsCompatible })) {
        $message = "Specrew requires {0} >= {1} but found {2}. Run '{3}' to upgrade." -f $dependency.Platform, $dependency.MinimumVersion, $dependency.Version, $dependency.SuggestedUpgrade

        $null = $failures.Add([pscustomobject]@{
                ExitCode = 1
                Message  = $message
                Outcome  = ("{0}: requires upgrade ({1})" -f $dependency.Platform, $dependency.SuggestedUpgrade)
            })
    }

    return @($failures)
}

function Resolve-DependencyValidationIssue {
    param(
        [Parameter(Mandatory = $true)]
        [pscustomobject[]]$Results,

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

        [Parameter(Mandatory = $true)]
        [switch]$PreviewOnly,

        [Parameter(Mandatory = $true)]
        [bool]$IncludeMissing,

        [Parameter(Mandatory = $true)]
        [bool]$AfterInstallAttempt
    )

    $failures = @(Get-DependencyValidationIssue -Results $Results -IncludeMissing $IncludeMissing -AfterInstallAttempt $AfterInstallAttempt)
    if ($failures.Count -eq 0) {
        return 0
    }

    foreach ($failure in $failures) {
        if ($PreviewOnly) {
            Write-Warning ("[dry-run] {0}" -f $failure.Message)
            Add-Action -Actions $Actions -Step 'dependency' -Outcome $failure.Outcome
            continue
        }

        Write-Error $failure.Message -ErrorAction Continue
    }

    if ($failures.ExitCode -contains 4) {
        return 4
    }

    return 1
}