1
0
mirror of https://github.com/tw93/Mole.git synced 2026-02-14 18:47:28 +00:00

feat(windows): add Windows support Phase 2 - cleanup features

Add cleanup modules and command scripts for Windows:
- lib/clean: user, caches, dev, apps, system cleanup modules
- bin/clean: deep cleanup orchestrator with dry-run and whitelist
- bin/uninstall: interactive app uninstaller
- bin/optimize: system optimization and health checks
- bin/purge: project artifact cleanup

All scripts support dry-run mode and follow safe deletion practices.
This commit is contained in:
Bhadra
2026-01-08 15:46:45 +05:30
parent a53b48ef3b
commit 6e0d850d6a
10 changed files with 4174 additions and 0 deletions

300
windows/bin/clean.ps1 Normal file
View File

@@ -0,0 +1,300 @@
# Mole - Clean Command
# Deep cleanup for Windows with dry-run support and whitelist
#Requires -Version 5.1
[CmdletBinding()]
param(
[switch]$DryRun,
[switch]$System,
[switch]$DebugMode,
[switch]$Whitelist,
[switch]$ShowHelp
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
# Script location
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$libDir = Join-Path (Split-Path -Parent $scriptDir) "lib"
# Import core modules
. "$libDir\core\base.ps1"
. "$libDir\core\log.ps1"
. "$libDir\core\ui.ps1"
. "$libDir\core\file_ops.ps1"
# Import cleanup modules
. "$libDir\clean\user.ps1"
. "$libDir\clean\caches.ps1"
. "$libDir\clean\dev.ps1"
. "$libDir\clean\apps.ps1"
. "$libDir\clean\system.ps1"
# ============================================================================
# Configuration
# ============================================================================
$script:ExportListFile = "$env:USERPROFILE\.config\mole\clean-list.txt"
# ============================================================================
# Help
# ============================================================================
function Show-CleanHelp {
$esc = [char]27
Write-Host ""
Write-Host "$esc[1;35mMole Clean$esc[0m - Deep cleanup for Windows"
Write-Host ""
Write-Host "$esc[33mUsage:$esc[0m mole clean [options]"
Write-Host ""
Write-Host "$esc[33mOptions:$esc[0m"
Write-Host " -DryRun Preview changes without deleting (recommended first run)"
Write-Host " -System Include system-level cleanup (requires admin)"
Write-Host " -Whitelist Manage protected paths"
Write-Host " -DebugMode Enable debug logging"
Write-Host " -ShowHelp Show this help message"
Write-Host ""
Write-Host "$esc[33mExamples:$esc[0m"
Write-Host " mole clean -DryRun # Preview what would be cleaned"
Write-Host " mole clean # Run standard cleanup"
Write-Host " mole clean -System # Include system cleanup (as admin)"
Write-Host ""
}
# ============================================================================
# Whitelist Management
# ============================================================================
function Edit-Whitelist {
$whitelistPath = $script:Config.WhitelistFile
$whitelistDir = Split-Path -Parent $whitelistPath
# Ensure directory exists
if (-not (Test-Path $whitelistDir)) {
New-Item -ItemType Directory -Path $whitelistDir -Force | Out-Null
}
# Create default whitelist if doesn't exist
if (-not (Test-Path $whitelistPath)) {
$defaultContent = @"
# Mole Whitelist - Paths listed here will never be cleaned
# Use full paths or patterns with wildcards (*)
#
# Examples:
# C:\Users\YourName\Documents\ImportantProject
# C:\Users\*\AppData\Local\MyApp
# $env:LOCALAPPDATA\CriticalApp
#
# Add your protected paths below:
"@
Set-Content -Path $whitelistPath -Value $defaultContent
}
# Open in default editor
Write-Info "Opening whitelist file: $whitelistPath"
Start-Process notepad.exe -ArgumentList $whitelistPath -Wait
Write-Success "Whitelist saved"
}
# ============================================================================
# Cleanup Summary
# ============================================================================
function Show-CleanupSummary {
param(
[hashtable]$Stats,
[bool]$IsDryRun
)
$esc = [char]27
Write-Host ""
Write-Host "$esc[1;35m" -NoNewline
if ($IsDryRun) {
Write-Host "Dry run complete - no changes made" -NoNewline
}
else {
Write-Host "Cleanup complete" -NoNewline
}
Write-Host "$esc[0m"
Write-Host ""
if ($Stats.TotalSizeKB -gt 0) {
$sizeGB = [Math]::Round($Stats.TotalSizeKB / 1024 / 1024, 2)
if ($IsDryRun) {
Write-Host " Potential space: $esc[32m${sizeGB}GB$esc[0m"
Write-Host " Items found: $($Stats.FilesCleaned)"
Write-Host " Categories: $($Stats.TotalItems)"
Write-Host ""
Write-Host " Detailed list: $esc[90m$($script:ExportListFile)$esc[0m"
Write-Host " Run without -DryRun to apply cleanup"
}
else {
Write-Host " Space freed: $esc[32m${sizeGB}GB$esc[0m"
Write-Host " Items cleaned: $($Stats.FilesCleaned)"
Write-Host " Categories: $($Stats.TotalItems)"
Write-Host ""
Write-Host " Free space now: $(Get-FreeSpace)"
}
}
else {
if ($IsDryRun) {
Write-Host " No significant reclaimable space detected."
}
else {
Write-Host " System was already clean; no additional space freed."
}
Write-Host " Free space now: $(Get-FreeSpace)"
}
Write-Host ""
}
# ============================================================================
# Main Cleanup Flow
# ============================================================================
function Start-Cleanup {
param(
[bool]$IsDryRun,
[bool]$IncludeSystem
)
$esc = [char]27
# Clear screen
Clear-Host
Write-Host ""
Write-Host "$esc[1;35mClean Your Windows$esc[0m"
Write-Host ""
# Show mode
if ($IsDryRun) {
Write-Host "$esc[33mDry Run Mode$esc[0m - Preview only, no deletions"
Write-Host ""
# Prepare export file
$exportDir = Split-Path -Parent $script:ExportListFile
if (-not (Test-Path $exportDir)) {
New-Item -ItemType Directory -Path $exportDir -Force | Out-Null
}
$header = @"
# Mole Cleanup Preview - $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
#
# How to protect files:
# 1. Copy any path below to $($script:Config.WhitelistFile)
# 2. Run: mole clean -Whitelist
#
"@
Set-Content -Path $script:ExportListFile -Value $header
}
else {
Write-Host "$esc[90m$($script:Icons.Solid) Use -DryRun to preview, -Whitelist to manage protected paths$esc[0m"
Write-Host ""
}
# System cleanup confirmation
if ($IncludeSystem -and -not $IsDryRun) {
if (-not (Test-IsAdmin)) {
Write-Warning "System cleanup requires administrator privileges"
Write-Host " Run PowerShell as Administrator for full cleanup"
Write-Host ""
$IncludeSystem = $false
}
else {
Write-Host "$esc[32m$($script:Icons.Success)$esc[0m Running with Administrator privileges"
Write-Host ""
}
}
# Show system info
$winVer = Get-WindowsVersion
Write-Host "$esc[34m$($script:Icons.Admin)$esc[0m $($winVer.Name) | Free space: $(Get-FreeSpace)"
Write-Host ""
# Reset stats
Reset-CleanupStats
Set-DryRunMode -Enabled $IsDryRun
# Run cleanup modules
try {
# User essentials (temp, logs, etc.)
Invoke-UserCleanup -TempDaysOld 7 -LogDaysOld 7
# Browser caches
Clear-BrowserCaches
# Application caches
Clear-AppCaches
# Developer tools
Invoke-DevToolsCleanup
# Applications cleanup
Invoke-AppCleanup
# System cleanup (if requested and admin)
if ($IncludeSystem -and (Test-IsAdmin)) {
Invoke-SystemCleanup
}
}
catch {
Write-Error "Cleanup error: $_"
}
# Get final stats
$stats = Get-CleanupStats
# Show summary
Show-CleanupSummary -Stats $stats -IsDryRun $IsDryRun
}
# ============================================================================
# Main Entry Point
# ============================================================================
function Main {
# Enable debug if requested
if ($DebugMode) {
$env:MOLE_DEBUG = "1"
$DebugPreference = "Continue"
}
# Show help
if ($ShowHelp) {
Show-CleanHelp
return
}
# Manage whitelist
if ($Whitelist) {
Edit-Whitelist
return
}
# Set dry-run mode
if ($DryRun) {
$env:MOLE_DRY_RUN = "1"
}
else {
$env:MOLE_DRY_RUN = "0"
}
# Run cleanup
try {
Start-Cleanup -IsDryRun $DryRun -IncludeSystem $System
}
finally {
# Cleanup temp files
Clear-TempFiles
}
}
# Run main
Main

545
windows/bin/optimize.ps1 Normal file
View File

@@ -0,0 +1,545 @@
# Mole - Optimize Command
# System optimization and health checks for Windows
#Requires -Version 5.1
[CmdletBinding()]
param(
[switch]$DryRun,
[switch]$DebugMode,
[switch]$ShowHelp
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
# Script location
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$libDir = Join-Path (Split-Path -Parent $scriptDir) "lib"
# Import core modules
. "$libDir\core\base.ps1"
. "$libDir\core\log.ps1"
. "$libDir\core\ui.ps1"
. "$libDir\core\file_ops.ps1"
# ============================================================================
# Configuration
# ============================================================================
$script:OptimizationsApplied = 0
$script:IssuesFound = 0
$script:IssuesFixed = 0
# ============================================================================
# Help
# ============================================================================
function Show-OptimizeHelp {
$esc = [char]27
Write-Host ""
Write-Host "$esc[1;35mMole Optimize$esc[0m - System optimization and health checks"
Write-Host ""
Write-Host "$esc[33mUsage:$esc[0m mole optimize [options]"
Write-Host ""
Write-Host "$esc[33mOptions:$esc[0m"
Write-Host " -DryRun Preview optimizations without applying"
Write-Host " -DebugMode Enable debug logging"
Write-Host " -ShowHelp Show this help message"
Write-Host ""
Write-Host "$esc[33mOptimizations:$esc[0m"
Write-Host " - Disk defragmentation/TRIM (SSD optimization)"
Write-Host " - Windows Search index optimization"
Write-Host " - DNS cache flush"
Write-Host " - Network optimization"
Write-Host " - Startup program analysis"
Write-Host " - System file verification"
Write-Host ""
}
# ============================================================================
# System Health Information
# ============================================================================
function Get-SystemHealth {
<#
.SYNOPSIS
Collect system health metrics
#>
$health = @{}
# Memory info
$os = Get-WmiObject Win32_OperatingSystem
$health.MemoryTotalGB = [Math]::Round($os.TotalVisibleMemorySize / 1MB, 1)
$health.MemoryUsedGB = [Math]::Round(($os.TotalVisibleMemorySize - $os.FreePhysicalMemory) / 1MB, 1)
$health.MemoryUsedPercent = [Math]::Round((($os.TotalVisibleMemorySize - $os.FreePhysicalMemory) / $os.TotalVisibleMemorySize) * 100, 0)
# Disk info
$disk = Get-WmiObject Win32_LogicalDisk -Filter "DeviceID='$env:SystemDrive'"
$health.DiskTotalGB = [Math]::Round($disk.Size / 1GB, 0)
$health.DiskUsedGB = [Math]::Round(($disk.Size - $disk.FreeSpace) / 1GB, 0)
$health.DiskUsedPercent = [Math]::Round((($disk.Size - $disk.FreeSpace) / $disk.Size) * 100, 0)
# Uptime
$uptime = (Get-Date) - (Get-CimInstance Win32_OperatingSystem).LastBootUpTime
$health.UptimeDays = [Math]::Round($uptime.TotalDays, 1)
# CPU info
$cpu = Get-WmiObject Win32_Processor
$health.CPUName = $cpu.Name
$health.CPUCores = $cpu.NumberOfLogicalProcessors
return $health
}
function Show-SystemHealth {
param([hashtable]$Health)
$esc = [char]27
Write-Host "$esc[34m$($script:Icons.Admin)$esc[0m System " -NoNewline
Write-Host "$($Health.MemoryUsedGB)/$($Health.MemoryTotalGB)GB RAM | " -NoNewline
Write-Host "$($Health.DiskUsedGB)/$($Health.DiskTotalGB)GB Disk | " -NoNewline
Write-Host "Uptime $($Health.UptimeDays)d"
}
# ============================================================================
# Optimization Tasks
# ============================================================================
function Optimize-DiskDrive {
<#
.SYNOPSIS
Optimize disk (defrag for HDD, TRIM for SSD)
#>
$esc = [char]27
Write-Host ""
Write-Host "$esc[34m$($script:Icons.Arrow) Disk Optimization$esc[0m"
if (-not (Test-IsAdmin)) {
Write-Host " $esc[33m$($script:Icons.Warning)$esc[0m Requires administrator privileges"
return
}
if ($script:DryRun) {
Write-Host " $esc[33m$($script:Icons.DryRun)$esc[0m Would optimize $env:SystemDrive"
$script:OptimizationsApplied++
return
}
try {
# Check if SSD or HDD
$diskNumber = (Get-Partition -DriveLetter $env:SystemDrive[0]).DiskNumber
$mediaType = (Get-PhysicalDisk | Where-Object { $_.DeviceId -eq $diskNumber }).MediaType
if ($mediaType -eq "SSD") {
Write-Host " Running TRIM on SSD..."
$null = Optimize-Volume -DriveLetter $env:SystemDrive[0] -ReTrim -ErrorAction Stop
Write-Host " $esc[32m$($script:Icons.Success)$esc[0m SSD TRIM completed"
}
else {
Write-Host " Running defragmentation on HDD..."
$null = Optimize-Volume -DriveLetter $env:SystemDrive[0] -Defrag -ErrorAction Stop
Write-Host " $esc[32m$($script:Icons.Success)$esc[0m Defragmentation completed"
}
$script:OptimizationsApplied++
}
catch {
Write-Host " $esc[31m$($script:Icons.Error)$esc[0m Disk optimization failed: $_"
}
}
function Optimize-SearchIndex {
<#
.SYNOPSIS
Rebuild Windows Search index if needed
#>
$esc = [char]27
Write-Host ""
Write-Host "$esc[34m$($script:Icons.Arrow) Windows Search$esc[0m"
$searchService = Get-Service -Name WSearch -ErrorAction SilentlyContinue
if (-not $searchService) {
Write-Host " $esc[90mWindows Search service not found$esc[0m"
return
}
if ($searchService.Status -ne 'Running') {
Write-Host " $esc[33m$($script:Icons.Warning)$esc[0m Windows Search service is not running"
if ($script:DryRun) {
Write-Host " $esc[33m$($script:Icons.DryRun)$esc[0m Would start search service"
return
}
try {
Start-Service -Name WSearch -ErrorAction Stop
Write-Host " $esc[32m$($script:Icons.Success)$esc[0m Started Windows Search service"
$script:OptimizationsApplied++
}
catch {
Write-Host " $esc[31m$($script:Icons.Error)$esc[0m Could not start search service"
}
}
else {
Write-Host " $esc[32m$($script:Icons.Success)$esc[0m Search service running"
}
}
function Clear-DnsCache {
<#
.SYNOPSIS
Clear DNS resolver cache
#>
$esc = [char]27
Write-Host ""
Write-Host "$esc[34m$($script:Icons.Arrow) DNS Cache$esc[0m"
if ($script:DryRun) {
Write-Host " $esc[33m$($script:Icons.DryRun)$esc[0m Would flush DNS cache"
$script:OptimizationsApplied++
return
}
try {
Clear-DnsClientCache -ErrorAction Stop
Write-Host " $esc[32m$($script:Icons.Success)$esc[0m DNS cache flushed"
$script:OptimizationsApplied++
}
catch {
Write-Host " $esc[31m$($script:Icons.Error)$esc[0m Could not flush DNS cache: $_"
}
}
function Optimize-Network {
<#
.SYNOPSIS
Network stack optimization
#>
$esc = [char]27
Write-Host ""
Write-Host "$esc[34m$($script:Icons.Arrow) Network Optimization$esc[0m"
if (-not (Test-IsAdmin)) {
Write-Host " $esc[33m$($script:Icons.Warning)$esc[0m Requires administrator privileges"
return
}
if ($script:DryRun) {
Write-Host " $esc[33m$($script:Icons.DryRun)$esc[0m Would reset Winsock catalog"
Write-Host " $esc[33m$($script:Icons.DryRun)$esc[0m Would reset TCP/IP stack"
$script:OptimizationsApplied += 2
return
}
try {
# Reset Winsock
$null = netsh winsock reset 2>&1
Write-Host " $esc[32m$($script:Icons.Success)$esc[0m Winsock catalog reset"
$script:OptimizationsApplied++
}
catch {
Write-Host " $esc[31m$($script:Icons.Error)$esc[0m Winsock reset failed"
}
try {
# Flush ARP cache
$null = netsh interface ip delete arpcache 2>&1
Write-Host " $esc[32m$($script:Icons.Success)$esc[0m ARP cache cleared"
$script:OptimizationsApplied++
}
catch {
Write-Host " $esc[31m$($script:Icons.Error)$esc[0m ARP cache clear failed"
}
}
function Get-StartupPrograms {
<#
.SYNOPSIS
Analyze startup programs
#>
$esc = [char]27
Write-Host ""
Write-Host "$esc[34m$($script:Icons.Arrow) Startup Programs$esc[0m"
$startupPaths = @(
"HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run"
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run"
"HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Run"
)
$startupCount = 0
foreach ($path in $startupPaths) {
if (Test-Path $path) {
$items = Get-ItemProperty -Path $path -ErrorAction SilentlyContinue
$props = @($items.PSObject.Properties | Where-Object {
$_.Name -notin @('PSPath', 'PSParentPath', 'PSChildName', 'PSDrive', 'PSProvider')
})
$startupCount += $props.Count
}
}
# Also check startup folder
$startupFolder = "$env:APPDATA\Microsoft\Windows\Start Menu\Programs\Startup"
if (Test-Path $startupFolder) {
$startupFiles = @(Get-ChildItem -Path $startupFolder -File -ErrorAction SilentlyContinue)
$startupCount += $startupFiles.Count
}
if ($startupCount -gt 10) {
Write-Host " $esc[33m$($script:Icons.Warning)$esc[0m $startupCount startup programs (high)"
Write-Host " $esc[90mConsider disabling unnecessary startup items in Task Manager$esc[0m"
$script:IssuesFound++
}
elseif ($startupCount -gt 5) {
Write-Host " $esc[33m$($script:Icons.Warning)$esc[0m $startupCount startup programs (moderate)"
$script:IssuesFound++
}
else {
Write-Host " $esc[32m$($script:Icons.Success)$esc[0m $startupCount startup programs"
}
}
function Test-SystemFiles {
<#
.SYNOPSIS
Run System File Checker (SFC)
#>
$esc = [char]27
Write-Host ""
Write-Host "$esc[34m$($script:Icons.Arrow) System File Verification$esc[0m"
if (-not (Test-IsAdmin)) {
Write-Host " $esc[33m$($script:Icons.Warning)$esc[0m Requires administrator privileges"
return
}
if ($script:DryRun) {
Write-Host " $esc[33m$($script:Icons.DryRun)$esc[0m Would run System File Checker"
return
}
Write-Host " Running System File Checker (this may take several minutes)..."
try {
$sfcResult = Start-Process -FilePath "sfc.exe" -ArgumentList "/scannow" `
-Wait -PassThru -NoNewWindow -RedirectStandardOutput "$env:TEMP\sfc_output.txt" -ErrorAction Stop
$output = Get-Content "$env:TEMP\sfc_output.txt" -ErrorAction SilentlyContinue
Remove-Item "$env:TEMP\sfc_output.txt" -Force -ErrorAction SilentlyContinue
if ($output -match "did not find any integrity violations") {
Write-Host " $esc[32m$($script:Icons.Success)$esc[0m No integrity violations found"
}
elseif ($output -match "found corrupt files and successfully repaired") {
Write-Host " $esc[32m$($script:Icons.Success)$esc[0m Corrupt files were repaired"
$script:IssuesFixed++
}
elseif ($output -match "found corrupt files but was unable to fix") {
Write-Host " $esc[31m$($script:Icons.Error)$esc[0m Found corrupt files that could not be repaired"
Write-Host " $esc[90mRun 'DISM /Online /Cleanup-Image /RestoreHealth' then retry SFC$esc[0m"
$script:IssuesFound++
}
else {
Write-Host " $esc[32m$($script:Icons.Success)$esc[0m Scan completed"
}
}
catch {
Write-Host " $esc[31m$($script:Icons.Error)$esc[0m System File Checker failed: $_"
}
}
function Test-DiskHealth {
<#
.SYNOPSIS
Check disk health status
#>
$esc = [char]27
Write-Host ""
Write-Host "$esc[34m$($script:Icons.Arrow) Disk Health$esc[0m"
try {
$disks = Get-PhysicalDisk -ErrorAction Stop
foreach ($disk in $disks) {
$status = $disk.HealthStatus
$name = $disk.FriendlyName
if ($status -eq "Healthy") {
Write-Host " $esc[32m$($script:Icons.Success)$esc[0m $name - Healthy"
}
elseif ($status -eq "Warning") {
Write-Host " $esc[33m$($script:Icons.Warning)$esc[0m $name - Warning"
Write-Host " $esc[90mDisk may have issues, consider backing up data$esc[0m"
$script:IssuesFound++
}
else {
Write-Host " $esc[31m$($script:Icons.Error)$esc[0m $name - $status"
Write-Host " $esc[31mDisk has critical issues, back up data immediately!$esc[0m"
$script:IssuesFound++
}
}
}
catch {
Write-Host " $esc[90mCould not check disk health$esc[0m"
}
}
function Test-WindowsUpdate {
<#
.SYNOPSIS
Check Windows Update status
#>
$esc = [char]27
Write-Host ""
Write-Host "$esc[34m$($script:Icons.Arrow) Windows Update$esc[0m"
try {
$updateSession = New-Object -ComObject Microsoft.Update.Session
$updateSearcher = $updateSession.CreateUpdateSearcher()
Write-Host " Checking for updates..."
$searchResult = $updateSearcher.Search("IsInstalled=0")
$importantUpdates = $searchResult.Updates | Where-Object {
$_.MsrcSeverity -in @('Critical', 'Important')
}
if ($importantUpdates.Count -gt 0) {
Write-Host " $esc[33m$($script:Icons.Warning)$esc[0m $($importantUpdates.Count) important updates available"
Write-Host " $esc[90mRun Windows Update to install$esc[0m"
$script:IssuesFound++
}
elseif ($searchResult.Updates.Count -gt 0) {
Write-Host " $esc[90m$($script:Icons.List)$esc[0m $($searchResult.Updates.Count) optional updates available"
}
else {
Write-Host " $esc[32m$($script:Icons.Success)$esc[0m System is up to date"
}
}
catch {
Write-Host " $esc[90mCould not check Windows Update status$esc[0m"
}
}
# ============================================================================
# Summary
# ============================================================================
function Show-OptimizeSummary {
$esc = [char]27
Write-Host ""
Write-Host "$esc[1;35m" -NoNewline
if ($script:DryRun) {
Write-Host "Dry Run Complete - No Changes Made" -NoNewline
}
else {
Write-Host "Optimization Complete" -NoNewline
}
Write-Host "$esc[0m"
Write-Host ""
if ($script:DryRun) {
Write-Host " Would apply $esc[33m$($script:OptimizationsApplied)$esc[0m optimizations"
Write-Host " Run without -DryRun to apply changes"
}
else {
Write-Host " Optimizations applied: $esc[32m$($script:OptimizationsApplied)$esc[0m"
if ($script:IssuesFixed -gt 0) {
Write-Host " Issues fixed: $esc[32m$($script:IssuesFixed)$esc[0m"
}
if ($script:IssuesFound -gt 0) {
Write-Host " Issues found: $esc[33m$($script:IssuesFound)$esc[0m"
}
else {
Write-Host " System health: $esc[32mGood$esc[0m"
}
}
Write-Host ""
}
# ============================================================================
# Main Entry Point
# ============================================================================
function Main {
# Enable debug if requested
if ($DebugMode) {
$env:MOLE_DEBUG = "1"
$DebugPreference = "Continue"
}
# Show help
if ($ShowHelp) {
Show-OptimizeHelp
return
}
# Set dry-run mode
$script:DryRun = $DryRun
# Clear screen
Clear-Host
$esc = [char]27
Write-Host ""
Write-Host "$esc[1;35mOptimize and Check$esc[0m"
Write-Host ""
if ($script:DryRun) {
Write-Host "$esc[33m$($script:Icons.DryRun) DRY RUN MODE$esc[0m - No changes will be made"
Write-Host ""
}
# Show system health
$health = Get-SystemHealth
Show-SystemHealth -Health $health
# Run optimizations
Optimize-DiskDrive
Optimize-SearchIndex
Clear-DnsCache
Optimize-Network
# Run health checks
Get-StartupPrograms
Test-DiskHealth
Test-WindowsUpdate
# System file check is slow, ask first
if (-not $script:DryRun -and (Test-IsAdmin)) {
Write-Host ""
$runSfc = Read-Host "Run System File Checker? This may take several minutes (y/N)"
if ($runSfc -eq 'y' -or $runSfc -eq 'Y') {
Test-SystemFiles
}
}
# Summary
Show-OptimizeSummary
}
# Run main
Main

610
windows/bin/purge.ps1 Normal file
View File

@@ -0,0 +1,610 @@
# Mole - Purge Command
# Aggressive cleanup of project build artifacts
#Requires -Version 5.1
[CmdletBinding()]
param(
[switch]$DebugMode,
[switch]$Paths,
[switch]$ShowHelp
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
# Script location
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$libDir = Join-Path (Split-Path -Parent $scriptDir) "lib"
# Import core modules
. "$libDir\core\base.ps1"
. "$libDir\core\log.ps1"
. "$libDir\core\ui.ps1"
. "$libDir\core\file_ops.ps1"
# ============================================================================
# Configuration
# ============================================================================
$script:DefaultSearchPaths = @(
"$env:USERPROFILE\Documents"
"$env:USERPROFILE\Projects"
"$env:USERPROFILE\Code"
"$env:USERPROFILE\Development"
"$env:USERPROFILE\workspace"
"$env:USERPROFILE\github"
"$env:USERPROFILE\repos"
"$env:USERPROFILE\src"
"D:\Projects"
"D:\Code"
"D:\Development"
)
$script:ConfigFile = "$env:USERPROFILE\.config\mole\purge_paths.txt"
# Artifact patterns to clean
$script:ArtifactPatterns = @(
@{ Name = "node_modules"; Type = "Directory"; Language = "JavaScript/Node.js" }
@{ Name = "vendor"; Type = "Directory"; Language = "PHP/Go" }
@{ Name = ".venv"; Type = "Directory"; Language = "Python" }
@{ Name = "venv"; Type = "Directory"; Language = "Python" }
@{ Name = "__pycache__"; Type = "Directory"; Language = "Python" }
@{ Name = ".pytest_cache"; Type = "Directory"; Language = "Python" }
@{ Name = "target"; Type = "Directory"; Language = "Rust/Java" }
@{ Name = "build"; Type = "Directory"; Language = "General" }
@{ Name = "dist"; Type = "Directory"; Language = "General" }
@{ Name = ".next"; Type = "Directory"; Language = "Next.js" }
@{ Name = ".nuxt"; Type = "Directory"; Language = "Nuxt.js" }
@{ Name = ".turbo"; Type = "Directory"; Language = "Turborepo" }
@{ Name = ".parcel-cache"; Type = "Directory"; Language = "Parcel" }
@{ Name = "bin"; Type = "Directory"; Language = ".NET" }
@{ Name = "obj"; Type = "Directory"; Language = ".NET" }
@{ Name = ".gradle"; Type = "Directory"; Language = "Java/Gradle" }
@{ Name = ".idea"; Type = "Directory"; Language = "JetBrains IDE" }
@{ Name = "*.log"; Type = "File"; Language = "Logs" }
)
$script:TotalSizeCleaned = 0
$script:ItemsCleaned = 0
# ============================================================================
# Help
# ============================================================================
function Show-PurgeHelp {
$esc = [char]27
Write-Host ""
Write-Host "$esc[1;35mMole Purge$esc[0m - Clean project build artifacts"
Write-Host ""
Write-Host "$esc[33mUsage:$esc[0m mole purge [options]"
Write-Host ""
Write-Host "$esc[33mOptions:$esc[0m"
Write-Host " -Paths Edit custom scan directories"
Write-Host " -DebugMode Enable debug logging"
Write-Host " -ShowHelp Show this help message"
Write-Host ""
Write-Host "$esc[33mDefault Search Paths:$esc[0m"
foreach ($path in $script:DefaultSearchPaths) {
if (Test-Path $path) {
Write-Host " $esc[32m+$esc[0m $path"
}
else {
Write-Host " $esc[90m-$esc[0m $path (not found)"
}
}
Write-Host ""
Write-Host "$esc[33mArtifacts Cleaned:$esc[0m"
Write-Host " node_modules, vendor, venv, target, build, dist, __pycache__, etc."
Write-Host ""
}
# ============================================================================
# Path Management
# ============================================================================
function Get-SearchPaths {
<#
.SYNOPSIS
Get list of paths to scan for projects
#>
$paths = @()
# Load custom paths if available
if (Test-Path $script:ConfigFile) {
$customPaths = Get-Content $script:ConfigFile -ErrorAction SilentlyContinue |
Where-Object { $_ -and -not $_.StartsWith('#') } |
ForEach-Object { $_.Trim() }
foreach ($path in $customPaths) {
if (Test-Path $path) {
$paths += $path
}
}
}
# Add default paths if no custom paths or custom paths don't exist
if ($paths.Count -eq 0) {
foreach ($path in $script:DefaultSearchPaths) {
if (Test-Path $path) {
$paths += $path
}
}
}
return $paths
}
function Edit-SearchPaths {
<#
.SYNOPSIS
Open search paths configuration for editing
#>
$configDir = Split-Path -Parent $script:ConfigFile
if (-not (Test-Path $configDir)) {
New-Item -ItemType Directory -Path $configDir -Force | Out-Null
}
if (-not (Test-Path $script:ConfigFile)) {
$defaultContent = @"
# Mole Purge - Custom Search Paths
# Add directories to scan for project artifacts (one per line)
# Lines starting with # are ignored
#
# Examples:
# D:\MyProjects
# E:\Work\Code
#
# Default paths (used if this file is empty):
# $env:USERPROFILE\Documents
# $env:USERPROFILE\Projects
# $env:USERPROFILE\Code
"@
Set-Content -Path $script:ConfigFile -Value $defaultContent
}
Write-Info "Opening paths configuration: $($script:ConfigFile)"
Start-Process notepad.exe -ArgumentList $script:ConfigFile -Wait
Write-Success "Configuration saved"
}
# ============================================================================
# Project Discovery
# ============================================================================
function Find-Projects {
<#
.SYNOPSIS
Find all development projects in search paths
#>
param([string[]]$SearchPaths)
$projects = @()
# Project markers
$projectMarkers = @(
"package.json" # Node.js
"composer.json" # PHP
"Cargo.toml" # Rust
"go.mod" # Go
"pom.xml" # Java/Maven
"build.gradle" # Java/Gradle
"requirements.txt" # Python
"pyproject.toml" # Python
"*.csproj" # .NET
"*.sln" # .NET Solution
)
$esc = [char]27
$pathCount = 0
$totalPaths = $SearchPaths.Count
foreach ($searchPath in $SearchPaths) {
$pathCount++
Write-Progress -Activity "Scanning for projects" `
-Status "Searching: $searchPath" `
-PercentComplete (($pathCount / $totalPaths) * 100)
foreach ($marker in $projectMarkers) {
try {
$found = Get-ChildItem -Path $searchPath -Filter $marker -Recurse -Depth 4 -ErrorAction SilentlyContinue
foreach ($item in $found) {
$projectPath = Split-Path -Parent $item.FullName
# Skip if already found or if it's inside node_modules, etc.
if ($projects.Path -contains $projectPath) { continue }
if ($projectPath -like "*\node_modules\*") { continue }
if ($projectPath -like "*\vendor\*") { continue }
if ($projectPath -like "*\.git\*") { continue }
# Find artifacts in this project
$artifacts = @(Find-ProjectArtifacts -ProjectPath $projectPath)
if ($artifacts.Count -gt 0) {
$totalSize = ($artifacts | Measure-Object -Property SizeKB -Sum).Sum
$projects += [PSCustomObject]@{
Path = $projectPath
Name = Split-Path -Leaf $projectPath
Marker = $marker
Artifacts = $artifacts
TotalSizeKB = $totalSize
TotalSizeHuman = Format-ByteSize -Bytes ($totalSize * 1024)
}
}
}
}
catch {
Write-Debug "Error scanning $searchPath for $marker : $_"
}
}
}
Write-Progress -Activity "Scanning for projects" -Completed
# Sort by size (largest first)
return $projects | Sort-Object -Property TotalSizeKB -Descending
}
function Find-ProjectArtifacts {
<#
.SYNOPSIS
Find cleanable artifacts in a project directory
#>
param([string]$ProjectPath)
$artifacts = @()
foreach ($pattern in $script:ArtifactPatterns) {
$items = Get-ChildItem -Path $ProjectPath -Filter $pattern.Name -Force -ErrorAction SilentlyContinue
foreach ($item in $items) {
if ($pattern.Type -eq "Directory" -and $item.PSIsContainer) {
$sizeKB = Get-PathSizeKB -Path $item.FullName
$artifacts += [PSCustomObject]@{
Path = $item.FullName
Name = $item.Name
Type = "Directory"
Language = $pattern.Language
SizeKB = $sizeKB
SizeHuman = Format-ByteSize -Bytes ($sizeKB * 1024)
}
}
elseif ($pattern.Type -eq "File" -and -not $item.PSIsContainer) {
$sizeKB = [Math]::Ceiling($item.Length / 1024)
$artifacts += [PSCustomObject]@{
Path = $item.FullName
Name = $item.Name
Type = "File"
Language = $pattern.Language
SizeKB = $sizeKB
SizeHuman = Format-ByteSize -Bytes ($sizeKB * 1024)
}
}
}
}
return $artifacts
}
# ============================================================================
# Project Selection UI
# ============================================================================
function Show-ProjectSelectionMenu {
<#
.SYNOPSIS
Interactive menu for selecting projects to clean
#>
param([array]$Projects)
if ($Projects.Count -eq 0) {
Write-Warning "No projects with cleanable artifacts found"
return @()
}
$esc = [char]27
$selectedIndices = @{}
$currentIndex = 0
$pageSize = 12
$pageStart = 0
[Console]::CursorVisible = $false
try {
while ($true) {
Clear-Host
# Header
Write-Host ""
Write-Host "$esc[1;35mSelect Projects to Clean$esc[0m"
Write-Host ""
Write-Host "$esc[90mUse: $($script:Icons.NavUp)$($script:Icons.NavDown) navigate | Space select | A select all | Enter confirm | Q quit$esc[0m"
Write-Host ""
# Display projects
$pageEnd = [Math]::Min($pageStart + $pageSize, $Projects.Count)
for ($i = $pageStart; $i -lt $pageEnd; $i++) {
$project = $Projects[$i]
$isSelected = $selectedIndices.ContainsKey($i)
$isCurrent = ($i -eq $currentIndex)
$checkbox = if ($isSelected) { "$esc[32m[$($script:Icons.Success)]$esc[0m" } else { "[ ]" }
if ($isCurrent) {
Write-Host "$esc[7m" -NoNewline
}
$name = $project.Name
if ($name.Length -gt 30) {
$name = $name.Substring(0, 27) + "..."
}
$artifactCount = $project.Artifacts.Count
Write-Host (" {0} {1,-32} {2,10} ({3} items)" -f $checkbox, $name, $project.TotalSizeHuman, $artifactCount) -NoNewline
if ($isCurrent) {
Write-Host "$esc[0m"
}
else {
Write-Host ""
}
}
# Footer
Write-Host ""
$selectedCount = $selectedIndices.Count
if ($selectedCount -gt 0) {
$totalSize = 0
foreach ($idx in $selectedIndices.Keys) {
$totalSize += $Projects[$idx].TotalSizeKB
}
$totalSizeHuman = Format-ByteSize -Bytes ($totalSize * 1024)
Write-Host "$esc[33mSelected:$esc[0m $selectedCount projects ($totalSizeHuman)"
}
# Page indicator
$totalPages = [Math]::Ceiling($Projects.Count / $pageSize)
$currentPage = [Math]::Floor($pageStart / $pageSize) + 1
Write-Host "$esc[90mPage $currentPage of $totalPages | Total: $($Projects.Count) projects$esc[0m"
# Handle input
$key = [Console]::ReadKey($true)
switch ($key.Key) {
'UpArrow' {
if ($currentIndex -gt 0) {
$currentIndex--
if ($currentIndex -lt $pageStart) {
$pageStart = [Math]::Max(0, $pageStart - $pageSize)
}
}
}
'DownArrow' {
if ($currentIndex -lt $Projects.Count - 1) {
$currentIndex++
if ($currentIndex -ge $pageStart + $pageSize) {
$pageStart += $pageSize
}
}
}
'PageUp' {
$pageStart = [Math]::Max(0, $pageStart - $pageSize)
$currentIndex = $pageStart
}
'PageDown' {
$pageStart = [Math]::Min($Projects.Count - $pageSize, $pageStart + $pageSize)
if ($pageStart -lt 0) { $pageStart = 0 }
$currentIndex = $pageStart
}
'Spacebar' {
if ($selectedIndices.ContainsKey($currentIndex)) {
$selectedIndices.Remove($currentIndex)
}
else {
$selectedIndices[$currentIndex] = $true
}
}
'A' {
# Select/deselect all
if ($selectedIndices.Count -eq $Projects.Count) {
$selectedIndices.Clear()
}
else {
for ($i = 0; $i -lt $Projects.Count; $i++) {
$selectedIndices[$i] = $true
}
}
}
'Enter' {
if ($selectedIndices.Count -gt 0) {
$selected = @()
foreach ($idx in $selectedIndices.Keys) {
$selected += $Projects[$idx]
}
return $selected
}
}
'Escape' { return @() }
'Q' { return @() }
}
}
}
finally {
[Console]::CursorVisible = $true
}
}
# ============================================================================
# Cleanup
# ============================================================================
function Remove-ProjectArtifacts {
<#
.SYNOPSIS
Remove artifacts from selected projects
#>
param([array]$Projects)
$esc = [char]27
Write-Host ""
Write-Host "$esc[1;35mCleaning Project Artifacts$esc[0m"
Write-Host ""
foreach ($project in $Projects) {
Write-Host "$esc[34m$($script:Icons.Arrow)$esc[0m $($project.Name)"
foreach ($artifact in $project.Artifacts) {
if (Test-Path $artifact.Path) {
try {
if ($artifact.Type -eq "Directory") {
Remove-Item -Path $artifact.Path -Recurse -Force -ErrorAction Stop
}
else {
Remove-Item -Path $artifact.Path -Force -ErrorAction Stop
}
Write-Host " $esc[32m$($script:Icons.Success)$esc[0m $($artifact.Name) ($($artifact.SizeHuman))"
$script:TotalSizeCleaned += $artifact.SizeKB
$script:ItemsCleaned++
}
catch {
Write-Host " $esc[31m$($script:Icons.Error)$esc[0m $($artifact.Name) - $_"
}
}
}
}
}
# ============================================================================
# Summary
# ============================================================================
function Show-PurgeSummary {
$esc = [char]27
Write-Host ""
Write-Host "$esc[1;35mPurge Complete$esc[0m"
Write-Host ""
if ($script:TotalSizeCleaned -gt 0) {
$sizeGB = [Math]::Round($script:TotalSizeCleaned / 1024 / 1024, 2)
Write-Host " Space freed: $esc[32m${sizeGB}GB$esc[0m"
Write-Host " Items cleaned: $($script:ItemsCleaned)"
Write-Host " Free space now: $(Get-FreeSpace)"
}
else {
Write-Host " No artifacts to clean."
Write-Host " Free space now: $(Get-FreeSpace)"
}
Write-Host ""
}
# ============================================================================
# Main Entry Point
# ============================================================================
function Main {
# Enable debug if requested
if ($DebugMode) {
$env:MOLE_DEBUG = "1"
$DebugPreference = "Continue"
}
# Show help
if ($ShowHelp) {
Show-PurgeHelp
return
}
# Edit paths
if ($Paths) {
Edit-SearchPaths
return
}
# Clear screen
Clear-Host
$esc = [char]27
Write-Host ""
Write-Host "$esc[1;35mPurge Project Artifacts$esc[0m"
Write-Host ""
# Get search paths
$searchPaths = Get-SearchPaths
if ($searchPaths.Count -eq 0) {
Write-Warning "No valid search paths found"
Write-Host "Run 'mole purge -Paths' to configure search directories"
return
}
Write-Info "Searching in $($searchPaths.Count) directories..."
# Find projects
$projects = Find-Projects -SearchPaths $searchPaths
if ($projects.Count -eq 0) {
Write-Host ""
Write-Host "$esc[32m$($script:Icons.Success)$esc[0m No cleanable artifacts found"
Write-Host ""
return
}
$totalSize = ($projects | Measure-Object -Property TotalSizeKB -Sum).Sum
$totalSizeHuman = Format-ByteSize -Bytes ($totalSize * 1024)
Write-Host ""
Write-Host "Found $esc[33m$($projects.Count)$esc[0m projects with $esc[33m$totalSizeHuman$esc[0m of artifacts"
Write-Host ""
# Project selection
$selected = Show-ProjectSelectionMenu -Projects $projects
if ($selected.Count -eq 0) {
Write-Info "No projects selected"
return
}
# Confirm
Clear-Host
Write-Host ""
$selectedSize = ($selected | Measure-Object -Property TotalSizeKB -Sum).Sum
$selectedSizeHuman = Format-ByteSize -Bytes ($selectedSize * 1024)
Write-Host "$esc[33mThe following will be cleaned ($selectedSizeHuman):$esc[0m"
Write-Host ""
foreach ($project in $selected) {
Write-Host " $($script:Icons.List) $($project.Name) ($($project.TotalSizeHuman))"
foreach ($artifact in $project.Artifacts) {
Write-Host " $esc[90m$($artifact.Name) - $($artifact.SizeHuman)$esc[0m"
}
}
Write-Host ""
$confirm = Read-Host "Continue? (y/N)"
if ($confirm -eq 'y' -or $confirm -eq 'Y') {
Remove-ProjectArtifacts -Projects $selected
Show-PurgeSummary
}
else {
Write-Info "Cancelled"
}
}
# Run main
Main

596
windows/bin/uninstall.ps1 Normal file
View File

@@ -0,0 +1,596 @@
# Mole - Uninstall Command
# Interactive application uninstaller for Windows
#Requires -Version 5.1
[CmdletBinding()]
param(
[switch]$DebugMode,
[switch]$Rescan,
[switch]$ShowHelp
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
# Script location
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$libDir = Join-Path (Split-Path -Parent $scriptDir) "lib"
# Import core modules
. "$libDir\core\base.ps1"
. "$libDir\core\log.ps1"
. "$libDir\core\ui.ps1"
. "$libDir\core\file_ops.ps1"
# ============================================================================
# Configuration
# ============================================================================
$script:CacheDir = "$env:USERPROFILE\.cache\mole"
$script:AppCacheFile = "$script:CacheDir\app_scan_cache.json"
$script:CacheTTLHours = 24
# ============================================================================
# Help
# ============================================================================
function Show-UninstallHelp {
$esc = [char]27
Write-Host ""
Write-Host "$esc[1;35mMole Uninstall$esc[0m - Interactive application uninstaller"
Write-Host ""
Write-Host "$esc[33mUsage:$esc[0m mole uninstall [options]"
Write-Host ""
Write-Host "$esc[33mOptions:$esc[0m"
Write-Host " -Rescan Force rescan of installed applications"
Write-Host " -DebugMode Enable debug logging"
Write-Host " -ShowHelp Show this help message"
Write-Host ""
Write-Host "$esc[33mFeatures:$esc[0m"
Write-Host " - Scans installed programs from registry and Windows Apps"
Write-Host " - Shows program size and last used date"
Write-Host " - Interactive selection with arrow keys"
Write-Host " - Cleans leftover files after uninstall"
Write-Host ""
}
# ============================================================================
# Protected Applications
# ============================================================================
$script:ProtectedApps = @(
"Microsoft Windows"
"Windows Feature Experience Pack"
"Microsoft Edge"
"Microsoft Edge WebView2"
"Windows Security"
"Microsoft Visual C++ *"
"Microsoft .NET *"
".NET Desktop Runtime*"
"Microsoft Update Health Tools"
"NVIDIA Graphics Driver*"
"AMD Software*"
"Intel*Driver*"
)
function Test-ProtectedApp {
param([string]$AppName)
foreach ($pattern in $script:ProtectedApps) {
if ($AppName -like $pattern) {
return $true
}
}
return $false
}
# ============================================================================
# Application Discovery
# ============================================================================
function Get-InstalledApplications {
<#
.SYNOPSIS
Scan and return all installed applications
#>
param([switch]$ForceRescan)
# Check cache
if (-not $ForceRescan -and (Test-Path $script:AppCacheFile)) {
$cacheInfo = Get-Item $script:AppCacheFile
$cacheAge = (Get-Date) - $cacheInfo.LastWriteTime
if ($cacheAge.TotalHours -lt $script:CacheTTLHours) {
Write-Debug "Loading from cache..."
try {
$cached = Get-Content $script:AppCacheFile | ConvertFrom-Json
return $cached
}
catch {
Write-Debug "Cache read failed, rescanning..."
}
}
}
Write-Info "Scanning installed applications..."
$apps = @()
# Registry paths for installed programs
$registryPaths = @(
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*"
"HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
"HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*"
)
$count = 0
$total = $registryPaths.Count
foreach ($path in $registryPaths) {
$count++
Write-Progress -Activity "Scanning applications" -Status "Registry path $count of $total" -PercentComplete (($count / $total) * 50)
$items = Get-ItemProperty -Path $path -ErrorAction SilentlyContinue |
Where-Object {
$_.DisplayName -and
$_.UninstallString -and
-not (Test-ProtectedApp $_.DisplayName)
}
foreach ($item in $items) {
# Calculate size
$sizeKB = 0
if ($item.EstimatedSize) {
$sizeKB = [long]$item.EstimatedSize
}
elseif ($item.InstallLocation -and (Test-Path $item.InstallLocation)) {
$sizeKB = Get-PathSizeKB -Path $item.InstallLocation
}
# Get install date
$installDate = $null
if ($item.InstallDate) {
try {
$installDate = [DateTime]::ParseExact($item.InstallDate, "yyyyMMdd", $null)
}
catch { }
}
$apps += [PSCustomObject]@{
Name = $item.DisplayName
Publisher = $item.Publisher
Version = $item.DisplayVersion
SizeKB = $sizeKB
SizeHuman = Format-ByteSize -Bytes ($sizeKB * 1024)
InstallLocation = $item.InstallLocation
UninstallString = $item.UninstallString
InstallDate = $installDate
Source = "Registry"
}
}
}
# UWP / Store Apps
Write-Progress -Activity "Scanning applications" -Status "Scanning Windows Apps" -PercentComplete 75
try {
$uwpApps = Get-AppxPackage -ErrorAction SilentlyContinue |
Where-Object {
$_.IsFramework -eq $false -and
$_.SignatureKind -ne 'System' -and
-not (Test-ProtectedApp $_.Name)
}
foreach ($uwp in $uwpApps) {
# Get friendly name
$name = $uwp.Name
try {
$manifest = Get-AppxPackageManifest -Package $uwp.PackageFullName -ErrorAction SilentlyContinue
if ($manifest.Package.Properties.DisplayName -and
-not $manifest.Package.Properties.DisplayName.StartsWith("ms-resource:")) {
$name = $manifest.Package.Properties.DisplayName
}
}
catch { }
# Calculate size
$sizeKB = 0
if ($uwp.InstallLocation -and (Test-Path $uwp.InstallLocation)) {
$sizeKB = Get-PathSizeKB -Path $uwp.InstallLocation
}
$apps += [PSCustomObject]@{
Name = $name
Publisher = $uwp.Publisher
Version = $uwp.Version
SizeKB = $sizeKB
SizeHuman = Format-ByteSize -Bytes ($sizeKB * 1024)
InstallLocation = $uwp.InstallLocation
UninstallString = $null
PackageFullName = $uwp.PackageFullName
InstallDate = $null
Source = "WindowsStore"
}
}
}
catch {
Write-Debug "Could not enumerate UWP apps: $_"
}
Write-Progress -Activity "Scanning applications" -Completed
# Sort by size (largest first)
$apps = $apps | Sort-Object -Property SizeKB -Descending
# Cache results
if (-not (Test-Path $script:CacheDir)) {
New-Item -ItemType Directory -Path $script:CacheDir -Force | Out-Null
}
$apps | ConvertTo-Json -Depth 5 | Set-Content $script:AppCacheFile
return $apps
}
# ============================================================================
# Application Selection UI
# ============================================================================
function Show-AppSelectionMenu {
<#
.SYNOPSIS
Interactive menu for selecting applications to uninstall
#>
param([array]$Apps)
if ($Apps.Count -eq 0) {
Write-Warning "No applications found to uninstall"
return @()
}
$esc = [char]27
$selectedIndices = @{}
$currentIndex = 0
$pageSize = 15
$pageStart = 0
$searchTerm = ""
$filteredApps = $Apps
# Hide cursor
[Console]::CursorVisible = $false
try {
while ($true) {
Clear-Host
# Header
Write-Host ""
Write-Host "$esc[1;35mSelect Applications to Uninstall$esc[0m"
Write-Host ""
Write-Host "$esc[90mUse: $($script:Icons.NavUp)$($script:Icons.NavDown) navigate | Space select | Enter confirm | Q quit | / search$esc[0m"
Write-Host ""
# Search indicator
if ($searchTerm) {
Write-Host "$esc[33mSearch:$esc[0m $searchTerm ($($filteredApps.Count) matches)"
Write-Host ""
}
# Display apps
$pageEnd = [Math]::Min($pageStart + $pageSize, $filteredApps.Count)
for ($i = $pageStart; $i -lt $pageEnd; $i++) {
$app = $filteredApps[$i]
$isSelected = $selectedIndices.ContainsKey($app.Name)
$isCurrent = ($i -eq $currentIndex)
# Selection indicator
$checkbox = if ($isSelected) { "$esc[32m[$($script:Icons.Success)]$esc[0m" } else { "[ ]" }
# Highlight current
if ($isCurrent) {
Write-Host "$esc[7m" -NoNewline # Reverse video
}
# App info
$name = $app.Name
if ($name.Length -gt 40) {
$name = $name.Substring(0, 37) + "..."
}
$size = $app.SizeHuman
if (-not $size -or $size -eq "0B") {
$size = "N/A"
}
Write-Host (" {0} {1,-42} {2,10}" -f $checkbox, $name, $size) -NoNewline
if ($isCurrent) {
Write-Host "$esc[0m" # Reset
}
else {
Write-Host ""
}
}
# Footer
Write-Host ""
$selectedCount = $selectedIndices.Count
if ($selectedCount -gt 0) {
$totalSize = 0
foreach ($key in $selectedIndices.Keys) {
$app = $Apps | Where-Object { $_.Name -eq $key }
if ($app.SizeKB) {
$totalSize += $app.SizeKB
}
}
$totalSizeHuman = Format-ByteSize -Bytes ($totalSize * 1024)
Write-Host "$esc[33mSelected:$esc[0m $selectedCount apps ($totalSizeHuman)"
}
# Page indicator
$totalPages = [Math]::Ceiling($filteredApps.Count / $pageSize)
$currentPage = [Math]::Floor($pageStart / $pageSize) + 1
Write-Host "$esc[90mPage $currentPage of $totalPages$esc[0m"
# Handle input
$key = [Console]::ReadKey($true)
switch ($key.Key) {
'UpArrow' {
if ($currentIndex -gt 0) {
$currentIndex--
if ($currentIndex -lt $pageStart) {
$pageStart = [Math]::Max(0, $pageStart - $pageSize)
}
}
}
'DownArrow' {
if ($currentIndex -lt $filteredApps.Count - 1) {
$currentIndex++
if ($currentIndex -ge $pageStart + $pageSize) {
$pageStart += $pageSize
}
}
}
'PageUp' {
$pageStart = [Math]::Max(0, $pageStart - $pageSize)
$currentIndex = $pageStart
}
'PageDown' {
$pageStart = [Math]::Min($filteredApps.Count - $pageSize, $pageStart + $pageSize)
if ($pageStart -lt 0) { $pageStart = 0 }
$currentIndex = $pageStart
}
'Spacebar' {
$app = $filteredApps[$currentIndex]
if ($selectedIndices.ContainsKey($app.Name)) {
$selectedIndices.Remove($app.Name)
}
else {
$selectedIndices[$app.Name] = $true
}
}
'Enter' {
if ($selectedIndices.Count -gt 0) {
# Return selected apps
$selected = $Apps | Where-Object { $selectedIndices.ContainsKey($_.Name) }
return $selected
}
}
'Escape' {
return @()
}
'Q' {
return @()
}
'Oem2' { # Forward slash
# Search mode
Write-Host ""
Write-Host "Search: " -NoNewline
[Console]::CursorVisible = $true
$searchTerm = Read-Host
[Console]::CursorVisible = $false
if ($searchTerm) {
$filteredApps = $Apps | Where-Object { $_.Name -like "*$searchTerm*" }
}
else {
$filteredApps = $Apps
}
$currentIndex = 0
$pageStart = 0
}
'Backspace' {
if ($searchTerm) {
$searchTerm = ""
$filteredApps = $Apps
$currentIndex = 0
$pageStart = 0
}
}
}
}
}
finally {
[Console]::CursorVisible = $true
}
}
# ============================================================================
# Uninstallation
# ============================================================================
function Uninstall-SelectedApps {
<#
.SYNOPSIS
Uninstall the selected applications
#>
param([array]$Apps)
$esc = [char]27
Write-Host ""
Write-Host "$esc[1;35mUninstalling Applications$esc[0m"
Write-Host ""
$successCount = 0
$failCount = 0
foreach ($app in $Apps) {
Write-Host "$esc[34m$($script:Icons.Arrow)$esc[0m Uninstalling: $($app.Name)" -NoNewline
try {
if ($app.Source -eq "WindowsStore") {
# UWP app
if ($app.PackageFullName) {
Remove-AppxPackage -Package $app.PackageFullName -ErrorAction Stop
Write-Host " $esc[32m$($script:Icons.Success)$esc[0m"
$successCount++
}
}
else {
# Registry app with uninstall string
$uninstallString = $app.UninstallString
# Handle different uninstall types
if ($uninstallString -like "MsiExec.exe*") {
# MSI uninstall
$productCode = [regex]::Match($uninstallString, '\{[0-9A-F-]+\}').Value
if ($productCode) {
$process = Start-Process -FilePath "msiexec.exe" `
-ArgumentList "/x", $productCode, "/qn", "/norestart" `
-Wait -PassThru -NoNewWindow
if ($process.ExitCode -eq 0 -or $process.ExitCode -eq 3010) {
Write-Host " $esc[32m$($script:Icons.Success)$esc[0m"
$successCount++
}
else {
Write-Host " $esc[33m(requires interaction)$esc[0m"
# Fallback to interactive uninstall
Start-Process -FilePath "msiexec.exe" -ArgumentList "/x", $productCode -Wait
$successCount++
}
}
}
else {
# Direct executable uninstall
# Try silent uninstall first
$silentArgs = @("/S", "/silent", "/quiet", "-s", "-silent", "-quiet", "/VERYSILENT")
$uninstalled = $false
foreach ($arg in $silentArgs) {
try {
$process = Start-Process -FilePath "cmd.exe" `
-ArgumentList "/c", "`"$uninstallString`"", $arg `
-Wait -PassThru -NoNewWindow -ErrorAction SilentlyContinue
if ($process.ExitCode -eq 0) {
Write-Host " $esc[32m$($script:Icons.Success)$esc[0m"
$successCount++
$uninstalled = $true
break
}
}
catch { }
}
if (-not $uninstalled) {
# Fallback to interactive
Write-Host " $esc[33m(launching uninstaller)$esc[0m"
Start-Process -FilePath "cmd.exe" -ArgumentList "/c", "`"$uninstallString`"" -Wait
$successCount++
}
}
}
# Clean leftover files
if ($app.InstallLocation -and (Test-Path $app.InstallLocation)) {
Write-Host " $esc[90mCleaning leftover files...$esc[0m"
Remove-SafeItem -Path $app.InstallLocation -Description "Leftover files" -Recurse
}
}
catch {
Write-Host " $esc[31m$($script:Icons.Error)$esc[0m"
Write-Debug "Uninstall failed: $_"
$failCount++
}
}
# Summary
Write-Host ""
Write-Host "$esc[1;35mUninstall Complete$esc[0m"
Write-Host " Successfully uninstalled: $esc[32m$successCount$esc[0m"
if ($failCount -gt 0) {
Write-Host " Failed: $esc[31m$failCount$esc[0m"
}
Write-Host ""
# Clear cache
if (Test-Path $script:AppCacheFile) {
Remove-Item $script:AppCacheFile -Force -ErrorAction SilentlyContinue
}
}
# ============================================================================
# Main Entry Point
# ============================================================================
function Main {
# Enable debug if requested
if ($DebugMode) {
$env:MOLE_DEBUG = "1"
$DebugPreference = "Continue"
}
# Show help
if ($ShowHelp) {
Show-UninstallHelp
return
}
# Clear screen
Clear-Host
# Get installed apps
$apps = Get-InstalledApplications -ForceRescan:$Rescan
if ($apps.Count -eq 0) {
Write-Warning "No applications found"
return
}
Write-Info "Found $($apps.Count) applications"
# Show selection menu
$selected = Show-AppSelectionMenu -Apps $apps
if ($selected.Count -eq 0) {
Write-Info "No applications selected"
return
}
# Confirm uninstall
$esc = [char]27
Clear-Host
Write-Host ""
Write-Host "$esc[33mThe following applications will be uninstalled:$esc[0m"
Write-Host ""
foreach ($app in $selected) {
Write-Host " $($script:Icons.List) $($app.Name) ($($app.SizeHuman))"
}
Write-Host ""
$confirm = Read-Host "Continue? (y/N)"
if ($confirm -eq 'y' -or $confirm -eq 'Y') {
Uninstall-SelectedApps -Apps $selected
}
else {
Write-Info "Cancelled"
}
}
# Run main
Main