1
0
mirror of https://github.com/tw93/Mole.git synced 2026-02-04 16:14:44 +00:00

refactor(windows): fix cmdlet shadowing and enforce dependency isolation

This commit is contained in:
Tw93
2026-01-09 15:10:26 +08:00
parent ee2c798b29
commit 124e498f15
11 changed files with 399 additions and 423 deletions

2
go.mod
View File

@@ -19,7 +19,6 @@ require (
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
@@ -34,7 +33,6 @@ require (
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.3.8 // indirect
)

View File

@@ -69,12 +69,12 @@ function Show-CleanHelp {
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 = @"
@@ -91,11 +91,11 @@ function Edit-Whitelist {
"@
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"
}
@@ -108,9 +108,9 @@ function Show-CleanupSummary {
[hashtable]$Stats,
[bool]$IsDryRun
)
$esc = [char]27
Write-Host ""
Write-Host "$esc[1;35m" -NoNewline
if ($IsDryRun) {
@@ -121,10 +121,10 @@ function Show-CleanupSummary {
}
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)"
@@ -150,7 +150,7 @@ function Show-CleanupSummary {
}
Write-Host " Free space now: $(Get-FreeSpace)"
}
Write-Host ""
}
@@ -163,26 +163,26 @@ function Start-Cleanup {
[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')
#
@@ -198,11 +198,11 @@ function Start-Cleanup {
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-MoleWarning "System cleanup requires administrator privileges"
Write-Host " Run PowerShell as Administrator for full cleanup"
Write-Host ""
$IncludeSystem = $false
@@ -212,45 +212,45 @@ function Start-Cleanup {
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: $_"
Write-MoleError "Cleanup error: $_"
}
# Get final stats
$stats = Get-CleanupStats
# Show summary
Show-CleanupSummary -Stats $stats -IsDryRun $IsDryRun
}
@@ -265,19 +265,19 @@ function Main {
$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"
@@ -285,7 +285,7 @@ function Main {
else {
$env:MOLE_DRY_RUN = "0"
}
# Run cleanup
try {
Start-Cleanup -IsDryRun $DryRun -IncludeSystem $System

View File

@@ -107,22 +107,22 @@ 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 ($null -eq $paths -or @($paths).Count -eq 0) {
foreach ($path in $script:DefaultSearchPaths) {
@@ -131,7 +131,7 @@ function Get-SearchPaths {
}
}
}
return $paths
}
@@ -140,13 +140,13 @@ 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
@@ -165,10 +165,10 @@ function Edit-SearchPaths {
"@
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"
}
@@ -182,9 +182,9 @@ function Find-Projects {
Find all development projects in search paths
#>
param([string[]]$SearchPaths)
$projects = @()
# Project markers
$projectMarkers = @(
"package.json" # Node.js
@@ -198,42 +198,42 @@ function Find-Projects {
"*.csproj" # .NET
"*.sln" # .NET Solution
)
$esc = [char]27
$pathCount = 0
$totalPaths = if ($null -eq $SearchPaths) { 0 } else { @($SearchPaths).Count }
if ($totalPaths -eq 0) {
return $projects
}
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.
$existingPaths = @($projects | ForEach-Object { $_.Path })
if ($existingPaths -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)
$artifactCount = if ($null -eq $artifacts) { 0 } else { $artifacts.Count }
if ($artifactCount -gt 0) {
$totalSize = ($artifacts | Measure-Object -Property SizeKB -Sum).Sum
if ($null -eq $totalSize) { $totalSize = 0 }
$projects += [PSCustomObject]@{
Path = $projectPath
Name = Split-Path -Leaf $projectPath
@@ -250,9 +250,9 @@ function Find-Projects {
}
}
}
Write-Progress -Activity "Scanning for projects" -Completed
# Sort by size (largest first)
return $projects | Sort-Object -Property TotalSizeKB -Descending
}
@@ -263,16 +263,16 @@ function Find-ProjectArtifacts {
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
@@ -284,7 +284,7 @@ function Find-ProjectArtifacts {
}
elseif ($pattern.Type -eq "File" -and -not $item.PSIsContainer) {
$sizeKB = [Math]::Ceiling($item.Length / 1024)
$artifacts += [PSCustomObject]@{
Path = $item.FullName
Name = $item.Name
@@ -296,7 +296,7 @@ function Find-ProjectArtifacts {
}
}
}
return $artifacts
}
@@ -310,55 +310,55 @@ function Show-ProjectSelectionMenu {
Interactive menu for selecting projects to clean
#>
param([array]$Projects)
$projectCount = if ($null -eq $Projects) { 0 } else { @($Projects).Count }
if ($projectCount -eq 0) {
Write-Warning "No projects with cleanable artifacts found"
Write-MoleWarning "No projects with cleanable artifacts found"
return @()
}
$esc = [char]27
$selectedIndices = @{}
$currentIndex = 0
$pageSize = 12
$pageStart = 0
try { [Console]::CursorVisible = $false } catch { }
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, $projectCount)
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 = if ($null -eq $project.Artifacts) { 0 } else { @($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"
}
@@ -366,7 +366,7 @@ function Show-ProjectSelectionMenu {
Write-Host ""
}
}
# Footer
Write-Host ""
$selectedCount = $selectedIndices.Count
@@ -378,15 +378,15 @@ function Show-ProjectSelectionMenu {
$totalSizeHuman = Format-ByteSize -Bytes ($totalSize * 1024)
Write-Host "$esc[33mSelected:$esc[0m $selectedCount projects ($totalSizeHuman)"
}
# Page indicator
$totalPages = [Math]::Ceiling($projectCount / $pageSize)
$currentPage = [Math]::Floor($pageStart / $pageSize) + 1
Write-Host "$esc[90mPage $currentPage of $totalPages | Total: $projectCount projects$esc[0m"
# Handle input
$key = [Console]::ReadKey($true)
switch ($key.Key) {
'UpArrow' {
if ($currentIndex -gt 0) {
@@ -461,21 +461,21 @@ function Remove-ProjectArtifacts {
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) {
# Use safe removal with protection checks (returns boolean)
$success = Remove-SafeItem -Path $artifact.Path -Description $artifact.Name -Recurse
if ($success) {
Write-Host " $esc[32m$($script:Icons.Success)$esc[0m $($artifact.Name) ($($artifact.SizeHuman))"
$script:TotalSizeCleaned += $artifact.SizeKB
@@ -495,11 +495,11 @@ function Remove-ProjectArtifacts {
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"
@@ -510,7 +510,7 @@ function Show-PurgeSummary {
Write-Host " No artifacts to clean."
Write-Host " Free space now: $(Get-FreeSpace)"
}
Write-Host ""
}
@@ -524,84 +524,84 @@ function Main {
$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 ($null -eq $searchPaths -or $searchPaths.Count -eq 0) {
Write-Warning "No valid search paths found"
Write-MoleWarning "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 ($null -eq $projects -or $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
if ($null -eq $totalSize) { $totalSize = 0 }
$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 ($null -eq $selected -or $selected.Count -eq 0) {
Write-Info "No projects selected"
return
}
# Confirm
Clear-Host
Write-Host ""
$selectedSize = ($selected | Measure-Object -Property TotalSizeKB -Sum).Sum
if ($null -eq $selectedSize) { $selectedSize = 0 }
$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

View File

@@ -75,7 +75,7 @@ $script:ProtectedApps = @(
function Test-ProtectedApp {
param([string]$AppName)
foreach ($pattern in $script:ProtectedApps) {
if ($AppName -like $pattern) {
return $true
@@ -94,12 +94,12 @@ function Get-InstalledApplications {
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 {
@@ -111,44 +111,44 @@ function Get-InstalledApplications {
}
}
}
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)
try {
$regItems = Get-ItemProperty -Path $path -ErrorAction SilentlyContinue
foreach ($item in $regItems) {
# Skip items without required properties
$displayName = $null
$uninstallString = $null
try { $displayName = $item.DisplayName } catch { }
try { $uninstallString = $item.UninstallString } catch { }
if ([string]::IsNullOrWhiteSpace($displayName) -or [string]::IsNullOrWhiteSpace($uninstallString)) {
continue
}
if (Test-ProtectedApp $displayName) {
continue
}
# Calculate size
$sizeKB = 0
try {
@@ -160,7 +160,7 @@ function Get-InstalledApplications {
}
}
catch { }
# Get install date
$installDate = $null
try {
@@ -169,16 +169,16 @@ function Get-InstalledApplications {
}
}
catch { }
# Get other properties safely
$publisher = $null
$version = $null
$installLocation = $null
try { $publisher = $item.Publisher } catch { }
try { $version = $item.DisplayVersion } catch { }
try { $installLocation = $item.InstallLocation } catch { }
$apps += [PSCustomObject]@{
Name = $displayName
Publisher = $publisher
@@ -196,36 +196,36 @@ function Get-InstalledApplications {
Write-Debug "Error scanning registry path $path : $_"
}
}
# UWP / Store Apps
Write-Progress -Activity "Scanning applications" -Status "Scanning Windows Apps" -PercentComplete 75
try {
$uwpApps = Get-AppxPackage -ErrorAction SilentlyContinue |
Where-Object {
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
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
@@ -243,18 +243,18 @@ function Get-InstalledApplications {
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
}
@@ -268,12 +268,12 @@ function Show-AppSelectionMenu {
Interactive menu for selecting applications to uninstall
#>
param([array]$Apps)
if ($Apps.Count -eq 0) {
Write-Warning "No applications found to uninstall"
Write-MoleWarning "No applications found to uninstall"
return @()
}
$esc = [char]27
$selectedIndices = @{}
$currentIndex = 0
@@ -281,56 +281,56 @@ function Show-AppSelectionMenu {
$pageStart = 0
$searchTerm = ""
$filteredApps = $Apps
# Hide cursor (may fail in non-interactive terminals)
try { [Console]::CursorVisible = $false } catch { }
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
}
@@ -338,7 +338,7 @@ function Show-AppSelectionMenu {
Write-Host ""
}
}
# Footer
Write-Host ""
$selectedCount = $selectedIndices.Count
@@ -353,15 +353,15 @@ function Show-AppSelectionMenu {
$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) {
@@ -417,7 +417,7 @@ function Show-AppSelectionMenu {
try { [Console]::CursorVisible = $true } catch { }
$searchTerm = Read-Host
try { [Console]::CursorVisible = $false } catch { }
if ($searchTerm) {
$filteredApps = $Apps | Where-Object { $_.Name -like "*$searchTerm*" }
}
@@ -453,19 +453,19 @@ function Uninstall-SelectedApps {
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
@@ -478,7 +478,7 @@ function Uninstall-SelectedApps {
else {
# Registry app with uninstall string
$uninstallString = $app.UninstallString
# Handle different uninstall types
if ($uninstallString -like "MsiExec.exe*") {
# MSI uninstall
@@ -487,7 +487,7 @@ function Uninstall-SelectedApps {
$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++
@@ -505,13 +505,13 @@ function Uninstall-SelectedApps {
# 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++
@@ -521,7 +521,7 @@ function Uninstall-SelectedApps {
}
catch { }
}
if (-not $uninstalled) {
# Fallback to interactive - don't count as automatic success
Write-Host " $esc[33m(launching uninstaller - verify completion manually)$esc[0m"
@@ -530,7 +530,7 @@ function Uninstall-SelectedApps {
}
}
}
# Clean leftover files
if ($app.InstallLocation -and (Test-Path $app.InstallLocation)) {
Write-Host " $esc[90mCleaning leftover files...$esc[0m"
@@ -543,7 +543,7 @@ function Uninstall-SelectedApps {
$failCount++
}
}
# Summary
Write-Host ""
Write-Host "$esc[1;35mUninstall Complete$esc[0m"
@@ -552,7 +552,7 @@ function Uninstall-SelectedApps {
Write-Host " Failed: $esc[31m$failCount$esc[0m"
}
Write-Host ""
# Clear cache
if (Test-Path $script:AppCacheFile) {
Remove-Item $script:AppCacheFile -Force -ErrorAction SilentlyContinue
@@ -569,48 +569,48 @@ function Main {
$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"
Write-MoleWarning "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
}

View File

@@ -3,32 +3,8 @@ module github.com/tw93/mole/windows
go 1.24.0
require (
github.com/charmbracelet/bubbletea v0.25.0
github.com/charmbracelet/lipgloss v0.9.1
github.com/charmbracelet/bubbletea v1.3.10
github.com/shirou/gopsutil/v3 v3.24.5
)
require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/mattn/go-isatty v0.0.18 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/shoenig/go-m1cpu v0.1.7 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/term v0.6.0 // indirect
golang.org/x/text v0.3.8 // indirect
github.com/yusufpapurcu/wmi v1.2.4
golang.org/x/sys v0.36.0
)

View File

@@ -20,10 +20,10 @@ Set-StrictMode -Version Latest
# ============================================================================
$script:VERSION = "1.0.0"
$script:SourceDir = if ($MyInvocation.MyCommand.Path) {
Split-Path -Parent $MyInvocation.MyCommand.Path
} else {
$PSScriptRoot
$script:SourceDir = if ($MyInvocation.MyCommand.Path) {
Split-Path -Parent $MyInvocation.MyCommand.Path
} else {
$PSScriptRoot
}
$script:ShortcutName = "Mole"
@@ -55,13 +55,13 @@ function Write-Success {
Write-Host " $($c.Green)OK$($c.NC) $Message"
}
function Write-Warning {
function Write-MoleWarning {
param([string]$Message)
$c = $script:Colors
Write-Host " $($c.Yellow)WARN$($c.NC) $Message"
}
function Write-Error {
function Write-MoleError {
param([string]$Message)
$c = $script:Colors
Write-Host " $($c.Red)ERROR$($c.NC) $Message"
@@ -77,7 +77,7 @@ function Show-Banner {
function Show-InstallerHelp {
Show-Banner
$c = $script:Colors
Write-Host " $($c.Green)USAGE:$($c.NC)"
Write-Host ""
@@ -125,49 +125,49 @@ function Test-IsAdmin {
function Add-ToUserPath {
param([string]$Directory)
$currentPath = [Environment]::GetEnvironmentVariable("PATH", "User")
if ($currentPath -split ";" | Where-Object { $_ -eq $Directory }) {
Write-Info "Already in PATH: $Directory"
return $true
}
$newPath = if ($currentPath) { "$currentPath;$Directory" } else { $Directory }
try {
[Environment]::SetEnvironmentVariable("PATH", $newPath, "User")
Write-Success "Added to PATH: $Directory"
# Update current session
$env:PATH = "$env:PATH;$Directory"
return $true
}
catch {
Write-Error "Failed to update PATH: $_"
Write-MoleError "Failed to update PATH: $_"
return $false
}
}
function Remove-FromUserPath {
param([string]$Directory)
$currentPath = [Environment]::GetEnvironmentVariable("PATH", "User")
if (-not $currentPath) {
return $true
}
$paths = $currentPath -split ";" | Where-Object { $_ -ne $Directory -and $_ -ne "" }
$newPath = $paths -join ";"
try {
[Environment]::SetEnvironmentVariable("PATH", $newPath, "User")
Write-Success "Removed from PATH: $Directory"
return $true
}
catch {
Write-Error "Failed to update PATH: $_"
Write-MoleError "Failed to update PATH: $_"
return $false
}
}
@@ -178,11 +178,11 @@ function New-StartMenuShortcut {
[string]$ShortcutName,
[string]$Description
)
$startMenuPath = [Environment]::GetFolderPath("StartMenu")
$programsPath = Join-Path $startMenuPath "Programs"
$shortcutPath = Join-Path $programsPath "$ShortcutName.lnk"
try {
$shell = New-Object -ComObject WScript.Shell
$shortcut = $shell.CreateShortcut($shortcutPath)
@@ -191,23 +191,23 @@ function New-StartMenuShortcut {
$shortcut.Description = $Description
$shortcut.WorkingDirectory = Split-Path -Parent $TargetPath
$shortcut.Save()
Write-Success "Created shortcut: $shortcutPath"
return $true
}
catch {
Write-Error "Failed to create shortcut: $_"
Write-MoleError "Failed to create shortcut: $_"
return $false
}
}
function Remove-StartMenuShortcut {
param([string]$ShortcutName)
$startMenuPath = [Environment]::GetFolderPath("StartMenu")
$programsPath = Join-Path $startMenuPath "Programs"
$shortcutPath = Join-Path $programsPath "$ShortcutName.lnk"
if (Test-Path $shortcutPath) {
try {
Remove-Item $shortcutPath -Force
@@ -215,11 +215,11 @@ function Remove-StartMenuShortcut {
return $true
}
catch {
Write-Error "Failed to remove shortcut: $_"
Write-MoleError "Failed to remove shortcut: $_"
return $false
}
}
return $true
}
@@ -230,16 +230,16 @@ function Remove-StartMenuShortcut {
function Install-Mole {
Write-Info "Installing Mole v$script:VERSION..."
Write-Host ""
# Check if already installed
if ((Test-Path $InstallDir) -and -not $Force) {
Write-Error "Mole is already installed at: $InstallDir"
Write-MoleError "Mole is already installed at: $InstallDir"
Write-Host ""
Write-Host " Use -Force to overwrite or -Uninstall to remove first"
Write-Host ""
return $false
}
# Create install directory
if (-not (Test-Path $InstallDir)) {
try {
@@ -247,14 +247,14 @@ function Install-Mole {
Write-Success "Created directory: $InstallDir"
}
catch {
Write-Error "Failed to create directory: $_"
Write-MoleError "Failed to create directory: $_"
return $false
}
}
# Copy files
Write-Info "Copying files..."
$filesToCopy = @(
"mole.ps1"
"go.mod"
@@ -262,11 +262,11 @@ function Install-Mole {
"lib"
"cmd"
)
foreach ($item in $filesToCopy) {
$src = Join-Path $script:SourceDir $item
$dst = Join-Path $InstallDir $item
if (Test-Path $src) {
try {
if ((Get-Item $src).PSIsContainer) {
@@ -282,12 +282,12 @@ function Install-Mole {
Write-Success "Copied: $item"
}
catch {
Write-Error "Failed to copy $item`: $_"
Write-MoleError "Failed to copy $item`: $_"
return $false
}
}
}
# Create scripts and tests directories if they don't exist
$extraDirs = @("scripts", "tests")
foreach ($dir in $extraDirs) {
@@ -296,7 +296,7 @@ function Install-Mole {
New-Item -ItemType Directory -Path $dirPath -Force | Out-Null
}
}
# Create launcher batch file for easier access
# Note: Store %~dp0 immediately to avoid issues with delayed expansion in the parse loop
$batchContent = @"
@@ -318,26 +318,26 @@ powershell.exe -ExecutionPolicy Bypass -NoLogo -NoProfile -Command "& '%MOLE_DIR
$batchPath = Join-Path $InstallDir "mole.cmd"
Set-Content -Path $batchPath -Value $batchContent -Encoding ASCII
Write-Success "Created launcher: mole.cmd"
# Add to PATH if requested
if ($AddToPath) {
Write-Host ""
Add-ToUserPath -Directory $InstallDir
}
# Create shortcut if requested
if ($CreateShortcut) {
Write-Host ""
$targetPath = Join-Path $InstallDir "mole.ps1"
New-StartMenuShortcut -TargetPath $targetPath -ShortcutName $script:ShortcutName -Description "Windows System Maintenance Toolkit"
}
Write-Host ""
Write-Success "Mole installed successfully!"
Write-Host ""
Write-Host " Location: $InstallDir"
Write-Host ""
if ($AddToPath) {
Write-Host " Run 'mole' from any terminal to start"
}
@@ -348,7 +348,7 @@ powershell.exe -ExecutionPolicy Bypass -NoLogo -NoProfile -Command "& '%MOLE_DIR
Write-Host " Or add to PATH with:"
Write-Host " .\install.ps1 -AddToPath"
}
Write-Host ""
return $true
}
@@ -360,32 +360,32 @@ powershell.exe -ExecutionPolicy Bypass -NoLogo -NoProfile -Command "& '%MOLE_DIR
function Uninstall-Mole {
Write-Info "Uninstalling Mole..."
Write-Host ""
# Check for existing installation
$configPath = Join-Path $env:LOCALAPPDATA "Mole"
$installPath = if (Test-Path $InstallDir) { $InstallDir } elseif (Test-Path $configPath) { $configPath } else { $null }
if (-not $installPath) {
Write-Warning "Mole is not installed"
Write-MoleWarning "Mole is not installed"
return $true
}
# Remove from PATH
Remove-FromUserPath -Directory $installPath
# Remove shortcut
Remove-StartMenuShortcut -ShortcutName $script:ShortcutName
# Remove installation directory
try {
Remove-Item -Path $installPath -Recurse -Force
Write-Success "Removed directory: $installPath"
}
catch {
Write-Error "Failed to remove directory: $_"
Write-MoleError "Failed to remove directory: $_"
return $false
}
# Remove config directory if different from install
$configDir = Join-Path $env:USERPROFILE ".config\mole"
if (Test-Path $configDir) {
@@ -397,11 +397,11 @@ function Uninstall-Mole {
Write-Success "Removed config: $configDir"
}
catch {
Write-Warning "Failed to remove config: $_"
Write-MoleWarning "Failed to remove config: $_"
}
}
}
Write-Host ""
Write-Success "Mole uninstalled successfully!"
Write-Host ""
@@ -417,9 +417,9 @@ function Main {
Show-InstallerHelp
return
}
Show-Banner
if ($Uninstall) {
Uninstall-Mole
}

View File

@@ -23,18 +23,18 @@ function Clear-SystemTempFiles {
.SYNOPSIS
Clean system-level temporary files (requires admin)
#>
if (-not (Test-IsAdmin)) {
Write-Debug "Skipping system temp cleanup - requires admin"
return
}
# Windows Temp folder
$winTemp = "$env:WINDIR\Temp"
if (Test-Path $winTemp) {
Remove-OldFiles -Path $winTemp -DaysOld 7 -Description "Windows temp files"
}
# System temp (different from Windows temp)
$systemTemp = "$env:SYSTEMROOT\Temp"
if ((Test-Path $systemTemp) -and ($systemTemp -ne $winTemp)) {
@@ -52,12 +52,12 @@ function Clear-WindowsLogs {
Clean Windows log files (requires admin)
#>
param([int]$DaysOld = 7)
if (-not (Test-IsAdmin)) {
Write-Debug "Skipping Windows logs cleanup - requires admin"
return
}
# Windows Logs directory
$logPaths = @(
"$env:WINDIR\Logs\CBS"
@@ -71,13 +71,13 @@ function Clear-WindowsLogs {
"$env:PROGRAMDATA\Microsoft\Windows\WER\ReportQueue"
"$env:PROGRAMDATA\Microsoft\Windows\WER\ReportArchive"
)
foreach ($path in $logPaths) {
if (Test-Path $path) {
Remove-OldFiles -Path $path -DaysOld $DaysOld -Description "$(Split-Path -Leaf $path) logs"
}
}
# Setup logs (*.log files in Windows directory)
$setupLogs = Get-ChildItem -Path "$env:WINDIR\*.log" -File -ErrorAction SilentlyContinue |
Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-$DaysOld) }
@@ -96,22 +96,22 @@ function Clear-WindowsUpdateFiles {
.SYNOPSIS
Clean Windows Update download cache (requires admin)
#>
if (-not (Test-IsAdmin)) {
Write-Debug "Skipping Windows Update cleanup - requires admin"
return
}
# Stop Windows Update service
$wuService = Get-Service -Name wuauserv -ErrorAction SilentlyContinue
$wasRunning = $wuService.Status -eq 'Running'
if ($wasRunning) {
if (Test-DryRunMode) {
Write-DryRun "Windows Update cache (service would be restarted)"
return
}
try {
Stop-Service -Name wuauserv -Force -ErrorAction Stop
}
@@ -120,14 +120,14 @@ function Clear-WindowsUpdateFiles {
return
}
}
try {
# Clean download cache
$wuDownloadPath = "$env:WINDIR\SoftwareDistribution\Download"
if (Test-Path $wuDownloadPath) {
Clear-DirectoryContents -Path $wuDownloadPath -Description "Windows Update download cache"
}
# Clean DataStore (old update history - be careful!)
# Only clean temp files, not the actual database
$wuDataStore = "$env:WINDIR\SoftwareDistribution\DataStore\Logs"
@@ -152,15 +152,15 @@ function Clear-InstallerCache {
.SYNOPSIS
Clean Windows Installer cache (orphaned patches)
#>
if (-not (Test-IsAdmin)) {
return
}
# Windows Installer patch cache
# WARNING: Be very careful here - only clean truly orphaned files
$installerPath = "$env:WINDIR\Installer"
# Only clean .tmp files and very old .msp files that are likely orphaned
if (Test-Path $installerPath) {
$tmpFiles = Get-ChildItem -Path $installerPath -Filter "*.tmp" -File -ErrorAction SilentlyContinue
@@ -169,7 +169,7 @@ function Clear-InstallerCache {
Remove-SafeItems -Paths $paths -Description "Installer temp files"
}
}
# Installer logs in temp
$installerLogs = Get-ChildItem -Path $env:TEMP -Filter "MSI*.LOG" -File -ErrorAction SilentlyContinue |
Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-30) }
@@ -188,26 +188,26 @@ function Invoke-ComponentStoreCleanup {
.SYNOPSIS
Run Windows Component Store cleanup (DISM)
#>
if (-not (Test-IsAdmin)) {
Write-Debug "Skipping component store cleanup - requires admin"
return
}
if (Test-DryRunMode) {
Write-DryRun "Component Store cleanup (DISM)"
Set-SectionActivity
return
}
try {
Write-Info "Running Component Store cleanup (this may take a while)..."
# Run DISM cleanup
$result = Start-Process -FilePath "dism.exe" `
-ArgumentList "/Online", "/Cleanup-Image", "/StartComponentCleanup" `
-Wait -PassThru -NoNewWindow -ErrorAction Stop
if ($result.ExitCode -eq 0) {
Write-Success "Component Store cleanup"
Set-SectionActivity
@@ -230,13 +230,13 @@ function Clear-MemoryDumps {
.SYNOPSIS
Clean Windows memory dumps
#>
$dumpPaths = @(
"$env:WINDIR\MEMORY.DMP"
"$env:WINDIR\Minidump"
"$env:LOCALAPPDATA\CrashDumps"
)
foreach ($path in $dumpPaths) {
if (Test-Path $path -PathType Leaf) {
# Single file (MEMORY.DMP)
@@ -258,29 +258,29 @@ function Clear-SystemFontCache {
.SYNOPSIS
Clear Windows font cache (requires admin and may need restart)
#>
if (-not (Test-IsAdmin)) {
return
}
$fontCacheService = Get-Service -Name "FontCache" -ErrorAction SilentlyContinue
if ($fontCacheService) {
if (Test-DryRunMode) {
Write-DryRun "System font cache"
return
}
try {
# Stop font cache service
Stop-Service -Name "FontCache" -Force -ErrorAction SilentlyContinue
# Clear font cache files
$fontCachePath = "$env:WINDIR\ServiceProfiles\LocalService\AppData\Local\FontCache"
if (Test-Path $fontCachePath) {
Clear-DirectoryContents -Path $fontCachePath -Description "System font cache"
}
# Restart font cache service
Start-Service -Name "FontCache" -ErrorAction SilentlyContinue
}
@@ -301,19 +301,19 @@ function Invoke-DiskCleanupTool {
Run Windows built-in Disk Cleanup tool with predefined settings
#>
param([switch]$Full)
if (-not (Test-IsAdmin)) {
Write-Debug "Skipping Disk Cleanup tool - requires admin for full cleanup"
}
if (Test-DryRunMode) {
Write-DryRun "Windows Disk Cleanup tool"
return
}
# Set up registry keys for automated cleanup
$cleanupKey = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VolumeCaches"
$cleanupItems = @(
"Active Setup Temp Folders"
"Downloaded Program Files"
@@ -331,7 +331,7 @@ function Invoke-DiskCleanupTool {
"Windows Error Reporting System Archive Files"
"Windows Error Reporting System Queue Files"
)
if ($Full -and (Test-IsAdmin)) {
$cleanupItems += @(
"Previous Installations"
@@ -341,7 +341,7 @@ function Invoke-DiskCleanupTool {
"Windows Upgrade Log Files"
)
}
# Enable cleanup items in registry
foreach ($item in $cleanupItems) {
$itemPath = Join-Path $cleanupKey $item
@@ -349,13 +349,13 @@ function Invoke-DiskCleanupTool {
Set-ItemProperty -Path $itemPath -Name "StateFlags0100" -Value 2 -Type DWord -ErrorAction SilentlyContinue
}
}
try {
# Run disk cleanup
$process = Start-Process -FilePath "cleanmgr.exe" `
-ArgumentList "/sagerun:100" `
-Wait -PassThru -NoNewWindow -ErrorAction Stop
if ($process.ExitCode -eq 0) {
Write-Success "Windows Disk Cleanup"
Set-SectionActivity
@@ -379,41 +379,41 @@ function Invoke-SystemCleanup {
[switch]$IncludeComponentStore,
[switch]$IncludeDiskCleanup
)
Start-Section "System cleanup"
if (-not (Test-IsAdmin)) {
Write-Warning "Running without admin - some cleanup tasks will be skipped"
Write-MoleWarning "Running without admin - some cleanup tasks will be skipped"
}
# System temp files
Clear-SystemTempFiles
# Windows logs
Clear-WindowsLogs -DaysOld 7
# Windows Update cache
Clear-WindowsUpdateFiles
# Installer cache
Clear-InstallerCache
# Memory dumps
Clear-MemoryDumps
# Font cache
Clear-SystemFontCache
# Optional: Component Store (can take a long time)
if ($IncludeComponentStore) {
Invoke-ComponentStoreCleanup
}
# Optional: Windows Disk Cleanup tool
if ($IncludeDiskCleanup) {
Invoke-DiskCleanupTool -Full
}
Stop-Section
}

View File

@@ -6,8 +6,8 @@ Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
# Prevent multiple sourcing
if ((Get-Variable -Name 'MOLE_COMMON_LOADED' -Scope Script -ErrorAction SilentlyContinue) -and $script:MOLE_COMMON_LOADED) {
return
if ((Get-Variable -Name 'MOLE_COMMON_LOADED' -Scope Script -ErrorAction SilentlyContinue) -and $script:MOLE_COMMON_LOADED) {
return
}
$script:MOLE_COMMON_LOADED = $true
@@ -61,18 +61,18 @@ function Initialize-Mole {
.SYNOPSIS
Initialize Mole environment
#>
# Ensure config directory exists
$configPath = Get-ConfigPath
# Ensure cache directory exists
$cachePath = Get-CachePath
# Set up cleanup trap
$null = Register-EngineEvent -SourceIdentifier PowerShell.Exiting -Action {
Clear-TempFiles
}
Write-Debug "Mole initialized"
Write-Debug "Config: $configPath"
Write-Debug "Cache: $cachePath"
@@ -90,8 +90,8 @@ function Request-AdminPrivileges {
Restarts the script with elevated privileges using UAC
#>
if (-not (Test-IsAdmin)) {
Write-Warning "Some operations require administrator privileges."
Write-MoleWarning "Some operations require administrator privileges."
if (Read-Confirmation -Prompt "Restart with admin privileges?" -Default $true) {
$scriptPath = $MyInvocation.PSCommandPath
if ($scriptPath) {
@@ -113,7 +113,7 @@ function Invoke-AsAdmin {
[Parameter(Mandatory)]
[scriptblock]$ScriptBlock
)
if (Test-IsAdmin) {
& $ScriptBlock
}
@@ -126,5 +126,5 @@ function Invoke-AsAdmin {
# ============================================================================
# Exports (functions are available via dot-sourcing)
# ============================================================================
# All functions from base.ps1, log.ps1, file_ops.ps1, and ui.ps1 are
# All functions from base.ps1, log.ps1, file_ops.ps1, and ui.ps1 are
# automatically available when this file is dot-sourced.

View File

@@ -37,16 +37,16 @@ function Write-LogMessage {
[string]$Color,
[string]$Icon
)
$timestamp = Get-Date -Format "HH:mm:ss"
$colorCode = $script:Colors[$Color]
$nc = $script:Colors.NC
$formattedIcon = if ($Icon) { "$Icon " } else { "" }
$output = " ${colorCode}${formattedIcon}${nc}${Message}"
Write-Host $output
# Also write to log file if configured
if ($script:LogConfig.LogFile) {
"$timestamp [$Level] $Message" | Out-File -Append -FilePath $script:LogConfig.LogFile -Encoding UTF8
@@ -71,7 +71,8 @@ function Write-Success {
Write-LogMessage -Message $Message -Level "SUCCESS" -Color "Green" -Icon $script:Icons.Success
}
function Write-Warning {
function Write-MoleWarning {
<#
.SYNOPSIS
Write a warning message
@@ -80,7 +81,7 @@ function Write-Warning {
Write-LogMessage -Message $Message -Level "WARN" -Color "Yellow" -Icon $script:Icons.Warning
}
function Write-Error {
function Write-MoleError {
<#
.SYNOPSIS
Write an error message
@@ -89,13 +90,14 @@ function Write-Error {
Write-LogMessage -Message $Message -Level "ERROR" -Color "Red" -Icon $script:Icons.Error
}
function Write-Debug {
<#
.SYNOPSIS
Write a debug message (only if debug mode is enabled)
#>
param([string]$Message)
if ($script:LogConfig.DebugEnabled) {
$gray = $script:Colors.Gray
$nc = $script:Colors.NC
@@ -128,15 +130,15 @@ function Start-Section {
Start a new section with a title
#>
param([string]$Title)
$script:CurrentSection.Active = $true
$script:CurrentSection.Activity = $false
$script:CurrentSection.Name = $Title
$purple = $script:Colors.PurpleBold
$nc = $script:Colors.NC
$arrow = $script:Icons.Arrow
Write-Host ""
Write-Host "${purple}${arrow} ${Title}${nc}"
}
@@ -176,11 +178,11 @@ function Start-Spinner {
Start an inline spinner with message
#>
param([string]$Message = "Working...")
$script:SpinnerIndex = 0
$gray = $script:Colors.Gray
$nc = $script:Colors.NC
Write-Host -NoNewline " ${gray}$($script:SpinnerFrames[0]) $Message${nc}"
}
@@ -190,12 +192,12 @@ function Update-Spinner {
Update the spinner animation
#>
param([string]$Message)
$script:SpinnerIndex = ($script:SpinnerIndex + 1) % $script:SpinnerFrames.Count
$frame = $script:SpinnerFrames[$script:SpinnerIndex]
$gray = $script:Colors.Gray
$nc = $script:Colors.NC
# Move cursor to beginning of line and clear
Write-Host -NoNewline "`r ${gray}$frame $Message${nc} "
}
@@ -223,15 +225,15 @@ function Write-Progress {
[string]$Message = "",
[int]$Width = 30
)
$percent = if ($Total -gt 0) { [Math]::Round(($Current / $Total) * 100) } else { 0 }
$filled = [Math]::Round(($Width * $Current) / [Math]::Max($Total, 1))
$empty = $Width - $filled
$bar = ("[" + ("=" * $filled) + (" " * $empty) + "]")
$cyan = $script:Colors.Cyan
$nc = $script:Colors.NC
Write-Host -NoNewline "`r ${cyan}$bar${nc} ${percent}% $Message "
}
@@ -253,7 +255,7 @@ function Set-LogFile {
Set a log file for persistent logging
#>
param([string]$Path)
$script:LogConfig.LogFile = $Path
$dir = Split-Path -Parent $Path
if ($dir -and -not (Test-Path $dir)) {

View File

@@ -6,10 +6,10 @@
param(
[Parameter(Position = 0)]
[string]$Command,
[Parameter(Position = 1, ValueFromRemainingArguments)]
[string[]]$CommandArgs,
[switch]$Version,
[switch]$ShowHelp
)
@@ -49,9 +49,9 @@ function Show-MainHelp {
$gray = $script:Colors.Gray
$green = $script:Colors.Green
$nc = $script:Colors.NC
Show-Banner
Write-Host " ${cyan}Windows System Maintenance Toolkit${nc}"
Write-Host " ${gray}Clean, optimize, and maintain your Windows system${nc}"
Write-Host ""
@@ -95,50 +95,50 @@ function Show-MainHelp {
function Show-MainMenu {
$options = @(
@{
Name = "Clean"
Description = "Deep system cleanup"
@{
Name = "Clean"
Description = "Deep system cleanup"
Command = "clean"
Icon = $script:Icons.Trash
}
@{
Name = "Uninstall"
Description = "Remove applications"
@{
Name = "Uninstall"
Description = "Remove applications"
Command = "uninstall"
Icon = $script:Icons.Folder
}
@{
Name = "Analyze"
Description = "Disk space analyzer"
@{
Name = "Analyze"
Description = "Disk space analyzer"
Command = "analyze"
Icon = $script:Icons.File
}
@{
Name = "Status"
Description = "System monitor"
@{
Name = "Status"
Description = "System monitor"
Command = "status"
Icon = $script:Icons.Solid
}
@{
Name = "Optimize"
Description = "System optimization"
@{
Name = "Optimize"
Description = "System optimization"
Command = "optimize"
Icon = $script:Icons.Arrow
}
@{
Name = "Purge"
Description = "Clean dev artifacts"
@{
Name = "Purge"
Description = "Clean dev artifacts"
Command = "purge"
Icon = $script:Icons.List
}
)
$selected = Show-Menu -Title "What would you like to do?" -Options $options -AllowBack
if ($null -eq $selected) {
return $null
}
return $selected.Command
}
@@ -151,16 +151,16 @@ function Invoke-MoleCommand {
[string]$CommandName,
[string[]]$Arguments
)
$scriptPath = Join-Path $script:MOLE_BIN "$CommandName.ps1"
if (-not (Test-Path $scriptPath)) {
Write-Error "Unknown command: $CommandName"
Write-MoleError "Unknown command: $CommandName"
Write-Host ""
Write-Host "Run 'mole -ShowHelp' for available commands"
return
}
# Execute the command script with arguments using splatting
# This properly handles switch parameters passed as strings
$argCount = if ($null -eq $Arguments) { 0 } else { @($Arguments).Count }
@@ -168,11 +168,11 @@ function Invoke-MoleCommand {
# Build a hashtable for splatting
$splatParams = @{}
$positionalArgs = @()
foreach ($arg in $Arguments) {
# Remove surrounding quotes if present
$cleanArg = $arg.Trim("'`"")
if ($cleanArg -match '^-(\w+)$') {
# It's a switch parameter (e.g., -DryRun)
$paramName = $Matches[1]
@@ -189,7 +189,7 @@ function Invoke-MoleCommand {
$positionalArgs += $cleanArg
}
}
# Execute with splatting
if ($positionalArgs.Count -gt 0) {
& $scriptPath @splatParams @positionalArgs
@@ -212,11 +212,11 @@ function Show-SystemInfo {
$gray = $script:Colors.Gray
$green = $script:Colors.Green
$nc = $script:Colors.NC
$winInfo = Get-WindowsVersion
$freeSpace = Get-FreeSpace
$isAdmin = if (Test-IsAdmin) { "${green}Yes${nc}" } else { "${gray}No${nc}" }
Write-Host ""
Write-Host " ${gray}System:${nc} $($winInfo.Name)"
Write-Host " ${gray}Free Space:${nc} $freeSpace on $($env:SystemDrive)"
@@ -231,13 +231,13 @@ function Show-SystemInfo {
function Main {
# Initialize
Initialize-Mole
# Handle switches passed as strings (when called via batch file with quoted args)
# e.g., mole '-ShowHelp' becomes $Command = "-ShowHelp" instead of $ShowHelp = $true
$effectiveShowHelp = $ShowHelp
$effectiveVersion = $Version
$effectiveCommand = $Command
if ($Command -match '^-(.+)$') {
$switchName = $Matches[1]
switch ($switchName) {
@@ -248,43 +248,43 @@ function Main {
'v' { $effectiveVersion = $true; $effectiveCommand = $null }
}
}
# Handle version flag
if ($effectiveVersion) {
Show-Version
return
}
# Handle help flag
if ($effectiveShowHelp -and -not $effectiveCommand) {
Show-MainHelp
return
}
# If command specified, route to it
if ($effectiveCommand) {
$validCommands = @("clean", "uninstall", "analyze", "status", "optimize", "purge")
if ($effectiveCommand -in $validCommands) {
Invoke-MoleCommand -CommandName $effectiveCommand -Arguments $CommandArgs
}
else {
Write-Error "Unknown command: $effectiveCommand"
Write-MoleError "Unknown command: $effectiveCommand"
Write-Host ""
Write-Host "Available commands: $($validCommands -join ', ')"
Write-Host "Run 'mole -ShowHelp' for more information"
}
return
}
# Interactive mode
Clear-Host
Show-Banner
Show-SystemInfo
while ($true) {
$selectedCommand = Show-MainMenu
if ($null -eq $selectedCommand) {
Clear-Host
Write-Host ""
@@ -292,10 +292,10 @@ function Main {
Write-Host ""
break
}
Clear-Host
Invoke-MoleCommand -CommandName $selectedCommand -Arguments @()
Write-Host ""
Write-Host " Press any key to continue..."
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
@@ -311,7 +311,7 @@ try {
}
catch {
Write-Host ""
Write-Error "An error occurred: $_"
Write-MoleError "An error occurred: $_"
Write-Host ""
exit 1
}

View File

@@ -5,7 +5,7 @@ BeforeAll {
# Get the windows directory path (tests are in windows/tests/)
$script:WindowsDir = Split-Path -Parent $PSScriptRoot
$script:LibDir = Join-Path $script:WindowsDir "lib"
# Import core modules
. "$script:LibDir\core\base.ps1"
. "$script:LibDir\core\log.ps1"
@@ -22,7 +22,7 @@ Describe "Base Module" {
$script:Colors.Red | Should -Not -BeNullOrEmpty
$script:Colors.NC | Should -Not -BeNullOrEmpty
}
It "Should define icon set" {
$script:Icons | Should -Not -BeNullOrEmpty
$script:Icons.Success | Should -Not -BeNullOrEmpty
@@ -30,14 +30,14 @@ Describe "Base Module" {
$script:Icons.Warning | Should -Not -BeNullOrEmpty
}
}
Context "Test-IsAdmin" {
It "Should return a boolean" {
$result = Test-IsAdmin
$result | Should -BeOfType [bool]
}
}
Context "Get-WindowsVersion" {
It "Should return version info" {
$result = Get-WindowsVersion
@@ -47,7 +47,7 @@ Describe "Base Module" {
$result.Build | Should -Not -BeNullOrEmpty
}
}
Context "Get-FreeSpace" {
It "Should return free space string" {
$result = Get-FreeSpace
@@ -55,7 +55,7 @@ Describe "Base Module" {
# Format is like "100.00GB" or "50.5MB" (no space between number and unit)
$result | Should -Match "\d+(\.\d+)?(B|KB|MB|GB|TB)"
}
It "Should accept drive parameter" {
$result = Get-FreeSpace -Drive "C:"
$result | Should -Not -BeNullOrEmpty
@@ -69,14 +69,14 @@ Describe "File Operations Module" {
$script:TestDir = Join-Path $env:TEMP "mole_test_$(Get-Random)"
New-Item -ItemType Directory -Path $script:TestDir -Force | Out-Null
}
AfterAll {
# Cleanup test directory
if (Test-Path $script:TestDir) {
Remove-Item -Path $script:TestDir -Recurse -Force -ErrorAction SilentlyContinue
}
}
Context "Format-ByteSize" {
It "Should format bytes correctly" {
# Actual format: no space, uses N0/N1/N2 formatting
@@ -85,81 +85,81 @@ Describe "File Operations Module" {
Format-ByteSize -Bytes 1048576 | Should -Be "1.0MB"
Format-ByteSize -Bytes 1073741824 | Should -Be "1.00GB"
}
It "Should handle large numbers" {
Format-ByteSize -Bytes 1099511627776 | Should -Be "1.00TB"
}
}
Context "Get-PathSize" {
BeforeEach {
# Create test file
$testFile = Join-Path $script:TestDir "testfile.txt"
"Hello World" | Set-Content -Path $testFile
}
It "Should return size for file" {
$testFile = Join-Path $script:TestDir "testfile.txt"
$result = Get-PathSize -Path $testFile
$result | Should -BeGreaterThan 0
}
It "Should return size for directory" {
$result = Get-PathSize -Path $script:TestDir
$result | Should -BeGreaterThan 0
}
It "Should return 0 for non-existent path" {
$result = Get-PathSize -Path "C:\NonExistent\Path\12345"
$result | Should -Be 0
}
}
Context "Test-ProtectedPath" {
It "Should protect Windows directory" {
Test-ProtectedPath -Path "C:\Windows" | Should -Be $true
Test-ProtectedPath -Path "C:\Windows\System32" | Should -Be $true
}
It "Should protect Windows Defender paths" {
Test-ProtectedPath -Path "C:\Program Files\Windows Defender" | Should -Be $true
Test-ProtectedPath -Path "C:\ProgramData\Microsoft\Windows Defender" | Should -Be $true
}
It "Should not protect temp directories" {
Test-ProtectedPath -Path $env:TEMP | Should -Be $false
}
}
Context "Test-SafePath" {
It "Should return false for protected paths" {
Test-SafePath -Path "C:\Windows" | Should -Be $false
Test-SafePath -Path "C:\Windows\System32" | Should -Be $false
}
It "Should return true for safe paths" {
Test-SafePath -Path $env:TEMP | Should -Be $true
}
It "Should return false for empty paths" {
# Test-SafePath has mandatory path parameter, so empty/null throws
# But internally it should handle empty strings gracefully
{ Test-SafePath -Path "" } | Should -Throw
}
}
Context "Remove-SafeItem" {
BeforeEach {
$script:TestFile = Join-Path $script:TestDir "safe_remove_test.txt"
"Test content" | Set-Content -Path $script:TestFile
}
It "Should remove file successfully" {
$result = Remove-SafeItem -Path $script:TestFile
$result | Should -Be $true
Test-Path $script:TestFile | Should -Be $false
}
It "Should respect DryRun mode" {
$env:MOLE_DRY_RUN = "1"
try {
@@ -174,7 +174,7 @@ Describe "File Operations Module" {
Set-DryRunMode -Enabled $false
}
}
It "Should not remove protected paths" {
$result = Remove-SafeItem -Path "C:\Windows\System32"
$result | Should -Be $false
@@ -187,22 +187,22 @@ Describe "Logging Module" {
It "Should have Write-Info function" {
{ Write-Info "Test message" } | Should -Not -Throw
}
It "Should have Write-Success function" {
{ Write-Success "Test message" } | Should -Not -Throw
}
It "Should have Write-Warning function" {
# Note: The actual function is Write-Warning (conflicts with built-in)
{ Write-Warning "Test message" } | Should -Not -Throw
It "Should have Write-MoleWarning function" {
# Note: The actual function is Write-MoleWarning
{ Write-MoleWarning "Test message" } | Should -Not -Throw
}
It "Should have Write-Error function" {
# Note: The actual function is Write-Error (conflicts with built-in)
{ Write-Error "Test message" } | Should -Not -Throw
It "Should have Write-MoleError function" {
# Note: The actual function is Write-MoleError
{ Write-MoleError "Test message" } | Should -Not -Throw
}
}
Context "Section Functions" {
It "Should start and stop sections without error" {
{ Start-Section -Title "Test Section" } | Should -Not -Throw
@@ -217,23 +217,23 @@ Describe "UI Module" {
{ Show-Banner } | Should -Not -Throw
}
}
Context "Show-Header" {
It "Should display header without error" {
{ Show-Header -Title "Test Header" } | Should -Not -Throw
}
It "Should accept subtitle parameter" {
{ Show-Header -Title "Test" -Subtitle "Subtitle" } | Should -Not -Throw
}
}
Context "Show-Summary" {
It "Should display summary without error" {
{ Show-Summary -SizeBytes 1024 -ItemCount 5 } | Should -Not -Throw
}
}
Context "Read-Confirmation" {
It "Should have Read-Confirmation function" {
Get-Command Read-Confirmation -ErrorAction SilentlyContinue | Should -Not -BeNullOrEmpty