Public/New-HDTWdsService.ps1

function New-HDTWdsService {
    <#
        .SYNOPSIS
            Creates the real IWdsService adapter over the WDS module.
 
        .DESCRIPTION
            A THIN ADAPTER, AND DELIBERATELY DUMB. It constructs arguments for
            three WDS cmdlets and projects
            what they return; every decision that could be got wrong - whether an
            image of that name already exists, whether it has to be removed
            first, what to call it - lives in Import-HDTBootImageToWds, where it
            is unit tested against New-HDTFakeWdsService.
 
              GetBootImage(architecture) Get-WdsBootImage
              ImportBootImage(path, imageName, architecture) Import-WdsBootImage
              RemoveBootImage(imageName, architecture) Remove-WdsBootImage
 
            THIS FILE HAS NEVER RUN ON THIS HOST, AND AS OF 2026-08-14 IT HAS
            NEVER RUN ANYWHERE IN THIS REPOSITORY. That is not an oversight, it
            is a refusal:
 
              * this machine is Windows 11 Pro. The WDS PowerShell module and
                wdsutil.exe ship with a Windows SERVER role, so there is nothing
                here to adapt;
              * standing one up is forbidden by PROJECT.md's lab safety rules.
                CM01 runs a PXE responder on 'Default Switch', and PROJECT.md
                rule 3 confines PXE/WDS testing to the isolated 'HDT Lab' switch
                - a second responder beside CM01's would either break the user's
                SCCM lab or answer our test VMs and silently invalidate the test.
 
            SO IT GETS NO CONTRACT ROW against the real implementation, and
            tests/contract carries no IWdsService file at all. The ONE thing this
            machine can prove about it is asserted in
            tests/unit/Import-HDTBootImageToWds.Tests.ps1, against this function
            rather than a simulation of it: on a host with no WDS module, the
            constructor refuses with a named HDTDependencyError. Everything else
            about the WDS path is asserted against the fake, and that is said
            here in a plain sentence rather than implied away.
 
            THE CONSTRUCTOR IS THE ONLY BRANCH, for the same reason
            Get-HDTAdkPath's existence check is: an adapter that is not unit
            tested must stay branch-free, and a dependency gate that names what
            is missing is the exception every adapter here is allowed - because
            the alternative is "The term 'Get-WdsBootImage' is not recognized",
            which tells an administrator nothing about which role to install.
 
            Import-WdsBootImage is invoked with -SkipVerify:$false. Verification
            is the slow half of an import and it is the half that catches a
            truncated WIM before a fleet tries to boot it.
 
        .PARAMETER Journal
            The shared cross-service operation journal. When supplied, every
            recorded call is appended to it in addition to $Operations, numbered
            globally across services.
 
        .INPUTS
            None. This command does not accept pipeline input.
 
        .OUTPUTS
            System.Management.Automation.PSCustomObject with the three
            IWdsService ScriptMethods. Note that Get-Member -MemberType Method
            does NOT list a ScriptMethod - use -MemberType Method, ScriptMethod.
 
        .EXAMPLE
            $wds = New-HDTWdsService
            Import-HDTBootImageToWds -Path 'C:\HDTLab\Share\Boot\HDTPE_x64.wim' -WdsService $wds
 
            On a Windows Server with the WDS role. On a client OS the first line
            throws HDTDependencyError naming the module and the role.
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '',
        Justification = 'Builds a stateless service adapter object; it changes no state.')]
    [CmdletBinding()]
    [OutputType([object])]
    param(
        [Parameter()]
        [AllowNull()]
        [System.Collections.ArrayList] $Journal
    )

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

    # THE DEPENDENCY GATE. See the help: the alternative message is "The term
    # 'Get-WdsBootImage' is not recognized", which names neither the module nor
    # the role that carries it.
    if (@(Get-Module -ListAvailable -Name 'WDS' -ErrorAction SilentlyContinue).Count -eq 0) {
        $PSCmdlet.ThrowTerminatingError((New-HDTErrorRecord -ErrorId 'HDTDependencyError' -Category NotInstalled `
                    -TargetObject 'WDS' `
                    -Message ("the WDS PowerShell module is not available on this machine, so HDT cannot import a boot image into Windows Deployment Services. WDS is a Windows Server role: install it with Install-WindowsFeature WDS -IncludeManagementTools on a Windows Server, and run HDT's import from there. HDT does not ship a PXE server; for a site with an existing TFTP or HTTP stack instead, use New-HDTPxePayload.")))
    }

    $service = [pscustomobject] @{
        Operations  = [System.Collections.ArrayList]::new()
        Journal     = $Journal
        ServiceName = 'WdsService'
    }

    $service | Add-Member -MemberType ScriptMethod -Name Record -Value {
        param([string] $Operation, [object[]] $Argument)

        [void] $this.Operations.Add([pscustomobject] @{
                Sequence  = $this.Operations.Count + 1
                Operation = $Operation
                Arguments = $Argument
            })

        if ($null -ne $this.Journal) {
            [void] $this.Journal.Add([pscustomobject] @{
                    Sequence  = $this.Journal.Count + 1
                    Service   = $this.ServiceName
                    Operation = $Operation
                    Arguments = $Argument
                })
        }
    }

    $service | Add-Member -MemberType ScriptMethod -Name GetOperationName -Value {
        return , ([string[]] @($this.Operations | ForEach-Object { $_.Operation }))
    }

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

        $this.Record('GetBootImage', @($Architecture))

        $row = @(Get-WdsBootImage -Architecture $Architecture -ErrorAction SilentlyContinue |
                ForEach-Object {
                    [pscustomobject] @{
                        ImageName    = [string] $_.ImageName
                        Architecture = [string] $_.Architecture
                        FileName     = [string] $_.FileName
                        Version      = [string] $_.Version
                    }
                })

        # The unary comma is mandatory: a ScriptMethod collapses a single-element
        # array to a scalar without it (tests/helpers/README.md F3), and one boot
        # image is the normal case.
        return , ([object[]] $row)
    }

    $service | Add-Member -MemberType ScriptMethod -Name ImportBootImage -Value {
        param([string] $Path, [string] $ImageName, [string] $Architecture)

        $this.Record('ImportBootImage', @($Path, $ImageName, $Architecture))

        Import-WdsBootImage -Path $Path -NewImageName $ImageName -SkipVerify:$false | Out-Null
    }

    $service | Add-Member -MemberType ScriptMethod -Name RemoveBootImage -Value {
        param([string] $ImageName, [string] $Architecture)

        $this.Record('RemoveBootImage', @($ImageName, $Architecture))

        Remove-WdsBootImage -ImageName $ImageName -Architecture $Architecture | Out-Null
    }

    return $service
}