mirror of
https://github.com/tw93/Mole.git
synced 2026-02-04 16:49:41 +00:00
621 lines
20 KiB
PowerShell
621 lines
20 KiB
PowerShell
# Mole - Purge Command
|
|
# Aggressive cleanup of project build artifacts
|
|
|
|
#Requires -Version 5.1
|
|
[CmdletBinding()]
|
|
param(
|
|
[Alias('debug')]
|
|
[switch]$DebugMode,
|
|
|
|
[Alias('paths')]
|
|
[switch]$Paths,
|
|
|
|
[Alias('help')]
|
|
[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-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"
|
|
}
|
|
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 (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
|
|
$script:ItemsCleaned++
|
|
}
|
|
else {
|
|
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-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
|
|
}
|
|
else {
|
|
Write-Info "Cancelled"
|
|
}
|
|
}
|
|
|
|
# Run main
|
|
Main
|