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.'
    }

    # 007: browser caches - each measured only if that browser is actually installed, never
    # shown as zero for an absent one.
    $browserCaches = @(
        @{ Name = 'Edge cache'; Path = "$env:LOCALAPPDATA\Microsoft\Edge\User Data\Default\Cache" },
        @{ Name = 'Chrome cache'; Path = "$env:LOCALAPPDATA\Google\Chrome\User Data\Default\Cache" },
        @{ Name = 'Firefox cache'; Path = "$env:APPDATA\Mozilla\Firefox\Profiles" }
    )
    foreach ($bc in $browserCaches) {
        if ($bc.Name -eq 'Firefox cache') {
            $profileDirs = @(Get-ChildItem -Path $bc.Path -Directory -Filter '*.default*' -ErrorAction SilentlyContinue)
            if ($profileDirs.Count -eq 0) { continue }
            $cachePaths = $profileDirs | ForEach-Object { Join-Path $_.FullName 'cache2\*' }
        }
        else {
            if (-not (Test-Path $bc.Path)) { continue }
            $cachePaths = @("$($bc.Path)\*")
        }
        $targets += [pscustomobject]@{
            Name = $bc.Name; Paths = $cachePaths
            MeasuredBytes = (Measure-OctaPathBytes -Paths $cachePaths)
            RiskLevel = 'Safe'; Reversible = $false
            IrreversibleReason = 'Browser caches are regenerated as needed; deleted cache files are not restored.'
        }
    }

    # 009/Kudu parity: gaming platform caches - each independently existence-gated, same pattern
    # as browser caches above (never shown for a platform that isn't installed). Paths verified
    # against public vendor documentation (research.md); Steam/Epic not live-tested on the dev
    # machine (neither installed there) - closed out via VM validation instead.
    $steamInstallPath = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\WOW6432Node\Valve\Steam' -ErrorAction SilentlyContinue).InstallPath
    $gamingCaches = @(
        @{ Name = 'Steam shader cache'; Path = if ($steamInstallPath) { Join-Path $steamInstallPath 'steamapps\shadercache' } else { $null } },
        @{ Name = 'Epic Games Launcher cache'; Path = "$env:LOCALAPPDATA\EpicGamesLauncher\Saved\webcache" },
        @{ Name = 'NVIDIA DirectX shader cache'; Path = "$env:LOCALAPPDATA\NVIDIA\DXCache" },
        @{ Name = 'NVIDIA OpenGL shader cache'; Path = "$env:LOCALAPPDATA\NVIDIA\GLCache" }
    )
    foreach ($gc in $gamingCaches) {
        if (-not $gc.Path -or -not (Test-Path -LiteralPath $gc.Path)) { continue }
        $targets += [pscustomobject]@{
            Name = $gc.Name; Paths = @("$($gc.Path)\*")
            MeasuredBytes = (Measure-OctaPathBytes -Paths @("$($gc.Path)\*"))
            RiskLevel = 'Safe'; Reversible = $false
            IrreversibleReason = 'Shader/launcher caches are regenerated as needed; deleted cache files are not restored.'
        }
    }

    $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 Clear-OctaFileSecurely {
    <#
        .SYNOPSIS
        Overwrites a file's content with random bytes, streamed in bounded-size chunks (never
        loads the whole file into memory), before the caller deletes it normally. 007 US3.
    #>

    [CmdletBinding()]
    param([Parameter(Mandatory)][string]$Path)

    try {
        $stream = [System.IO.File]::Open($Path, [System.IO.FileMode]::Open, [System.IO.FileAccess]::Write)
        try {
            $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()
            $buffer = New-Object byte[] 65536
            $remaining = $stream.Length
            $stream.Position = 0
            while ($remaining -gt 0) {
                $chunk = [Math]::Min($buffer.Length, $remaining)
                $rng.GetBytes($buffer, 0, $chunk)
                $stream.Write($buffer, 0, $chunk)
                $remaining -= $chunk
            }
            $stream.Flush()
        }
        finally {
            $stream.Dispose()
        }
    }
    catch {
        # Locked/inaccessible file - left for the normal Remove-Item pass to skip/report.
    }
}

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.

        .PARAMETER SecureDelete
        007 US3: overwrites each file's content before deleting it, instead of a normal delete
        that leaves recoverable data on disk. Slower - discloses this before the user confirms.
    #>

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

    $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 ($SecureDelete) {
        Write-Host 'Secure delete is on: files will be overwritten before removal (slower).'
    }
    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) {
                if ($SecureDelete) {
                    Get-ChildItem -Path $p -Recurse -File -Force -ErrorAction SilentlyContinue | ForEach-Object {
                        Clear-OctaFileSecurely -Path $_.FullName
                    }
                }
                Remove-Item -Path $p -Recurse -Force -ErrorAction SilentlyContinue
            }
        }
        $freedBytes += $t.MeasuredBytes
    }

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