src/PowerShell/ExecOutput.ps1

# cdp PowerShell domain: ExecOutput.ps1
# Loaded by src/cdp.psm1; do not dot-source peer files.

function ConvertTo-CdpExecResultDocument {
    param([Parameter(Mandatory = $true)][object]$Item)

    [PSCustomObject][ordered]@{
        name = [string]$Item.Name
        rawPath = [string]$Item.RawPath
        resolvedPath = [string]$Item.ResolvedPath
        status = [string]$Item.Status
        exitCode = if ($null -eq $Item.ExitCode) { $null } else { [int]$Item.ExitCode }
        elapsedMs = [int]$Item.ElapsedMs
        stdout = [string]$Item.Stdout
        stderr = [string]$Item.Stderr
        error = if ([string]::IsNullOrWhiteSpace([string]$Item.Error)) { $null } else { [string]$Item.Error }
    }
}

function Test-CdpExecUnavailableStatus {
    param([Parameter(Mandatory = $true)][string]$Status)

    $Status -in @(
        'missing_project', 'ambiguous_project', 'disabled_project',
        'path_profile_invalid', 'path_missing'
    )
}

function Get-CdpExecExitCode {
    param([Parameter(Mandatory = $true)][AllowEmptyCollection()][object[]]$Results, [switch]$FailFast)

    if ($FailFast -and @($Results | Where-Object Status -eq 'canceled').Count -gt 0) { return 2 }
    $failed = @($Results | Where-Object {
        $_.Status -in @('failed', 'timed_out', 'canceled') -or
        (Test-CdpExecUnavailableStatus -Status ([string]$_.Status))
    })
    if ($failed.Count -gt 0) { return 1 }
    0
}

function New-CdpExecSummary {
    param([Parameter(Mandatory = $true)][AllowEmptyCollection()][object[]]$Results, [switch]$FailFast)

    [PSCustomObject][ordered]@{
        total = $Results.Count
        planned = @($Results | Where-Object Status -eq 'planned').Count
        succeeded = @($Results | Where-Object Status -eq 'succeeded').Count
        failed = @($Results | Where-Object Status -eq 'failed').Count
        timedOut = @($Results | Where-Object Status -eq 'timed_out').Count
        canceled = @($Results | Where-Object Status -eq 'canceled').Count
        unavailable = @($Results | Where-Object { Test-CdpExecUnavailableStatus -Status ([string]$_.Status) }).Count
        exitCode = Get-CdpExecExitCode -Results $Results -FailFast:$FailFast
    }
}

function New-CdpExecDocument {
    param([Parameter(Mandatory = $true)][object]$Plan, [int]$DurationMs)

    $results = @($Plan.Items | ForEach-Object { ConvertTo-CdpExecResultDocument -Item $_ })
    [PSCustomObject][ordered]@{
        schemaVersion = 1
        generatedAt = [DateTime]::UtcNow.ToString('yyyy-MM-ddTHH:mm:ss.fffZ', [Globalization.CultureInfo]::InvariantCulture)
        durationMs = $DurationMs
        selector = $Plan.Selector
        command = [PSCustomObject]@{ executable = [string]$Plan.Command; arguments = @($Plan.Arguments) }
        options = [PSCustomObject]@{
            jobs = [int]$Plan.Jobs
            timeoutSeconds = [int]$Plan.TimeoutSeconds
            failFast = [bool]$Plan.FailFast
            dryRun = [bool]$Plan.DryRun
        }
        summary = New-CdpExecSummary -Results $results -FailFast:$Plan.FailFast
        results = $results
    }
}

function Write-CdpExecJson {
    param([Parameter(Mandatory = $true)][object]$Document)

    try { $json = $Document | ConvertTo-Json -Depth 8 -ErrorAction Stop }
    catch {
        [Console]::Error.WriteLine('Error: Failed to serialize exec JSON.')
        $global:LASTEXITCODE = 3
        return
    }
    $global:LASTEXITCODE = [int]$Document.summary.exitCode
    Write-Output $json
}

function Write-CdpExecTextBlock {
    param([string]$Label, [AllowEmptyString()][string]$Text, [ConsoleColor]$Color)

    if ([string]::IsNullOrEmpty($Text)) { return }
    Write-Host " ${Label}:" -ForegroundColor $Color
    foreach ($line in @($Text -split "`r?`n")) {
        if ($line.Length -gt 0) { Write-Host " $line" }
    }
}

function Write-CdpExecHuman {
    param([Parameter(Mandatory = $true)][object]$Document)

    Write-Host ''
    Write-Host "cdp exec ($($Document.summary.total) projects)" -ForegroundColor Cyan
    Write-Host ('-' * 88) -ForegroundColor DarkGray
    $index = 1
    foreach ($result in $Document.results) {
        $color = if ($result.status -eq 'succeeded') { 'Green' } elseif ($result.status -eq 'planned') { 'Cyan' } elseif ($result.status -eq 'canceled') { 'Yellow' } else { 'Red' }
        Write-Host ("[{0:00}] {1} {2}" -f $index, $result.name, $result.status) -ForegroundColor $color
        Write-Host " raw: $($result.rawPath)" -ForegroundColor DarkGray
        Write-Host " resolved: $($result.resolvedPath)" -ForegroundColor DarkGray
        if ($null -ne $result.exitCode) { Write-Host " exit: $($result.exitCode) elapsed: $($result.elapsedMs)ms" }
        elseif ($result.elapsedMs -gt 0) { Write-Host " elapsed: $($result.elapsedMs)ms" }
        Write-CdpExecTextBlock -Label stdout -Text ([string]$result.stdout) -Color Gray
        Write-CdpExecTextBlock -Label stderr -Text ([string]$result.stderr) -Color Yellow
        if ($result.error) { Write-Host " error: $($result.error)" -ForegroundColor Yellow }
        $index++
    }
    Write-Host ('-' * 88) -ForegroundColor DarkGray
    Write-Host ("succeeded={0} failed={1} timed_out={2} canceled={3} unavailable={4}" -f `
        $Document.summary.succeeded, $Document.summary.failed, $Document.summary.timedOut,
        $Document.summary.canceled, $Document.summary.unavailable)
    $global:LASTEXITCODE = [int]$Document.summary.exitCode
}

function Write-CdpExecResult {
    param([Parameter(Mandatory = $true)][object]$Plan, [int]$DurationMs, [switch]$Json)

    $document = New-CdpExecDocument -Plan $Plan -DurationMs $DurationMs
    if ($Json) { Write-CdpExecJson -Document $document } else { Write-CdpExecHuman -Document $document }
}

function Write-CdpExecFatal {
    param([Parameter(Mandatory = $true)][string]$Message, [switch]$Json)

    if ($Json) { [Console]::Error.WriteLine("Error: $Message") }
    else { Write-Host "Error: $Message" -ForegroundColor Red }
    $global:LASTEXITCODE = 3
}