Public/Start-HDTConsole.ps1

function Start-HDTConsole {
    <#
        .SYNOPSIS
            Opens the admin console, dealing with the apartment and the terminal.

        .DESCRIPTION
            WHAT AN ADMINISTRATOR TYPES. Show-HDTConsole is the command - it
            opens the window on the thread that calls it and returns an answer -
            and this is the wrapper around it that handles the three things a
            person at a prompt cares about and a script does not:

              the apartment WPF needs STA, and pwsh is MTA by default;
              the terminal a console window this process owns is hidden;
              -Detach the window in its own process, prompt straight back.

            IT IS A FUNCTION SO IMPORTING THE MODULE IS ENOUGH.
            Start-HDTConsole.ps1 sits beside the module and is still there for
            Explorer and for a shortcut, but a .ps1 in a folder is not a command:
            Import-Module gives you functions, and nothing puts a script file on
            the command path. That file is now a shim that calls this.

            THE TERMINAL IS ONLY HIDDEN WHEN THIS PROCESS OWNS IT.
            GetConsoleProcessList says how many processes are attached; one is a
            console spawned for this alone - a double-click, or Explorer's Run
            with PowerShell - and hiding it is right. More than one is a terminal
            somebody was already working in, and hiding it takes their prompt,
            their history and, under Windows Terminal, every other tab with it.
            -Detach is the answer in that case.

            IN THE ISE, -Detach STARTS powershell.exe RATHER THAN A SECOND ISE.
            The ISE is not a console host: it takes -File, -Mta and -NoProfile
            and has never had -STA. It is also already STA, so the window opens
            there without -Detach at all.

            IT IS AN ADAPTER AND IT STAYS ONE. Win32 window state and
            Start-Process cannot be proven under Pester; what CAN be is that the
            command exists, is exported, and that the .ps1 beside it delegates
            here rather than growing a second copy of the same logic - which is
            what tests/unit/Start-HDTConsole.Tests.ps1 asserts.

        .PARAMETER Path
            One or more deployment shares. Every extra argument is another share,
            so 'C:\HDTLab\Share' '\\host\HdtShare' opens both in one window.

        .PARAMETER Title
            The window title.

        .PARAMETER Theme
            Light or Dark. Light by default.

        .PARAMETER Detach
            Start the window in its own hidden process and return at once,
            leaving the prompt you ran this from usable.

            IT BEHAVES THE SAME UNDER conhost AND WINDOWS TERMINAL, which hiding
            does not: a console app under Windows Terminal runs on a
            pseudoconsole, so the window GetConsoleWindow hands back was never on
            screen and hiding it changes nothing anybody can see.

        .INPUTS
            None. This command does not accept pipeline input.

        .OUTPUTS
            System.Management.Automation.PSCustomObject - what Show-HDTConsole
            returned. Nothing, with -Detach: the window belongs to another
            process by then.

        .EXAMPLE
            Start-HDTConsole

            The console on C:\HDTLab\Share.

        .EXAMPLE
            Start-HDTConsole -Detach C:\HDTLab\Share

            The window opens and the prompt comes straight back.

        .EXAMPLE
            Start-HDTConsole 'C:\HDTLab\Share' '\\HDT-HOST\HdtShare' -Theme Dark

            Two shares, one window.

        .LINK
            Show-HDTConsole
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '',
        Justification = 'Opens a window. Nothing here writes; the editing commands the window offers carry their own ShouldProcess, and a confirmation prompt in front of a GUI is a prompt nobody sees.')]
    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param(
        [Parameter(Position = 0, ValueFromRemainingArguments = $true)]
        [ValidateNotNullOrEmpty()]
        [string[]] $Path = @('C:\HDTLab\Share'),

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string] $Title = 'Hephaestus Deployment Toolkit',

        [Parameter()]
        [ValidateSet('Light', 'Dark')]
        [string] $Theme = 'Light',

        [Parameter()]
        [switch] $Detach
    )

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

    # -- the terminal, which is not part of the product ----------------------

    if (-not ('HDTConsoleNative.Terminal' -as [type])) {
        Add-Type -Namespace 'HDTConsoleNative' -Name 'Terminal' -MemberDefinition @'
[DllImport("kernel32.dll")]
public static extern IntPtr GetConsoleWindow();

[DllImport("kernel32.dll")]
public static extern int GetConsoleProcessList(int[] lpdwProcessList, int dwProcessCount);

[DllImport("user32.dll")]
public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
'@

    }

    $consoleWindow = [HDTConsoleNative.Terminal]::GetConsoleWindow()
    $consoleHidden = $false

    if ($consoleWindow -ne [IntPtr]::Zero) {
        $attached = New-Object -TypeName 'int[]' -ArgumentList 8
        $attachedCount = [HDTConsoleNative.Terminal]::GetConsoleProcessList($attached, $attached.Length)

        if ($attachedCount -eq 1) {
            [void] [HDTConsoleNative.Terminal]::ShowWindow($consoleWindow, 0)   # SW_HIDE
            $consoleHidden = $true
        }
    }

    # -- the relaunch, for -Detach and for an MTA host -----------------------
    #
    # QUOTED, BECAUSE Start-Process DOES NOT QUOTE. -ArgumentList joins an array
    # with spaces into one command line, so '-Title Hephaestus Deployment
    # Toolkit' arrives as three arguments and the child dies binding them -
    # invisibly, because the child is hidden. Every value is quoted, not only
    # the ones that look like they need it: a share path with a space in it is
    # ordinary.
    $launcher = Join-Path -Path $script:HDTModuleRoot -ChildPath 'Start-HDTConsole.ps1'

    $argument = New-Object -TypeName System.Collections.ArrayList

    [void] $argument.Add('-NoProfile')
    [void] $argument.Add('-STA')
    [void] $argument.Add('-File')
    [void] $argument.Add('"{0}"' -f $launcher)
    [void] $argument.Add('-Title')
    [void] $argument.Add('"{0}"' -f $Title)
    [void] $argument.Add('-Theme')
    [void] $argument.Add('"{0}"' -f $Theme)
    [void] $argument.Add('-Path')

    foreach ($one in @($Path)) { [void] $argument.Add('"{0}"' -f $one) }

    # NOT THIS PROCESS'S OWN EXECUTABLE. In powershell.exe they are the same
    # thing; in the ISE this process is powershell_ise.exe, which takes -File,
    # -Mta and -NoProfile and has never had -STA - so -Detach started a second
    # ISE with a switch it does not have and reported an error about apartments.
    # Get-HDTPowerShellPath falls back to the console host in $PSHOME.
    #
    # NOT $host either: that is an automatic variable, and assigning to it is
    # both an analyzer error and a way to break every Write-Host in the session.
    $shell = Get-HDTPowerShellPath -ProcessPath ([System.Diagnostics.Process]::GetCurrentProcess().MainModule.FileName) -InstallPath $PSHOME

    if ($Detach) {
        [void] (Start-Process -FilePath $shell -ArgumentList ([string[]] @($argument)) -WindowStyle Hidden)
        return
    }

    if ([System.Threading.Thread]::CurrentThread.GetApartmentState() -ne [System.Threading.ApartmentState]::STA) {
        $process = Start-Process -FilePath $shell -ArgumentList ([string[]] @($argument)) `
            -WindowStyle Hidden -PassThru -Wait

        if ($consoleHidden) {
            [void] [HDTConsoleNative.Terminal]::ShowWindow($consoleWindow, 5)   # SW_SHOW
        }

        return [pscustomobject] @{
            Action    = 'Relaunched'
            ExitCode  = [int] $process.ExitCode
            NodeCount = 0
        }
    }

    # -- the window ----------------------------------------------------------

    try {
        return Show-HDTConsole -Path $Path -Title $Title -Theme $Theme
    } finally {
        # HIDDEN IS A PRESENTATION CHOICE, NOT A PLACE TO GET STUCK. Whether the
        # window closed cleanly or never opened, the terminal comes back - there
        # has to be something to read and something to type into.
        if ($consoleHidden) {
            [void] [HDTConsoleNative.Terminal]::ShowWindow($consoleWindow, 5)   # SW_SHOW
        }
    }
}