public/Invoke-OctaUninstaller.ps1

function Get-OctaInstalledPrograms {
    <#
        .SYNOPSIS
        Reads the three registry locations Windows' own "Apps & features" page reads from -
        public, long-established Windows administration knowledge, not derived from any
        specific third-party tool.
    #>

    [CmdletBinding()]
    param()

    $paths = @(
        'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*',
        'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*',
        'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*'
    )

    Get-ItemProperty -Path $paths -ErrorAction SilentlyContinue |
        Where-Object { $_.DisplayName } |
        Select-Object DisplayName, Publisher, InstallDate, UninstallString, InstallLocation, PSPath |
        Sort-Object DisplayName -Unique
}

function Get-OctaUninstallMarkerPath {
    $folder = Join-Path $env:LOCALAPPDATA 'Octa'
    if (-not (Test-Path $folder)) {
        New-Item -ItemType Directory -Path $folder -Force | Out-Null
    }
    return Join-Path $folder 'last-uninstall.json'
}

function Invoke-OctaUninstaller {
    <#
        .SYNOPSIS
        Lists installed programs (FR-001), uninstalls one via its own registered uninstaller
        (FR-001), and finds/removes leftovers from the most recent uninstall (FR-002). Two
        separate, explicit confirmations - uninstalling, then cleaning leftovers - since they're
        different risks (running a program's own uninstaller vs. deleting files Octa found on
        its own).
    #>

    [CmdletBinding()]
    param(
        [string]$Remove,
        [switch]$CleanLeftovers,
        [switch]$Yes
    )

    if (-not $Remove -and -not $CleanLeftovers) {
        $programs = @(Get-OctaInstalledPrograms)
        foreach ($p in $programs) {
            Write-Host ("{0,-50} {1}" -f $p.DisplayName, $p.Publisher)
        }
        return $programs
    }

    if ($Remove) {
        $program = Get-OctaInstalledPrograms | Where-Object { $_.DisplayName -eq $Remove } | Select-Object -First 1
        if (-not $program) {
            return [pscustomobject]@{ Status = 'NotFound'; Message = "Program not found: $Remove" }
        }
        if (-not $program.UninstallString) {
            return [pscustomobject]@{ Status = 'Unsupported'; Message = "'$Remove' has no registered uninstaller." }
        }

        try {
            if ($program.UninstallString -match '^msiexec') {
                Start-Process -FilePath 'msiexec.exe' -ArgumentList ($program.UninstallString -replace '^msiexec(\.exe)?\s*', '') -Wait -ErrorAction Stop
            }
            else {
                Start-Process -FilePath 'cmd.exe' -ArgumentList "/c `"$($program.UninstallString)`"" -Wait -ErrorAction Stop
            }
        }
        catch {
            return [pscustomobject]@{ Status = 'Error'; Message = $_.Exception.Message }
        }

        $stillInstalled = Get-OctaInstalledPrograms | Where-Object { $_.DisplayName -eq $Remove }
        if ($stillInstalled) {
            return [pscustomobject]@{ Status = 'Failed'; Message = "'$Remove' still appears installed after running its uninstaller - it may require interactive confirmation, or the uninstall was cancelled." }
        }

        [pscustomobject]@{
            DisplayName     = $program.DisplayName
            Publisher       = $program.Publisher
            InstallLocation = $program.InstallLocation
        } | ConvertTo-Json | Set-Content -Path (Get-OctaUninstallMarkerPath) -Encoding utf8

        return [pscustomobject]@{ Status = 'Success'; Message = "Uninstalled $Remove" }
    }

    if ($CleanLeftovers) {
        $markerPath = Get-OctaUninstallMarkerPath
        if (-not (Test-Path $markerPath)) {
            return [pscustomobject]@{ Status = 'NothingToClean'; Message = 'No recent uninstall to clean up after. Run --remove first.' }
        }
        $marker = Get-Content -Path $markerPath -Raw | ConvertFrom-Json

        $leftovers = @()
        if ($marker.InstallLocation -and (Test-Path -LiteralPath $marker.InstallLocation)) {
            $leftovers += [pscustomobject]@{ Type = 'Folder'; Path = $marker.InstallLocation }
        }
        if ($marker.Publisher) {
            foreach ($base in @('HKCU:\Software', 'HKLM:\SOFTWARE')) {
                $candidate = Join-Path $base $marker.Publisher
                if (Test-Path -LiteralPath $candidate) {
                    $leftovers += [pscustomobject]@{ Type = 'RegistryKey'; Path = $candidate }
                }
            }
        }
        # 009/Kudu parity: also check the exact Publisher/DisplayName of THIS specific
        # just-uninstalled program under the usual app-data locations - narrower than a generic
        # "scan everything and guess" pass (research.md found that unreliable against real data),
        # this only ever acts on a program Octa already knows for certain was just removed.
        $candidateNames = @($marker.Publisher, $marker.DisplayName) | Where-Object { $_ } | Select-Object -Unique
        foreach ($base in @($env:LOCALAPPDATA, $env:APPDATA, $env:PROGRAMDATA)) {
            foreach ($name in $candidateNames) {
                $candidate = Join-Path $base $name
                if ((Test-Path -LiteralPath $candidate) -and -not ($leftovers.Path -contains $candidate)) {
                    $leftovers += [pscustomobject]@{ Type = 'Folder'; Path = $candidate }
                }
            }
        }

        if ($leftovers.Count -eq 0) {
            return [pscustomobject]@{ Status = 'NothingToClean'; Message = "No leftovers found for $($marker.DisplayName)." }
        }

        Write-Host "Leftovers found for $($marker.DisplayName):"
        foreach ($l in $leftovers) { Write-Host (" [{0}] {1}" -f $l.Type, $l.Path) }

        if (-not $Yes) {
            $response = Read-Host "Delete these leftovers? This is irreversible (y/N)"
            if ($response -notin @('y', 'Y', 's', 'S')) {
                return [pscustomobject]@{ Status = 'Cancelled' }
            }
        }

        foreach ($l in $leftovers) {
            Remove-Item -LiteralPath $l.Path -Recurse -Force -ErrorAction SilentlyContinue
        }
        Remove-Item -Path $markerPath -Force -ErrorAction SilentlyContinue

        return [pscustomobject]@{ Status = 'Success'; Removed = $leftovers }
    }
}