Public/Get-GreenroomInstance.ps1

# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 Tyler Vigario

<#
.SYNOPSIS
  Get the greenroom sessions running on this host.

.DESCRIPTION
  Emits one object per running session. This is the module replacement for
  `greenroom list` and `greenroom status`, which were separate verbs only because a
  script had to choose its own output shape -- `list` piped through
  Format-Table -AutoSize and `status` printed a block of text.

  The difference is not cosmetic. `greenroom list` wrote FORMATTED TEXT to the host,
  so anything downstream received formatting records rather than data:

      PS> greenroom status laptop-admin | ForEach-Object { " $_" }
        Microsoft.PowerShell.Commands.Internal.Format.FormatStartData
        Microsoft.PowerShell.Commands.Internal.Format.GroupStartData
        ...

  That is measured output from this repository's own tooling. Emitting objects and
  leaving presentation to Greenroom.format.ps1xml makes the table a VIEW of the data
  rather than the only thing produced, so filtering works:

      Get-GreenroomInstance | Where-Object Elevated
      Get-GreenroomInstance | Where-Object { -not $_.Visible } | Show-GreenroomSession

.PARAMETER Name
  Only return instances with this name. Wildcards are supported.

.EXAMPLE
  Get-GreenroomInstance

.EXAMPLE
  Get-GreenroomInstance -Name laptop-admin | Select-Object -ExpandProperty Window

.EXAMPLE
  Get-GreenroomInstance | Where-Object { $_.Window -eq $null }
  Instances whose window could not be resolved. Add -Verbose for the reason each.

.OUTPUTS
  Greenroom.Instance
#>

function Get-GreenroomInstance {
    [CmdletBinding()]
    [OutputType('Greenroom.Instance')]
    param(
        [Parameter(Position = 0)]
        [SupportsWildcards()]
        [string]$Name
    )

    $sessions = @(Get-SessionProcess)

    if ($Name) {
        $sessions = @($sessions | Where-Object { $_.Instance -like $Name })
    }

    # Advisory notes go to the Warning stream, not the host. Write-Host cannot be
    # captured, redirected or suppressed, so a caller assembling a report has no way
    # to opt out of it -- which is why PSAvoidUsingWriteHost is a rule rather than a
    # style opinion. Both notes below are about the SHELL, not the sessions: run
    # elevated and they stop applying.
    if (-not (Test-SelfElevated)) {
        $opaque = @($sessions | Where-Object Opaque)
        if ($opaque.Count -gt 0) {
            Write-Warning "$($opaque.Count) claude.exe process(es) expose no command line to this shell, which is how a higher-integrity process looks from an unelevated one. They cannot be named from here, and some may not be greenroom at all. Re-run elevated for a complete listing."
        }
    }

    $stale = @()

    foreach ($s in $sessions) {
        $th  = if ($s.Opaque) { $null } else { Get-TerminalHost $s.Claude }
        $hnd = $null
        $vis = $null

        # The version the task actually runs, which is not necessarily the version loaded
        # here -- see Get-InstanceAssetVersion. Skipped for an opaque session because its
        # name is unreadable, and the name is what identifies the task.
        $asset = if ($s.Opaque) { $null } else { Get-InstanceAssetVersion -Name $s.Instance }
        if ($asset -and $asset -ne $script:GreenroomModuleVersion) {
            $stale += [PSCustomObject]@{ Instance = $s.Instance; AssetVersion = $asset }
        }

        if ($th) {
            # -Verbose flows through, so `Get-GreenroomInstance -Verbose` explains every
            # unresolved window without this function knowing how to phrase it.
            $hnd = Resolve-SessionWindow -HostPid $th.ProcessId -ClaudePid $s.Pid -Name $s.Instance
            if ($hnd) { $vis = Test-WindowVisible -Handle $hnd }
        }

        # $null rather than the strings 'unresolved' and 'n/a' the script used. A column
        # that is sometimes a handle and sometimes prose cannot be compared or filtered;
        # $null can, and the format view renders it as an empty cell either way.
        [PSCustomObject]@{
            PSTypeName  = 'Greenroom.Instance'
            Instance    = $s.Instance
            ClaudePid   = $s.Pid
            TerminalPid = if ($th) { [int]$th.ProcessId } else { $null }
            Window      = $hnd
            Visible     = $vis
            Elevated    = if ($s.Opaque) { $null } else { Test-InstanceElevated -Name $s.Instance }
            Opaque      = $s.Opaque
            AssetVersion = $asset
        }
    }

    # Emitted after the objects so it cannot interleave with them, and to the Warning
    # stream for the same reason as the note above. This is the only signal that a version
    # bump did not take: the supervisor keeps running the old assets, every version readout
    # names the new module, and nothing errors. Restart- will not fix it -- it re-runs the
    # task, and the task is what still names the old version.
    if ($stale.Count -gt 0) {
        $which = ($stale | ForEach-Object { "$($_.Instance) runs $($_.AssetVersion)" }) -join '; '
        Write-Warning ("module $script:GreenroomModuleVersion is loaded but $which. A version bump does not " +
                       'reach an instance until it is re-registered, because the task names the versioned ' +
                       'asset path: Install-GreenroomInstance -Name <name> -NoStart, then Restart-GreenroomSession.')
    }
}