src/UI/65-Cleanup.ps1

# 65-Cleanup.ps1 — duplicate-version cleanup across ALL installed modules:
# Update-Module/-PSResource never remove old versions, so they pile up (the
# single most-requested module-management fix in the research for #36/#37).

function script:Build-PSMMCleanupView {
    param(
        [Parameter(Mandatory)] $State,
        [Parameter(Mandatory)] $Dupes,
        [string]$StatusMarkup
    )
    $n = $Dupes.Count
    $win = Get-PSMMWinSize
    $vp = Get-PSMMViewport -State $State -Count $n -Rows ($win.Height - 11)
    $T = [Spectre.Console.Table]::new()
    $T.Border = [Spectre.Console.TableBorder]::Rounded
    foreach ($h in ' ', 'Module', 'Keep', 'Remove', 'Scopes') { [void][Spectre.Console.TableExtensions]::AddColumn($T, $h) }
    for ($i = $vp.First; $i -le $vp.Last; $i++) {
        $d = $Dupes[$i]
        $nm = ConvertTo-PSMMSafe (Get-PSMMTrunc $d.Name 40)
        if ($i -eq $State.Cursor) { $nm = "[$script:PSMM_ColAccent]$nm[/]" }
        $obsVers = (@($d.Obsolete | ForEach-Object { "v$($_.Version)" }) -join ', ')
        $scopes = (@($d.Obsolete.Scope | Select-Object -Unique) -join ', ')
        [void][Spectre.Console.TableExtensions]::AddRow($T, [string[]]@(
                $(if ($i -eq $State.Cursor) { "[$script:PSMM_ColAccent]>[/]" } else { ' ' }),
                $nm, "[green3]v$($d.Latest)[/]", (ConvertTo-PSMMSafe (Get-PSMMTrunc $obsVers 40)), $scopes))
    }
    $pos = Get-PSMMPositionMarkup -State $State -Count $n -Viewport $vp
    $items = [System.Collections.Generic.List[Spectre.Console.Rendering.IRenderable]]::new()
    $items.Add([Spectre.Console.Markup]::new("[$script:PSMM_ColAccent]Clean up old module versions[/] [$script:PSMM_ColMute]$n module(s) with multiple versions on disk[/]$pos"))
    $items.Add($T)
    if (-not $script:PSMM_UI.Elevated) {
        $items.Add([Spectre.Console.Markup]::new('[grey66]session is not elevated: AllUsers copies are skipped automatically[/]'))
    }
    $items.Add([Spectre.Console.Markup]::new((Get-PSMMHint -Pairs @('up/dn=move', 'enter=clean this module', 'A=clean ALL', 'r=rescan', '?=help', 'esc=back', 'Ctrl+Q=quit'))))
    if ($StatusMarkup) { $items.Add([Spectre.Console.Markup]::new($StatusMarkup)) }
    [Spectre.Console.Rows]::new($items)
}

function script:Show-PSMMCleanup {
    $ui = $script:PSMM_UI
    Clear-PSMMScreen
    Write-PSMMLine "[$script:PSMM_ColAccent]scanning installed modules for duplicate versions...[/]"
    $dupes = @(Get-PSMMDuplicateVersion)
    $status = ''
    while ($true) {
        if ($ui.HardQuit) { return }
        if (-not $dupes.Count) {
            Clear-PSMMScreen
            Write-PSMMLine "[$script:PSMM_ColAccent]Clean up old module versions[/]"
            Write-PSMMLine '[green3]No module has more than one installed version - nothing to clean.[/]'
            $null = Wait-PSMMKey -Message 'back'
            return
        }
        $st = New-PSMMListState
        $st.Status = $status
        $action = @{ Name = $null }
        Clear-PSMMScreen
        Invoke-PSMMLive -Body {
            param($ctx)
            while ($true) {
                if ($script:PSMM_UI.HardQuit) { return }
                $ctx.UpdateTarget((Build-PSMMCleanupView -State $st -Dupes $dupes -StatusMarkup $st.Status))
                $ctx.Refresh()
                $k = Read-PSMMKeyResize
                if ($null -eq $k) { continue }
                if (Test-PSMMHardQuitKey $k) { $script:PSMM_UI.HardQuit = $true; return }
                $st.Status = ''
                if (Invoke-PSMMListNav -State $st -KeyInfo $k -Count $dupes.Count) { continue }
                switch ($k.Key) {
                    ([ConsoleKey]::Enter)  { $action.Name = 'one'; return }
                    ([ConsoleKey]::A)      { if (($k.Modifiers -band [ConsoleModifiers]::Shift) -ne 0) { $action.Name = 'all'; return }; continue }
                    ([ConsoleKey]::R)      { $action.Name = 'rescan'; return }
                    ([ConsoleKey]::Escape) { $action.Name = 'back'; return }
                    default { if ($k.KeyChar -eq '?') { $action.Name = 'help'; return } }
                }
            }
        }
        if ($ui.HardQuit) { return }
        switch ($action.Name) {
            'back' { return }
            'help' { Show-PSMMHelpScreen -Topic 'cleanup' }
            'rescan' {
                Clear-PSMMScreen
                Write-PSMMLine "[$script:PSMM_ColAccent]rescanning...[/]"
                $dupes = @(Get-PSMMDuplicateVersion)
                $status = '[green3]rescanned[/]'
            }
            'one' {
                $d = $dupes[$st.Cursor]
                $status = Invoke-PSMMDupeCleanup -Dupes @($d)
                $dupes = @(Get-PSMMDuplicateVersion)
            }
            'all' {
                $status = Invoke-PSMMDupeCleanup -Dupes $dupes
                $dupes = @(Get-PSMMDuplicateVersion)
            }
        }
    }
}

# Remove obsolete versions for the given dupe records (with one confirm).
function script:Invoke-PSMMDupeCleanup {
    param([Parameter(Mandatory)] $Dupes)
    $ui = $script:PSMM_UI
    $work = @(foreach ($d in $Dupes) {
        foreach ($v in $d.Obsolete) {
            if ($v.Scope -eq 'AllUsers' -and -not $ui.Elevated) { continue }
            [pscustomobject]@{ Name = $d.Name; Version = $v.Version; Scope = $v.Scope }
        }
    })
    $skipped = @(foreach ($d in $Dupes) { @($d.Obsolete | Where-Object { $_.Scope -eq 'AllUsers' -and -not $ui.Elevated }) }).Count
    Clear-PSMMScreen
    Write-PSMMLine "[$script:PSMM_ColAccent]Clean up old versions[/]"
    if ($skipped) { Write-PSMMLine "[orange1]$skipped AllUsers version(s) skipped - session is not elevated[/]" }
    if (-not $work.Count) { $null = Wait-PSMMKey; return '[orange1]nothing removable without elevation[/]' }
    foreach ($w in $work) { Write-PSMMLine " $(ConvertTo-PSMMSafe $w.Name) v$($w.Version) [grey66]($($w.Scope))[/]" }
    if (-not (Read-SpectreConfirm -Message "Remove these $($work.Count) old version(s)?" -DefaultAnswer 'n')) { return '[grey66]cleanup cancelled[/]' }
    $ok = 0; $failed = 0
    foreach ($w in $work) {
        Write-PSMMLine "[$script:PSMM_ColAccent]removing $(ConvertTo-PSMMSafe $w.Name) v$($w.Version)...[/]"
        try { Uninstall-PSMMModuleVersion -Name $w.Name -Version "$($w.Version)"; $ok++ }
        catch { $failed++; Write-PSMMLine "[indianred1] $(ConvertTo-PSMMSafe $_.Exception.Message)[/]" }
    }
    if ($failed) { "[orange1]removed $ok, $failed failed[/]" } else { "[green3]removed $ok old version(s)[/]" }
}