Public/Start-SACInteractive.ps1
|
function Start-SACInteractive { <# .SYNOPSIS Interactive CLI main menu for the Surgical Autodesk Cleaner toolkit. .DESCRIPTION Presents a top-level action menu that surfaces all SAC tools: Surgical Cleanup, Master Purge, User Profile Reset, Licensing Reset, Pre-Flight Scan, and Profile Backup Restore. Supports Out-ConsoleGridView on PowerShell 7+ with automatic fallback to a native console menu. #> [CmdletBinding()] param() # ------------------------------------------------------------------------- # Shared helper: multi-select list (GridView on PS7+, text fallback) # ------------------------------------------------------------------------- function Invoke-SACSelection { param ( [Parameter(Mandatory=$true)][array]$Items, [string]$Title ) if ($Items.Count -eq 0) { return @() } if ($PSVersionTable.PSVersion.Major -ge 7) { if (-not (Get-Command Out-ConsoleGridView -ErrorAction SilentlyContinue)) { try { Import-Module Microsoft.PowerShell.ConsoleGuiTools -ErrorAction Stop } catch { try { Write-Host "Installing Microsoft.PowerShell.ConsoleGuiTools for enhanced UI..." -ForegroundColor Cyan Install-Module Microsoft.PowerShell.ConsoleGuiTools -Force -Scope CurrentUser -ErrorAction Stop Import-Module Microsoft.PowerShell.ConsoleGuiTools -ErrorAction Stop } catch {} } } if (Get-Command Out-ConsoleGridView -ErrorAction SilentlyContinue) { try { $selected = $Items | Out-ConsoleGridView -Title "$Title (CTRL-A = Select All | SPACE = Toggle | ENTER = Confirm)" -OutputMode Multiple -ErrorAction Stop if ($selected) { return @($selected) } return @() } catch { Write-Host "`n[!] UI rendering unavailable. Falling back to text menu..." -ForegroundColor Yellow } } } Write-Host "`n--- $Title ---" -ForegroundColor Cyan Write-Host "0. ALL ITEMS" -ForegroundColor Yellow for ($i = 0; $i -lt $Items.Count; $i++) { Write-Host "$($i+1). $($Items[$i])" } $inputStr = Read-Host "`nEnter numbers to select (comma-separated, or 0 for all)" if ($inputStr -match '0') { return $Items } $selected = @() $indices = $inputStr -split ',' | ForEach-Object { $_.Trim() } foreach ($idx in $indices) { if ($idx -match '^\d+$' -and [int]$idx -gt 0 -and [int]$idx -le $Items.Count) { $selected += $Items[[int]$idx - 1] } } return $selected } # ------------------------------------------------------------------------- # Discover installed Autodesk products for header context # ------------------------------------------------------------------------- function Get-InstalledAutodeskSummary { # Known primary/parent product keywords — these float to the top $PrimaryProducts = @( 'AutoCAD','Revit','Civil 3D','Inventor','Navisworks','3ds Max','Maya', 'Fusion','Vault','ReCap','Advance Steel','BIM 360','InfraWorks' ) $all = Get-ItemProperty -Path @( 'HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*', 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*' ) -ErrorAction SilentlyContinue | Where-Object { $_.Publisher -match 'Autodesk' -or $_.DisplayName -match 'Autodesk' } | Where-Object { $_.DisplayName -match '\b20\d{2}\b' } | Select-Object -ExpandProperty DisplayName -Unique # Score: 0 = primary product, 1 = everything else $sorted = $all | Sort-Object { $name = $_ $isPrimary = ($PrimaryProducts | Where-Object { $name -match [regex]::Escape($_) }).Count -gt 0 if ($isPrimary) { 0 } else { 1 } }, { $_ } # secondary sort: alphabetical within each group return $sorted } # ------------------------------------------------------------------------- # Surgical Cleanup flow (product + year selection → Start-SACCleanup) # ------------------------------------------------------------------------- function Invoke-SurgicalCleanupFlow { param ([bool]$ScanOnly = $false) Write-Host "`nScanning registry for installed Autodesk products..." -ForegroundColor Cyan $UninstallKeys = Get-ItemProperty -Path @( 'HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*', 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*' ) -ErrorAction SilentlyContinue | Where-Object { $_.Publisher -match 'Autodesk' -or $_.DisplayName -match 'Autodesk' } if (-not $UninstallKeys) { Write-Host "No Autodesk products found on this system." -ForegroundColor Yellow return } $FoundProducts = @() foreach ($app in $UninstallKeys) { if ($app.DisplayName -match '\b(20\d{2})\b') { $year = $matches[1] $FoundProducts += [PSCustomObject]@{ DisplayName = $app.DisplayName Year = [int]$year } } } if ($FoundProducts.Count -eq 0) { Write-Host "No versioned Autodesk products found." -ForegroundColor Yellow return } $AvailableYears = $FoundProducts.Year | Select-Object -Unique | Sort-Object $AllAppNames = @() foreach ($fp in $FoundProducts) { $name = $fp.DisplayName -replace '\s*\b20\d{2}\b.*', '' -replace '^Autodesk\s+', '' $name = $name.Trim() if ([string]::IsNullOrWhiteSpace($name)) { $name = "Autodesk Core" } if ($AllAppNames -notcontains $name) { $AllAppNames += $name } } $AllAppNames = $AllAppNames | Sort-Object Write-Host "`nHow would you like to target?" -ForegroundColor Cyan Write-Host " [1] By Product" Write-Host " [2] By Year" Write-Host " [3] By Product + Year (recommended)" $modeInput = Read-Host "Select [1-3]" if ([string]::IsNullOrWhiteSpace($modeInput) -or $modeInput -notin @("1","2","3")) { $modeInput = "3" } $SelectedProducts = @() $SelectedYears = @() if ($modeInput -eq "1") { $SelectedProducts = Invoke-SACSelection -Items $AllAppNames -Title "Select Products" if (-not $SelectedProducts) { Write-Host "No products selected." -ForegroundColor Red; return } $SelectedYears = $AvailableYears } elseif ($modeInput -eq "2") { $SelectedYears = Invoke-SACSelection -Items $AvailableYears -Title "Select Years" if (-not $SelectedYears) { Write-Host "No years selected." -ForegroundColor Red; return } $SelectedProducts = $AllAppNames } else { $SelectedYears = Invoke-SACSelection -Items $AvailableYears -Title "Step 1 of 2 — Select Target Years" if (-not $SelectedYears) { Write-Host "No years selected." -ForegroundColor Red; return } $FilteredProducts = @() foreach ($fp in ($FoundProducts | Where-Object { $_.Year -in $SelectedYears })) { $name = $fp.DisplayName -replace '\s*\b20\d{2}\b.*', '' -replace '^Autodesk\s+', '' $name = $name.Trim() if ([string]::IsNullOrWhiteSpace($name)) { $name = "Autodesk Core" } if ($FilteredProducts -notcontains $name) { $FilteredProducts += $name } } $FilteredProducts = $FilteredProducts | Sort-Object $SelectedProducts = Invoke-SACSelection -Items $FilteredProducts -Title "Step 2 of 2 — Select Products for $($SelectedYears -join ', ')" if (-not $SelectedProducts) { Write-Host "No products selected." -ForegroundColor Red; return } } if ($ScanOnly) { Write-Host "`nBuilding pre-flight scan report..." -ForegroundColor Cyan $Report = @() foreach ($product in $SelectedProducts) { foreach ($year in $SelectedYears) { $PackageName = "*$product*$year*" $keys = Get-ItemProperty -Path @( 'HKLM:\Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*', 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*' ) -ErrorAction SilentlyContinue | Where-Object { $_.DisplayName -like $PackageName } foreach ($app in $keys) { $actionType = if ($app.PSChildName -match '^{.*}$' -and $app.UninstallString -match '^MsiExec\.exe') { "MSI Uninstall" } else { "Custom Uninstall" } $Report += [PSCustomObject]@{ Action="$actionType"; ComponentType="Application" TargetProduct=$product; TargetYear=$year DisplayName=$app.DisplayName; Detail=$app.UninstallString } } } } $ProcessesToKill = @("acad*","AcEventSync*","AcQMod*","revit*","3dsmax*","maya*","inventor*","roamer*","navisworks*","recap*","dwgviewr*") foreach ($procPattern in $ProcessesToKill) { Get-Process -Name $procPattern -ErrorAction SilentlyContinue | ForEach-Object { $Report += [PSCustomObject]@{ Action="Terminate Process"; ComponentType="Active Process" TargetProduct="Global"; TargetYear="Global" DisplayName="$($_.ProcessName).exe"; Detail="PID: $($_.Id)" } } } $SafePathsToSearch = @( "$($env:ProgramFiles)\Autodesk","$(${env:ProgramFiles(x86)})\Autodesk", "$($env:ProgramData)\Autodesk","$($env:PUBLIC)\Documents\Autodesk", "C:\Users\*\AppData\Local\Autodesk","C:\Users\*\AppData\Roaming\Autodesk" ) foreach ($product in $SelectedProducts) { foreach ($year in $SelectedYears) { foreach ($basePath in $SafePathsToSearch) { Get-ChildItem -Path $basePath -Filter "*$product*$year*" -Directory -ErrorAction SilentlyContinue | ForEach-Object { $Report += [PSCustomObject]@{ Action="Purge Directory"; ComponentType="Orphaned Folder" TargetProduct=$product; TargetYear=$year DisplayName=$_.Name; Detail=$_.FullName } } } } } if ($Report.Count -gt 0) { $Report = $Report | Select-Object -Unique * $OutPath = "$([Environment]::GetFolderPath('Desktop'))\Autodesk_ScanReport_$(Get-Date -Format 'yyyyMMdd_HHmmss').csv" $Report | Export-Csv -Path $OutPath -NoTypeInformation -Force Write-Host "Scan Complete! $($Report.Count) actions identified." -ForegroundColor Green Write-Host "Report saved to: $OutPath`n" -ForegroundColor Green } else { Write-Host "Scan Complete! No matching components would be removed.`n" -ForegroundColor Yellow } } else { Write-Host "`nExecuting Surgical Cleanup..." -ForegroundColor Cyan Start-SACCleanup -TargetProducts $SelectedProducts -TargetYears $SelectedYears -AnyVendor } } # ------------------------------------------------------------------------- # Main menu loop # ------------------------------------------------------------------------- $installedProducts = Get-InstalledAutodeskSummary while ($true) { Clear-Host # Fixed inner width (between the ║ borders) = 60 chars $W = 60 function Write-BoxLine { param( [string]$Text = "", [string]$Color = "Cyan", [string]$BorderColor = "DarkCyan" ) $inner = " $Text".PadRight($W) Write-Host "║" -ForegroundColor $BorderColor -NoNewline Write-Host $inner -ForegroundColor $Color -NoNewline Write-Host "║" -ForegroundColor $BorderColor } $border = "═" * $W $borderLine = "║" + (" " * $W) + "║" # ── Top border + title ── Write-Host "╔$border╗" -ForegroundColor DarkCyan Write-BoxLine -Text (" SURGICAL AUTODESK CLEANER v1.1.0").PadLeft(($W + " SURGICAL AUTODESK CLEANER v1.1.0".Length) / 2) -Color "Cyan" Write-Host "╠$border╣" -ForegroundColor DarkCyan # ── Detected products ── if ($installedProducts -and $installedProducts.Count -gt 0) { Write-BoxLine -Text "Detected Products:" -Color "DarkCyan" foreach ($p in ($installedProducts | Select-Object -First 5)) { $truncated = if ($p.Length -gt ($W - 4)) { $p.Substring(0, $W - 7) + "..." } else { $p } Write-BoxLine -Text " $truncated" -Color "Gray" } if ($installedProducts.Count -gt 5) { Write-BoxLine -Text " ...and $($installedProducts.Count - 5) more" -Color "DarkGray" } } else { Write-BoxLine -Text " No versioned Autodesk products detected." -Color "Yellow" } # ── Menu options ── Write-Host "╠$border╣" -ForegroundColor DarkCyan Write-Host $borderLine -ForegroundColor DarkCyan Write-BoxLine -Text " [1] Surgical Cleanup Targeted uninstall by product/year" Write-BoxLine -Text " [2] Master Purge Scorched-earth full system removal" Write-BoxLine -Text " [3] Reset User Profile Rename/clear per-user AppData and reg" Write-BoxLine -Text " [4] Reset Licensing Wipe CLM, token cache and FlexNet" Write-BoxLine -Text " [5] Pre-Flight Scan Simulate cleanup, export CSV report" Write-BoxLine -Text " [6] Restore User Profile List/restore SAC backup folders" Write-BoxLine -Text " [Q] Quit" Write-Host $borderLine -ForegroundColor DarkCyan Write-Host "╚$border╝" -ForegroundColor DarkCyan Write-Host "" $choice = Read-Host " Select an option" switch ($choice.Trim().ToUpper()) { "1" { Invoke-SurgicalCleanupFlow -ScanOnly $false Write-Host "`nPress any key to return to menu..." -ForegroundColor DarkGray $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") } "2" { Write-Host "`n *** MASTER PURGE — This will remove ALL Autodesk software ***" -ForegroundColor Red Write-Host " This action is irreversible. All products, services, and" -ForegroundColor Yellow Write-Host " registry data will be forcefully removed from this machine.`n" -ForegroundColor Yellow $confirm = Read-Host " Type 'PURGE' to confirm" if ($confirm -eq "PURGE") { Start-SACPurge } else { Write-Host " Master Purge cancelled." -ForegroundColor Yellow } Write-Host "`nPress any key to return to menu..." -ForegroundColor DarkGray $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") } "3" { Write-Host "`n Roaming folders will be RENAMED (backed up) by default." -ForegroundColor Cyan Write-Host " Local cache folders will be DELETED outright.`n" -ForegroundColor Cyan $delRoaming = $false $delChoice = Read-Host " Delete Roaming instead of rename? (y/N)" if ($delChoice.Trim().ToLower() -eq "y") { $delRoaming = $true } if ($delRoaming) { Reset-SACUserProfile -DeleteRoaming } else { Reset-SACUserProfile } Write-Host "`nPress any key to return to menu..." -ForegroundColor DarkGray $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") } "4" { Write-Host "`n FlexNet (adsk* stubs) removal is optional.`n" -ForegroundColor Cyan $flexChoice = Read-Host " Also remove Autodesk FlexNet stubs? (y/N)" if ($flexChoice.Trim().ToLower() -eq "y") { Reset-SACLicensing -IncludeFlexNet } else { Reset-SACLicensing } Write-Host "`nPress any key to return to menu..." -ForegroundColor DarkGray $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") } "5" { Invoke-SurgicalCleanupFlow -ScanOnly $true Write-Host "`nPress any key to return to menu..." -ForegroundColor DarkGray $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") } "6" { Restore-SACUserProfile -List Write-Host "" $restoreChoice = Read-Host " Restore a specific backup? Enter full path (or press Enter to return)" if (-not [string]::IsNullOrWhiteSpace($restoreChoice)) { Restore-SACUserProfile -Restore -BackupPath $restoreChoice.Trim() } Write-Host "`nPress any key to return to menu..." -ForegroundColor DarkGray $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown") } "Q" { Write-Host "`nExiting Surgical Autodesk Cleaner. Goodbye.`n" -ForegroundColor Cyan return } default { Write-Host "`n Invalid option. Please select 1-6 or Q." -ForegroundColor Yellow Start-Sleep -Seconds 1 } } } } |