From 124e498f156c5d1e46f5efb529ed6eb8de5c6a03 Mon Sep 17 00:00:00 2001 From: Tw93 Date: Fri, 9 Jan 2026 15:10:26 +0800 Subject: [PATCH] refactor(windows): fix cmdlet shadowing and enforce dependency isolation --- go.mod | 2 - windows/bin/clean.ps1 | 62 +++++++-------- windows/bin/purge.ps1 | 146 +++++++++++++++++----------------- windows/bin/uninstall.ps1 | 148 +++++++++++++++++------------------ windows/go.mod | 30 +------ windows/install.ps1 | 108 ++++++++++++------------- windows/lib/clean/system.ps1 | 100 +++++++++++------------ windows/lib/core/common.ps1 | 20 ++--- windows/lib/core/log.ps1 | 38 ++++----- windows/mole.ps1 | 94 +++++++++++----------- windows/tests/Core.Tests.ps1 | 74 +++++++++--------- 11 files changed, 399 insertions(+), 423 deletions(-) diff --git a/go.mod b/go.mod index 01c230d..70db580 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/windows/bin/clean.ps1 b/windows/bin/clean.ps1 index 4e6311c..ce66194 100644 --- a/windows/bin/clean.ps1 +++ b/windows/bin/clean.ps1 @@ -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 diff --git a/windows/bin/purge.ps1 b/windows/bin/purge.ps1 index 7b8d9a5..2d1f4ba 100644 --- a/windows/bin/purge.ps1 +++ b/windows/bin/purge.ps1 @@ -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 diff --git a/windows/bin/uninstall.ps1 b/windows/bin/uninstall.ps1 index bd1a325..5c4ca67 100644 --- a/windows/bin/uninstall.ps1 +++ b/windows/bin/uninstall.ps1 @@ -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 } diff --git a/windows/go.mod b/windows/go.mod index 8463fe9..5cb9f41 100644 --- a/windows/go.mod +++ b/windows/go.mod @@ -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 ) diff --git a/windows/install.ps1 b/windows/install.ps1 index 95b829c..f4efd12 100644 --- a/windows/install.ps1 +++ b/windows/install.ps1 @@ -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 } diff --git a/windows/lib/clean/system.ps1 b/windows/lib/clean/system.ps1 index 035590f..f84593b 100644 --- a/windows/lib/clean/system.ps1 +++ b/windows/lib/clean/system.ps1 @@ -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 } diff --git a/windows/lib/core/common.ps1 b/windows/lib/core/common.ps1 index 95ea2cf..2cef321 100644 --- a/windows/lib/core/common.ps1 +++ b/windows/lib/core/common.ps1 @@ -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. diff --git a/windows/lib/core/log.ps1 b/windows/lib/core/log.ps1 index 24a7768..b1849bf 100644 --- a/windows/lib/core/log.ps1 +++ b/windows/lib/core/log.ps1 @@ -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)) { diff --git a/windows/mole.ps1 b/windows/mole.ps1 index 08edd98..f5c5eab 100644 --- a/windows/mole.ps1 +++ b/windows/mole.ps1 @@ -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 } diff --git a/windows/tests/Core.Tests.ps1 b/windows/tests/Core.Tests.ps1 index 8b765e4..1eebc31 100644 --- a/windows/tests/Core.Tests.ps1 +++ b/windows/tests/Core.Tests.ps1 @@ -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