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:
2
go.mod
2
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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user