Public/New-HDTConsoleProgressHost.ps1

function New-HDTConsoleProgressHost {
    <#
        .SYNOPSIS
            The IProgressHost for a machine that cannot draw a window: it writes
            DESIGN 11.1's console lines instead.
 
        .DESCRIPTION
            THE FALLBACK IS A HOST, NOT A BRANCH AT THE CALL SITE. When the
            window cannot be opened, Start-HDTProgressDisplay hands back one of
            these instead of the WPF one - so the engine calls Update the same
            way whatever machine it is on, and there is no `if ($mode -eq
            'Window')` anywhere for somebody to forget.
 
            That matters more than it looks. DESIGN 11.1's fallback exists for
            "a boot image built without the right components, an exotic display,
            a serial console" - all machines nobody is testing on - and a branch
            at every call site is a branch that is wrong on exactly those.
 
            IT REPEATS NOTHING. The same line twice tells a technician the
            deployment is stuck when it is only the caller polling; a console
            scrolls, so every line printed pushes the last real change further
            up. Only a line that DIFFERS is written.
 
            THE LINE ITSELF IS Format-HDTProgressLine's, which is pure and
            tested. What is left here is writing it, which is why this adapter
            can be exempt from tests at all (CLAUDE.md rule 1).
 
            Write-Information, NOT Write-Host. The payload sets
            $InformationPreference and the stream is redirectable, so a lab
            harness can capture the deployment's own account of itself. Write-Host
            goes to a screen nobody is reading on the machines this exists for.
 
        .OUTPUTS
            A PSCustomObject with Open(xaml), Update(progress), SetComputerName
            and Close() - the same shape New-HDTProgressHost has, so callers
            cannot tell them apart.
 
        .EXAMPLE
            $display = Start-HDTProgressDisplay -XamlPath $p
            $display.DisplayHost.Update($progress)
 
            Writes a window on a machine that has one and a line on a machine
            that does not.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '',
        Justification = 'Builds a stateless service adapter object; it changes no state.')]
    # Open takes markup it has no use for, ON PURPOSE: the signature has to match
    # New-HDTProgressHost's or the two are not interchangeable, and being
    # interchangeable is the entire reason this object exists.
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '',
        Justification = 'Open mirrors the WPF host signature so the two hosts are interchangeable; there is nothing to load.')]
    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param()

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

    $service = [pscustomobject] @{
        ComputerName = ''
        LastLine     = ''
    }

    # NOTHING TO OPEN. The signature matches the WPF host so the two are
    # interchangeable, and that is the whole point of this object.
    $service | Add-Member -MemberType ScriptMethod -Name Open -Value {
        param([string] $Xaml)
    }

    $service | Add-Member -MemberType ScriptMethod -Name SetComputerName -Value {
        param([string] $Name)

        $this.ComputerName = $Name
    }

    $service | Add-Member -MemberType ScriptMethod -Name Update -Value {
        param([object] $Progress)

        if ($null -eq $Progress) { return }

        $line = Format-HDTProgressLine -Progress $Progress
        if ($line -eq $this.LastLine) { return }

        $this.LastLine = $line
        Write-Information -MessageData $line -InformationAction Continue
    }

    $service | Add-Member -MemberType ScriptMethod -Name Close -Value { }

    return $service
}