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:
300
windows/bin/clean.ps1
Normal file
300
windows/bin/clean.ps1
Normal 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
545
windows/bin/optimize.ps1
Normal 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
610
windows/bin/purge.ps1
Normal 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
596
windows/bin/uninstall.ps1
Normal 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
|
||||
Reference in New Issue
Block a user