Public/Export-PckSolutionBackup.ps1

function Export-PckSolutionBackup {
    <#
    .SYNOPSIS
    Exports a solution to a timestamp-suffixed zip on disk in one call.

    .DESCRIPTION
    POST /ExportSolution, decode the base64 payload, write the file. The result
    is a backup, explicitly not a source of truth (design rule 5.9): the
    repository defines the agent, and an export is what you reach for when
    something went wrong, not what you deploy from.

    Path discipline per design rule 5.7: -Path is used as given when absolute,
    resolved against PCK_WORKSPACE_ROOT when relative, and refused when relative
    with no root set. Nothing depends on the current directory.

    .EXAMPLE
    Export-PckSolutionBackup -SolutionName WorkbenchSupportAssistant -Path C:\backups

    .EXAMPLE
    Export-PckSolutionBackup -SolutionName WorkbenchSupportAssistant -Path backups -Managed -Json
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string] $SolutionName,

        [Parameter(Mandatory)]
        [string] $Path,

        [switch] $Managed,

        [switch] $Json
    )

    # The solution unique name becomes part of a filename; refuse anything that
    # could not be a solution unique name before it can traverse a path.
    if ($SolutionName -notmatch '^[A-Za-z0-9_]+$') {
        throw [PckError]::new(
            "Solution name '$SolutionName' is not a valid solution unique name (letters, digits, underscores).",
            $script:PckExitCode.General)
    }

    $directory = Resolve-PckOutputPath -Path $Path
    if (-not (Test-Path -LiteralPath $directory)) {
        New-Item -ItemType Directory -Path $directory -Force | Out-Null
    }

    $resp = Invoke-PckDataverseRequest -Method Post -Path 'ExportSolution' -Body @{
        SolutionName = $SolutionName
        Managed      = [bool]$Managed
    } -NoSolution

    if (-not ($resp -and $resp.PSObject.Properties['ExportSolutionFile']) -or
        [string]::IsNullOrWhiteSpace([string]$resp.ExportSolutionFile)) {
        throw [PckError]::new(
            "ExportSolution returned no file payload for '$SolutionName'.",
            $script:PckExitCode.General)
    }

    $stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
    $suffix = $Managed ? '-managed' : ''
    $file = Join-Path $directory "$SolutionName-$stamp$suffix.zip"
    [System.IO.File]::WriteAllBytes($file, [Convert]::FromBase64String($resp.ExportSolutionFile))

    $result = [pscustomobject]@{
        SolutionName = $SolutionName
        Managed      = [bool]$Managed
        Path         = $file
        Bytes        = (Get-Item -LiteralPath $file).Length
        Timestamp    = $stamp
    }
    if ($Json) { $result | ConvertTo-Json -Depth 4 } else { $result }
}