Public/Get-HDTConsoleTreeNode.ps1

function Get-HDTConsoleTreeNode {
    <#
        .SYNOPSIS
            Turns one or more console workspaces into the ordered rows the
            console window displays.
 
        .DESCRIPTION
            EVERYTHING THAT REACHES THE SCREEN IS DECIDED HERE. The window is
            loaded by XamlReader and driven by an injected IConsoleHost that adds
            these rows to a list and shows the selected row's detail; the host
            makes no decisions and formats nothing, which is what leaves it
            branch-free and honestly exempt from TDD. If the
            host built the rows, the only thing that could ever check the
            console's output would be a person looking at a screen.
 
            SEVERAL SHARES, UNDER ONE ROOT. An administrator works on more than
            one deployment share - a lab share and a production share, or one per
            site - so the tree is rooted at 'Deployment Shares' and every share
            hangs off it, exactly as Deployment Workbench roots them. The root is
            there with one share as well as with six: a console whose shape
            changes depending on how many shares are open is a console whose
            screenshots cannot be compared, and it makes "where do I add the next
            one" a question rather than a place.
 
            THE ORDER IS DEPLOYMENT WORKBENCH'S, deliberately: each share, then
            Task Sequences, Operating Systems and the Boot Image beneath it
            ("deliberately close to Deployment Workbench so muscle
            memory transfers"). Depth carries the nesting and Display carries the
            indentation already applied, so a flat ListBox in a monospaced font
            reads as a tree without a converter, a TreeView, or a line of XAML
            that has to be right first time in a window nobody can unit test.
 
            EVERY ROW CARRIES THE COMMAND THAT PRODUCED IT: "the
            console may not do anything the cmdlets can't. Every action it
            performs maps to a cmdlet invocation, and the console shows that
            invocation - so an admin can learn the automation surface by clicking
            around, and script anything they can do in the UI." Command is that
            invocation, spelled the way it would be typed.
 
            EVERY ROW ALSO CARRIES ITS SHARE'S BANNER, so the window can name the
            share a selected row belongs to without the host working out which
            share that was.
 
            NUMBERS AND DATES ARE FORMATTED INVARIANTLY. A build time rendered in
            the viewer's zone means two administrators reading the same share
            disagree about when the image was built, and a size grouped by the
            viewer's culture means a screenshot cannot be compared to a test.
            The manifest records UTC; the console shows UTC and says so.
 
            A ROW IS NEVER HIDDEN BECAUSE IT IS BROKEN. An item whose document
            would not parse appears with Status 'Error' and the engine's message
            in its detail; so does a whole share that would not open. An empty
            category says '(none)' rather than showing nothing, because an empty
            list and a list that failed to load look identical on a screen.
 
        .PARAMETER Workspace
            One or more shares from Get-HDTConsoleWorkspace, in the order they
            should appear.
 
        .INPUTS
            None. This command does not accept pipeline input.
 
        .OUTPUTS
            System.Management.Automation.PSCustomObject[], in display order:
 
              Depth 0 the root, 1 a share, 2 a category, 3 an item
              Kind Root, Share, Category, TaskSequence, OperatingSystem,
                       BootImage or Empty
              Status Ok, Error or Missing
              Text the row, without indentation
              Display the row as the list shows it, indented by Depth
              Detail the multi-line text the detail pane shows
              Command the module command that produced the row
              HeaderTitle, HeaderRoot, HeaderDeployRoot
                       what the banner says while the row is selected
 
        .EXAMPLE
            Get-HDTConsoleTreeNode -Workspace (Get-HDTConsoleWorkspace -Path 'C:\HDTLab\Share') |
                Format-Table Depth, Kind, Text
 
            The whole window, on a console, with no window.
 
        .EXAMPLE
            $share = 'C:\HDTLab\Share', '\\192.168.2.108\HDTShare' |
                ForEach-Object { Get-HDTConsoleWorkspace -Path $_ }
            Get-HDTConsoleTreeNode -Workspace $share
 
            Two shares in one tree.
    #>

    [CmdletBinding()]
    [OutputType([pscustomobject[]])]
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [ValidateNotNull()]
        [object[]] $Workspace
    )

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

    $share = @($Workspace)
    $node = New-Object -TypeName System.Collections.ArrayList

    $rootCommand = "Get-HDTConsoleWorkspace -Path '{0}'" -f (@($share | ForEach-Object { $_.Root }) -join "', '")

    $listed = foreach ($current in $share) {
        '{0,-8} {1}' -f $current.Status, $current.Root
    }

    $rootField = @(
        New-HDTConsoleField -Label 'Shares' -Value $share.Count
        New-HDTConsoleField -Label 'Opened' -Value (@($listed) -join [System.Environment]::NewLine)
    )

    $rootNode = New-HDTConsoleNode -Depth 0 -Kind 'Root' -Status 'Ok' `
        -Text ('Deployment Shares ({0})' -f $share.Count) `
        -Field $rootField `
        -Command $rootCommand `
        -Header ([pscustomobject] @{
                Title      = 'Deployment Shares'
                Root       = '(select a share)'
                DeployRoot = '(select a share)'
            })

    [void] $node.Add($rootNode)

    foreach ($current in $share) {
        $subtree = @(Get-HDTConsoleShareNode -Workspace $current)

        # The share's own row is first, by construction, and everything after it
        # is already nested underneath it.
        [void] $rootNode.Children.Add($subtree[0])

        foreach ($row in $subtree) {
            [void] $node.Add($row)
        }
    }

    return [pscustomobject[]] @($node)
}