mirror of
https://github.com/tw93/Mole.git
synced 2026-02-04 15:04:42 +00:00
* feat: Add Windows package manager publishing infrastructure (#343) - Add comprehensive release build scripts: - build-release.ps1: Creates portable ZIP + SHA256 checksums - build-exe.ps1: Standalone executable builder (PS2EXE) - build-msi.ps1: MSI installer builder (WiX Toolset) - Add GitHub Actions workflow: - Automated builds on version tags - Runs tests before building - Auto-creates GitHub releases with artifacts - Add package manager manifests: - WinGet: Complete manifests ready for microsoft/winget-pkgs - Chocolatey: Full package with install/uninstall scripts - Scoop: JSON manifest ready for submission - Add comprehensive documentation: - RELEASE.md: Complete guide for building and publishing - Package-specific READMEs with submission instructions - ISSUE-343-SUMMARY.md: Quick reference and next steps Successfully tested: Built mole-1.0.0-x64.zip (5 MB) with SHA256 checksums Addresses #343 * chore: update contributors [skip ci] * fix: Support uppercase V and -windows suffix in release workflow * fix: Remove duplicate parameter definitions in clean, optimize, and purge commands - bin/clean.ps1: Remove duplicate System, GameMedia, DebugMode, Whitelist params - bin/optimize.ps1: Remove duplicate DebugMode param - bin/purge.ps1: Remove duplicate DebugMode and Paths params These duplicates were causing parser errors in tests. * fix: Update test regex to match --dry-run format in help text The help output shows --dry-run (kebab-case) but test was checking for DryRun (PascalCase). Updated regex to accept both formats. * fix: Handle Pester 5.x result object properties correctly Pester 5.x uses different property names (Passed.Count, Failed.Count) instead of PassedCount, FailedCount. Added fallback logic to support both formats. * fix: Simplify Pester result parsing with better fallback logic Since Pester already prints test results, just check for failures and assume success if we can't parse the result object. This handles different Pester versions more gracefully. * feat: Add MSI and EXE builds to release workflow - Install WiX Toolset for MSI creation - Install PS2EXE module for standalone EXE - Build all three formats: ZIP, MSI, EXE - MSI and EXE builds marked optional (continue-on-error) - Upload all artifacts to GitHub release - Update release notes with installation instructions for all formats - Add SHA256 verification instructions for each format * fix: Resolve MSI and EXE build failures MSI build fix: - Use UTF8 without BOM for temp WXS file to avoid XML parsing errors - WiX compiler requires clean UTF8 encoding without byte order mark EXE build fix: - Remove hashtable iteration that modified collection during enumeration - Exclude null iconFile parameter from ps2exe params instead of removing it - Prevents 'Collection was modified' exception * fix: Properly handle encoding and version format for MSI and EXE builds MSI fix: - Use System.IO.File.ReadAllText/WriteAllText for consistent UTF8 without BOM - Prevents XML parsing errors in WiX compiler EXE fix: - Extract numeric version only (strip '-windows' suffix) for ps2exe - ps2exe requires version in format n.n.n.n (numeric only) - Fallback to 1.0.0.0 if version parsing fails * fix: Use WriteAllBytes to ensure no BOM in MSI WXS file - Convert string to UTF8 bytes manually - Write bytes directly to file - This guarantees no byte order mark is added - Prevents WiX XML parsing error at position 7 * fix: Read WXS source as bytes to completely avoid BOM issues - Read source file as raw bytes - Convert bytes to string using UTF8Encoding without BOM - Replace version in string - Convert back to bytes and write - This completely avoids PowerShell's Get-Content BOM handling * chore: Simplify release workflow - remove MSI build, minimal release notes - Remove MSI build steps (has persistent BOM/encoding issues) - Remove WiX Toolset installation - Simplify release notes to bare minimum - Focus on ZIP and EXE artifacts only --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
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('d')]
|
|
[switch]$DebugMode,
|
|
|
|
[Alias('p')]
|
|
[switch]$Paths,
|
|
|
|
[Alias('h')]
|
|
[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;35mmo purge$esc[0m - Clean project build artifacts"
|
|
Write-Host ""
|
|
Write-Host "$esc[33mUsage:$esc[0m mo purge [options]"
|
|
Write-Host ""
|
|
Write-Host "$esc[33mOptions:$esc[0m"
|
|
Write-Host " --paths Edit custom scan directories"
|
|
Write-Host " --debug Enable debug logging"
|
|
Write-Host " --help 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 'mo 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
|