Public/Uninstall-GreenroomInstance.ps1

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

<#
.SYNOPSIS
  Remove one greenroom instance: its task, its running session and its state.

.DESCRIPTION

  NEVER touches the working directory -- it holds your work. Trust entries in
  ~/.claude.json are also left alone, which is a known gap rather than a decision:
  install seeds two per instance and nothing removes them.

  A third of the script this replaces is simply gone. -InstallDir and -RemoveScripts
  existed to delete copied scripts and a generated cmd shim out of a bin directory,
  along with the marker-matching that stopped it deleting somebody else's file of the
  same name. A module has none of that: the code lives in the module, and removing the
  code is Uninstall-PSResource. Removing an INSTANCE and removing the SOFTWARE are
  different operations, and conflating them is what made that flag necessary.

  Order matters. The task is unregistered first, so its trigger cannot start a
  replacement watchdog while the rest of this runs.

  Kills are identity-re-verified immediately before each one, which the script did not
  do: it enumerated, then killed. A pid recorded moments earlier can already belong to
  something else.

.PARAMETER Name
  The instance to remove.

.PARAMETER KeepState
  Leave the state directory (config, window record, logs) in place.

.OUTPUTS
  Greenroom.UninstallResult -- what was actually removed, as data rather than prose.

.EXAMPLE
  Uninstall-GreenroomInstance -Name modtest

.EXAMPLE
  Uninstall-GreenroomInstance -Name modtest -WhatIf

.EXAMPLE
  Uninstall-GreenroomInstance -Name modtest -KeepState
  Keeps the logs, which is what you want when removing an instance to diagnose it.
#>

function Uninstall-GreenroomInstance {
    [CmdletBinding(SupportsShouldProcess)]
    [OutputType('Greenroom.UninstallResult')]
    param(
        [Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [Alias('Instance')]
        [string]$Name,

        [switch]$KeepState
    )

    process {
        $task     = "greenroom-$Name"
        $stateDir = Join-Path (Get-GreenroomStateRoot) $Name
        $esc      = [regex]::Escape($Name)

        # Read the config BEFORE anything is removed. config.json lives inside the state
        # directory, so reading it afterwards always returns $null.
        $cfg = Get-InstanceConfig -Name $Name

        $taskExists = [bool](Get-ScheduledTask -TaskName $task -ErrorAction SilentlyContinue)
        if (-not $taskExists -and -not (Test-Path $stateDir)) {
            Write-Error -Category ObjectNotFound -Message "'$Name' is not installed: no task '$task' and no state directory."
            return
        }

        if (-not $PSCmdlet.ShouldProcess($Name, 'Uninstall-GreenroomInstance')) { return }

        # The task first. Its trigger would otherwise be free to start a replacement
        # watchdog while the kills below are still running.
        if ($taskExists) {
            Stop-ScheduledTask -TaskName $task -ErrorAction SilentlyContinue
            Unregister-ScheduledTask -TaskName $task -Confirm:$false
            Write-Verbose "task removed: $task"
        }
        else {
            Write-Verbose "no scheduled task named $task"
        }

        $shells   = 'pwsh.exe', 'powershell.exe'
        $watchdog = Stop-VerifiedProcess -ProcessName $shells      -Pattern ('greenroom-watchdog.*-Instance\s+"?' + $esc + '("|\s|$)') -Label 'watchdog'
        $session  = Stop-VerifiedProcess -ProcessName 'claude.exe' -Pattern ('--remote-control\s+"?' + $esc + '("|\s|$)')                 -Label 'session'
        $launcher = Stop-VerifiedProcess -ProcessName $shells      -Pattern ('greenroom-launch.*-Instance\s+"?' + $esc + '("|\s|$)')   -Label 'launcher'

        $stateRemoved = $false
        if ($KeepState) {
            Write-Verbose "state kept: $stateDir"
        }
        elseif (Test-Path $stateDir) {
            Remove-Item $stateDir -Recurse -Force
            $stateRemoved = $true
            Write-Verbose "state removed: $stateDir"
        }

        # Returned rather than printed. The facts about what survived are the ones an
        # operator needs, and as data they can be asserted on instead of read.
        [PSCustomObject]@{
            PSTypeName       = 'Greenroom.UninstallResult'
            Instance         = $Name
            TaskRemoved      = $taskExists
            WatchdogStopped  = $watchdog
            SessionStopped   = $session
            LauncherStopped  = $launcher
            StateRemoved     = $stateRemoved
            WorkingDirectory = if ($cfg) { $cfg.workingDirectory } else { $null }
        }
    }
}