1
0
mirror of https://github.com/tw93/Mole.git synced 2026-02-04 16:49:41 +00:00
Files
Mole/windows/bin/purge.ps1
Bhadra cfe8ea0724 fix(windows): address code review issues
- Fix openInExplorer to actually execute explorer.exe (was a no-op)
- Load System.Windows.Forms assembly before using Clipboard
- Use Remove-SafeItem in purge for consistent safety checks
- Fix Dropbox typo (was DroplboxCache)
2026-01-08 20:08:41 +05:30

616 lines
20 KiB
PowerShell

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