PwshSetup.psm1

#Requires -Version 5.1

# PwshSetup 0.0.1, built by Publish-SetupModule.ps1. Do not edit this file.
# On pwsh 7.2+ it loads the whole module from PwshSetupCore.ps1.
# On Windows PowerShell it exports Assert-SetupAdmin and Test-SetupAdmin only.

<#
.SYNOPSIS
    Public: Assert-SetupAdmin and Test-SetupAdmin.
.DESCRIPTION
    Stays Windows PowerShell 5.1 syntax and plain ASCII throughout: the build makes this file the module's loader, and
    the loader is what Windows PowerShell imports.
#>


$script:RerunVariable = 'PWSHSETUP_RERUN'

function Test-SetupAdmin
{
    <#
    .SYNOPSIS
        $true when the session runs as administrator.
    .EXAMPLE
        if (-not (Test-SetupAdmin)) { 'Not elevated' }
    #>

    [CmdletBinding()]
    [OutputType([bool])]
    param()

    $identity = [Security.Principal.WindowsIdentity]::GetCurrent()
    $principal = New-Object -TypeName 'Security.Principal.WindowsPrincipal' -ArgumentList $identity
    return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}

function Write-SetupBootstrapLog
{
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '',
        Justification = 'Runs before PwshAnsi can draw, possibly on Windows PowerShell.')]
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [string] $Message
    )

    Write-Host -Object ('[PwshSetup] {0}' -f $Message) -ForegroundColor Yellow
}

function ConvertTo-SetupPortableArgument
{
    # Switches become booleans: a SwitchParameter does not come back from Import-Clixml as one.
    [CmdletBinding()]
    [OutputType([hashtable])]
    param(
        [hashtable] $Arguments = @{}
    )

    $bound = @{}
    foreach ($key in $Arguments.Keys)
    {
        $value = $Arguments[$key]
        if ($value -is [System.Management.Automation.SwitchParameter])
        {
            $value = $value.IsPresent
        }
        $bound[$key] = $value
    }
    return $bound
}

function Find-SetupTerminal
{
    # wt.exe on the PATH, or at the per-user WindowsApps alias, or $null.
    [CmdletBinding()]
    [OutputType([string])]
    param()

    $terminal = Get-Command -Name 'wt.exe' -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
    if ($terminal)
    {
        return $terminal.Source
    }
    if ($env:LOCALAPPDATA)
    {
        $alias = Join-Path -Path $env:LOCALAPPDATA -ChildPath 'Microsoft\WindowsApps\wt.exe'
        if (Test-Path -LiteralPath $alias)
        {
            return $alias
        }
    }
    return $null
}

function Get-SetupTerminalArgument
{
    # Windows Terminal splits its commands at semicolons, and a trailing backslash would escape the closing quote.
    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory)] [string] $Pwsh,
        [Parameter(Mandatory)] [string] $EncodedCommand
    )

    $arguments = @()
    if ($PWD.Provider.Name -eq 'FileSystem')
    {
        $directory = $PWD.ProviderPath.Replace(';', '\;') -replace '\\$', '\\'
        $arguments += @('-d', ('"{0}"' -f $directory))
    }
    $arguments += @(('"{0}"' -f $Pwsh), '-NoProfile', '-ExecutionPolicy', 'Bypass', '-EncodedCommand', $EncodedCommand)
    return ($arguments -join ' ')
}

function Get-SetupRerunCommand
{
    # The command the elevated window runs: marks the rerun, restores the folder and the parameters, and keeps the
    # window open on a failure that ends the script early.
    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory)] [string] $ScriptPath,
        [Parameter(Mandatory)] [string] $ArgumentFile,
        [Parameter(Mandatory)] [string] $Marker
    )

    $quote = { param($Text) "'" + ($Text -replace "'", "''") + "'" }
    $lines = @(('$env:{0} = {1}' -f $script:RerunVariable, (& $quote $Marker)))
    if ($PWD.Provider.Name -eq 'FileSystem')
    {
        $lines += ('Set-Location -LiteralPath {0}' -f (& $quote $PWD.ProviderPath))
    }
    $lines += @(
        ('$p = Import-Clixml -LiteralPath {0}' -f (& $quote $ArgumentFile))
        ('Remove-Item -LiteralPath {0} -Force' -f (& $quote $ArgumentFile))
        'try'
        '{'
        (' & {0} @p' -f (& $quote $ScriptPath))
        '}'
        'catch'
        '{'
        ' Write-Host -Object ($_ | Out-String) -ForegroundColor Red'
        ' $null = Read-Host -Prompt ''Press Enter to close'''
        ' exit 1'
        '}'
        'exit $LASTEXITCODE'
    )
    return ($lines -join "`n")
}

function Assert-SetupAdmin
{
    <#
    .SYNOPSIS
        Ensures the calling script runs as administrator.
    .DESCRIPTION
        Already elevated: returns at once.
        Otherwise reruns the calling script elevated, with its parameters, after one UAC prompt: in a new Windows
        Terminal window when wt.exe is found, in a new console window when not. This window ends there.
        Call it after Assert-PwshAnsi, so the elevated run starts on pwsh.
    .PARAMETER Arguments
        Parameters to pass to the rerun. Defaults to the bound parameters of the calling script.
    .PARAMETER ScriptPath
        Script to rerun. Defaults to the calling script.
    .EXAMPLE
        Assert-SetupAdmin
    #>

    [CmdletBinding()]
    param(
        [hashtable] $Arguments,
        [string] $ScriptPath = $MyInvocation.PSCommandPath
    )

    if (Test-SetupAdmin)
    {
        return
    }
    if (-not $ScriptPath)
    {
        throw 'Assert-SetupAdmin: call it from a script file, or pass -ScriptPath.'
    }

    $done = (Get-Item -Path ('env:{0}' -f $script:RerunVariable) -ErrorAction SilentlyContinue).Value
    if ($done -and (($done -split ',') -contains 'admin'))
    {
        throw 'Assert-SetupAdmin: the elevated rerun is still not running as administrator.'
    }

    if ($null -eq $Arguments)
    {
        $Arguments = @{}
        $caller = Get-PSCallStack | Select-Object -Skip 1 -First 1
        if ($caller)
        {
            $bound = $caller.InvocationInfo.BoundParameters
            foreach ($key in $bound.Keys)
            {
                $Arguments[$key] = $bound[$key]
            }
        }
    }

    $argumentFile = Join-Path -Path ([IO.Path]::GetTempPath()) -ChildPath ('PwshSetup-{0}.clixml' -f [guid]::NewGuid())
    ConvertTo-SetupPortableArgument -Arguments $Arguments | Export-Clixml -LiteralPath $argumentFile

    $marker = (@($done, 'admin') | Where-Object -FilterScript { $_ }) -join ','
    $command = Get-SetupRerunCommand -ScriptPath $ScriptPath -ArgumentFile $argumentFile -Marker $marker
    $encoded = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($command))
    $pwsh = (Get-Process -Id $PID).Path

    Write-SetupBootstrapLog -Message 'Restarting the script as administrator (UAC)...'
    try
    {
        # Name the local list after the host it is for, so it does not collide with the typed
        # $Arguments parameter — PowerShell variable names are case-insensitive, so $arguments
        # would be the same [hashtable] slot and the assignment would throw on type coercion.
        $terminal = Find-SetupTerminal
        if ($terminal)
        {
            $terminalArguments = Get-SetupTerminalArgument -Pwsh $pwsh -EncodedCommand $encoded
            Start-Process -FilePath $terminal -ArgumentList $terminalArguments -Verb RunAs -ErrorAction Stop
        }
        else
        {
            $pwshArguments = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-EncodedCommand', $encoded)
            Start-Process -FilePath $pwsh -ArgumentList $pwshArguments -Verb RunAs -ErrorAction Stop
        }
    }
    catch
    {
        Remove-Item -LiteralPath $argumentFile -Force -ErrorAction SilentlyContinue
        throw ('Assert-SetupAdmin: setup needs administrator rights, and elevation was not granted. {0}' -f $_.Exception.Message)
    }

    Write-SetupBootstrapLog -Message 'Carrying on in the elevated window.'
    exit 0
}

# Assert-PwshSetup.psm1
# Must stay Windows PowerShell 5.1 syntax throughout.
# Exports only Assert-PwshSetup; all other functions are internal helpers with a SetupBoot prefix to
# keep them clear of other PwshSetup public and internal functions.

$script:SetupBootMinPwsh  = [version]'7.2'
$script:SetupBootRerunVar = 'PWSHSETUP_RERUN'

function ConvertTo-SetupBootEncodedCommand {
    param([Parameter(Mandatory)][string]$Command)
    return [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($Command))
}

function ConvertTo-SetupBootVersion {
    param($Value)
    if (-not $Value) { return $null }
    $match = [regex]::Match([string]$Value, '^v?(\d+)\.(\d+)\.(\d+)')
    if (-not $match.Success) { return $null }
    return [version]('{0}.{1}.{2}' -f $match.Groups[1].Value, $match.Groups[2].Value, $match.Groups[3].Value)
}

# Yellow: an action that changes the system. DarkYellow (orange): its outcome or a skip.
function Write-SetupBootLog {
    param([Parameter(Mandatory)][string]$Message, [switch]$Outcome)
    $color = 'Yellow'
    if ($Outcome) { $color = 'DarkYellow' }
    Write-Host "[PwshSetup] $Message" -ForegroundColor $color
}

function Enable-SetupBootTls12 {
    [Net.ServicePointManager]::SecurityProtocol =
        [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
}

# Version of a pwsh executable: file version first (no process start), then a probe.
function Get-SetupBootPwshVersion {
    param([Parameter(Mandatory)][string]$Path)
    if (-not (Test-Path -LiteralPath $Path)) { return $null }
    try { $version = ConvertTo-SetupBootVersion (Get-Item -LiteralPath $Path).VersionInfo.FileVersion } catch { $version = $null }
    if ($version) { return $version }
    $probe = ConvertTo-SetupBootEncodedCommand '[string]$PSVersionTable.PSVersion'
    try { return ConvertTo-SetupBootVersion (& $Path -NoProfile -NonInteractive -EncodedCommand $probe 2>$null | Select-Object -Last 1) }
    catch { return $null }
}

# Newest pwsh on this machine at or above the minimum, as @{ Path; Version }, or $null.
function Find-SetupBootPwsh {
    $candidates = @(Get-Command pwsh -CommandType Application -All -ErrorAction SilentlyContinue |
        Select-Object -ExpandProperty Source)
    if ($env:ProgramFiles) { $candidates += Join-Path $env:ProgramFiles 'PowerShell\7\pwsh.exe' }
    if ($env:LOCALAPPDATA) { $candidates += Join-Path $env:LOCALAPPDATA 'Microsoft\WindowsApps\pwsh.exe' }

    $best = $null
    foreach ($path in ($candidates | Select-Object -Unique)) {
        $version = Get-SetupBootPwshVersion -Path $path
        if (-not $version) { continue }
        if ($version -lt $script:SetupBootMinPwsh) { continue }
        if ($best -and $version -le $best.Version) { continue }
        $best = [pscustomobject]@{ Path = $path; Version = $version }
    }
    return $best
}

# Latest stable pwsh release. Throws when it cannot be checked.
function Get-SetupBootLatestPwshVersion {
    Enable-SetupBootTls12
    $info = Invoke-RestMethod -Uri 'https://aka.ms/pwsh-buildinfo-stable' -UseBasicParsing -TimeoutSec 10 -ErrorAction Stop
    return [version]([string]$info.ReleaseTag).TrimStart('v')
}

function Test-SetupBootElevated {
    $identity  = [Security.Principal.WindowsIdentity]::GetCurrent()
    $principal = New-Object Security.Principal.WindowsPrincipal $identity
    return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}

# Installs pwsh from the official MSI: Program Files, PATH, Start menu, and Microsoft Update.
# Only msiexec is elevated (one UAC prompt), never the script itself.
# Restart Manager stays on: other open pwsh windows are closed gracefully, never the PC rebooted.
function Install-SetupBootPwsh {
    param([Parameter(Mandatory)][version]$Version)
    $arch = $env:PROCESSOR_ARCHITEW6432
    if (-not $arch) { $arch = $env:PROCESSOR_ARCHITECTURE }
    $arch = switch ($arch) { 'ARM64' { 'arm64' } 'x86' { 'x86' } default { 'x64' } }

    $name = "PowerShell-$Version-win-$arch.msi"
    $url  = "https://github.com/PowerShell/PowerShell/releases/download/v$Version/$name"
    $msi  = Join-Path ([IO.Path]::GetTempPath()) $name
    $arguments = @('/i', "`"$msi`"", '/quiet', '/norestart', 'ADD_PATH=1', 'USE_MU=1', 'ENABLE_MU=1')

    Enable-SetupBootTls12
    try {
        Write-SetupBootLog "Downloading $name..."
        Invoke-WebRequest -Uri $url -OutFile $msi -UseBasicParsing -ErrorAction Stop

        $start = @{ FilePath = 'msiexec.exe'; ArgumentList = $arguments; Wait = $true; PassThru = $true }
        if (Test-SetupBootElevated) { Write-SetupBootLog "Running the PowerShell $Version installer..." }
        else {
            Write-SetupBootLog "Requesting administrator rights to install PowerShell $Version (UAC)..."
            $start.Verb = 'RunAs'
        }
        $process = Start-Process @start -ErrorAction Stop
        if ($process.ExitCode -notin 0, 3010) { throw "msiexec exited with $($process.ExitCode)." }
        if ($process.ExitCode -eq 3010) { Write-SetupBootLog "PowerShell $Version installed; a reboot is needed to finish." -Outcome }
        else { Write-SetupBootLog "PowerShell $Version installed." -Outcome }
    }
    finally { Remove-Item -LiteralPath $msi -Force -ErrorAction SilentlyContinue }
}

function Test-SetupBootRerunStage {
    param([Parameter(Mandatory)][string]$Stage)
    $done = (Get-Item -Path "env:$($script:SetupBootRerunVar)" -ErrorAction SilentlyContinue).Value
    if (-not $done) { return $false }
    return ($done -split ',') -contains $Stage
}

# Windows PowerShell and pwsh each load their own copy of PwshSetup, so the update stage is marked
# per edition: an update in one must not stop the other from updating its own copy.
function Get-SetupBootModuleStage {
    return "module-$($PSVersionTable.PSEdition)"
}

function ConvertTo-SetupBootPortableArgument {
    param([hashtable]$Arguments = @{})
    $bound = @{}
    foreach ($key in $Arguments.Keys) {
        $value = $Arguments[$key]
        if ($value -is [System.Management.Automation.SwitchParameter]) { $value = $value.IsPresent }
        $bound[$key] = $value
    }
    return $bound
}

# A classic console window: an interactive one that is neither Windows Terminal nor an editor's
# terminal. A rerun inside it keeps its font, which lacks the glyphs PwshAnsi draws with.
function Test-SetupBootClassicConsole {
    if ($env:WT_SESSION -or $env:TERM_PROGRAM) { return $false }
    try { return -not ([Console]::IsInputRedirected -or [Console]::IsOutputRedirected) }
    catch { return $false }
}

# wt.exe on the PATH, or at the standard per-user WindowsApps alias, or $null. The alias path is
# checked explicitly because a freshly installed Windows Terminal is reachable from a new shell
# but not from this already-running one, where PATH was cached at startup.
function Find-SetupBootTerminal {
    $wt = Get-Command -Name 'wt.exe' -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
    if ($wt) { return $wt.Source }
    if ($env:LOCALAPPDATA) {
        $alias = Join-Path $env:LOCALAPPDATA 'Microsoft\WindowsApps\wt.exe'
        if (Test-Path -LiteralPath $alias) { return $alias }
    }
    return $null
}

# Installs Windows Terminal for the current user via winget. Appx packages install without
# admin, so no UAC prompt is raised. Throws when winget is missing or returns non-zero.
function Install-SetupBootWindowsTerminal {
    $winget = Get-Command -Name 'winget.exe' -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
    if (-not $winget) { throw 'winget is not available on this machine.' }

    $arguments = @(
        'install', '--id', 'Microsoft.WindowsTerminal',
        '--silent', '--accept-source-agreements', '--accept-package-agreements',
        '--scope', 'user'
    )
    Write-SetupBootLog 'Installing Windows Terminal via winget...'
    $process = Start-Process -FilePath $winget.Source -ArgumentList $arguments -Wait -PassThru -NoNewWindow -ErrorAction Stop
    if ($process.ExitCode -ne 0) { throw "winget exited with $($process.ExitCode)." }
    Write-SetupBootLog 'Windows Terminal installed.' -Outcome
}

# Windows Terminal arguments for a new tab in the most recent window, started in the current folder.
# Windows Terminal splits its commands at semicolons, and a trailing backslash would escape the
# closing quote, so both are escaped.
function Get-SetupBootTerminalTabArgument {
    param([Parameter(Mandatory)][string]$Pwsh, [Parameter(Mandatory)][string]$EncodedCommand)
    $tab = @('-w', '0', 'new-tab')
    if ($PWD.Provider.Name -eq 'FileSystem') {
        $directory = $PWD.ProviderPath.Replace(';', '\;') -replace '\\$', '\\'
        $tab += @('-d', ('"{0}"' -f $directory))
    }
    $tab += @(('"{0}"' -f $Pwsh), '-NoProfile', '-ExecutionPolicy', 'Bypass', '-EncodedCommand', $EncodedCommand)
    return ($tab -join ' ')
}

function Invoke-SetupBootRerun {
    param(
        [Parameter(Mandatory)][string]$Pwsh,
        [Parameter(Mandatory)][string]$ScriptPath,
        [Parameter(Mandatory)][string]$Stage,
        [hashtable]$Arguments = @{},
        [switch]$PauseOnExit
    )
    $bound  = ConvertTo-SetupBootPortableArgument $Arguments
    $clixml = Join-Path ([IO.Path]::GetTempPath()) ('PwshSetup-{0}.clixml' -f [guid]::NewGuid())
    $bound | Export-Clixml -LiteralPath $clixml

    # The stage marker travels inside the command: a Windows Terminal tab is started by the
    # terminal, not by this process, so it does not inherit this environment.
    $done    = (Get-Item -Path "env:$($script:SetupBootRerunVar)" -ErrorAction SilentlyContinue).Value
    $marker  = (@($done, $Stage) | Where-Object { $_ }) -join ','
    $quote   = { param($s) "'" + ($s -replace "'", "''") + "'" }
    $command = @(
        "`$env:$($script:SetupBootRerunVar) = $(& $quote $marker)"
        "`$p = Import-Clixml -LiteralPath $(& $quote $clixml)"
        "Remove-Item -LiteralPath $(& $quote $clixml) -Force"
        "& $(& $quote $ScriptPath) @p"
        'exit $LASTEXITCODE'
    ) -join "`n"
    $encoded = ConvertTo-SetupBootEncodedCommand $command

    # Moving to pwsh from a classic console opens a Windows Terminal tab instead, so the script
    # draws with a font that has the glyphs. Windows Terminal is installed on first use when it
    # is missing. The tab runs on its own: this window ends here.
    $terminal = $null
    if ($Stage -eq 'pwsh' -and (Test-SetupBootClassicConsole)) {
        $terminal = Find-SetupBootTerminal
        if (-not $terminal) {
            try { Install-SetupBootWindowsTerminal; $terminal = Find-SetupBootTerminal }
            catch { Write-SetupBootLog "Windows Terminal unavailable, staying in this window: $($_.Exception.Message)" }
        }
    }
    if ($terminal) {
        Write-SetupBootLog "Restarting the script in a Windows Terminal tab with $Pwsh..." -Outcome
        Start-Process -FilePath $terminal -ArgumentList (Get-SetupBootTerminalTabArgument -Pwsh $Pwsh -EncodedCommand $encoded)
        exit 0
    }

    $reason = 'to load the new PwshSetup'
    if ($Stage -eq 'pwsh') { $reason = "in $Pwsh" }
    Write-SetupBootLog "Restarting the script $reason..." -Outcome

    Set-Item -Path "env:$($script:SetupBootRerunVar)" -Value $marker
    & $Pwsh -NoProfile -ExecutionPolicy Bypass -EncodedCommand $encoded
    $code = $LASTEXITCODE
    if ($PauseOnExit) { $null = Read-Host 'Press Enter to close' }
    exit $code
}

# Returns the path to a pwsh 7.2+ to rerun under, or $null when the current host already qualifies.
# Only installs when no pwsh 7.2+ is found at all; never updates an existing installation.
function Update-SetupBootPwsh {
    if ($PSVersionTable.PSVersion -ge $script:SetupBootMinPwsh) { return $null }

    $current = Find-SetupBootPwsh
    if ($current) { return $current.Path }

    try {
        $latest = Get-SetupBootLatestPwshVersion
        Write-SetupBootLog "PowerShell $($script:SetupBootMinPwsh)+ not found. Installing PowerShell $latest..."
        Install-SetupBootPwsh -Version $latest
        $installed = Find-SetupBootPwsh
        if (-not $installed) { throw "PowerShell $latest was installed but cannot be found." }
        return $installed.Path
    }
    catch {
        throw "PowerShell $($script:SetupBootMinPwsh)+ is missing and could not be installed: $($_.Exception.Message)"
    }
}

# $true when a newer PwshSetup was installed and the script must rerun to load it.
# Update failures log a yellow warning and return $false; they never throw.
function Update-PwshSetupModule {
    param([Parameter(Mandatory)][version]$Current)
    if (Test-SetupBootRerunStage (Get-SetupBootModuleStage)) { return $false }

    $latest = $null
    try {
        $latest = [version](Find-Module PwshSetup -ErrorAction Stop).Version
        if ($latest -le $Current) { return $false }

        Write-SetupBootLog "Updating PwshSetup $Current -> $latest..."
        Install-Module PwshSetup -RequiredVersion $latest -Scope CurrentUser -Force -ErrorAction Stop
        Write-SetupBootLog "PwshSetup $latest installed." -Outcome
        return $true
    }
    catch {
        if ($latest -and $latest -gt $Current) {
            Write-SetupBootLog "Failed to update PwshSetup: $($_.Exception.Message) Continuing on $Current."
        }
        return $false
    }
}

function Assert-PwshSetup {
    <#
    .SYNOPSIS
        Ensures the calling script runs on pwsh 7.2+ with the latest PwshSetup.
    .DESCRIPTION
        1. Unless -SkipUpdate is set, checks the gallery for a newer PwshSetup and
           reruns the script to load it when one is found. Windows PowerShell and pwsh
           keep separate copies, so each edition updates its own.
        2. On Windows PowerShell, reruns the script on pwsh 7.2+, installing pwsh first
           when none is present.
        Already on pwsh 7.2+ with the latest PwshSetup (or -SkipUpdate): returns immediately.
        PwshSetup gallery check or update fails: logs a warning and continues.
    .EXAMPLE
        Assert-PwshSetup
    .EXAMPLE
        Assert-PwshSetup -SkipUpdate
    #>

    [CmdletBinding()]
    param(
        [hashtable]$Arguments,
        [string]$ScriptPath = $MyInvocation.PSCommandPath,
        [switch]$SkipUpdate
    )
    if (-not $ScriptPath) { throw 'Assert-PwshSetup: call it from a script file, or pass -ScriptPath.' }

    if ($null -eq $Arguments) {
        $Arguments = @{}
        $caller = Get-PSCallStack | Select-Object -Skip 1 -First 1
        if ($caller) {
            $bound = $caller.InvocationInfo.BoundParameters
            foreach ($key in $bound.Keys) { $Arguments[$key] = $bound[$key] }
        }
    }

    # The update comes first: on Windows PowerShell the move to pwsh ends this process, and the
    # pwsh run only ever sees its own copy, so this is the one chance to update this edition's.
    if (-not $SkipUpdate -and (Update-PwshSetupModule -Current $MyInvocation.MyCommand.Module.Version)) {
        Invoke-SetupBootRerun -Pwsh (Get-Process -Id $PID).Path -ScriptPath $ScriptPath -Stage (Get-SetupBootModuleStage) -Arguments $Arguments
    }

    $pwsh = Update-SetupBootPwsh
    if ($pwsh) { Invoke-SetupBootRerun -Pwsh $pwsh -ScriptPath $ScriptPath -Stage 'pwsh' -Arguments $Arguments }
}

if ($PSVersionTable.PSVersion -ge [version]'7.2')
{
    . (Join-Path -Path $PSScriptRoot -ChildPath 'PwshSetupCore.ps1')
}

# The manifest's FunctionsToExport decides what is public; helpers stay hidden.
Export-ModuleMember -Function *