public/Invoke-OctaDiskCleanup.ps1

function Get-OctaCleanupTargets {
    <#
        .SYNOPSIS
        Measures real, current reclaimable bytes per target (FR-045). Never caches/estimates.
    #>

    [CmdletBinding()]
    param()

    function Measure-OctaPathBytes {
        param([string[]]$Paths)
        $total = 0L
        foreach ($p in $Paths) {
            $items = Get-ChildItem -Path $p -Recurse -Force -ErrorAction SilentlyContinue
            $total += ($items | Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
        }
        return $total
    }

    $targets = @()

    $targets += [pscustomobject]@{
        Name = 'User temp files'; Paths = @("$env:TEMP\*")
        MeasuredBytes = (Measure-OctaPathBytes -Paths @("$env:TEMP\*"))
        RiskLevel = 'Safe'; Reversible = $false
        IrreversibleReason = 'Deleted temp files are not recoverable.'
    }
    $targets += [pscustomobject]@{
        Name = 'System temp files'; Paths = @("$env:WINDIR\Temp\*")
        MeasuredBytes = (Measure-OctaPathBytes -Paths @("$env:WINDIR\Temp\*"))
        RiskLevel = 'Safe'; Reversible = $false
        IrreversibleReason = 'Deleted temp files are not recoverable.'
    }
    $targets += [pscustomobject]@{
        Name = 'Prefetch'; Paths = @("$env:WINDIR\Prefetch\*.pf")
        MeasuredBytes = (Measure-OctaPathBytes -Paths @("$env:WINDIR\Prefetch\*.pf"))
        RiskLevel = 'Safe'; Reversible = $false
        IrreversibleReason = 'Windows regenerates prefetch data as needed; deleted files are not restored.'
    }
    $targets += [pscustomobject]@{
        Name = 'Thumbnail cache'; Paths = @("$env:LOCALAPPDATA\Microsoft\Windows\Explorer\thumbcache_*.db")
        MeasuredBytes = (Measure-OctaPathBytes -Paths @("$env:LOCALAPPDATA\Microsoft\Windows\Explorer\thumbcache_*.db"))
        RiskLevel = 'Safe'; Reversible = $false
        IrreversibleReason = 'Thumbnails are regenerated on demand; deleted cache files are not restored.'
    }
    $targets += [pscustomobject]@{
        Name = 'Windows Update leftovers'; Paths = @("$env:WINDIR\SoftwareDistribution\Download\*")
        MeasuredBytes = (Measure-OctaPathBytes -Paths @("$env:WINDIR\SoftwareDistribution\Download\*"))
        RiskLevel = 'Safe'; Reversible = $false
        IrreversibleReason = 'Windows re-downloads update files if needed again.'
    }
    $targets += [pscustomobject]@{
        Name = 'Crash dumps'; Paths = @("$env:WINDIR\Minidump\*", "$env:WINDIR\memory.dmp")
        MeasuredBytes = (Measure-OctaPathBytes -Paths @("$env:WINDIR\Minidump\*", "$env:WINDIR\memory.dmp"))
        RiskLevel = 'Safe'; Reversible = $false
        IrreversibleReason = 'Crash dump files are diagnostic-only; deleting them loses that diagnostic history.'
    }

    $recycleBinBytes = 0L
    try {
        $shell = New-Object -ComObject Shell.Application
        $recycleBin = $shell.Namespace(10)
        $recycleBinBytes = ($recycleBin.Items() | ForEach-Object { $_.Size } | Measure-Object -Sum).Sum
    }
    catch { $recycleBinBytes = 0L }
    $targets += [pscustomobject]@{
        Name = 'Recycle Bin'; Paths = @('(Recycle Bin)')
        MeasuredBytes = $recycleBinBytes
        RiskLevel = 'Safe'; Reversible = $false
        IrreversibleReason = 'Emptying the Recycle Bin permanently deletes its contents.'
    }

    $windowsOldPath = "$env:SystemDrive\Windows.old"
    if (Test-Path $windowsOldPath) {
        $targets += [pscustomobject]@{
            Name = 'Old Windows installation (Windows.old)'; Paths = @($windowsOldPath)
            MeasuredBytes = (Measure-OctaPathBytes -Paths @("$windowsOldPath\*"))
            RiskLevel = 'Risky'; Reversible = $false
            IrreversibleReason = 'Removing this forfeits the ability to roll back a Windows feature upgrade.'
        }
    }

    return $targets
}

function Invoke-OctaDiskCleanup {
    <#
        .SYNOPSIS
        Dry-run and (optionally) apply disk cleanup across known, standard Windows locations.
        FR-045/FR-046. Attempts a System Restore Point before deleting anything, even though
        the deletions themselves are not undo-able the way a registry action is.
    #>

    [CmdletBinding()]
    param(
        [switch]$Apply,
        [switch]$Yes
    )

    $targets = Get-OctaCleanupTargets
    $totalBytes = ($targets | Measure-Object -Property MeasuredBytes -Sum).Sum

    Write-Host 'Disk Cleanup targets (all irreversible except where noted):'
    foreach ($t in $targets) {
        $gb = [Math]::Round($t.MeasuredBytes / 1GB, 2)
        Write-Host (" {0,-40} {1,8} GB [{2}]" -f $t.Name, $gb, $t.RiskLevel)
    }
    Write-Host ("Total reclaimable: {0} GB" -f [Math]::Round($totalBytes / 1GB, 2))

    if (-not $Apply) {
        return [pscustomobject]@{ Status = 'DryRun'; Targets = $targets; TotalBytes = $totalBytes }
    }

    $safeTargets = @($targets | Where-Object { $_.RiskLevel -ne 'Risky' })
    $riskyTargets = @($targets | Where-Object { $_.RiskLevel -eq 'Risky' })

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

    $applyTargets = $safeTargets
    if ($riskyTargets.Count -gt 0) {
        Write-Host 'Risky targets (confirm separately):'
        foreach ($t in $riskyTargets) { Write-Host " $($t.Name)" }
        if ($Yes) {
            $applyTargets += $riskyTargets
        }
        else {
            $riskyResponse = Read-Host 'Delete these too? (y/N)'
            if ($riskyResponse -in @('y', 'Y', 's', 'S')) { $applyTargets += $riskyTargets }
        }
    }

    New-OctaRestorePoint -Description 'Octa disk cleanup' | Out-Null

    $freedBytes = 0L
    foreach ($t in $applyTargets) {
        if ($t.Name -eq 'Recycle Bin') {
            Clear-RecycleBin -Force -ErrorAction SilentlyContinue
        }
        else {
            foreach ($p in $t.Paths) {
                Remove-Item -Path $p -Recurse -Force -ErrorAction SilentlyContinue
            }
        }
        $freedBytes += $t.MeasuredBytes
    }

    return [pscustomobject]@{ Status = 'Success'; FreedBytes = $freedBytes }
}