Modules/businessdev.ALbuild.Containers/Public/Invoke-BcContainerCommand.ps1

function Invoke-BcContainerCommand {
    <#
    .SYNOPSIS
        Runs a PowerShell script block inside a running Business Central (Windows) container.
 
    .DESCRIPTION
        Executes the script block inside the container via 'docker exec ... powershell
        -EncodedCommand', returning the captured stdout. Simple variables can be passed with
        -Variables (marshalled as JSON across the process boundary). This is the single seam
        through which higher-level container cmdlets run Business Central management cmdlets.
 
    .PARAMETER ContainerName
        The target container.
 
    .PARAMETER ScriptBlock
        The PowerShell to run inside the container.
 
    .PARAMETER Variables
        Optional hashtable of simple values made available as variables inside the container.
 
    .PARAMETER DockerExecutable
        The Docker executable to use (default 'docker').
 
    .EXAMPLE
        Invoke-BcContainerCommand -ContainerName bld -ScriptBlock { Get-NavServerInstance }
 
    .OUTPUTS
        System.String (the command's stdout).
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory, Position = 0)]
        [ValidateNotNullOrEmpty()]
        [string] $ContainerName,

        [Parameter(Mandatory, Position = 1)]
        [ValidateNotNull()]
        [scriptblock] $ScriptBlock,

        [hashtable] $Variables = @{},

        # Echo the in-container output live (so a long operation shows progress) instead of returning
        # it only after the command completes. The full output is still captured and returned.
        [switch] $StreamOutput,

        [string] $DockerExecutable = 'docker'
    )

    $scriptText = ConvertTo-BcEncodedCommand -ScriptBlock $ScriptBlock -Variables $Variables -AsText
    $encoded = [System.Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($scriptText))

    # 'docker exec ... powershell -EncodedCommand <base64>' is a single command line, capped at
    # ~32 KB on Windows. Large in-container scripts (dependency install, publish, toolkit) exceed it,
    # so stage them as a .ps1 in the container's shared folder and run with -File instead.
    $cmdFileHost = $null
    if ($encoded.Length -lt 24000) {
        $arguments = @('exec', $ContainerName, 'powershell', '-NoLogo', '-NoProfile', '-EncodedCommand', $encoded)
    }
    else {
        $hostShare = Get-BcContainerHostShare -Name $ContainerName
        if (-not (Test-Path -LiteralPath $hostShare)) { New-Item -ItemType Directory -Force -Path $hostShare | Out-Null }
        $leaf = ".albuild-cmd-$([guid]::NewGuid().ToString('N')).ps1"
        $cmdFileHost = Join-Path $hostShare $leaf
        # UTF-8 with BOM so the container's Windows PowerShell reads non-ASCII correctly.
        [System.IO.File]::WriteAllText($cmdFileHost, $scriptText, [System.Text.UTF8Encoding]::new($true))
        $arguments = @('exec', $ContainerName, 'powershell', '-NoLogo', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', "C:\run\my\$leaf")
    }

    $dockerArgs = @{
        DockerExecutable = $DockerExecutable
        PassThru         = $true
        Quiet            = $true
        Arguments        = $arguments
    }
    if ($StreamOutput) { $dockerArgs['StreamOutput'] = $true }
    try { $result = Invoke-BcDocker @dockerArgs }
    finally { if ($cmdFileHost) { Remove-Item -LiteralPath $cmdFileHost -Force -ErrorAction SilentlyContinue } }

    if (-not $result.Success) {
        # Prefer an explicit, already-clean failure summary the in-container script emits as
        # 'ALBUILD-ERROR:' lines and exits on - this avoids PowerShell's error-record scaffolding
        # (source line, '~~~~', 'At line:', '+ CategoryInfo'/'+ FullyQualifiedErrorId'), which is
        # fragmented unreliably by CLIXML serialisation. Fall back to the captured stderr/stdout,
        # decoded from CLIXML and reduced to its core message.
        $markerLines = @($result.StdOut -split "`r?`n" |
                Where-Object { $_ -match '^ALBUILD-ERROR:' } |
                ForEach-Object { ($_ -replace '^ALBUILD-ERROR:', '').TrimEnd() })
        if ($markerLines.Count -gt 0) {
            $detail = ($markerLines -join [Environment]::NewLine).Trim()
        }
        else {
            $raw = if ([string]::IsNullOrWhiteSpace($result.StdErr)) { $result.StdOut } else { $result.StdErr }
            $detail = Format-BcErrorMessage -Text (ConvertFrom-BcCliXml -Text $raw)
        }
        throw "Command in container '$ContainerName' failed (exit $($result.ExitCode)).$(if ([string]::IsNullOrWhiteSpace($detail)) { '' } else { [Environment]::NewLine + $detail.Trim() })"
    }

    return $result.StdOut
}