Templates/Launcher/Start-HDTRefresh.ps1

<#
    .SYNOPSIS
        THE FULL-OS ENTRY POINT - what an administrator runs to start a Refresh
        on a machine that is already running Windows.
 
    .DESCRIPTION
        MDT's \\server\Share$\Scripts\LiteTouch.vbs, in HDT. It is the file an
        administrator opens the share and runs on the machine that is about to be
        replaced, and it is seeded onto every share by New-HDTWorkspace from
        src\Hephaestus\Templates\Launcher.
 
        RUN IT AS ADMINISTRATOR, from the share:
 
            \\server\HdtShare\Scripts\Start-HDTRefresh.cmd
 
        The Start-HDTRefresh.cmd beside this file is the double-clickable half,
        because Windows opens a PowerShell script in an editor rather than
        running it, and that wrapper is also what sets HDT_LAUNCHED_BY.
 
        IT CONTAINS NO DEPLOYMENT LOGIC AND MAKES NO DECISION, WHICH IS THE
        POINT. LiteTouch.vbs is thin for the same reason: it finds where it was
        launched from, checks the machine can do this, and hands everything to
        the heavy lifting. HDT's heavy lifting is Start-HDTDeployment.ps1 - the
        same file startnet.cmd runs in WinPE - which has derived its own phase
        since M9 and needs no second entry point. Every branch this file would
        otherwise take belongs to Get-HDTRefreshLaunchPlan, where a fake can
        drive it; tests/unit/WorkspaceLauncherTemplate.Tests.ps1 PARSES this file
        and asserts it names no disk cmdlet, no DISM, no bcdedit, calls the plan
        exactly once and hands over exactly once.
 
        THE RUN IS A REFRESH BECAUSE IT STARTED HERE, and nothing declares it.
        Get-HDTDeploymentPhase reads the system drive - not X:, so FullOS - and
        Get-HDTDeploymentType maps that to REFRESH (DESIGN 3.2). This file passes
        no phase, no type and no -Phase to anything: a run that could be told
        what it is could talk its way past the guard that only a REFRESH unlocks.
 
        THE SHARE IS THIS FILE'S OWN LOCATION. The administrator reached
        \\server\Share\Scripts\ to run it, so that path demonstrably works from
        this machine right now - which a deployRoot written into a document
        months ago may not.
 
    .PARAMETER SequenceId
        The task sequence to run. Omitted, the sequence comes from the wizard or
        from the share's rules, which is where "which task sequence does this
        machine get" is answered everywhere else in HDT.
 
    .EXAMPLE
        \\HDT-HOST\HdtShare\Scripts\Start-HDTRefresh.cmd
 
        What an administrator runs. Elevated, on the machine to be replaced.
 
    .EXAMPLE
        .\Start-HDTRefresh.ps1 -SequenceId REFRESH
 
        The same run with the sequence named, so nobody is asked for it.
#>

[CmdletBinding()]
param(
    [Parameter()]
    [AllowEmptyString()]
    [string] $SequenceId = ''
)

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

# -- the share, worked out before anything can be imported --------------------
#
# THE ONE THING THIS FILE HAS TO DO FOR ITSELF, and LiteTouch.vbs has the same
# line for the same reason (sScriptDir, from WScript.ScriptFullName): the engine
# lives on the share, so the share has to be located before the engine can be
# asked anything. Get-HDTRefreshLaunchPlan re-derives it from $PSScriptRoot and
# it is the plan's answer that is used from there on - this is only enough to
# find the module.
#
# [IO.Path]::GetDirectoryName RATHER THAN Split-Path, because this routinely runs
# from a UNC path and the provider-aware cmdlets resolve the drive.
$shareRoot = [System.IO.Path]::GetDirectoryName($PSScriptRoot.TrimEnd('\', '/'))
$env:PSModulePath = '{0};{1}' -f ([System.IO.Path]::Combine($shareRoot, 'Modules')), $env:PSModulePath

Import-Module -Name 'powershell-yaml' -Force -ErrorAction Stop
Import-Module -Name 'Hephaestus' -Force -ErrorAction Stop

# -- what this session is, read through the adapter ---------------------------
#
# CLAUDE.md RULE 5. The system drive is what makes this run a REFRESH, so it is
# read through the one thing in HDT that reads an environment variable rather
# than through $env: - which is what lets the decision be tested at a desk.
$environment = New-HDTEnvironmentProvider
$systemDrive = [string] $environment.GetVariable('SystemDrive')

# THE ELEVATION CHECK IS READ HERE AND DECIDED IN THE PLAN. MDT asks the same
# question in the same place, by writing a file into %SystemRoot%; this asks
# Windows directly, which is the same fact without the side effect.
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$isElevated = ([Security.Principal.WindowsPrincipal] $identity).IsInRole(
    [Security.Principal.WindowsBuiltInRole]::Administrator)

# -- every decision, in one call ----------------------------------------------
$plan = Get-HDTRefreshLaunchPlan -ScriptRoot $PSScriptRoot -SystemDrive $systemDrive `
    -IsElevated $isElevated -SequenceId $SequenceId

Write-Information ("HDT Refresh: share '{0}' ({1}), phase {2}, deployment type {3}" -f
    $plan.Root, $plan.WorkspaceId, $plan.Phase, $plan.DeploymentType) -InformationAction Continue

# -- the bootstrap document, beside the run's own state and logs --------------
#
# THE SAME FILE THE BOOT IMAGE CARRIES. Update-HDTBootImage bakes one into
# X:\HDT\bootstrap.json because a booting machine has no other way to be told
# where it is; this run already knows, because it is standing in the share. Same
# reader, same shape - so nothing downstream has to learn that this run started
# differently.
New-Item -ItemType Directory -Path ([System.IO.Path]::GetDirectoryName($plan.BootstrapPath)) -Force | Out-Null
Set-Content -LiteralPath $plan.BootstrapPath -Value $plan.BootstrapText -Encoding UTF8

# -- the screens, which default to a drive this machine does not have ---------
#
# THE SAME FILE startnet.cmd RUNS DEFAULTS ITS WINDOWS TO X:\HDT\UI\, because in
# WinPE that is where Update-HDTBootImage staged them and X: is the RAM disk's
# fixed letter. This machine never booted a WIM, so X: is not a drive at all -
# and until this line existed a Refresh inherited every one of those defaults and
# drew nothing: no wizard, no progress board, no failure screen.
#
# THE PLAN ANSWERS THEM OUT OF THE ENGINE ON THE SHARE, from the UI\ folder
# beside the payload about to be run, and it works the SET out by reading that
# payload's own param block rather than from a list. So a screen added to the
# engine tomorrow arrives here without this file changing - which is the whole
# reason six of them were wrong at once.
$uiArgument = $plan.UiArgument

Write-Information ("HDT Refresh: {0} screen(s) from '{1}'" -f @($uiArgument.Keys).Count, $plan.UiRoot) -InformationAction Continue

# -- the handover, once -------------------------------------------------------
#
# THE FILE startnet.cmd RUNS, and deliberately the same one. It derives the
# phase, scans for a run already in progress with -Phase, opens the wizard,
# resolves the rules and calls Invoke-HDTTaskSequence exactly once. A second
# full-OS entry point would be a second answer to "which one is running".
& $plan.PayloadPath -BootstrapPath $plan.BootstrapPath -ModuleRoot $plan.ModuleRoot -SequenceId $plan.SequenceId @uiArgument