1
0
mirror of https://github.com/tw93/Mole.git synced 2026-02-04 15:39:42 +00:00

chore: remove windows support from dev branch (moved to dedicated windows branch)

This commit is contained in:
Tw93
2026-01-10 13:19:56 +08:00
parent e84a457c2f
commit 9434b41fbe
34 changed files with 0 additions and 10074 deletions

View File

@@ -1,196 +0,0 @@
name: Windows CI
on:
push:
branches: [main, dev]
paths:
- 'windows/**'
- '.github/workflows/windows.yml'
pull_request:
branches: [main, dev]
paths:
- 'windows/**'
- '.github/workflows/windows.yml'
jobs:
build:
name: Build & Test
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.24"
cache-dependency-path: windows/go.sum
- name: Build Go binaries
working-directory: windows
run: |
go build -o bin/analyze.exe ./cmd/analyze/
go build -o bin/status.exe ./cmd/status/
- name: Run Go tests
working-directory: windows
run: go test -v ./...
- name: Validate PowerShell syntax
shell: pwsh
run: |
$scripts = Get-ChildItem -Path windows -Filter "*.ps1" -Recurse
$errors = @()
foreach ($script in $scripts) {
$parseErrors = $null
$null = [System.Management.Automation.Language.Parser]::ParseFile(
$script.FullName,
[ref]$null,
[ref]$parseErrors
)
if ($parseErrors) {
Write-Host "ERROR: $($script.FullName)" -ForegroundColor Red
foreach ($err in $parseErrors) {
Write-Host " $($err.Message)" -ForegroundColor Red
}
$errors += $script.FullName
} else {
Write-Host "OK: $($script.Name)" -ForegroundColor Green
}
}
if ($errors.Count -gt 0) {
Write-Host "`n$($errors.Count) script(s) have syntax errors!" -ForegroundColor Red
exit 1
}
pester:
name: Pester Tests
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Install Pester
shell: pwsh
run: |
Set-PSRepository -Name PSGallery -InstallationPolicy Trusted
Install-Module -Name Pester -MinimumVersion 5.0.0 -Force -SkipPublisherCheck
- name: Run Pester tests
shell: pwsh
run: |
Import-Module Pester -MinimumVersion 5.0.0
$config = New-PesterConfiguration
$config.Run.Path = "windows/tests"
$config.Run.Exit = $true
$config.Output.Verbosity = "Detailed"
$config.TestResult.Enabled = $true
$config.TestResult.OutputPath = "windows/test-results.xml"
$config.TestResult.OutputFormat = "NUnitXml"
Invoke-Pester -Configuration $config
- name: Upload test results
uses: actions/upload-artifact@v4
if: always()
with:
name: pester-results
path: windows/test-results.xml
compatibility:
name: Windows Compatibility
strategy:
matrix:
os: [windows-2022, windows-2019]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Test PowerShell 5.1
shell: powershell
run: |
Write-Host "Testing on ${{ matrix.os }} with PowerShell $($PSVersionTable.PSVersion)"
# Test main entry point
$result = & powershell -ExecutionPolicy Bypass -File "windows\mole.ps1" -ShowHelp 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Host "mole.ps1 -ShowHelp failed" -ForegroundColor Red
exit 1
}
Write-Host "✓ mole.ps1 works on ${{ matrix.os }}"
- name: Test command scripts
shell: powershell
run: |
$commands = @("clean", "uninstall", "optimize", "purge", "analyze", "status")
foreach ($cmd in $commands) {
$scriptPath = "windows\bin\$cmd.ps1"
if (Test-Path $scriptPath) {
$result = & powershell -ExecutionPolicy Bypass -File $scriptPath -ShowHelp 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Host "✗ $cmd.ps1 failed" -ForegroundColor Red
exit 1
}
Write-Host "✓ $cmd.ps1 works"
}
}
security:
name: Security Checks
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Check for unsafe patterns
shell: pwsh
run: |
Write-Host "Checking for unsafe removal patterns..."
$unsafePatterns = @(
"Remove-Item.*-Recurse.*-Force.*\\\$env:SystemRoot",
"Remove-Item.*-Recurse.*-Force.*C:\\Windows",
"Remove-Item.*-Recurse.*-Force.*C:\\Program Files"
)
$files = Get-ChildItem -Path windows -Filter "*.ps1" -Recurse
$issues = @()
foreach ($file in $files) {
$content = Get-Content $file.FullName -Raw
foreach ($pattern in $unsafePatterns) {
if ($content -match $pattern) {
$issues += "$($file.Name): matches unsafe pattern"
}
}
}
if ($issues.Count -gt 0) {
Write-Host "Unsafe patterns found:" -ForegroundColor Red
$issues | ForEach-Object { Write-Host " $_" -ForegroundColor Red }
exit 1
}
Write-Host "✓ No unsafe patterns found" -ForegroundColor Green
- name: Verify protection checks
shell: pwsh
run: |
Write-Host "Verifying protection logic..."
# Source file_ops to get Test-IsProtectedPath
. windows\lib\core\base.ps1
. windows\lib\core\file_ops.ps1
$protectedPaths = @(
"C:\Windows",
"C:\Windows\System32",
"C:\Program Files",
"C:\Program Files (x86)"
)
foreach ($path in $protectedPaths) {
if (-not (Test-ProtectedPath -Path $path)) {
Write-Host "✗ $path should be protected!" -ForegroundColor Red
exit 1
}
Write-Host "✓ $path is protected" -ForegroundColor Green
}

16
windows/.gitignore vendored
View File

@@ -1,16 +0,0 @@
# Windows Mole - .gitignore
# Build artifacts
bin/*.exe
# Go build cache
.gocache/
# IDE files
.idea/
.vscode/
*.code-workspace
# Test artifacts
*.test
coverage.out

View File

@@ -1,44 +0,0 @@
# Mole Windows - Makefile
# Build Go tools for Windows
.PHONY: all build clean analyze status
# Default target
all: build
# Build both tools
build: analyze status
# Build analyze tool
analyze:
@echo "Building analyze..."
@go build -o bin/analyze.exe ./cmd/analyze/
# Build status tool
status:
@echo "Building status..."
@go build -o bin/status.exe ./cmd/status/
# Clean build artifacts
clean:
@echo "Cleaning..."
@rm -f bin/analyze.exe bin/status.exe
# Install (copy to PATH)
install: build
@echo "Installing to $(USERPROFILE)/bin..."
@mkdir -p "$(USERPROFILE)/bin"
@cp bin/analyze.exe "$(USERPROFILE)/bin/"
@cp bin/status.exe "$(USERPROFILE)/bin/"
# Run tests
test:
@go test -v ./...
# Format code
fmt:
@go fmt ./...
# Vet code
vet:
@go vet ./...

View File

@@ -1,169 +0,0 @@
# Mole for Windows
Windows support for [Mole](https://github.com/tw93/Mole) - A system maintenance toolkit.
## Requirements
- Windows 10/11
- PowerShell 5.1 or later (pre-installed on Windows 10/11)
- Go 1.24+ (for building TUI tools)
## Installation
### Quick Install
```powershell
# Clone the repository
git clone https://github.com/tw93/Mole.git
cd Mole/windows
# Run the installer
.\install.ps1 -AddToPath
```
### Manual Installation
```powershell
# Install to custom location
.\install.ps1 -InstallDir C:\Tools\Mole -AddToPath
# Create Start Menu shortcut
.\install.ps1 -AddToPath -CreateShortcut
```
### Uninstall
```powershell
.\install.ps1 -Uninstall
```
## Usage
```powershell
# Interactive menu
mole
# Show help
mole -ShowHelp
# Show version
mole -Version
# Commands
mole clean # Deep system cleanup
mole clean -DryRun # Preview cleanup without deleting
mole uninstall # Interactive app uninstaller
mole optimize # System optimization
mole purge # Clean developer artifacts
mole analyze # Disk space analyzer
mole status # System health monitor
```
## Commands
| Command | Description |
|---------|-------------|
| `clean` | Deep cleanup of temp files, caches, and logs |
| `uninstall` | Interactive application uninstaller |
| `optimize` | System optimization and health checks |
| `purge` | Clean project build artifacts (node_modules, etc.) |
| `analyze` | Interactive disk space analyzer (TUI) |
| `status` | Real-time system health monitor (TUI) |
## Environment Variables
| Variable | Description |
|----------|-------------|
| `MOLE_DRY_RUN=1` | Preview changes without making them |
| `MOLE_DEBUG=1` | Enable debug output |
| `MO_ANALYZE_PATH` | Starting path for analyze tool |
## Directory Structure
```
windows/
├── mole.ps1 # Main CLI entry point
├── install.ps1 # Windows installer
├── Makefile # Build automation for Go tools
├── go.mod # Go module definition
├── go.sum # Go dependencies
├── bin/
│ ├── clean.ps1 # Deep cleanup orchestrator
│ ├── uninstall.ps1 # Interactive app uninstaller
│ ├── optimize.ps1 # System optimization
│ ├── purge.ps1 # Project artifact cleanup
│ ├── analyze.ps1 # Disk analyzer wrapper
│ └── status.ps1 # Status monitor wrapper
├── cmd/
│ ├── analyze/ # Disk analyzer (Go TUI)
│ │ └── main.go
│ └── status/ # System status (Go TUI)
│ └── main.go
└── lib/
├── core/
│ ├── base.ps1 # Core definitions and utilities
│ ├── common.ps1 # Common functions loader
│ ├── file_ops.ps1 # Safe file operations
│ ├── log.ps1 # Logging functions
│ └── ui.ps1 # Interactive UI components
└── clean/
├── user.ps1 # User cleanup (temp, downloads, etc.)
├── caches.ps1 # Browser and app caches
├── dev.ps1 # Developer tool caches
├── apps.ps1 # Application leftovers
└── system.ps1 # System cleanup (requires admin)
```
## Building TUI Tools
The analyze and status commands require Go to be installed:
```powershell
cd windows
# Build both tools
make build
# Or build individually
go build -o bin/analyze.exe ./cmd/analyze/
go build -o bin/status.exe ./cmd/status/
# The wrapper scripts will auto-build if Go is available
```
## Configuration
Mole stores its configuration in:
- Config: `~\.config\mole\`
- Cache: `~\.cache\mole\`
- Whitelist: `~\.config\mole\whitelist.txt`
- Purge paths: `~\.config\mole\purge_paths.txt`
## Development Phases
### Phase 1: Core Infrastructure ✅
- [x] `install.ps1` - Windows installer
- [x] `mole.ps1` - Main CLI entry point
- [x] `lib/core/*` - Core utility libraries
### Phase 2: Cleanup Features ✅
- [x] `bin/clean.ps1` - Deep cleanup orchestrator
- [x] `bin/uninstall.ps1` - App removal with leftover detection
- [x] `bin/optimize.ps1` - System optimization
- [x] `bin/purge.ps1` - Project artifact cleanup
- [x] `lib/clean/*` - Cleanup modules
### Phase 3: TUI Tools ✅
- [x] `cmd/analyze/` - Disk usage analyzer (Go)
- [x] `cmd/status/` - Real-time system monitor (Go)
- [x] `bin/analyze.ps1` - Analyzer wrapper
- [x] `bin/status.ps1` - Status wrapper
### Phase 4: Testing & CI (Planned)
- [ ] `tests/` - Pester tests
- [ ] GitHub Actions workflows
- [ ] `scripts/build.ps1` - Build automation
## License
Same license as the main Mole project.

View File

@@ -1,79 +0,0 @@
# Mole - Analyze Command
# Disk space analyzer wrapper
#Requires -Version 5.1
param(
[Parameter(Position = 0)]
[string]$Path,
[switch]$ShowHelp
)
$ErrorActionPreference = "Stop"
# Script location
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$windowsDir = Split-Path -Parent $scriptDir
$binPath = Join-Path $windowsDir "bin\analyze.exe"
# Help
function Show-AnalyzeHelp {
$esc = [char]27
Write-Host ""
Write-Host "$esc[1;35mMole Analyze$esc[0m - Interactive disk space analyzer"
Write-Host ""
Write-Host "$esc[33mUsage:$esc[0m mole analyze [path]"
Write-Host ""
Write-Host "$esc[33mOptions:$esc[0m"
Write-Host " [path] Path to analyze (default: user profile)"
Write-Host " -ShowHelp Show this help message"
Write-Host ""
Write-Host "$esc[33mKeybindings:$esc[0m"
Write-Host " Up/Down Navigate entries"
Write-Host " Enter Enter directory"
Write-Host " Backspace Go back"
Write-Host " Space Multi-select"
Write-Host " d Delete selected"
Write-Host " f Toggle large files view"
Write-Host " o Open in Explorer"
Write-Host " r Refresh"
Write-Host " q Quit"
Write-Host ""
}
if ($ShowHelp) {
Show-AnalyzeHelp
return
}
# Check if binary exists
if (-not (Test-Path $binPath)) {
Write-Host "Building analyze tool..." -ForegroundColor Cyan
$cmdDir = Join-Path $windowsDir "cmd\analyze"
$binDir = Join-Path $windowsDir "bin"
if (-not (Test-Path $binDir)) {
New-Item -ItemType Directory -Path $binDir -Force | Out-Null
}
Push-Location $windowsDir
try {
$result = & go build -o "$binPath" "./cmd/analyze/" 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Host "Failed to build analyze tool: $result" -ForegroundColor Red
Pop-Location
return
}
}
finally {
Pop-Location
}
}
# Set path environment variable if provided
if ($Path) {
$env:MO_ANALYZE_PATH = $Path
}
# Run the binary
& $binPath

View File

@@ -1,300 +0,0 @@
# Mole - Clean Command
# Deep cleanup for Windows with dry-run support and whitelist
#Requires -Version 5.1
[CmdletBinding()]
param(
[switch]$DryRun,
[switch]$System,
[switch]$DebugMode,
[switch]$Whitelist,
[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"
# Import cleanup modules
. "$libDir\clean\user.ps1"
. "$libDir\clean\caches.ps1"
. "$libDir\clean\dev.ps1"
. "$libDir\clean\apps.ps1"
. "$libDir\clean\system.ps1"
# ============================================================================
# Configuration
# ============================================================================
$script:ExportListFile = "$env:USERPROFILE\.config\mole\clean-list.txt"
# ============================================================================
# Help
# ============================================================================
function Show-CleanHelp {
$esc = [char]27
Write-Host ""
Write-Host "$esc[1;35mMole Clean$esc[0m - Deep cleanup for Windows"
Write-Host ""
Write-Host "$esc[33mUsage:$esc[0m mole clean [options]"
Write-Host ""
Write-Host "$esc[33mOptions:$esc[0m"
Write-Host " -DryRun Preview changes without deleting (recommended first run)"
Write-Host " -System Include system-level cleanup (requires admin)"
Write-Host " -Whitelist Manage protected paths"
Write-Host " -DebugMode Enable debug logging"
Write-Host " -ShowHelp Show this help message"
Write-Host ""
Write-Host "$esc[33mExamples:$esc[0m"
Write-Host " mole clean -DryRun # Preview what would be cleaned"
Write-Host " mole clean # Run standard cleanup"
Write-Host " mole clean -System # Include system cleanup (as admin)"
Write-Host ""
}
# ============================================================================
# Whitelist Management
# ============================================================================
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 = @"
# Mole Whitelist - Paths listed here will never be cleaned
# Use full paths or patterns with wildcards (*)
#
# Examples:
# C:\Users\YourName\Documents\ImportantProject
# C:\Users\*\AppData\Local\MyApp
# $env:LOCALAPPDATA\CriticalApp
#
# Add your protected paths below:
"@
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"
}
# ============================================================================
# Cleanup Summary
# ============================================================================
function Show-CleanupSummary {
param(
[hashtable]$Stats,
[bool]$IsDryRun
)
$esc = [char]27
Write-Host ""
Write-Host "$esc[1;35m" -NoNewline
if ($IsDryRun) {
Write-Host "Dry run complete - no changes made" -NoNewline
}
else {
Write-Host "Cleanup complete" -NoNewline
}
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)"
Write-Host " Categories: $($Stats.TotalItems)"
Write-Host ""
Write-Host " Detailed list: $esc[90m$($script:ExportListFile)$esc[0m"
Write-Host " Run without -DryRun to apply cleanup"
}
else {
Write-Host " Space freed: $esc[32m${sizeGB}GB$esc[0m"
Write-Host " Items cleaned: $($Stats.FilesCleaned)"
Write-Host " Categories: $($Stats.TotalItems)"
Write-Host ""
Write-Host " Free space now: $(Get-FreeSpace)"
}
}
else {
if ($IsDryRun) {
Write-Host " No significant reclaimable space detected."
}
else {
Write-Host " System was already clean; no additional space freed."
}
Write-Host " Free space now: $(Get-FreeSpace)"
}
Write-Host ""
}
# ============================================================================
# Main Cleanup Flow
# ============================================================================
function Start-Cleanup {
param(
[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')
#
# How to protect files:
# 1. Copy any path below to $($script:Config.WhitelistFile)
# 2. Run: mole clean -Whitelist
#
"@
Set-Content -Path $script:ExportListFile -Value $header
}
else {
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-MoleWarning "System cleanup requires administrator privileges"
Write-Host " Run PowerShell as Administrator for full cleanup"
Write-Host ""
$IncludeSystem = $false
}
else {
Write-Host "$esc[32m$($script:Icons.Success)$esc[0m Running with Administrator privileges"
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-MoleError "Cleanup error: $_"
}
# Get final stats
$stats = Get-CleanupStats
# Show summary
Show-CleanupSummary -Stats $stats -IsDryRun $IsDryRun
}
# ============================================================================
# Main Entry Point
# ============================================================================
function Main {
# Enable debug if requested
if ($DebugMode) {
$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"
}
else {
$env:MOLE_DRY_RUN = "0"
}
# Run cleanup
try {
Start-Cleanup -IsDryRun $DryRun -IncludeSystem $System
}
finally {
# Cleanup temp files
Clear-TempFiles
}
}
# Run main
Main

View File

@@ -1,545 +0,0 @@
# Mole - Optimize Command
# System optimization and health checks for Windows
#Requires -Version 5.1
[CmdletBinding()]
param(
[switch]$DryRun,
[switch]$DebugMode,
[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:OptimizationsApplied = 0
$script:IssuesFound = 0
$script:IssuesFixed = 0
# ============================================================================
# Help
# ============================================================================
function Show-OptimizeHelp {
$esc = [char]27
Write-Host ""
Write-Host "$esc[1;35mMole Optimize$esc[0m - System optimization and health checks"
Write-Host ""
Write-Host "$esc[33mUsage:$esc[0m mole optimize [options]"
Write-Host ""
Write-Host "$esc[33mOptions:$esc[0m"
Write-Host " -DryRun Preview optimizations without applying"
Write-Host " -DebugMode Enable debug logging"
Write-Host " -ShowHelp Show this help message"
Write-Host ""
Write-Host "$esc[33mOptimizations:$esc[0m"
Write-Host " - Disk defragmentation/TRIM (SSD optimization)"
Write-Host " - Windows Search index optimization"
Write-Host " - DNS cache flush"
Write-Host " - Network optimization"
Write-Host " - Startup program analysis"
Write-Host " - System file verification"
Write-Host ""
}
# ============================================================================
# System Health Information
# ============================================================================
function Get-SystemHealth {
<#
.SYNOPSIS
Collect system health metrics
#>
$health = @{}
# Memory info
$os = Get-WmiObject Win32_OperatingSystem
$health.MemoryTotalGB = [Math]::Round($os.TotalVisibleMemorySize / 1MB, 1)
$health.MemoryUsedGB = [Math]::Round(($os.TotalVisibleMemorySize - $os.FreePhysicalMemory) / 1MB, 1)
$health.MemoryUsedPercent = [Math]::Round((($os.TotalVisibleMemorySize - $os.FreePhysicalMemory) / $os.TotalVisibleMemorySize) * 100, 0)
# Disk info
$disk = Get-WmiObject Win32_LogicalDisk -Filter "DeviceID='$env:SystemDrive'"
$health.DiskTotalGB = [Math]::Round($disk.Size / 1GB, 0)
$health.DiskUsedGB = [Math]::Round(($disk.Size - $disk.FreeSpace) / 1GB, 0)
$health.DiskUsedPercent = [Math]::Round((($disk.Size - $disk.FreeSpace) / $disk.Size) * 100, 0)
# Uptime
$uptime = (Get-Date) - (Get-CimInstance Win32_OperatingSystem).LastBootUpTime
$health.UptimeDays = [Math]::Round($uptime.TotalDays, 1)
# CPU info
$cpu = Get-WmiObject Win32_Processor
$health.CPUName = $cpu.Name
$health.CPUCores = $cpu.NumberOfLogicalProcessors
return $health
}
function Show-SystemHealth {
param([hashtable]$Health)
$esc = [char]27
Write-Host "$esc[34m$($script:Icons.Admin)$esc[0m System " -NoNewline
Write-Host "$($Health.MemoryUsedGB)/$($Health.MemoryTotalGB)GB RAM | " -NoNewline
Write-Host "$($Health.DiskUsedGB)/$($Health.DiskTotalGB)GB Disk | " -NoNewline
Write-Host "Uptime $($Health.UptimeDays)d"
}
# ============================================================================
# Optimization Tasks
# ============================================================================
function Optimize-DiskDrive {
<#
.SYNOPSIS
Optimize disk (defrag for HDD, TRIM for SSD)
#>
$esc = [char]27
Write-Host ""
Write-Host "$esc[34m$($script:Icons.Arrow) Disk Optimization$esc[0m"
if (-not (Test-IsAdmin)) {
Write-Host " $esc[33m$($script:Icons.Warning)$esc[0m Requires administrator privileges"
return
}
if ($script:DryRun) {
Write-Host " $esc[33m$($script:Icons.DryRun)$esc[0m Would optimize $env:SystemDrive"
$script:OptimizationsApplied++
return
}
try {
# Check if SSD or HDD
$diskNumber = (Get-Partition -DriveLetter $env:SystemDrive[0]).DiskNumber
$mediaType = (Get-PhysicalDisk | Where-Object { $_.DeviceId -eq $diskNumber }).MediaType
if ($mediaType -eq "SSD") {
Write-Host " Running TRIM on SSD..."
$null = Optimize-Volume -DriveLetter $env:SystemDrive[0] -ReTrim -ErrorAction Stop
Write-Host " $esc[32m$($script:Icons.Success)$esc[0m SSD TRIM completed"
}
else {
Write-Host " Running defragmentation on HDD..."
$null = Optimize-Volume -DriveLetter $env:SystemDrive[0] -Defrag -ErrorAction Stop
Write-Host " $esc[32m$($script:Icons.Success)$esc[0m Defragmentation completed"
}
$script:OptimizationsApplied++
}
catch {
Write-Host " $esc[31m$($script:Icons.Error)$esc[0m Disk optimization failed: $_"
}
}
function Optimize-SearchIndex {
<#
.SYNOPSIS
Rebuild Windows Search index if needed
#>
$esc = [char]27
Write-Host ""
Write-Host "$esc[34m$($script:Icons.Arrow) Windows Search$esc[0m"
$searchService = Get-Service -Name WSearch -ErrorAction SilentlyContinue
if (-not $searchService) {
Write-Host " $esc[90mWindows Search service not found$esc[0m"
return
}
if ($searchService.Status -ne 'Running') {
Write-Host " $esc[33m$($script:Icons.Warning)$esc[0m Windows Search service is not running"
if ($script:DryRun) {
Write-Host " $esc[33m$($script:Icons.DryRun)$esc[0m Would start search service"
return
}
try {
Start-Service -Name WSearch -ErrorAction Stop
Write-Host " $esc[32m$($script:Icons.Success)$esc[0m Started Windows Search service"
$script:OptimizationsApplied++
}
catch {
Write-Host " $esc[31m$($script:Icons.Error)$esc[0m Could not start search service"
}
}
else {
Write-Host " $esc[32m$($script:Icons.Success)$esc[0m Search service running"
}
}
function Clear-DnsCache {
<#
.SYNOPSIS
Clear DNS resolver cache
#>
$esc = [char]27
Write-Host ""
Write-Host "$esc[34m$($script:Icons.Arrow) DNS Cache$esc[0m"
if ($script:DryRun) {
Write-Host " $esc[33m$($script:Icons.DryRun)$esc[0m Would flush DNS cache"
$script:OptimizationsApplied++
return
}
try {
Clear-DnsClientCache -ErrorAction Stop
Write-Host " $esc[32m$($script:Icons.Success)$esc[0m DNS cache flushed"
$script:OptimizationsApplied++
}
catch {
Write-Host " $esc[31m$($script:Icons.Error)$esc[0m Could not flush DNS cache: $_"
}
}
function Optimize-Network {
<#
.SYNOPSIS
Network stack optimization
#>
$esc = [char]27
Write-Host ""
Write-Host "$esc[34m$($script:Icons.Arrow) Network Optimization$esc[0m"
if (-not (Test-IsAdmin)) {
Write-Host " $esc[33m$($script:Icons.Warning)$esc[0m Requires administrator privileges"
return
}
if ($script:DryRun) {
Write-Host " $esc[33m$($script:Icons.DryRun)$esc[0m Would reset Winsock catalog"
Write-Host " $esc[33m$($script:Icons.DryRun)$esc[0m Would reset TCP/IP stack"
$script:OptimizationsApplied += 2
return
}
try {
# Reset Winsock
$null = netsh winsock reset 2>&1
Write-Host " $esc[32m$($script:Icons.Success)$esc[0m Winsock catalog reset"
$script:OptimizationsApplied++
}
catch {
Write-Host " $esc[31m$($script:Icons.Error)$esc[0m Winsock reset failed"
}
try {
# Flush ARP cache
$null = netsh interface ip delete arpcache 2>&1
Write-Host " $esc[32m$($script:Icons.Success)$esc[0m ARP cache cleared"
$script:OptimizationsApplied++
}
catch {
Write-Host " $esc[31m$($script:Icons.Error)$esc[0m ARP cache clear failed"
}
}
function Get-StartupPrograms {
<#
.SYNOPSIS
Analyze startup programs
#>
$esc = [char]27
Write-Host ""
Write-Host "$esc[34m$($script:Icons.Arrow) Startup Programs$esc[0m"
$startupPaths = @(
"HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run"
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run"
"HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Run"
)
$startupCount = 0
foreach ($path in $startupPaths) {
if (Test-Path $path) {
$items = Get-ItemProperty -Path $path -ErrorAction SilentlyContinue
$props = @($items.PSObject.Properties | Where-Object {
$_.Name -notin @('PSPath', 'PSParentPath', 'PSChildName', 'PSDrive', 'PSProvider')
})
$startupCount += $props.Count
}
}
# Also check startup folder
$startupFolder = "$env:APPDATA\Microsoft\Windows\Start Menu\Programs\Startup"
if (Test-Path $startupFolder) {
$startupFiles = @(Get-ChildItem -Path $startupFolder -File -ErrorAction SilentlyContinue)
$startupCount += $startupFiles.Count
}
if ($startupCount -gt 10) {
Write-Host " $esc[33m$($script:Icons.Warning)$esc[0m $startupCount startup programs (high)"
Write-Host " $esc[90mConsider disabling unnecessary startup items in Task Manager$esc[0m"
$script:IssuesFound++
}
elseif ($startupCount -gt 5) {
Write-Host " $esc[33m$($script:Icons.Warning)$esc[0m $startupCount startup programs (moderate)"
$script:IssuesFound++
}
else {
Write-Host " $esc[32m$($script:Icons.Success)$esc[0m $startupCount startup programs"
}
}
function Test-SystemFiles {
<#
.SYNOPSIS
Run System File Checker (SFC)
#>
$esc = [char]27
Write-Host ""
Write-Host "$esc[34m$($script:Icons.Arrow) System File Verification$esc[0m"
if (-not (Test-IsAdmin)) {
Write-Host " $esc[33m$($script:Icons.Warning)$esc[0m Requires administrator privileges"
return
}
if ($script:DryRun) {
Write-Host " $esc[33m$($script:Icons.DryRun)$esc[0m Would run System File Checker"
return
}
Write-Host " Running System File Checker (this may take several minutes)..."
try {
$sfcResult = Start-Process -FilePath "sfc.exe" -ArgumentList "/scannow" `
-Wait -PassThru -NoNewWindow -RedirectStandardOutput "$env:TEMP\sfc_output.txt" -ErrorAction Stop
$output = Get-Content "$env:TEMP\sfc_output.txt" -ErrorAction SilentlyContinue
Remove-Item "$env:TEMP\sfc_output.txt" -Force -ErrorAction SilentlyContinue
if ($output -match "did not find any integrity violations") {
Write-Host " $esc[32m$($script:Icons.Success)$esc[0m No integrity violations found"
}
elseif ($output -match "found corrupt files and successfully repaired") {
Write-Host " $esc[32m$($script:Icons.Success)$esc[0m Corrupt files were repaired"
$script:IssuesFixed++
}
elseif ($output -match "found corrupt files but was unable to fix") {
Write-Host " $esc[31m$($script:Icons.Error)$esc[0m Found corrupt files that could not be repaired"
Write-Host " $esc[90mRun 'DISM /Online /Cleanup-Image /RestoreHealth' then retry SFC$esc[0m"
$script:IssuesFound++
}
else {
Write-Host " $esc[32m$($script:Icons.Success)$esc[0m Scan completed"
}
}
catch {
Write-Host " $esc[31m$($script:Icons.Error)$esc[0m System File Checker failed: $_"
}
}
function Test-DiskHealth {
<#
.SYNOPSIS
Check disk health status
#>
$esc = [char]27
Write-Host ""
Write-Host "$esc[34m$($script:Icons.Arrow) Disk Health$esc[0m"
try {
$disks = Get-PhysicalDisk -ErrorAction Stop
foreach ($disk in $disks) {
$status = $disk.HealthStatus
$name = $disk.FriendlyName
if ($status -eq "Healthy") {
Write-Host " $esc[32m$($script:Icons.Success)$esc[0m $name - Healthy"
}
elseif ($status -eq "Warning") {
Write-Host " $esc[33m$($script:Icons.Warning)$esc[0m $name - Warning"
Write-Host " $esc[90mDisk may have issues, consider backing up data$esc[0m"
$script:IssuesFound++
}
else {
Write-Host " $esc[31m$($script:Icons.Error)$esc[0m $name - $status"
Write-Host " $esc[31mDisk has critical issues, back up data immediately!$esc[0m"
$script:IssuesFound++
}
}
}
catch {
Write-Host " $esc[90mCould not check disk health$esc[0m"
}
}
function Test-WindowsUpdate {
<#
.SYNOPSIS
Check Windows Update status
#>
$esc = [char]27
Write-Host ""
Write-Host "$esc[34m$($script:Icons.Arrow) Windows Update$esc[0m"
try {
$updateSession = New-Object -ComObject Microsoft.Update.Session
$updateSearcher = $updateSession.CreateUpdateSearcher()
Write-Host " Checking for updates..."
$searchResult = $updateSearcher.Search("IsInstalled=0")
$importantUpdates = $searchResult.Updates | Where-Object {
$_.MsrcSeverity -in @('Critical', 'Important')
}
if ($importantUpdates.Count -gt 0) {
Write-Host " $esc[33m$($script:Icons.Warning)$esc[0m $($importantUpdates.Count) important updates available"
Write-Host " $esc[90mRun Windows Update to install$esc[0m"
$script:IssuesFound++
}
elseif ($searchResult.Updates.Count -gt 0) {
Write-Host " $esc[90m$($script:Icons.List)$esc[0m $($searchResult.Updates.Count) optional updates available"
}
else {
Write-Host " $esc[32m$($script:Icons.Success)$esc[0m System is up to date"
}
}
catch {
Write-Host " $esc[90mCould not check Windows Update status$esc[0m"
}
}
# ============================================================================
# Summary
# ============================================================================
function Show-OptimizeSummary {
$esc = [char]27
Write-Host ""
Write-Host "$esc[1;35m" -NoNewline
if ($script:DryRun) {
Write-Host "Dry Run Complete - No Changes Made" -NoNewline
}
else {
Write-Host "Optimization Complete" -NoNewline
}
Write-Host "$esc[0m"
Write-Host ""
if ($script:DryRun) {
Write-Host " Would apply $esc[33m$($script:OptimizationsApplied)$esc[0m optimizations"
Write-Host " Run without -DryRun to apply changes"
}
else {
Write-Host " Optimizations applied: $esc[32m$($script:OptimizationsApplied)$esc[0m"
if ($script:IssuesFixed -gt 0) {
Write-Host " Issues fixed: $esc[32m$($script:IssuesFixed)$esc[0m"
}
if ($script:IssuesFound -gt 0) {
Write-Host " Issues found: $esc[33m$($script:IssuesFound)$esc[0m"
}
else {
Write-Host " System health: $esc[32mGood$esc[0m"
}
}
Write-Host ""
}
# ============================================================================
# Main Entry Point
# ============================================================================
function Main {
# Enable debug if requested
if ($DebugMode) {
$env:MOLE_DEBUG = "1"
$DebugPreference = "Continue"
}
# Show help
if ($ShowHelp) {
Show-OptimizeHelp
return
}
# Set dry-run mode
$script:DryRun = $DryRun
# Clear screen
Clear-Host
$esc = [char]27
Write-Host ""
Write-Host "$esc[1;35mOptimize and Check$esc[0m"
Write-Host ""
if ($script:DryRun) {
Write-Host "$esc[33m$($script:Icons.DryRun) DRY RUN MODE$esc[0m - No changes will be made"
Write-Host ""
}
# Show system health
$health = Get-SystemHealth
Show-SystemHealth -Health $health
# Run optimizations
Optimize-DiskDrive
Optimize-SearchIndex
Clear-DnsCache
Optimize-Network
# Run health checks
Get-StartupPrograms
Test-DiskHealth
Test-WindowsUpdate
# System file check is slow, ask first
if (-not $script:DryRun -and (Test-IsAdmin)) {
Write-Host ""
$runSfc = Read-Host "Run System File Checker? This may take several minutes (y/N)"
if ($runSfc -eq 'y' -or $runSfc -eq 'Y') {
Test-SystemFiles
}
}
# Summary
Show-OptimizeSummary
}
# Run main
Main

View File

@@ -1,615 +0,0 @@
# 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-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

View File

@@ -1,73 +0,0 @@
# Mole - Status Command
# System status monitor wrapper
#Requires -Version 5.1
param(
[switch]$ShowHelp
)
$ErrorActionPreference = "Stop"
# Script location
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$windowsDir = Split-Path -Parent $scriptDir
$binPath = Join-Path $windowsDir "bin\status.exe"
# Help
function Show-StatusHelp {
$esc = [char]27
Write-Host ""
Write-Host "$esc[1;35mMole Status$esc[0m - Real-time system health monitor"
Write-Host ""
Write-Host "$esc[33mUsage:$esc[0m mole status"
Write-Host ""
Write-Host "$esc[33mOptions:$esc[0m"
Write-Host " -ShowHelp Show this help message"
Write-Host ""
Write-Host "$esc[33mDisplays:$esc[0m"
Write-Host " - System health score (0-100)"
Write-Host " - CPU usage and model"
Write-Host " - Memory and swap usage"
Write-Host " - Disk space per drive"
Write-Host " - Top processes by CPU"
Write-Host " - Network interfaces"
Write-Host ""
Write-Host "$esc[33mKeybindings:$esc[0m"
Write-Host " c Toggle mole animation"
Write-Host " r Force refresh"
Write-Host " q Quit"
Write-Host ""
}
if ($ShowHelp) {
Show-StatusHelp
return
}
# Check if binary exists
if (-not (Test-Path $binPath)) {
Write-Host "Building status tool..." -ForegroundColor Cyan
$cmdDir = Join-Path $windowsDir "cmd\status"
$binDir = Join-Path $windowsDir "bin"
if (-not (Test-Path $binDir)) {
New-Item -ItemType Directory -Path $binDir -Force | Out-Null
}
Push-Location $windowsDir
try {
$result = & go build -o "$binPath" "./cmd/status/" 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Host "Failed to build status tool: $result" -ForegroundColor Red
Pop-Location
return
}
}
finally {
Pop-Location
}
}
# Run the binary
& $binPath

View File

@@ -1,623 +0,0 @@
# Mole - Uninstall Command
# Interactive application uninstaller for Windows
#Requires -Version 5.1
[CmdletBinding()]
param(
[switch]$DebugMode,
[switch]$Rescan,
[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:CacheDir = "$env:USERPROFILE\.cache\mole"
$script:AppCacheFile = "$script:CacheDir\app_scan_cache.json"
$script:CacheTTLHours = 24
# ============================================================================
# Help
# ============================================================================
function Show-UninstallHelp {
$esc = [char]27
Write-Host ""
Write-Host "$esc[1;35mMole Uninstall$esc[0m - Interactive application uninstaller"
Write-Host ""
Write-Host "$esc[33mUsage:$esc[0m mole uninstall [options]"
Write-Host ""
Write-Host "$esc[33mOptions:$esc[0m"
Write-Host " -Rescan Force rescan of installed applications"
Write-Host " -DebugMode Enable debug logging"
Write-Host " -ShowHelp Show this help message"
Write-Host ""
Write-Host "$esc[33mFeatures:$esc[0m"
Write-Host " - Scans installed programs from registry and Windows Apps"
Write-Host " - Shows program size and last used date"
Write-Host " - Interactive selection with arrow keys"
Write-Host " - Cleans leftover files after uninstall"
Write-Host ""
}
# ============================================================================
# Protected Applications
# ============================================================================
$script:ProtectedApps = @(
"Microsoft Windows"
"Windows Feature Experience Pack"
"Microsoft Edge"
"Microsoft Edge WebView2"
"Windows Security"
"Microsoft Visual C++ *"
"Microsoft .NET *"
".NET Desktop Runtime*"
"Microsoft Update Health Tools"
"NVIDIA Graphics Driver*"
"AMD Software*"
"Intel*Driver*"
)
function Test-ProtectedApp {
param([string]$AppName)
foreach ($pattern in $script:ProtectedApps) {
if ($AppName -like $pattern) {
return $true
}
}
return $false
}
# ============================================================================
# Application Discovery
# ============================================================================
function Get-InstalledApplications {
<#
.SYNOPSIS
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 {
$cached = Get-Content $script:AppCacheFile | ConvertFrom-Json
return $cached
}
catch {
Write-Debug "Cache read failed, rescanning..."
}
}
}
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 {
if ($item.EstimatedSize) {
$sizeKB = [long]$item.EstimatedSize
}
elseif ($item.InstallLocation -and (Test-Path $item.InstallLocation -ErrorAction SilentlyContinue)) {
$sizeKB = Get-PathSizeKB -Path $item.InstallLocation
}
}
catch { }
# Get install date
$installDate = $null
try {
if ($item.InstallDate) {
$installDate = [DateTime]::ParseExact($item.InstallDate, "yyyyMMdd", $null)
}
}
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
Version = $version
SizeKB = $sizeKB
SizeHuman = Format-ByteSize -Bytes ($sizeKB * 1024)
InstallLocation = $installLocation
UninstallString = $uninstallString
InstallDate = $installDate
Source = "Registry"
}
}
}
catch {
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 {
$_.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
-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
Version = $uwp.Version
SizeKB = $sizeKB
SizeHuman = Format-ByteSize -Bytes ($sizeKB * 1024)
InstallLocation = $uwp.InstallLocation
UninstallString = $null
PackageFullName = $uwp.PackageFullName
InstallDate = $null
Source = "WindowsStore"
}
}
}
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
}
# ============================================================================
# Application Selection UI
# ============================================================================
function Show-AppSelectionMenu {
<#
.SYNOPSIS
Interactive menu for selecting applications to uninstall
#>
param([array]$Apps)
if ($Apps.Count -eq 0) {
Write-MoleWarning "No applications found to uninstall"
return @()
}
$esc = [char]27
$selectedIndices = @{}
$currentIndex = 0
$pageSize = 15
$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
}
else {
Write-Host ""
}
}
# Footer
Write-Host ""
$selectedCount = $selectedIndices.Count
if ($selectedCount -gt 0) {
$totalSize = 0
foreach ($key in $selectedIndices.Keys) {
$app = $Apps | Where-Object { $_.Name -eq $key }
if ($app.SizeKB) {
$totalSize += $app.SizeKB
}
}
$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) {
$currentIndex--
if ($currentIndex -lt $pageStart) {
$pageStart = [Math]::Max(0, $pageStart - $pageSize)
}
}
}
'DownArrow' {
if ($currentIndex -lt $filteredApps.Count - 1) {
$currentIndex++
if ($currentIndex -ge $pageStart + $pageSize) {
$pageStart += $pageSize
}
}
}
'PageUp' {
$pageStart = [Math]::Max(0, $pageStart - $pageSize)
$currentIndex = $pageStart
}
'PageDown' {
$pageStart = [Math]::Min($filteredApps.Count - $pageSize, $pageStart + $pageSize)
if ($pageStart -lt 0) { $pageStart = 0 }
$currentIndex = $pageStart
}
'Spacebar' {
$app = $filteredApps[$currentIndex]
if ($selectedIndices.ContainsKey($app.Name)) {
$selectedIndices.Remove($app.Name)
}
else {
$selectedIndices[$app.Name] = $true
}
}
'Enter' {
if ($selectedIndices.Count -gt 0) {
# Return selected apps
$selected = $Apps | Where-Object { $selectedIndices.ContainsKey($_.Name) }
return $selected
}
}
'Escape' {
return @()
}
'Q' {
return @()
}
'Oem2' { # Forward slash
# Search mode
Write-Host ""
Write-Host "Search: " -NoNewline
try { [Console]::CursorVisible = $true } catch { }
$searchTerm = Read-Host
try { [Console]::CursorVisible = $false } catch { }
if ($searchTerm) {
$filteredApps = $Apps | Where-Object { $_.Name -like "*$searchTerm*" }
}
else {
$filteredApps = $Apps
}
$currentIndex = 0
$pageStart = 0
}
'Backspace' {
if ($searchTerm) {
$searchTerm = ""
$filteredApps = $Apps
$currentIndex = 0
$pageStart = 0
}
}
}
}
}
finally {
try { [Console]::CursorVisible = $true } catch { }
}
}
# ============================================================================
# Uninstallation
# ============================================================================
function Uninstall-SelectedApps {
<#
.SYNOPSIS
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
if ($app.PackageFullName) {
Remove-AppxPackage -Package $app.PackageFullName -ErrorAction Stop
Write-Host " $esc[32m$($script:Icons.Success)$esc[0m"
$successCount++
}
}
else {
# Registry app with uninstall string
$uninstallString = $app.UninstallString
# Handle different uninstall types
if ($uninstallString -like "MsiExec.exe*") {
# MSI uninstall
$productCode = [regex]::Match($uninstallString, '\{[0-9A-F-]+\}').Value
if ($productCode) {
$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++
}
else {
Write-Host " $esc[33m(requires interaction)$esc[0m"
# Fallback to interactive uninstall
Start-Process -FilePath "msiexec.exe" -ArgumentList "/x", $productCode -Wait
$successCount++
}
}
}
else {
# Direct executable uninstall
# 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++
$uninstalled = $true
break
}
}
catch { }
}
if (-not $uninstalled) {
# Fallback to interactive - don't count as automatic success
Write-Host " $esc[33m(launching uninstaller - verify completion manually)$esc[0m"
Start-Process -FilePath "cmd.exe" -ArgumentList "/c", "`"$uninstallString`"" -Wait
# Note: Not incrementing $successCount since we can't verify if user completed or cancelled
}
}
}
# Clean leftover files
if ($app.InstallLocation -and (Test-Path $app.InstallLocation)) {
Write-Host " $esc[90mCleaning leftover files...$esc[0m"
Remove-SafeItem -Path $app.InstallLocation -Description "Leftover files" -Recurse
}
}
catch {
Write-Host " $esc[31m$($script:Icons.Error)$esc[0m"
Write-Debug "Uninstall failed: $_"
$failCount++
}
}
# Summary
Write-Host ""
Write-Host "$esc[1;35mUninstall Complete$esc[0m"
Write-Host " Successfully uninstalled: $esc[32m$successCount$esc[0m"
if ($failCount -gt 0) {
Write-Host " Failed: $esc[31m$failCount$esc[0m"
}
Write-Host ""
# Clear cache
if (Test-Path $script:AppCacheFile) {
Remove-Item $script:AppCacheFile -Force -ErrorAction SilentlyContinue
}
}
# ============================================================================
# Main Entry Point
# ============================================================================
function Main {
# Enable debug if requested
if ($DebugMode) {
$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-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
}
else {
Write-Info "Cancelled"
}
}
# Run main
Main

Binary file not shown.

View File

@@ -1,780 +0,0 @@
//go:build windows
package main
import (
"context"
"flag"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"sort"
"strings"
"sync"
"sync/atomic"
"time"
tea "github.com/charmbracelet/bubbletea"
)
// Scanning limits to prevent infinite scanning
const (
dirSizeTimeout = 500 * time.Millisecond // Max time to calculate a single directory size
maxFilesPerDir = 10000 // Max files to scan per directory
maxScanDepth = 10 // Max recursion depth (shallow scan)
shallowScanDepth = 3 // Depth for quick size estimation
)
// ANSI color codes
const (
colorReset = "\033[0m"
colorBold = "\033[1m"
colorDim = "\033[2m"
colorPurple = "\033[35m"
colorPurpleBold = "\033[1;35m"
colorCyan = "\033[36m"
colorCyanBold = "\033[1;36m"
colorYellow = "\033[33m"
colorGreen = "\033[32m"
colorRed = "\033[31m"
colorGray = "\033[90m"
colorWhite = "\033[97m"
)
// Icons
const (
iconFolder = "📁"
iconFile = "📄"
iconDisk = "💾"
iconClean = "🧹"
iconTrash = "🗑️"
iconBack = "⬅️"
iconSelected = "✓"
iconArrow = "➤"
)
// Cleanable directory patterns
var cleanablePatterns = map[string]bool{
"node_modules": true,
"vendor": true,
".venv": true,
"venv": true,
"__pycache__": true,
".pytest_cache": true,
"target": true,
"build": true,
"dist": true,
".next": true,
".nuxt": true,
".turbo": true,
".parcel-cache": true,
"bin": true,
"obj": true,
".gradle": true,
".idea": true,
".vs": true,
}
// Skip patterns for scanning
var skipPatterns = map[string]bool{
"$Recycle.Bin": true,
"System Volume Information": true,
"Windows": true,
"Program Files": true,
"Program Files (x86)": true,
"ProgramData": true,
"Recovery": true,
"Config.Msi": true,
}
// Protected paths that should NEVER be deleted
var protectedPaths = []string{
`C:\Windows`,
`C:\Program Files`,
`C:\Program Files (x86)`,
`C:\ProgramData`,
`C:\Users\Default`,
`C:\Users\Public`,
`C:\Recovery`,
`C:\System Volume Information`,
}
// isProtectedPath checks if a path is protected from deletion
func isProtectedPath(path string) bool {
absPath, err := filepath.Abs(path)
if err != nil {
return true // If we can't resolve the path, treat it as protected
}
absPath = strings.ToLower(absPath)
// Check against protected paths
for _, protected := range protectedPaths {
protectedLower := strings.ToLower(protected)
if absPath == protectedLower || strings.HasPrefix(absPath, protectedLower+`\`) {
return true
}
}
// Check against skip patterns (system directories)
baseName := strings.ToLower(filepath.Base(absPath))
for pattern := range skipPatterns {
if strings.ToLower(pattern) == baseName {
// Only protect if it's at a root level (e.g., C:\Windows, not C:\Projects\Windows)
parent := filepath.Dir(absPath)
if len(parent) <= 3 { // e.g., "C:\"
return true
}
}
}
// Protect Windows directory itself
winDir := strings.ToLower(os.Getenv("WINDIR"))
sysRoot := strings.ToLower(os.Getenv("SYSTEMROOT"))
if winDir != "" && (absPath == winDir || strings.HasPrefix(absPath, winDir+`\`)) {
return true
}
if sysRoot != "" && (absPath == sysRoot || strings.HasPrefix(absPath, sysRoot+`\`)) {
return true
}
return false
}
// Entry types
type dirEntry struct {
Name string
Path string
Size int64
IsDir bool
LastAccess time.Time
IsCleanable bool
}
type fileEntry struct {
Name string
Path string
Size int64
}
type historyEntry struct {
Path string
Entries []dirEntry
LargeFiles []fileEntry
TotalSize int64
Selected int
}
// Model for Bubble Tea
type model struct {
path string
entries []dirEntry
largeFiles []fileEntry
history []historyEntry
selected int
totalSize int64
scanning bool
showLargeFiles bool
multiSelected map[string]bool
deleteConfirm bool
deleteTarget string
scanProgress int64
scanTotal int64
width int
height int
err error
cache map[string]historyEntry
}
// Messages
type scanCompleteMsg struct {
entries []dirEntry
largeFiles []fileEntry
totalSize int64
}
type scanProgressMsg struct {
current int64
total int64
}
type scanErrorMsg struct {
err error
}
type deleteCompleteMsg struct {
path string
err error
}
func newModel(startPath string) model {
return model{
path: startPath,
entries: []dirEntry{},
largeFiles: []fileEntry{},
history: []historyEntry{},
selected: 0,
scanning: true,
multiSelected: make(map[string]bool),
cache: make(map[string]historyEntry),
}
}
func (m model) Init() tea.Cmd {
return m.scanPath(m.path)
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
return m.handleKeyPress(msg)
case tea.WindowSizeMsg:
m.width = msg.Width
m.height = msg.Height
return m, nil
case scanCompleteMsg:
m.entries = msg.entries
m.largeFiles = msg.largeFiles
m.totalSize = msg.totalSize
m.scanning = false
m.selected = 0
// Cache result
m.cache[m.path] = historyEntry{
Path: m.path,
Entries: msg.entries,
LargeFiles: msg.largeFiles,
TotalSize: msg.totalSize,
}
return m, nil
case scanProgressMsg:
m.scanProgress = msg.current
m.scanTotal = msg.total
return m, nil
case scanErrorMsg:
m.err = msg.err
m.scanning = false
return m, nil
case deleteCompleteMsg:
m.deleteConfirm = false
m.deleteTarget = ""
if msg.err != nil {
m.err = msg.err
} else {
// Rescan after delete
m.scanning = true
delete(m.cache, m.path)
return m, m.scanPath(m.path)
}
return m, nil
}
return m, nil
}
func (m model) handleKeyPress(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
// Handle delete confirmation
if m.deleteConfirm {
switch msg.String() {
case "y", "Y":
target := m.deleteTarget
m.deleteConfirm = false
return m, m.deletePath(target)
case "n", "N", "esc":
m.deleteConfirm = false
m.deleteTarget = ""
return m, nil
}
return m, nil
}
switch msg.String() {
case "q", "ctrl+c":
return m, tea.Quit
case "up", "k":
if m.selected > 0 {
m.selected--
}
case "down", "j":
if m.selected < len(m.entries)-1 {
m.selected++
}
case "enter", "right", "l":
if !m.scanning && len(m.entries) > 0 {
entry := m.entries[m.selected]
if entry.IsDir {
// Save current state to history
m.history = append(m.history, historyEntry{
Path: m.path,
Entries: m.entries,
LargeFiles: m.largeFiles,
TotalSize: m.totalSize,
Selected: m.selected,
})
m.path = entry.Path
m.selected = 0
m.multiSelected = make(map[string]bool)
// Check cache
if cached, ok := m.cache[entry.Path]; ok {
m.entries = cached.Entries
m.largeFiles = cached.LargeFiles
m.totalSize = cached.TotalSize
return m, nil
}
m.scanning = true
return m, m.scanPath(entry.Path)
}
}
case "left", "h", "backspace":
if len(m.history) > 0 {
last := m.history[len(m.history)-1]
m.history = m.history[:len(m.history)-1]
m.path = last.Path
m.entries = last.Entries
m.largeFiles = last.LargeFiles
m.totalSize = last.TotalSize
m.selected = last.Selected
m.multiSelected = make(map[string]bool)
m.scanning = false
}
case "space":
if len(m.entries) > 0 {
entry := m.entries[m.selected]
if m.multiSelected[entry.Path] {
delete(m.multiSelected, entry.Path)
} else {
m.multiSelected[entry.Path] = true
}
}
case "d", "delete":
if len(m.entries) > 0 {
entry := m.entries[m.selected]
m.deleteConfirm = true
m.deleteTarget = entry.Path
}
case "D":
// Delete all selected
if len(m.multiSelected) > 0 {
m.deleteConfirm = true
m.deleteTarget = fmt.Sprintf("%d items", len(m.multiSelected))
}
case "f":
m.showLargeFiles = !m.showLargeFiles
case "r":
// Refresh
delete(m.cache, m.path)
m.scanning = true
return m, m.scanPath(m.path)
case "o":
// Open in Explorer
if len(m.entries) > 0 {
entry := m.entries[m.selected]
openInExplorer(entry.Path)
}
case "g":
m.selected = 0
case "G":
m.selected = len(m.entries) - 1
}
return m, nil
}
func (m model) View() string {
var b strings.Builder
// Header
b.WriteString(fmt.Sprintf("%s%s Mole Disk Analyzer %s\n", colorPurpleBold, iconDisk, colorReset))
b.WriteString(fmt.Sprintf("%s%s%s\n", colorGray, m.path, colorReset))
b.WriteString("\n")
// Show delete confirmation
if m.deleteConfirm {
b.WriteString(fmt.Sprintf("%s%s Delete %s? (y/n)%s\n", colorRed, iconTrash, m.deleteTarget, colorReset))
return b.String()
}
// Scanning indicator
if m.scanning {
b.WriteString(fmt.Sprintf("%s⠋ Scanning...%s\n", colorCyan, colorReset))
if m.scanTotal > 0 {
b.WriteString(fmt.Sprintf("%s %d / %d items%s\n", colorGray, m.scanProgress, m.scanTotal, colorReset))
}
return b.String()
}
// Error display
if m.err != nil {
b.WriteString(fmt.Sprintf("%sError: %v%s\n", colorRed, m.err, colorReset))
b.WriteString("\n")
}
// Total size
b.WriteString(fmt.Sprintf(" Total: %s%s%s\n", colorYellow, formatBytes(m.totalSize), colorReset))
b.WriteString("\n")
// Large files toggle
if m.showLargeFiles && len(m.largeFiles) > 0 {
b.WriteString(fmt.Sprintf("%s%s Large Files (>100MB):%s\n", colorCyanBold, iconFile, colorReset))
for i, f := range m.largeFiles {
if i >= 10 {
b.WriteString(fmt.Sprintf(" %s... and %d more%s\n", colorGray, len(m.largeFiles)-10, colorReset))
break
}
b.WriteString(fmt.Sprintf(" %s%s%s %s\n", colorYellow, formatBytes(f.Size), colorReset, truncatePath(f.Path, 60)))
}
b.WriteString("\n")
}
// Directory entries
visibleEntries := m.height - 12
if visibleEntries < 5 {
visibleEntries = 20
}
start := 0
if m.selected >= visibleEntries {
start = m.selected - visibleEntries + 1
}
for i := start; i < len(m.entries) && i < start+visibleEntries; i++ {
entry := m.entries[i]
prefix := " "
// Selection indicator
if i == m.selected {
prefix = fmt.Sprintf("%s%s%s ", colorCyan, iconArrow, colorReset)
} else if m.multiSelected[entry.Path] {
prefix = fmt.Sprintf("%s%s%s ", colorGreen, iconSelected, colorReset)
}
// Icon
icon := iconFile
if entry.IsDir {
icon = iconFolder
}
if entry.IsCleanable {
icon = iconClean
}
// Size and percentage
pct := float64(0)
if m.totalSize > 0 {
pct = float64(entry.Size) / float64(m.totalSize) * 100
}
// Bar
barWidth := 20
filled := int(pct / 100 * float64(barWidth))
bar := strings.Repeat("█", filled) + strings.Repeat("░", barWidth-filled)
// Color based on selection
nameColor := colorReset
if i == m.selected {
nameColor = colorCyanBold
}
b.WriteString(fmt.Sprintf("%s%s %s%8s%s %s%s%s %s%.1f%%%s %s\n",
prefix,
icon,
colorYellow, formatBytes(entry.Size), colorReset,
colorGray, bar, colorReset,
colorDim, pct, colorReset,
nameColor+entry.Name+colorReset,
))
}
// Footer with keybindings
b.WriteString("\n")
b.WriteString(fmt.Sprintf("%s↑↓%s navigate %s↵%s enter %s←%s back %sf%s files %sd%s delete %sr%s refresh %sq%s quit%s\n",
colorCyan, colorReset,
colorCyan, colorReset,
colorCyan, colorReset,
colorCyan, colorReset,
colorCyan, colorReset,
colorCyan, colorReset,
colorCyan, colorReset,
colorReset,
))
return b.String()
}
// scanPath scans a directory and returns entries
func (m model) scanPath(path string) tea.Cmd {
return func() tea.Msg {
entries, largeFiles, totalSize, err := scanDirectory(path)
if err != nil {
return scanErrorMsg{err: err}
}
return scanCompleteMsg{
entries: entries,
largeFiles: largeFiles,
totalSize: totalSize,
}
}
}
// deletePath deletes a file or directory with protection checks
func (m model) deletePath(path string) tea.Cmd {
return func() tea.Msg {
// Safety check: never delete protected paths
if isProtectedPath(path) {
return deleteCompleteMsg{
path: path,
err: fmt.Errorf("cannot delete protected system path: %s", path),
}
}
err := os.RemoveAll(path)
return deleteCompleteMsg{path: path, err: err}
}
}
// scanDirectory scans a directory concurrently
func scanDirectory(path string) ([]dirEntry, []fileEntry, int64, error) {
entries, err := os.ReadDir(path)
if err != nil {
return nil, nil, 0, err
}
var (
dirEntries []dirEntry
largeFiles []fileEntry
totalSize int64
mu sync.Mutex
wg sync.WaitGroup
)
numWorkers := runtime.NumCPU() * 2
if numWorkers > 32 {
numWorkers = 32
}
sem := make(chan struct{}, numWorkers)
var processedCount int64
for _, entry := range entries {
name := entry.Name()
entryPath := filepath.Join(path, name)
// Skip system directories
if skipPatterns[name] {
continue
}
wg.Add(1)
sem <- struct{}{}
go func(name, entryPath string, isDir bool) {
defer wg.Done()
defer func() { <-sem }()
var size int64
var lastAccess time.Time
var isCleanable bool
if isDir {
size = calculateDirSize(entryPath)
isCleanable = cleanablePatterns[name]
} else {
info, err := os.Stat(entryPath)
if err == nil {
size = info.Size()
lastAccess = info.ModTime()
}
}
mu.Lock()
defer mu.Unlock()
dirEntries = append(dirEntries, dirEntry{
Name: name,
Path: entryPath,
Size: size,
IsDir: isDir,
LastAccess: lastAccess,
IsCleanable: isCleanable,
})
totalSize += size
// Track large files
if !isDir && size >= 100*1024*1024 {
largeFiles = append(largeFiles, fileEntry{
Name: name,
Path: entryPath,
Size: size,
})
}
atomic.AddInt64(&processedCount, 1)
}(name, entryPath, entry.IsDir())
}
wg.Wait()
// Sort by size descending
sort.Slice(dirEntries, func(i, j int) bool {
return dirEntries[i].Size > dirEntries[j].Size
})
sort.Slice(largeFiles, func(i, j int) bool {
return largeFiles[i].Size > largeFiles[j].Size
})
return dirEntries, largeFiles, totalSize, nil
}
// calculateDirSize calculates the size of a directory with timeout and limits
// Uses shallow scanning for speed - estimates based on first few levels
func calculateDirSize(path string) int64 {
ctx, cancel := context.WithTimeout(context.Background(), dirSizeTimeout)
defer cancel()
var size int64
var fileCount int64
// Use a channel to signal completion
done := make(chan struct{})
go func() {
defer close(done)
quickScanDir(ctx, path, 0, &size, &fileCount)
}()
select {
case <-done:
// Completed normally
case <-ctx.Done():
// Timeout - return partial size (already accumulated)
}
return size
}
// quickScanDir does a fast shallow scan for size estimation
func quickScanDir(ctx context.Context, path string, depth int, size *int64, fileCount *int64) {
// Check context cancellation
select {
case <-ctx.Done():
return
default:
}
// Limit depth for speed
if depth > shallowScanDepth {
return
}
// Limit total files scanned
if atomic.LoadInt64(fileCount) > maxFilesPerDir {
return
}
entries, err := os.ReadDir(path)
if err != nil {
return
}
for _, entry := range entries {
// Check cancellation
select {
case <-ctx.Done():
return
default:
}
if atomic.LoadInt64(fileCount) > maxFilesPerDir {
return
}
entryPath := filepath.Join(path, entry.Name())
if entry.IsDir() {
name := entry.Name()
// Skip hidden and system directories
if skipPatterns[name] || (strings.HasPrefix(name, ".") && len(name) > 1) {
continue
}
quickScanDir(ctx, entryPath, depth+1, size, fileCount)
} else {
info, err := entry.Info()
if err == nil {
atomic.AddInt64(size, info.Size())
atomic.AddInt64(fileCount, 1)
}
}
}
}
// formatBytes formats bytes to human readable string
func formatBytes(bytes int64) string {
const unit = 1024
if bytes < unit {
return fmt.Sprintf("%d B", bytes)
}
div, exp := int64(unit), 0
for n := bytes / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
}
// truncatePath truncates a path to fit in maxLen
func truncatePath(path string, maxLen int) string {
if len(path) <= maxLen {
return path
}
return "..." + path[len(path)-maxLen+3:]
}
// openInExplorer opens a path in Windows Explorer
func openInExplorer(path string) {
// Use explorer.exe to open the path
go func() {
exec.Command("explorer.exe", "/select,", path).Run()
}()
}
func main() {
var startPath string
flag.StringVar(&startPath, "path", "", "Path to analyze")
flag.Parse()
// Check environment variable
if startPath == "" {
startPath = os.Getenv("MO_ANALYZE_PATH")
}
// Use command line argument
if startPath == "" && len(flag.Args()) > 0 {
startPath = flag.Args()[0]
}
// Default to user profile
if startPath == "" {
startPath = os.Getenv("USERPROFILE")
}
// Resolve to absolute path
absPath, err := filepath.Abs(startPath)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
// Check if path exists
if _, err := os.Stat(absPath); os.IsNotExist(err) {
fmt.Fprintf(os.Stderr, "Error: Path does not exist: %s\n", absPath)
os.Exit(1)
}
p := tea.NewProgram(newModel(absPath), tea.WithAltScreen())
if _, err := p.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}

View File

@@ -1,164 +0,0 @@
//go:build windows
package main
import (
"os"
"path/filepath"
"testing"
)
func TestFormatBytes(t *testing.T) {
tests := []struct {
input int64
expected string
}{
{0, "0 B"},
{512, "512 B"},
{1024, "1.0 KB"},
{1536, "1.5 KB"},
{1048576, "1.0 MB"},
{1073741824, "1.0 GB"},
{1099511627776, "1.0 TB"},
}
for _, test := range tests {
result := formatBytes(test.input)
if result != test.expected {
t.Errorf("formatBytes(%d) = %s, expected %s", test.input, result, test.expected)
}
}
}
func TestTruncatePath(t *testing.T) {
tests := []struct {
input string
maxLen int
expected string
}{
{"C:\\short", 20, "C:\\short"},
{"C:\\this\\is\\a\\very\\long\\path\\that\\should\\be\\truncated", 30, "...ong\\path\\that\\should\\be\\truncated"},
}
for _, test := range tests {
result := truncatePath(test.input, test.maxLen)
if len(result) > test.maxLen && test.maxLen < len(test.input) {
// For truncated paths, just verify length constraint
if len(result) > test.maxLen+10 { // Allow some flexibility
t.Errorf("truncatePath(%s, %d) length = %d, expected <= %d", test.input, test.maxLen, len(result), test.maxLen)
}
}
}
}
func TestCleanablePatterns(t *testing.T) {
expectedCleanable := []string{
"node_modules",
"vendor",
".venv",
"venv",
"__pycache__",
"target",
"build",
"dist",
}
for _, pattern := range expectedCleanable {
if !cleanablePatterns[pattern] {
t.Errorf("Expected %s to be in cleanablePatterns", pattern)
}
}
}
func TestSkipPatterns(t *testing.T) {
expectedSkip := []string{
"$Recycle.Bin",
"System Volume Information",
"Windows",
"Program Files",
}
for _, pattern := range expectedSkip {
if !skipPatterns[pattern] {
t.Errorf("Expected %s to be in skipPatterns", pattern)
}
}
}
func TestCalculateDirSize(t *testing.T) {
// Create a temp directory with known content
tmpDir, err := os.MkdirTemp("", "mole_test_*")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
// Create a test file with known size
testFile := filepath.Join(tmpDir, "test.txt")
content := []byte("Hello, World!") // 13 bytes
if err := os.WriteFile(testFile, content, 0644); err != nil {
t.Fatalf("Failed to write test file: %v", err)
}
size := calculateDirSize(tmpDir)
if size != int64(len(content)) {
t.Errorf("calculateDirSize() = %d, expected %d", size, len(content))
}
}
func TestNewModel(t *testing.T) {
model := newModel("C:\\")
if model.path != "C:\\" {
t.Errorf("newModel path = %s, expected C:\\", model.path)
}
if !model.scanning {
t.Error("newModel should start in scanning state")
}
if model.multiSelected == nil {
t.Error("newModel multiSelected should be initialized")
}
if model.cache == nil {
t.Error("newModel cache should be initialized")
}
}
func TestScanDirectory(t *testing.T) {
// Create a temp directory with known structure
tmpDir, err := os.MkdirTemp("", "mole_scan_test_*")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
// Create subdirectory
subDir := filepath.Join(tmpDir, "subdir")
if err := os.Mkdir(subDir, 0755); err != nil {
t.Fatalf("Failed to create subdir: %v", err)
}
// Create test files
testFile1 := filepath.Join(tmpDir, "file1.txt")
testFile2 := filepath.Join(subDir, "file2.txt")
os.WriteFile(testFile1, []byte("content1"), 0644)
os.WriteFile(testFile2, []byte("content2"), 0644)
entries, largeFiles, totalSize, err := scanDirectory(tmpDir)
if err != nil {
t.Fatalf("scanDirectory error: %v", err)
}
if len(entries) != 2 { // subdir + file1.txt
t.Errorf("Expected 2 entries, got %d", len(entries))
}
if totalSize == 0 {
t.Error("totalSize should be greater than 0")
}
// No large files in this test
_ = largeFiles
}

View File

@@ -1,674 +0,0 @@
//go:build windows
package main
import (
"context"
"fmt"
"os"
"os/exec"
"runtime"
"strconv"
"strings"
"sync"
"time"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/shirou/gopsutil/v3/cpu"
"github.com/shirou/gopsutil/v3/disk"
"github.com/shirou/gopsutil/v3/host"
"github.com/shirou/gopsutil/v3/mem"
"github.com/shirou/gopsutil/v3/net"
"github.com/shirou/gopsutil/v3/process"
)
// Styles
var (
titleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#C79FD7")).Bold(true)
headerStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#87CEEB")).Bold(true)
labelStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#888888"))
valueStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFFFFF"))
okStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#A5D6A7"))
warnStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFD75F"))
dangerStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FF5F5F")).Bold(true)
dimStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#666666"))
cardStyle = lipgloss.NewStyle().Border(lipgloss.RoundedBorder()).BorderForeground(lipgloss.Color("#444444")).Padding(0, 1)
)
// Metrics snapshot
type MetricsSnapshot struct {
CollectedAt time.Time
HealthScore int
HealthMessage string
// Hardware
Hostname string
OS string
Platform string
Uptime time.Duration
// CPU
CPUModel string
CPUCores int
CPUPercent float64
CPUPerCore []float64
// Memory
MemTotal uint64
MemUsed uint64
MemPercent float64
SwapTotal uint64
SwapUsed uint64
SwapPercent float64
// Disk
Disks []DiskInfo
// Network
Networks []NetworkInfo
// Processes
TopProcesses []ProcessInfo
}
type DiskInfo struct {
Device string
Mountpoint string
Total uint64
Used uint64
Free uint64
UsedPercent float64
Fstype string
}
type NetworkInfo struct {
Name string
BytesSent uint64
BytesRecv uint64
PacketsSent uint64
PacketsRecv uint64
}
type ProcessInfo struct {
PID int32
Name string
CPU float64
Memory float32
}
// Collector
type Collector struct {
prevNet map[string]net.IOCountersStat
prevNetTime time.Time
mu sync.Mutex
}
func NewCollector() *Collector {
return &Collector{
prevNet: make(map[string]net.IOCountersStat),
}
}
func (c *Collector) Collect() MetricsSnapshot {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
var (
snapshot MetricsSnapshot
wg sync.WaitGroup
mu sync.Mutex
)
snapshot.CollectedAt = time.Now()
// Host info
wg.Add(1)
go func() {
defer wg.Done()
if info, err := host.InfoWithContext(ctx); err == nil {
mu.Lock()
snapshot.Hostname = info.Hostname
snapshot.OS = info.OS
snapshot.Platform = fmt.Sprintf("%s %s", info.Platform, info.PlatformVersion)
snapshot.Uptime = time.Duration(info.Uptime) * time.Second
mu.Unlock()
}
}()
// CPU info
wg.Add(1)
go func() {
defer wg.Done()
if cpuInfo, err := cpu.InfoWithContext(ctx); err == nil && len(cpuInfo) > 0 {
mu.Lock()
snapshot.CPUModel = cpuInfo[0].ModelName
snapshot.CPUCores = runtime.NumCPU()
mu.Unlock()
}
if percent, err := cpu.PercentWithContext(ctx, 500*time.Millisecond, false); err == nil && len(percent) > 0 {
mu.Lock()
snapshot.CPUPercent = percent[0]
mu.Unlock()
}
if perCore, err := cpu.PercentWithContext(ctx, 500*time.Millisecond, true); err == nil {
mu.Lock()
snapshot.CPUPerCore = perCore
mu.Unlock()
}
}()
// Memory
wg.Add(1)
go func() {
defer wg.Done()
if memInfo, err := mem.VirtualMemoryWithContext(ctx); err == nil {
mu.Lock()
snapshot.MemTotal = memInfo.Total
snapshot.MemUsed = memInfo.Used
snapshot.MemPercent = memInfo.UsedPercent
mu.Unlock()
}
if swapInfo, err := mem.SwapMemoryWithContext(ctx); err == nil {
mu.Lock()
snapshot.SwapTotal = swapInfo.Total
snapshot.SwapUsed = swapInfo.Used
snapshot.SwapPercent = swapInfo.UsedPercent
mu.Unlock()
}
}()
// Disk
wg.Add(1)
go func() {
defer wg.Done()
if partitions, err := disk.PartitionsWithContext(ctx, false); err == nil {
var disks []DiskInfo
for _, p := range partitions {
// Skip non-physical drives
if !strings.HasPrefix(p.Device, "C:") &&
!strings.HasPrefix(p.Device, "D:") &&
!strings.HasPrefix(p.Device, "E:") &&
!strings.HasPrefix(p.Device, "F:") {
continue
}
if usage, err := disk.UsageWithContext(ctx, p.Mountpoint); err == nil {
disks = append(disks, DiskInfo{
Device: p.Device,
Mountpoint: p.Mountpoint,
Total: usage.Total,
Used: usage.Used,
Free: usage.Free,
UsedPercent: usage.UsedPercent,
Fstype: p.Fstype,
})
}
}
mu.Lock()
snapshot.Disks = disks
mu.Unlock()
}
}()
// Network
wg.Add(1)
go func() {
defer wg.Done()
if netIO, err := net.IOCountersWithContext(ctx, true); err == nil {
var networks []NetworkInfo
for _, io := range netIO {
// Skip loopback and inactive interfaces
if io.Name == "Loopback Pseudo-Interface 1" || (io.BytesSent == 0 && io.BytesRecv == 0) {
continue
}
networks = append(networks, NetworkInfo{
Name: io.Name,
BytesSent: io.BytesSent,
BytesRecv: io.BytesRecv,
PacketsSent: io.PacketsSent,
PacketsRecv: io.PacketsRecv,
})
}
mu.Lock()
snapshot.Networks = networks
mu.Unlock()
}
}()
// Top Processes
wg.Add(1)
go func() {
defer wg.Done()
procs, err := process.ProcessesWithContext(ctx)
if err != nil {
return
}
var procInfos []ProcessInfo
for _, p := range procs {
name, err := p.NameWithContext(ctx)
if err != nil {
continue
}
cpuPercent, _ := p.CPUPercentWithContext(ctx)
memPercent, _ := p.MemoryPercentWithContext(ctx)
if cpuPercent > 0.1 || memPercent > 0.1 {
procInfos = append(procInfos, ProcessInfo{
PID: p.Pid,
Name: name,
CPU: cpuPercent,
Memory: memPercent,
})
}
}
// Sort by CPU usage
for i := 0; i < len(procInfos)-1; i++ {
for j := i + 1; j < len(procInfos); j++ {
if procInfos[j].CPU > procInfos[i].CPU {
procInfos[i], procInfos[j] = procInfos[j], procInfos[i]
}
}
}
// Take top 5
if len(procInfos) > 5 {
procInfos = procInfos[:5]
}
mu.Lock()
snapshot.TopProcesses = procInfos
mu.Unlock()
}()
wg.Wait()
// Calculate health score
snapshot.HealthScore, snapshot.HealthMessage = calculateHealthScore(snapshot)
return snapshot
}
func calculateHealthScore(s MetricsSnapshot) (int, string) {
score := 100
var issues []string
// CPU penalty (30% weight)
if s.CPUPercent > 90 {
score -= 30
issues = append(issues, "High CPU")
} else if s.CPUPercent > 70 {
score -= 15
issues = append(issues, "Elevated CPU")
}
// Memory penalty (25% weight)
if s.MemPercent > 90 {
score -= 25
issues = append(issues, "High Memory")
} else if s.MemPercent > 80 {
score -= 12
issues = append(issues, "Elevated Memory")
}
// Disk penalty (20% weight)
for _, d := range s.Disks {
if d.UsedPercent > 95 {
score -= 20
issues = append(issues, fmt.Sprintf("Disk %s Critical", d.Device))
break
} else if d.UsedPercent > 85 {
score -= 10
issues = append(issues, fmt.Sprintf("Disk %s Low", d.Device))
break
}
}
// Swap penalty (10% weight)
if s.SwapPercent > 80 {
score -= 10
issues = append(issues, "High Swap")
}
if score < 0 {
score = 0
}
msg := "Excellent"
if len(issues) > 0 {
msg = strings.Join(issues, ", ")
} else if score >= 90 {
msg = "Excellent"
} else if score >= 70 {
msg = "Good"
} else if score >= 50 {
msg = "Fair"
} else {
msg = "Poor"
}
return score, msg
}
// Model for Bubble Tea
type model struct {
collector *Collector
metrics MetricsSnapshot
animFrame int
catHidden bool
ready bool
collecting bool
width int
height int
}
// Messages
type tickMsg time.Time
type metricsMsg MetricsSnapshot
func newModel() model {
return model{
collector: NewCollector(),
animFrame: 0,
}
}
func (m model) Init() tea.Cmd {
return tea.Batch(
m.collectMetrics(),
tickCmd(),
)
}
func tickCmd() tea.Cmd {
return tea.Tick(time.Second, func(t time.Time) tea.Msg {
return tickMsg(t)
})
}
func (m model) collectMetrics() tea.Cmd {
return func() tea.Msg {
return metricsMsg(m.collector.Collect())
}
}
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "q", "ctrl+c":
return m, tea.Quit
case "c":
m.catHidden = !m.catHidden
case "r":
m.collecting = true
return m, m.collectMetrics()
}
case tea.WindowSizeMsg:
m.width = msg.Width
m.height = msg.Height
case tickMsg:
m.animFrame++
if m.animFrame%2 == 0 && !m.collecting {
return m, tea.Batch(
m.collectMetrics(),
tickCmd(),
)
}
return m, tickCmd()
case metricsMsg:
m.metrics = MetricsSnapshot(msg)
m.ready = true
m.collecting = false
}
return m, nil
}
func (m model) View() string {
if !m.ready {
return "\n Loading system metrics..."
}
var b strings.Builder
// Header with mole animation
moleFrame := getMoleFrame(m.animFrame, m.catHidden)
b.WriteString("\n")
b.WriteString(titleStyle.Render(" 🐹 Mole System Status"))
b.WriteString(" ")
b.WriteString(moleFrame)
b.WriteString("\n\n")
// Health score
healthColor := okStyle
if m.metrics.HealthScore < 50 {
healthColor = dangerStyle
} else if m.metrics.HealthScore < 70 {
healthColor = warnStyle
}
b.WriteString(fmt.Sprintf(" Health: %s %s\n\n",
healthColor.Render(fmt.Sprintf("%d%%", m.metrics.HealthScore)),
dimStyle.Render(m.metrics.HealthMessage),
))
// System info
b.WriteString(headerStyle.Render(" 📍 System"))
b.WriteString("\n")
b.WriteString(fmt.Sprintf(" %s %s\n", labelStyle.Render("Host:"), valueStyle.Render(m.metrics.Hostname)))
b.WriteString(fmt.Sprintf(" %s %s\n", labelStyle.Render("OS:"), valueStyle.Render(m.metrics.Platform)))
b.WriteString(fmt.Sprintf(" %s %s\n", labelStyle.Render("Uptime:"), valueStyle.Render(formatDuration(m.metrics.Uptime))))
b.WriteString("\n")
// CPU
b.WriteString(headerStyle.Render(" ⚡ CPU"))
b.WriteString("\n")
cpuColor := getPercentColor(m.metrics.CPUPercent)
b.WriteString(fmt.Sprintf(" %s %s\n", labelStyle.Render("Model:"), valueStyle.Render(truncateString(m.metrics.CPUModel, 50))))
b.WriteString(fmt.Sprintf(" %s %s (%d cores)\n",
labelStyle.Render("Usage:"),
cpuColor.Render(fmt.Sprintf("%.1f%%", m.metrics.CPUPercent)),
m.metrics.CPUCores,
))
b.WriteString(fmt.Sprintf(" %s\n", renderProgressBar(m.metrics.CPUPercent, 30)))
b.WriteString("\n")
// Memory
b.WriteString(headerStyle.Render(" 🧠 Memory"))
b.WriteString("\n")
memColor := getPercentColor(m.metrics.MemPercent)
b.WriteString(fmt.Sprintf(" %s %s / %s %s\n",
labelStyle.Render("RAM:"),
memColor.Render(formatBytes(m.metrics.MemUsed)),
valueStyle.Render(formatBytes(m.metrics.MemTotal)),
memColor.Render(fmt.Sprintf("(%.1f%%)", m.metrics.MemPercent)),
))
b.WriteString(fmt.Sprintf(" %s\n", renderProgressBar(m.metrics.MemPercent, 30)))
if m.metrics.SwapTotal > 0 {
b.WriteString(fmt.Sprintf(" %s %s / %s\n",
labelStyle.Render("Swap:"),
valueStyle.Render(formatBytes(m.metrics.SwapUsed)),
valueStyle.Render(formatBytes(m.metrics.SwapTotal)),
))
}
b.WriteString("\n")
// Disk
b.WriteString(headerStyle.Render(" 💾 Disks"))
b.WriteString("\n")
for _, d := range m.metrics.Disks {
diskColor := getPercentColor(d.UsedPercent)
b.WriteString(fmt.Sprintf(" %s %s / %s %s\n",
labelStyle.Render(d.Device),
diskColor.Render(formatBytes(d.Used)),
valueStyle.Render(formatBytes(d.Total)),
diskColor.Render(fmt.Sprintf("(%.1f%%)", d.UsedPercent)),
))
b.WriteString(fmt.Sprintf(" %s\n", renderProgressBar(d.UsedPercent, 30)))
}
b.WriteString("\n")
// Top Processes
if len(m.metrics.TopProcesses) > 0 {
b.WriteString(headerStyle.Render(" 📊 Top Processes"))
b.WriteString("\n")
for _, p := range m.metrics.TopProcesses {
b.WriteString(fmt.Sprintf(" %s %s (CPU: %.1f%%, Mem: %.1f%%)\n",
dimStyle.Render(fmt.Sprintf("[%d]", p.PID)),
valueStyle.Render(truncateString(p.Name, 20)),
p.CPU,
p.Memory,
))
}
b.WriteString("\n")
}
// Network
if len(m.metrics.Networks) > 0 {
b.WriteString(headerStyle.Render(" 🌐 Network"))
b.WriteString("\n")
for i, n := range m.metrics.Networks {
if i >= 3 {
break
}
b.WriteString(fmt.Sprintf(" %s ↑%s ↓%s\n",
labelStyle.Render(truncateString(n.Name, 20)+":"),
valueStyle.Render(formatBytes(n.BytesSent)),
valueStyle.Render(formatBytes(n.BytesRecv)),
))
}
b.WriteString("\n")
}
// Footer
b.WriteString(dimStyle.Render(" [q] quit [r] refresh [c] toggle mole"))
b.WriteString("\n")
return b.String()
}
func getMoleFrame(frame int, hidden bool) string {
if hidden {
return ""
}
frames := []string{
"🐹",
"🐹.",
"🐹..",
"🐹...",
}
return frames[frame%len(frames)]
}
func renderProgressBar(percent float64, width int) string {
filled := int(percent / 100 * float64(width))
if filled > width {
filled = width
}
if filled < 0 {
filled = 0
}
color := okStyle
if percent > 85 {
color = dangerStyle
} else if percent > 70 {
color = warnStyle
}
bar := strings.Repeat("█", filled) + strings.Repeat("░", width-filled)
return color.Render(bar)
}
func getPercentColor(percent float64) lipgloss.Style {
if percent > 85 {
return dangerStyle
} else if percent > 70 {
return warnStyle
}
return okStyle
}
func formatBytes(bytes uint64) string {
const unit = 1024
if bytes < unit {
return fmt.Sprintf("%d B", bytes)
}
div, exp := uint64(unit), 0
for n := bytes / unit; n >= unit; n /= unit {
div *= unit
exp++
}
return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
}
func formatDuration(d time.Duration) string {
days := int(d.Hours() / 24)
hours := int(d.Hours()) % 24
minutes := int(d.Minutes()) % 60
if days > 0 {
return fmt.Sprintf("%dd %dh %dm", days, hours, minutes)
}
if hours > 0 {
return fmt.Sprintf("%dh %dm", hours, minutes)
}
return fmt.Sprintf("%dm", minutes)
}
func truncateString(s string, maxLen int) string {
if len(s) <= maxLen {
return s
}
return s[:maxLen-3] + "..."
}
// getWindowsVersion gets detailed Windows version using PowerShell
func getWindowsVersion() string {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, "powershell", "-Command",
"(Get-CimInstance Win32_OperatingSystem).Caption")
output, err := cmd.Output()
if err != nil {
return "Windows"
}
return strings.TrimSpace(string(output))
}
// getBatteryInfo gets battery info on Windows (for laptops)
func getBatteryInfo() (int, bool, bool) {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, "powershell", "-Command",
"(Get-CimInstance Win32_Battery).EstimatedChargeRemaining")
output, err := cmd.Output()
if err != nil {
return 0, false, false
}
percent, err := strconv.Atoi(strings.TrimSpace(string(output)))
if err != nil {
return 0, false, false
}
// Check if charging
cmdStatus := exec.CommandContext(ctx, "powershell", "-Command",
"(Get-CimInstance Win32_Battery).BatteryStatus")
statusOutput, _ := cmdStatus.Output()
status, _ := strconv.Atoi(strings.TrimSpace(string(statusOutput)))
isCharging := status == 2 // 2 = AC Power
return percent, isCharging, true
}
func main() {
p := tea.NewProgram(newModel(), tea.WithAltScreen())
if _, err := p.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}

View File

@@ -1,219 +0,0 @@
//go:build windows
package main
import (
"testing"
"time"
)
func TestFormatBytesUint64(t *testing.T) {
tests := []struct {
input uint64
expected string
}{
{0, "0 B"},
{512, "512 B"},
{1024, "1.0 KB"},
{1536, "1.5 KB"},
{1048576, "1.0 MB"},
{1073741824, "1.0 GB"},
{1099511627776, "1.0 TB"},
}
for _, test := range tests {
result := formatBytes(test.input)
if result != test.expected {
t.Errorf("formatBytes(%d) = %s, expected %s", test.input, result, test.expected)
}
}
}
func TestFormatDuration(t *testing.T) {
tests := []struct {
input time.Duration
expected string
}{
{5 * time.Minute, "5m"},
{2 * time.Hour, "2h 0m"},
{25 * time.Hour, "1d 1h 0m"},
{49*time.Hour + 30*time.Minute, "2d 1h 30m"},
}
for _, test := range tests {
result := formatDuration(test.input)
if result != test.expected {
t.Errorf("formatDuration(%v) = %s, expected %s", test.input, result, test.expected)
}
}
}
func TestTruncateString(t *testing.T) {
tests := []struct {
input string
maxLen int
expected string
}{
{"short", 10, "short"},
{"this is a long string", 10, "this is..."},
{"exact", 5, "exact"},
}
for _, test := range tests {
result := truncateString(test.input, test.maxLen)
if result != test.expected {
t.Errorf("truncateString(%s, %d) = %s, expected %s", test.input, test.maxLen, result, test.expected)
}
}
}
func TestCalculateHealthScore(t *testing.T) {
tests := []struct {
name string
snapshot MetricsSnapshot
minScore int
maxScore int
}{
{
name: "Healthy system",
snapshot: MetricsSnapshot{
CPUPercent: 20,
MemPercent: 40,
SwapPercent: 10,
Disks: []DiskInfo{
{UsedPercent: 50},
},
},
minScore: 90,
maxScore: 100,
},
{
name: "High CPU",
snapshot: MetricsSnapshot{
CPUPercent: 95,
MemPercent: 40,
SwapPercent: 10,
Disks: []DiskInfo{
{UsedPercent: 50},
},
},
minScore: 50,
maxScore: 75,
},
{
name: "High Memory",
snapshot: MetricsSnapshot{
CPUPercent: 20,
MemPercent: 95,
SwapPercent: 10,
Disks: []DiskInfo{
{UsedPercent: 50},
},
},
minScore: 60,
maxScore: 80,
},
{
name: "Critical Disk",
snapshot: MetricsSnapshot{
CPUPercent: 20,
MemPercent: 40,
SwapPercent: 10,
Disks: []DiskInfo{
{Device: "C:", UsedPercent: 98},
},
},
minScore: 60,
maxScore: 85,
},
{
name: "Multiple issues",
snapshot: MetricsSnapshot{
CPUPercent: 95,
MemPercent: 95,
SwapPercent: 85,
Disks: []DiskInfo{
{Device: "C:", UsedPercent: 98},
},
},
minScore: 0,
maxScore: 30,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
score, msg := calculateHealthScore(test.snapshot)
if score < test.minScore || score > test.maxScore {
t.Errorf("calculateHealthScore() = %d (%s), expected between %d and %d",
score, msg, test.minScore, test.maxScore)
}
})
}
}
func TestNewCollector(t *testing.T) {
collector := NewCollector()
if collector == nil {
t.Fatal("NewCollector returned nil")
}
if collector.prevNet == nil {
t.Error("prevNet map should be initialized")
}
}
func TestGetMoleFrame(t *testing.T) {
// Test visible frames
for i := 0; i < 8; i++ {
frame := getMoleFrame(i, false)
if frame == "" {
t.Errorf("getMoleFrame(%d, false) returned empty string", i)
}
}
// Test hidden
frame := getMoleFrame(0, true)
if frame != "" {
t.Errorf("getMoleFrame(0, true) = %s, expected empty string", frame)
}
}
func TestRenderProgressBar(t *testing.T) {
tests := []struct {
percent float64
width int
}{
{0, 20},
{50, 20},
{100, 20},
{75, 30},
}
for _, test := range tests {
result := renderProgressBar(test.percent, test.width)
if result == "" {
t.Errorf("renderProgressBar(%.0f, %d) returned empty string", test.percent, test.width)
}
}
}
func TestGetPercentColor(t *testing.T) {
// Just verify it doesn't panic
_ = getPercentColor(50)
_ = getPercentColor(75)
_ = getPercentColor(90)
}
func TestNewModel(t *testing.T) {
model := newModel()
if model.collector == nil {
t.Error("collector should be initialized")
}
if model.ready {
t.Error("ready should be false initially")
}
}

View File

@@ -1,10 +0,0 @@
module github.com/tw93/mole/windows
go 1.24.0
require (
github.com/charmbracelet/bubbletea v1.3.10
github.com/shirou/gopsutil/v3 v3.24.5
github.com/yusufpapurcu/wmi v1.2.4
golang.org/x/sys v0.36.0
)

View File

@@ -1,73 +0,0 @@
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM=
github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg=
github.com/charmbracelet/lipgloss v0.9.1 h1:PNyd3jvaJbg4jRHKWXnCj1akQm4rh8dbEzN1p/u1KWg=
github.com/charmbracelet/lipgloss v0.9.1/go.mod h1:1mPmG4cxScwUQALAAnacHaigiiHB9Pmr+v1VEawJl6I=
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY=
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98=
github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b h1:1XF24mVaiu7u+CFywTdcDo2ie1pzzhwjt6RHqzpMU34=
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho=
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
github.com/shoenig/go-m1cpu v0.1.7 h1:C76Yd0ObKR82W4vhfjZiCp0HxcSZ8Nqd84v+HZ0qyI0=
github.com/shoenig/go-m1cpu v0.1.7/go.mod h1:KkDOw6m3ZJQAPHbrzkZki4hnx+pDRR1Lo+ldA56wD5w=
github.com/shoenig/test v1.7.0 h1:eWcHtTXa6QLnBvm0jgEabMRN/uJ4DMV3M8xUGgRkZmk=
github.com/shoenig/test v1.7.0/go.mod h1:UxJ6u/x2v/TNs/LoLxBNJRV9DiwBBKYxXSyczsBHFoI=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -1,440 +0,0 @@
#!/usr/bin/env pwsh
# Mole Windows Installer
# Installs Mole Windows support to the system and adds to PATH
#Requires -Version 5.1
param(
[string]$InstallDir = "$env:LOCALAPPDATA\Mole",
[switch]$AddToPath,
[switch]$CreateShortcut,
[switch]$Uninstall,
[switch]$Force,
[switch]$ShowHelp
)
$ErrorActionPreference = "Stop"
Set-StrictMode -Version Latest
# ============================================================================
# Configuration
# ============================================================================
$script:VERSION = "1.0.0"
$script:SourceDir = if ($MyInvocation.MyCommand.Path) {
Split-Path -Parent $MyInvocation.MyCommand.Path
} else {
$PSScriptRoot
}
$script:ShortcutName = "Mole"
# Colors (using [char]27 for PowerShell 5.1 compatibility)
$script:ESC = [char]27
$script:Colors = @{
Red = "$($script:ESC)[31m"
Green = "$($script:ESC)[32m"
Yellow = "$($script:ESC)[33m"
Blue = "$($script:ESC)[34m"
Cyan = "$($script:ESC)[36m"
Gray = "$($script:ESC)[90m"
NC = "$($script:ESC)[0m"
}
# ============================================================================
# Helpers
# ============================================================================
function Write-Info {
param([string]$Message)
$c = $script:Colors
Write-Host " $($c.Blue)INFO$($c.NC) $Message"
}
function Write-Success {
param([string]$Message)
$c = $script:Colors
Write-Host " $($c.Green)OK$($c.NC) $Message"
}
function Write-MoleWarning {
param([string]$Message)
$c = $script:Colors
Write-Host " $($c.Yellow)WARN$($c.NC) $Message"
}
function Write-MoleError {
param([string]$Message)
$c = $script:Colors
Write-Host " $($c.Red)ERROR$($c.NC) $Message"
}
function Show-Banner {
$c = $script:Colors
Write-Host ""
Write-Host " $($c.Cyan)MOLE$($c.NC)"
Write-Host " $($c.Gray)Windows System Maintenance$($c.NC)"
Write-Host ""
}
function Show-InstallerHelp {
Show-Banner
$c = $script:Colors
Write-Host " $($c.Green)USAGE:$($c.NC)"
Write-Host ""
Write-Host " .\install.ps1 [options]"
Write-Host ""
Write-Host " $($c.Green)OPTIONS:$($c.NC)"
Write-Host ""
Write-Host " $($c.Cyan)-InstallDir <path>$($c.NC) Installation directory"
Write-Host " Default: $env:LOCALAPPDATA\Mole"
Write-Host ""
Write-Host " $($c.Cyan)-AddToPath$($c.NC) Add Mole to user PATH"
Write-Host ""
Write-Host " $($c.Cyan)-CreateShortcut$($c.NC) Create Start Menu shortcut"
Write-Host ""
Write-Host " $($c.Cyan)-Uninstall$($c.NC) Remove Mole from system"
Write-Host ""
Write-Host " $($c.Cyan)-Force$($c.NC) Overwrite existing installation"
Write-Host ""
Write-Host " $($c.Cyan)-ShowHelp$($c.NC) Show this help message"
Write-Host ""
Write-Host " $($c.Green)EXAMPLES:$($c.NC)"
Write-Host ""
Write-Host " $($c.Gray)# Install with defaults$($c.NC)"
Write-Host " .\install.ps1"
Write-Host ""
Write-Host " $($c.Gray)# Install and add to PATH$($c.NC)"
Write-Host " .\install.ps1 -AddToPath"
Write-Host ""
Write-Host " $($c.Gray)# Custom install location$($c.NC)"
Write-Host " .\install.ps1 -InstallDir C:\Tools\Mole -AddToPath"
Write-Host ""
Write-Host " $($c.Gray)# Full installation$($c.NC)"
Write-Host " .\install.ps1 -AddToPath -CreateShortcut"
Write-Host ""
Write-Host " $($c.Gray)# Uninstall$($c.NC)"
Write-Host " .\install.ps1 -Uninstall"
Write-Host ""
}
function Test-IsAdmin {
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = [Security.Principal.WindowsPrincipal]$identity
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}
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-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-MoleError "Failed to update PATH: $_"
return $false
}
}
function New-StartMenuShortcut {
param(
[string]$TargetPath,
[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)
$shortcut.TargetPath = "powershell.exe"
$shortcut.Arguments = "-NoExit -ExecutionPolicy Bypass -File `"$TargetPath`""
$shortcut.Description = $Description
$shortcut.WorkingDirectory = Split-Path -Parent $TargetPath
$shortcut.Save()
Write-Success "Created shortcut: $shortcutPath"
return $true
}
catch {
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
Write-Success "Removed shortcut: $shortcutPath"
return $true
}
catch {
Write-MoleError "Failed to remove shortcut: $_"
return $false
}
}
return $true
}
# ============================================================================
# Install
# ============================================================================
function Install-Mole {
Write-Info "Installing Mole v$script:VERSION..."
Write-Host ""
# Check if already installed
if ((Test-Path $InstallDir) -and -not $Force) {
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 {
New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
Write-Success "Created directory: $InstallDir"
}
catch {
Write-MoleError "Failed to create directory: $_"
return $false
}
}
# Copy files
Write-Info "Copying files..."
$filesToCopy = @(
"mole.ps1"
"go.mod"
"bin"
"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) {
# For directories, remove destination first if exists to avoid nesting
if (Test-Path $dst) {
Remove-Item -Path $dst -Recurse -Force
}
Copy-Item -Path $src -Destination $dst -Recurse -Force
}
else {
Copy-Item -Path $src -Destination $dst -Force
}
Write-Success "Copied: $item"
}
catch {
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) {
$dirPath = Join-Path $InstallDir $dir
if (-not (Test-Path $dirPath)) {
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 = @"
@echo off
setlocal EnableDelayedExpansion
rem Store the script directory immediately before any shifting
set "MOLE_DIR=%~dp0"
set "ARGS="
:parse
if "%~1"=="" goto run
set "ARGS=!ARGS! '%~1'"
shift
goto parse
:run
powershell.exe -ExecutionPolicy Bypass -NoLogo -NoProfile -Command "& '%MOLE_DIR%mole.ps1' !ARGS!"
"@
$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"
}
else {
Write-Host " Run the following to start:"
Write-Host " & `"$InstallDir\mole.ps1`""
Write-Host ""
Write-Host " Or add to PATH with:"
Write-Host " .\install.ps1 -AddToPath"
}
Write-Host ""
return $true
}
# ============================================================================
# Uninstall
# ============================================================================
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-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-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) {
Write-Info "Found config directory: $configDir"
$response = Read-Host " Remove config files? (y/N)"
if ($response -eq "y" -or $response -eq "Y") {
try {
Remove-Item -Path $configDir -Recurse -Force
Write-Success "Removed config: $configDir"
}
catch {
Write-MoleWarning "Failed to remove config: $_"
}
}
}
Write-Host ""
Write-Success "Mole uninstalled successfully!"
Write-Host ""
return $true
}
# ============================================================================
# Main
# ============================================================================
function Main {
if ($ShowHelp) {
Show-InstallerHelp
return
}
Show-Banner
if ($Uninstall) {
Uninstall-Mole
}
else {
Install-Mole
}
}
# Run
try {
Main
}
catch {
Write-Host ""
Write-Host " $($script:Colors.Red)ERROR$($script:Colors.NC) Installation failed: $_"
Write-Host ""
exit 1
}

View File

@@ -1,442 +0,0 @@
# Mole - Application-Specific Cleanup Module
# Cleans leftover data from uninstalled apps and app-specific caches
#Requires -Version 5.1
Set-StrictMode -Version Latest
# Prevent multiple sourcing
if ((Get-Variable -Name 'MOLE_CLEAN_APPS_LOADED' -Scope Script -ErrorAction SilentlyContinue) -and $script:MOLE_CLEAN_APPS_LOADED) { return }
$script:MOLE_CLEAN_APPS_LOADED = $true
# Import dependencies
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
. "$scriptDir\..\core\base.ps1"
. "$scriptDir\..\core\log.ps1"
. "$scriptDir\..\core\file_ops.ps1"
# ============================================================================
# Orphaned App Data Detection
# ============================================================================
function Get-InstalledPrograms {
<#
.SYNOPSIS
Get list of installed programs from registry
#>
$programs = @()
$registryPaths = @(
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*"
"HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
"HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*"
)
foreach ($path in $registryPaths) {
$items = Get-ItemProperty -Path $path -ErrorAction SilentlyContinue |
Where-Object { $_.DisplayName } |
Select-Object DisplayName, InstallLocation, Publisher
if ($items) {
$programs += $items
}
}
# Also check UWP apps
try {
$uwpApps = Get-AppxPackage -ErrorAction SilentlyContinue |
Select-Object @{N='DisplayName';E={$_.Name}}, @{N='InstallLocation';E={$_.InstallLocation}}, Publisher
if ($uwpApps) {
$programs += $uwpApps
}
}
catch {
Write-Debug "Could not enumerate UWP apps: $_"
}
return $programs
}
function Find-OrphanedAppData {
<#
.SYNOPSIS
Find app data folders for apps that are no longer installed
#>
param([int]$DaysOld = 60)
$installedPrograms = Get-InstalledPrograms
$installedNames = $installedPrograms | ForEach-Object { $_.DisplayName.ToLower() }
$orphanedPaths = @()
$cutoffDate = (Get-Date).AddDays(-$DaysOld)
# Check common app data locations
$appDataPaths = @(
@{ Path = $env:APPDATA; Type = "Roaming" }
@{ Path = $env:LOCALAPPDATA; Type = "Local" }
)
foreach ($location in $appDataPaths) {
if (-not (Test-Path $location.Path)) { continue }
$folders = Get-ChildItem -Path $location.Path -Directory -ErrorAction SilentlyContinue
foreach ($folder in $folders) {
# Skip system folders
$skipFolders = @('Microsoft', 'Windows', 'Packages', 'Programs', 'Temp', 'Roaming')
if ($folder.Name -in $skipFolders) { continue }
# Skip if recently modified
if ($folder.LastWriteTime -gt $cutoffDate) { continue }
# Check if app is installed using stricter matching
# Require exact match or that folder name is a clear prefix/suffix of app name
$isInstalled = $false
$folderLower = $folder.Name.ToLower()
foreach ($name in $installedNames) {
# Exact match
if ($name -eq $folderLower) {
$isInstalled = $true
break
}
# Folder is prefix of app name (e.g., "chrome" matches "chrome browser")
if ($name.StartsWith($folderLower) -and $folderLower.Length -ge 4) {
$isInstalled = $true
break
}
# App name is prefix of folder (e.g., "vscode" matches "vscode-data")
if ($folderLower.StartsWith($name) -and $name.Length -ge 4) {
$isInstalled = $true
break
}
}
if (-not $isInstalled) {
$orphanedPaths += @{
Path = $folder.FullName
Name = $folder.Name
Type = $location.Type
Size = (Get-PathSize -Path $folder.FullName)
LastModified = $folder.LastWriteTime
}
}
}
}
return $orphanedPaths
}
function Clear-OrphanedAppData {
<#
.SYNOPSIS
Clean orphaned application data
#>
param([int]$DaysOld = 60)
Start-Section "Orphaned app data"
$orphaned = Find-OrphanedAppData -DaysOld $DaysOld
if ($orphaned.Count -eq 0) {
Write-Info "No orphaned app data found"
Stop-Section
return
}
# Filter by size (only clean if > 10MB to avoid noise)
$significantOrphans = $orphaned | Where-Object { $_.Size -gt 10MB }
if ($significantOrphans.Count -gt 0) {
$totalSize = ($significantOrphans | Measure-Object -Property Size -Sum).Sum
$sizeHuman = Format-ByteSize -Bytes $totalSize
Write-Info "Found $($significantOrphans.Count) orphaned folders ($sizeHuman)"
foreach ($orphan in $significantOrphans) {
$orphanSize = Format-ByteSize -Bytes $orphan.Size
Remove-SafeItem -Path $orphan.Path -Description "$($orphan.Name) ($orphanSize)" -Recurse
}
}
Stop-Section
}
# ============================================================================
# Specific Application Cleanup
# ============================================================================
function Clear-OfficeCache {
<#
.SYNOPSIS
Clean Microsoft Office caches and temp files
#>
$officeCachePaths = @(
# Office 365 / 2019 / 2021
"$env:LOCALAPPDATA\Microsoft\Office\16.0\OfficeFileCache"
"$env:LOCALAPPDATA\Microsoft\Office\16.0\Wef"
"$env:LOCALAPPDATA\Microsoft\Outlook\RoamCache"
"$env:LOCALAPPDATA\Microsoft\Outlook\Offline Address Books"
# Older Office versions
"$env:LOCALAPPDATA\Microsoft\Office\15.0\OfficeFileCache"
# Office temp files
"$env:APPDATA\Microsoft\Templates\*.tmp"
"$env:APPDATA\Microsoft\Word\*.tmp"
"$env:APPDATA\Microsoft\Excel\*.tmp"
"$env:APPDATA\Microsoft\PowerPoint\*.tmp"
)
foreach ($path in $officeCachePaths) {
if ($path -like "*.tmp") {
$parent = Split-Path -Parent $path
if (Test-Path $parent) {
$tmpFiles = Get-ChildItem -Path $parent -Filter "*.tmp" -File -ErrorAction SilentlyContinue
if ($tmpFiles) {
$paths = $tmpFiles | ForEach-Object { $_.FullName }
Remove-SafeItems -Paths $paths -Description "Office temp files"
}
}
}
elseif (Test-Path $path) {
Clear-DirectoryContents -Path $path -Description "Office $(Split-Path -Leaf $path)"
}
}
}
function Clear-OneDriveCache {
<#
.SYNOPSIS
Clean OneDrive cache
#>
$oneDriveCachePaths = @(
"$env:LOCALAPPDATA\Microsoft\OneDrive\logs"
"$env:LOCALAPPDATA\Microsoft\OneDrive\setup\logs"
)
foreach ($path in $oneDriveCachePaths) {
if (Test-Path $path) {
Remove-OldFiles -Path $path -DaysOld 7 -Description "OneDrive logs"
}
}
}
function Clear-DropboxCache {
<#
.SYNOPSIS
Clean Dropbox cache
#>
# Dropbox cache is typically in the Dropbox folder itself
$dropboxInfoPath = "$env:LOCALAPPDATA\Dropbox\info.json"
if (Test-Path $dropboxInfoPath) {
try {
$dropboxInfo = Get-Content $dropboxInfoPath | ConvertFrom-Json
$dropboxPath = $dropboxInfo.personal.path
if ($dropboxPath) {
$dropboxCachePath = "$dropboxPath\.dropbox.cache"
if (Test-Path $dropboxCachePath) {
Clear-DirectoryContents -Path $dropboxCachePath -Description "Dropbox cache"
}
}
}
catch {
Write-Debug "Could not read Dropbox config: $_"
}
}
}
function Clear-GoogleDriveCache {
<#
.SYNOPSIS
Clean Google Drive cache
#>
$googleDriveCachePaths = @(
"$env:LOCALAPPDATA\Google\DriveFS\Logs"
"$env:LOCALAPPDATA\Google\DriveFS\*.tmp"
)
foreach ($path in $googleDriveCachePaths) {
if ($path -like "*.tmp") {
$parent = Split-Path -Parent $path
if (Test-Path $parent) {
$tmpFiles = Get-ChildItem -Path $parent -Filter "*.tmp" -ErrorAction SilentlyContinue
if ($tmpFiles) {
$paths = $tmpFiles | ForEach-Object { $_.FullName }
Remove-SafeItems -Paths $paths -Description "Google Drive temp"
}
}
}
elseif (Test-Path $path) {
Remove-OldFiles -Path $path -DaysOld 7 -Description "Google Drive logs"
}
}
}
function Clear-AdobeData {
<#
.SYNOPSIS
Clean Adobe application caches and temp files
#>
$adobeCachePaths = @(
"$env:APPDATA\Adobe\Common\Media Cache Files"
"$env:APPDATA\Adobe\Common\Peak Files"
"$env:APPDATA\Adobe\Common\Team Projects Cache"
"$env:LOCALAPPDATA\Adobe\*\Cache"
"$env:LOCALAPPDATA\Adobe\*\CameraRaw\Cache"
"$env:LOCALAPPDATA\Temp\Adobe"
)
foreach ($pattern in $adobeCachePaths) {
$paths = Resolve-Path $pattern -ErrorAction SilentlyContinue
foreach ($path in $paths) {
if (Test-Path $path.Path) {
Clear-DirectoryContents -Path $path.Path -Description "Adobe cache"
}
}
}
}
function Clear-AutodeskData {
<#
.SYNOPSIS
Clean Autodesk application caches
#>
$autodeskCachePaths = @(
"$env:LOCALAPPDATA\Autodesk\*\Cache"
"$env:APPDATA\Autodesk\*\cache"
)
foreach ($pattern in $autodeskCachePaths) {
$paths = Resolve-Path $pattern -ErrorAction SilentlyContinue
foreach ($path in $paths) {
if (Test-Path $path.Path) {
Clear-DirectoryContents -Path $path.Path -Description "Autodesk cache"
}
}
}
}
# ============================================================================
# Gaming Platform Cleanup
# ============================================================================
function Clear-GamingPlatformCaches {
<#
.SYNOPSIS
Clean gaming platform caches (Steam, Epic, Origin, etc.)
#>
# Steam
$steamPaths = @(
"${env:ProgramFiles(x86)}\Steam\appcache\httpcache"
"${env:ProgramFiles(x86)}\Steam\appcache\librarycache"
"${env:ProgramFiles(x86)}\Steam\logs"
)
foreach ($path in $steamPaths) {
if (Test-Path $path) {
Clear-DirectoryContents -Path $path -Description "Steam $(Split-Path -Leaf $path)"
}
}
# Epic Games Launcher
$epicPaths = @(
"$env:LOCALAPPDATA\EpicGamesLauncher\Saved\webcache"
"$env:LOCALAPPDATA\EpicGamesLauncher\Saved\Logs"
)
foreach ($path in $epicPaths) {
if (Test-Path $path) {
Clear-DirectoryContents -Path $path -Description "Epic Games $(Split-Path -Leaf $path)"
}
}
# EA App (Origin replacement)
$eaPaths = @(
"$env:LOCALAPPDATA\Electronic Arts\EA Desktop\cache"
"$env:APPDATA\Origin\*\cache"
)
foreach ($pattern in $eaPaths) {
$paths = Resolve-Path $pattern -ErrorAction SilentlyContinue
foreach ($path in $paths) {
if (Test-Path $path.Path) {
Clear-DirectoryContents -Path $path.Path -Description "EA/Origin cache"
}
}
}
# GOG Galaxy
$gogPaths = @(
"$env:LOCALAPPDATA\GOG.com\Galaxy\webcache"
"$env:PROGRAMDATA\GOG.com\Galaxy\logs"
)
foreach ($path in $gogPaths) {
if (Test-Path $path) {
Clear-DirectoryContents -Path $path -Description "GOG Galaxy $(Split-Path -Leaf $path)"
}
}
# Ubisoft Connect
$ubiPaths = @(
"$env:LOCALAPPDATA\Ubisoft Game Launcher\cache"
"$env:LOCALAPPDATA\Ubisoft Game Launcher\logs"
)
foreach ($path in $ubiPaths) {
if (Test-Path $path) {
Clear-DirectoryContents -Path $path -Description "Ubisoft $(Split-Path -Leaf $path)"
}
}
# Battle.net
$battlenetPaths = @(
"$env:APPDATA\Battle.net\Cache"
"$env:APPDATA\Battle.net\Logs"
)
foreach ($path in $battlenetPaths) {
if (Test-Path $path) {
Clear-DirectoryContents -Path $path -Description "Battle.net $(Split-Path -Leaf $path)"
}
}
}
# ============================================================================
# Main Application Cleanup Function
# ============================================================================
function Invoke-AppCleanup {
<#
.SYNOPSIS
Run all application-specific cleanup tasks
#>
param([switch]$IncludeOrphaned)
Start-Section "Applications"
# Productivity apps
Clear-OfficeCache
Clear-OneDriveCache
Clear-DropboxCache
Clear-GoogleDriveCache
# Creative apps
Clear-AdobeData
Clear-AutodeskData
# Gaming platforms
Clear-GamingPlatformCaches
Stop-Section
# Orphaned app data (separate section)
if ($IncludeOrphaned) {
Clear-OrphanedAppData -DaysOld 60
}
}
# ============================================================================
# Exports
# ============================================================================
# Functions: Get-InstalledPrograms, Find-OrphanedAppData, Clear-OfficeCache, etc.

View File

@@ -1,385 +0,0 @@
# Mole - Cache Cleanup Module
# Cleans Windows and application caches
#Requires -Version 5.1
Set-StrictMode -Version Latest
# Prevent multiple sourcing
if ((Get-Variable -Name 'MOLE_CLEAN_CACHES_LOADED' -Scope Script -ErrorAction SilentlyContinue) -and $script:MOLE_CLEAN_CACHES_LOADED) { return }
$script:MOLE_CLEAN_CACHES_LOADED = $true
# Import dependencies
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
. "$scriptDir\..\core\base.ps1"
. "$scriptDir\..\core\log.ps1"
. "$scriptDir\..\core\file_ops.ps1"
# ============================================================================
# Windows System Caches
# ============================================================================
function Clear-WindowsUpdateCache {
<#
.SYNOPSIS
Clean Windows Update cache (requires admin)
#>
if (-not (Test-IsAdmin)) {
Write-Debug "Skipping Windows Update cache - requires admin"
return
}
$wuPath = "$env:WINDIR\SoftwareDistribution\Download"
if (Test-Path $wuPath) {
# Stop Windows Update service first
if (Test-DryRunMode) {
Write-DryRun "Windows Update cache"
Set-SectionActivity
return
}
try {
Stop-Service -Name wuauserv -Force -ErrorAction SilentlyContinue
Clear-DirectoryContents -Path $wuPath -Description "Windows Update cache"
Start-Service -Name wuauserv -ErrorAction SilentlyContinue
}
catch {
Write-Debug "Could not clear Windows Update cache: $_"
Start-Service -Name wuauserv -ErrorAction SilentlyContinue
}
}
}
function Clear-DeliveryOptimizationCache {
<#
.SYNOPSIS
Clean Windows Delivery Optimization cache (requires admin)
#>
if (-not (Test-IsAdmin)) {
Write-Debug "Skipping Delivery Optimization cache - requires admin"
return
}
$doPath = "$env:WINDIR\ServiceProfiles\NetworkService\AppData\Local\Microsoft\Windows\DeliveryOptimization"
if (Test-Path $doPath) {
if (Test-DryRunMode) {
Write-DryRun "Delivery Optimization cache"
Set-SectionActivity
return
}
try {
Stop-Service -Name DoSvc -Force -ErrorAction SilentlyContinue
Clear-DirectoryContents -Path "$doPath\Cache" -Description "Delivery Optimization cache"
Start-Service -Name DoSvc -ErrorAction SilentlyContinue
}
catch {
Write-Debug "Could not clear Delivery Optimization cache: $_"
Start-Service -Name DoSvc -ErrorAction SilentlyContinue
}
}
}
function Clear-FontCache {
<#
.SYNOPSIS
Clean Windows font cache (requires admin)
#>
if (-not (Test-IsAdmin)) {
return
}
$fontCachePath = "$env:LOCALAPPDATA\Microsoft\Windows\Fonts\FontCache"
if (Test-Path $fontCachePath) {
Remove-SafeItem -Path $fontCachePath -Description "Font cache"
}
}
# ============================================================================
# Browser Caches
# ============================================================================
function Clear-BrowserCaches {
<#
.SYNOPSIS
Clean browser cache directories
#>
Start-Section "Browser caches"
# Chrome
$chromeCachePaths = @(
"$env:LOCALAPPDATA\Google\Chrome\User Data\Default\Cache"
"$env:LOCALAPPDATA\Google\Chrome\User Data\Default\Code Cache"
"$env:LOCALAPPDATA\Google\Chrome\User Data\Default\GPUCache"
"$env:LOCALAPPDATA\Google\Chrome\User Data\Default\Service Worker\CacheStorage"
"$env:LOCALAPPDATA\Google\Chrome\User Data\ShaderCache"
"$env:LOCALAPPDATA\Google\Chrome\User Data\GrShaderCache"
)
foreach ($path in $chromeCachePaths) {
if (Test-Path $path) {
Clear-DirectoryContents -Path $path -Description "Chrome $(Split-Path -Leaf $path)"
}
}
# Edge
$edgeCachePaths = @(
"$env:LOCALAPPDATA\Microsoft\Edge\User Data\Default\Cache"
"$env:LOCALAPPDATA\Microsoft\Edge\User Data\Default\Code Cache"
"$env:LOCALAPPDATA\Microsoft\Edge\User Data\Default\GPUCache"
"$env:LOCALAPPDATA\Microsoft\Edge\User Data\Default\Service Worker\CacheStorage"
"$env:LOCALAPPDATA\Microsoft\Edge\User Data\ShaderCache"
)
foreach ($path in $edgeCachePaths) {
if (Test-Path $path) {
Clear-DirectoryContents -Path $path -Description "Edge $(Split-Path -Leaf $path)"
}
}
# Firefox
$firefoxProfiles = "$env:APPDATA\Mozilla\Firefox\Profiles"
if (Test-Path $firefoxProfiles) {
$profiles = Get-ChildItem -Path $firefoxProfiles -Directory -ErrorAction SilentlyContinue
foreach ($profile in $profiles) {
$firefoxCachePaths = @(
"$($profile.FullName)\cache2"
"$($profile.FullName)\startupCache"
"$($profile.FullName)\shader-cache"
)
foreach ($path in $firefoxCachePaths) {
if (Test-Path $path) {
Clear-DirectoryContents -Path $path -Description "Firefox cache"
}
}
}
}
# Brave
$braveCachePath = "$env:LOCALAPPDATA\BraveSoftware\Brave-Browser\User Data\Default\Cache"
if (Test-Path $braveCachePath) {
Clear-DirectoryContents -Path $braveCachePath -Description "Brave cache"
}
# Opera
$operaCachePath = "$env:APPDATA\Opera Software\Opera Stable\Cache"
if (Test-Path $operaCachePath) {
Clear-DirectoryContents -Path $operaCachePath -Description "Opera cache"
}
Stop-Section
}
# ============================================================================
# Application Caches
# ============================================================================
function Clear-AppCaches {
<#
.SYNOPSIS
Clean common application caches
#>
Start-Section "Application caches"
# Spotify
$spotifyCachePaths = @(
"$env:LOCALAPPDATA\Spotify\Data"
"$env:LOCALAPPDATA\Spotify\Storage"
)
foreach ($path in $spotifyCachePaths) {
if (Test-Path $path) {
Clear-DirectoryContents -Path $path -Description "Spotify cache"
}
}
# Discord
$discordCachePaths = @(
"$env:APPDATA\discord\Cache"
"$env:APPDATA\discord\Code Cache"
"$env:APPDATA\discord\GPUCache"
)
foreach ($path in $discordCachePaths) {
if (Test-Path $path) {
Clear-DirectoryContents -Path $path -Description "Discord cache"
}
}
# Slack
$slackCachePaths = @(
"$env:APPDATA\Slack\Cache"
"$env:APPDATA\Slack\Code Cache"
"$env:APPDATA\Slack\GPUCache"
"$env:APPDATA\Slack\Service Worker\CacheStorage"
)
foreach ($path in $slackCachePaths) {
if (Test-Path $path) {
Clear-DirectoryContents -Path $path -Description "Slack cache"
}
}
# Teams
$teamsCachePaths = @(
"$env:APPDATA\Microsoft\Teams\Cache"
"$env:APPDATA\Microsoft\Teams\blob_storage"
"$env:APPDATA\Microsoft\Teams\databases"
"$env:APPDATA\Microsoft\Teams\GPUCache"
"$env:APPDATA\Microsoft\Teams\IndexedDB"
"$env:APPDATA\Microsoft\Teams\Local Storage"
"$env:APPDATA\Microsoft\Teams\tmp"
)
foreach ($path in $teamsCachePaths) {
if (Test-Path $path) {
Clear-DirectoryContents -Path $path -Description "Teams cache"
}
}
# VS Code
$vscodeCachePaths = @(
"$env:APPDATA\Code\Cache"
"$env:APPDATA\Code\CachedData"
"$env:APPDATA\Code\CachedExtensions"
"$env:APPDATA\Code\CachedExtensionVSIXs"
"$env:APPDATA\Code\Code Cache"
"$env:APPDATA\Code\GPUCache"
)
foreach ($path in $vscodeCachePaths) {
if (Test-Path $path) {
Clear-DirectoryContents -Path $path -Description "VS Code cache"
}
}
# Zoom
$zoomCachePath = "$env:APPDATA\Zoom\data"
if (Test-Path $zoomCachePath) {
Clear-DirectoryContents -Path $zoomCachePath -Description "Zoom cache"
}
# Adobe Creative Cloud
$adobeCachePaths = @(
"$env:LOCALAPPDATA\Adobe\*\Cache"
"$env:APPDATA\Adobe\Common\Media Cache Files"
"$env:APPDATA\Adobe\Common\Peak Files"
)
foreach ($pattern in $adobeCachePaths) {
$paths = Resolve-Path $pattern -ErrorAction SilentlyContinue
foreach ($path in $paths) {
if (Test-Path $path) {
Clear-DirectoryContents -Path $path.Path -Description "Adobe cache"
}
}
}
# Steam (download cache, not games)
$steamCachePath = "${env:ProgramFiles(x86)}\Steam\appcache"
if (Test-Path $steamCachePath) {
Clear-DirectoryContents -Path $steamCachePath -Description "Steam app cache"
}
# Epic Games Launcher
$epicCachePath = "$env:LOCALAPPDATA\EpicGamesLauncher\Saved\webcache"
if (Test-Path $epicCachePath) {
Clear-DirectoryContents -Path $epicCachePath -Description "Epic Games cache"
}
Stop-Section
}
# ============================================================================
# Windows Store / UWP App Caches
# ============================================================================
function Clear-StoreAppCaches {
<#
.SYNOPSIS
Clean Windows Store and UWP app caches
#>
# Microsoft Store cache
$storeCache = "$env:LOCALAPPDATA\Microsoft\Windows\WCN"
if (Test-Path $storeCache) {
Clear-DirectoryContents -Path $storeCache -Description "Windows Store cache"
}
# Store app temp files
$storeTemp = "$env:LOCALAPPDATA\Packages\Microsoft.WindowsStore_*\LocalCache"
$storePaths = Resolve-Path $storeTemp -ErrorAction SilentlyContinue
foreach ($path in $storePaths) {
if (Test-Path $path.Path) {
Clear-DirectoryContents -Path $path.Path -Description "Store LocalCache"
}
}
}
# ============================================================================
# .NET / Runtime Caches
# ============================================================================
function Clear-DotNetCaches {
<#
.SYNOPSIS
Clean .NET runtime caches
#>
# .NET temp files
$dotnetTemp = "$env:LOCALAPPDATA\Temp\Microsoft.NET"
if (Test-Path $dotnetTemp) {
Clear-DirectoryContents -Path $dotnetTemp -Description ".NET temp files"
}
# NGen cache (don't touch - managed by Windows)
# Assembly cache (don't touch - managed by CLR)
}
# ============================================================================
# Main Cache Cleanup Function
# ============================================================================
function Invoke-CacheCleanup {
<#
.SYNOPSIS
Run all cache cleanup tasks
#>
param(
[switch]$IncludeWindowsUpdate,
[switch]$IncludeBrowsers,
[switch]$IncludeApps
)
Start-Section "System caches"
# Windows system caches (if admin)
if (Test-IsAdmin) {
if ($IncludeWindowsUpdate) {
Clear-WindowsUpdateCache
Clear-DeliveryOptimizationCache
}
Clear-FontCache
}
Clear-StoreAppCaches
Clear-DotNetCaches
Stop-Section
# Browser caches
if ($IncludeBrowsers) {
Clear-BrowserCaches
}
# Application caches
if ($IncludeApps) {
Clear-AppCaches
}
}
# ============================================================================
# Exports
# ============================================================================
# Functions: Clear-WindowsUpdateCache, Clear-BrowserCaches, Clear-AppCaches, etc.

View File

@@ -1,537 +0,0 @@
# Mole - Developer Tools Cleanup Module
# Cleans development tool caches and build artifacts
#Requires -Version 5.1
Set-StrictMode -Version Latest
# Prevent multiple sourcing
if ((Get-Variable -Name 'MOLE_CLEAN_DEV_LOADED' -Scope Script -ErrorAction SilentlyContinue) -and $script:MOLE_CLEAN_DEV_LOADED) { return }
$script:MOLE_CLEAN_DEV_LOADED = $true
# Import dependencies
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
. "$scriptDir\..\core\base.ps1"
. "$scriptDir\..\core\log.ps1"
. "$scriptDir\..\core\file_ops.ps1"
# ============================================================================
# Node.js / JavaScript Ecosystem
# ============================================================================
function Clear-NpmCache {
<#
.SYNOPSIS
Clean npm, pnpm, yarn, and bun caches
#>
# npm cache
if (Get-Command npm -ErrorAction SilentlyContinue) {
if (Test-DryRunMode) {
Write-DryRun "npm cache"
}
else {
try {
$null = npm cache clean --force 2>&1
Write-Success "npm cache"
Set-SectionActivity
}
catch {
Write-Debug "npm cache clean failed: $_"
}
}
}
# npm cache directory (fallback)
$npmCachePath = "$env:APPDATA\npm-cache"
if (Test-Path $npmCachePath) {
Clear-DirectoryContents -Path $npmCachePath -Description "npm cache directory"
}
# pnpm store
$pnpmStorePath = "$env:LOCALAPPDATA\pnpm\store"
if (Test-Path $pnpmStorePath) {
if (Get-Command pnpm -ErrorAction SilentlyContinue) {
if (Test-DryRunMode) {
Write-DryRun "pnpm store"
}
else {
try {
$null = pnpm store prune 2>&1
Write-Success "pnpm store pruned"
Set-SectionActivity
}
catch {
Write-Debug "pnpm store prune failed: $_"
}
}
}
}
# Yarn cache
$yarnCachePaths = @(
"$env:LOCALAPPDATA\Yarn\Cache"
"$env:USERPROFILE\.yarn\cache"
)
foreach ($path in $yarnCachePaths) {
if (Test-Path $path) {
Clear-DirectoryContents -Path $path -Description "Yarn cache"
}
}
# Bun cache
$bunCachePath = "$env:USERPROFILE\.bun\install\cache"
if (Test-Path $bunCachePath) {
Clear-DirectoryContents -Path $bunCachePath -Description "Bun cache"
}
}
function Clear-NodeBuildCaches {
<#
.SYNOPSIS
Clean Node.js build-related caches
#>
# node-gyp
$nodeGypPath = "$env:LOCALAPPDATA\node-gyp\Cache"
if (Test-Path $nodeGypPath) {
Clear-DirectoryContents -Path $nodeGypPath -Description "node-gyp cache"
}
# Electron cache
$electronCachePath = "$env:LOCALAPPDATA\electron\Cache"
if (Test-Path $electronCachePath) {
Clear-DirectoryContents -Path $electronCachePath -Description "Electron cache"
}
# TypeScript cache
$tsCachePath = "$env:LOCALAPPDATA\TypeScript"
if (Test-Path $tsCachePath) {
Clear-DirectoryContents -Path $tsCachePath -Description "TypeScript cache"
}
}
# ============================================================================
# Python Ecosystem
# ============================================================================
function Clear-PythonCaches {
<#
.SYNOPSIS
Clean Python and pip caches
#>
# pip cache
if (Get-Command pip -ErrorAction SilentlyContinue) {
if (Test-DryRunMode) {
Write-DryRun "pip cache"
}
else {
try {
$null = pip cache purge 2>&1
Write-Success "pip cache"
Set-SectionActivity
}
catch {
Write-Debug "pip cache purge failed: $_"
}
}
}
# pip cache directory
$pipCachePath = "$env:LOCALAPPDATA\pip\Cache"
if (Test-Path $pipCachePath) {
Clear-DirectoryContents -Path $pipCachePath -Description "pip cache directory"
}
# Python bytecode caches (__pycache__)
# Note: These are typically in project directories, cleaned by purge command
# pyenv cache
$pyenvCachePath = "$env:USERPROFILE\.pyenv\cache"
if (Test-Path $pyenvCachePath) {
Clear-DirectoryContents -Path $pyenvCachePath -Description "pyenv cache"
}
# Poetry cache
$poetryCachePath = "$env:LOCALAPPDATA\pypoetry\Cache"
if (Test-Path $poetryCachePath) {
Clear-DirectoryContents -Path $poetryCachePath -Description "Poetry cache"
}
# conda packages
$condaCachePaths = @(
"$env:USERPROFILE\.conda\pkgs"
"$env:USERPROFILE\anaconda3\pkgs"
"$env:USERPROFILE\miniconda3\pkgs"
)
foreach ($path in $condaCachePaths) {
if (Test-Path $path) {
# Only clean index and temp files, not actual packages
$tempFiles = Get-ChildItem -Path $path -Filter "*.tmp" -ErrorAction SilentlyContinue
if ($tempFiles) {
$paths = $tempFiles | ForEach-Object { $_.FullName }
Remove-SafeItems -Paths $paths -Description "Conda temp files"
}
}
}
# Jupyter runtime
$jupyterRuntimePath = "$env:APPDATA\jupyter\runtime"
if (Test-Path $jupyterRuntimePath) {
Clear-DirectoryContents -Path $jupyterRuntimePath -Description "Jupyter runtime"
}
# pytest cache
$pytestCachePath = "$env:USERPROFILE\.pytest_cache"
if (Test-Path $pytestCachePath) {
Remove-SafeItem -Path $pytestCachePath -Description "pytest cache" -Recurse
}
}
# ============================================================================
# .NET / C# Ecosystem
# ============================================================================
function Clear-DotNetDevCaches {
<#
.SYNOPSIS
Clean .NET development caches
#>
# NuGet cache
$nugetCachePath = "$env:USERPROFILE\.nuget\packages"
# Don't clean packages by default - they're needed for builds
# Only clean http-cache and temp
$nugetHttpCache = "$env:LOCALAPPDATA\NuGet\v3-cache"
if (Test-Path $nugetHttpCache) {
Clear-DirectoryContents -Path $nugetHttpCache -Description "NuGet HTTP cache"
}
$nugetTempPath = "$env:LOCALAPPDATA\NuGet\plugins-cache"
if (Test-Path $nugetTempPath) {
Clear-DirectoryContents -Path $nugetTempPath -Description "NuGet plugins cache"
}
# MSBuild temp files
$msbuildTemp = "$env:LOCALAPPDATA\Microsoft\MSBuild"
if (Test-Path $msbuildTemp) {
$tempDirs = Get-ChildItem -Path $msbuildTemp -Directory -Filter "*temp*" -ErrorAction SilentlyContinue
foreach ($dir in $tempDirs) {
Clear-DirectoryContents -Path $dir.FullName -Description "MSBuild temp"
}
}
}
# ============================================================================
# Go Ecosystem
# ============================================================================
function Clear-GoCaches {
<#
.SYNOPSIS
Clean Go build and module caches
#>
if (Get-Command go -ErrorAction SilentlyContinue) {
if (Test-DryRunMode) {
Write-DryRun "Go cache"
}
else {
try {
$null = go clean -cache 2>&1
Write-Success "Go build cache"
Set-SectionActivity
}
catch {
Write-Debug "go clean -cache failed: $_"
}
}
}
# Go module cache
$goModCachePath = "$env:GOPATH\pkg\mod\cache"
if (-not $env:GOPATH) {
$goModCachePath = "$env:USERPROFILE\go\pkg\mod\cache"
}
if (Test-Path $goModCachePath) {
Clear-DirectoryContents -Path $goModCachePath -Description "Go module cache"
}
}
# ============================================================================
# Rust Ecosystem
# ============================================================================
function Clear-RustCaches {
<#
.SYNOPSIS
Clean Rust/Cargo caches
#>
# Cargo registry cache
$cargoRegistryCache = "$env:USERPROFILE\.cargo\registry\cache"
if (Test-Path $cargoRegistryCache) {
Clear-DirectoryContents -Path $cargoRegistryCache -Description "Cargo registry cache"
}
# Cargo git cache
$cargoGitCache = "$env:USERPROFILE\.cargo\git\checkouts"
if (Test-Path $cargoGitCache) {
Clear-DirectoryContents -Path $cargoGitCache -Description "Cargo git cache"
}
# Rustup downloads
$rustupDownloads = "$env:USERPROFILE\.rustup\downloads"
if (Test-Path $rustupDownloads) {
Clear-DirectoryContents -Path $rustupDownloads -Description "Rustup downloads"
}
}
# ============================================================================
# Java / JVM Ecosystem
# ============================================================================
function Clear-JvmCaches {
<#
.SYNOPSIS
Clean JVM ecosystem caches (Gradle, Maven, etc.)
#>
# Gradle caches
$gradleCachePaths = @(
"$env:USERPROFILE\.gradle\caches"
"$env:USERPROFILE\.gradle\daemon"
"$env:USERPROFILE\.gradle\wrapper\dists"
)
foreach ($path in $gradleCachePaths) {
if (Test-Path $path) {
# Only clean temp and old daemon logs
$tempFiles = Get-ChildItem -Path $path -Recurse -Filter "*.lock" -ErrorAction SilentlyContinue
if ($tempFiles) {
$paths = $tempFiles | ForEach-Object { $_.FullName }
Remove-SafeItems -Paths $paths -Description "Gradle lock files"
}
}
}
# Maven repository (only clean temp files)
$mavenRepoPath = "$env:USERPROFILE\.m2\repository"
if (Test-Path $mavenRepoPath) {
$tempFiles = Get-ChildItem -Path $mavenRepoPath -Recurse -Filter "*.lastUpdated" -ErrorAction SilentlyContinue
if ($tempFiles) {
$paths = $tempFiles | ForEach-Object { $_.FullName }
Remove-SafeItems -Paths $paths -Description "Maven update markers"
}
}
}
# ============================================================================
# Docker / Containers
# ============================================================================
function Clear-DockerCaches {
<#
.SYNOPSIS
Clean Docker build caches and unused data
#>
if (-not (Get-Command docker -ErrorAction SilentlyContinue)) {
return
}
# Check if Docker daemon is running
$dockerRunning = $false
try {
$null = docker info 2>&1
$dockerRunning = $true
}
catch {
Write-Debug "Docker daemon not running"
}
if ($dockerRunning) {
if (Test-DryRunMode) {
Write-DryRun "Docker build cache"
}
else {
try {
$null = docker builder prune -af 2>&1
Write-Success "Docker build cache"
Set-SectionActivity
}
catch {
Write-Debug "docker builder prune failed: $_"
}
}
}
# Docker Desktop cache (Windows)
$dockerDesktopCache = "$env:LOCALAPPDATA\Docker\wsl\data"
# Note: Don't clean this - it's the WSL2 virtual disk
}
# ============================================================================
# Cloud CLI Tools
# ============================================================================
function Clear-CloudCliCaches {
<#
.SYNOPSIS
Clean cloud CLI tool caches (AWS, Azure, GCP)
#>
# AWS CLI cache
$awsCachePath = "$env:USERPROFILE\.aws\cli\cache"
if (Test-Path $awsCachePath) {
Clear-DirectoryContents -Path $awsCachePath -Description "AWS CLI cache"
}
# Azure CLI logs
$azureLogsPath = "$env:USERPROFILE\.azure\logs"
if (Test-Path $azureLogsPath) {
Clear-DirectoryContents -Path $azureLogsPath -Description "Azure CLI logs"
}
# Google Cloud logs
$gcloudLogsPath = "$env:APPDATA\gcloud\logs"
if (Test-Path $gcloudLogsPath) {
Clear-DirectoryContents -Path $gcloudLogsPath -Description "gcloud logs"
}
# Kubernetes cache
$kubeCachePath = "$env:USERPROFILE\.kube\cache"
if (Test-Path $kubeCachePath) {
Clear-DirectoryContents -Path $kubeCachePath -Description "Kubernetes cache"
}
# Terraform plugin cache
$terraformCachePath = "$env:APPDATA\terraform.d\plugin-cache"
if (Test-Path $terraformCachePath) {
Clear-DirectoryContents -Path $terraformCachePath -Description "Terraform plugin cache"
}
}
# ============================================================================
# IDE Caches
# ============================================================================
function Clear-IdeCaches {
<#
.SYNOPSIS
Clean IDE caches (VS, VSCode, JetBrains, etc.)
#>
# Visual Studio cache
$vsCachePaths = @(
"$env:LOCALAPPDATA\Microsoft\VisualStudio\*\ComponentModelCache"
"$env:LOCALAPPDATA\Microsoft\VisualStudio\*\ImageCache"
)
foreach ($pattern in $vsCachePaths) {
$paths = Resolve-Path $pattern -ErrorAction SilentlyContinue
foreach ($path in $paths) {
if (Test-Path $path.Path) {
Clear-DirectoryContents -Path $path.Path -Description "Visual Studio cache"
}
}
}
# JetBrains IDEs caches
$jetbrainsBasePaths = @(
"$env:LOCALAPPDATA\JetBrains"
"$env:APPDATA\JetBrains"
)
foreach ($basePath in $jetbrainsBasePaths) {
if (Test-Path $basePath) {
$ideFolders = Get-ChildItem -Path $basePath -Directory -ErrorAction SilentlyContinue
foreach ($ideFolder in $ideFolders) {
$cacheFolders = @("caches", "index", "tmp")
foreach ($cacheFolder in $cacheFolders) {
$cachePath = Join-Path $ideFolder.FullName $cacheFolder
if (Test-Path $cachePath) {
Clear-DirectoryContents -Path $cachePath -Description "$($ideFolder.Name) $cacheFolder"
}
}
}
}
}
}
# ============================================================================
# Git Caches
# ============================================================================
function Clear-GitCaches {
<#
.SYNOPSIS
Clean Git temporary files and lock files
#>
# Git config locks (stale)
$gitConfigLock = "$env:USERPROFILE\.gitconfig.lock"
if (Test-Path $gitConfigLock) {
Remove-SafeItem -Path $gitConfigLock -Description "Git config lock"
}
# GitHub CLI cache
$ghCachePath = "$env:APPDATA\GitHub CLI"
if (Test-Path $ghCachePath) {
$cacheFiles = Get-ChildItem -Path $ghCachePath -Filter "*.json" -ErrorAction SilentlyContinue |
Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-7) }
if ($cacheFiles) {
$paths = $cacheFiles | ForEach-Object { $_.FullName }
Remove-SafeItems -Paths $paths -Description "GitHub CLI cache"
}
}
}
# ============================================================================
# Main Developer Tools Cleanup Function
# ============================================================================
function Invoke-DevToolsCleanup {
<#
.SYNOPSIS
Run all developer tools cleanup tasks
#>
Start-Section "Developer tools"
# JavaScript ecosystem
Clear-NpmCache
Clear-NodeBuildCaches
# Python ecosystem
Clear-PythonCaches
# .NET ecosystem
Clear-DotNetDevCaches
# Go ecosystem
Clear-GoCaches
# Rust ecosystem
Clear-RustCaches
# JVM ecosystem
Clear-JvmCaches
# Containers
Clear-DockerCaches
# Cloud CLI tools
Clear-CloudCliCaches
# IDEs
Clear-IdeCaches
# Git
Clear-GitCaches
Stop-Section
}
# ============================================================================
# Exports
# ============================================================================
# Functions: Clear-NpmCache, Clear-PythonCaches, Clear-DockerCaches, etc.

View File

@@ -1,423 +0,0 @@
# Mole - System Cleanup Module
# Cleans Windows system files that require administrator access
#Requires -Version 5.1
Set-StrictMode -Version Latest
# Prevent multiple sourcing
if ((Get-Variable -Name 'MOLE_CLEAN_SYSTEM_LOADED' -Scope Script -ErrorAction SilentlyContinue) -and $script:MOLE_CLEAN_SYSTEM_LOADED) { return }
$script:MOLE_CLEAN_SYSTEM_LOADED = $true
# Import dependencies
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
. "$scriptDir\..\core\base.ps1"
. "$scriptDir\..\core\log.ps1"
. "$scriptDir\..\core\file_ops.ps1"
# ============================================================================
# System Temp Files
# ============================================================================
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)) {
Remove-OldFiles -Path $systemTemp -DaysOld 7 -Description "System temp files"
}
}
# ============================================================================
# Windows Logs
# ============================================================================
function Clear-WindowsLogs {
<#
.SYNOPSIS
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"
"$env:WINDIR\Logs\DISM"
"$env:WINDIR\Logs\DPX"
"$env:WINDIR\Logs\WindowsUpdate"
"$env:WINDIR\Logs\SIH"
"$env:WINDIR\Logs\waasmedia"
"$env:WINDIR\Debug"
"$env:WINDIR\Panther"
"$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) }
if ($setupLogs) {
$paths = $setupLogs | ForEach-Object { $_.FullName }
Remove-SafeItems -Paths $paths -Description "Windows setup logs"
}
}
# ============================================================================
# Windows Update Cleanup
# ============================================================================
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
}
catch {
Write-Debug "Could not stop Windows Update service: $_"
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"
if (Test-Path $wuDataStore) {
Clear-DirectoryContents -Path $wuDataStore -Description "Windows Update logs"
}
}
finally {
# Always restart service if it was running, even if cleanup failed
if ($wasRunning) {
Start-Service -Name wuauserv -ErrorAction SilentlyContinue
}
}
}
# ============================================================================
# Installer Cleanup
# ============================================================================
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
if ($tmpFiles) {
$paths = $tmpFiles | ForEach-Object { $_.FullName }
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) }
if ($installerLogs) {
$paths = $installerLogs | ForEach-Object { $_.FullName }
Remove-SafeItems -Paths $paths -Description "Old MSI logs"
}
}
# ============================================================================
# Component Store Cleanup
# ============================================================================
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
}
else {
Write-Debug "DISM returned exit code: $($result.ExitCode)"
}
}
catch {
Write-Debug "Component Store cleanup failed: $_"
}
}
# ============================================================================
# Memory Dump Cleanup
# ============================================================================
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)
Remove-SafeItem -Path $path -Description "Memory dump"
}
elseif (Test-Path $path -PathType Container) {
# Directory (Minidump, CrashDumps)
Clear-DirectoryContents -Path $path -Description "$(Split-Path -Leaf $path)"
}
}
}
# ============================================================================
# Font Cache
# ============================================================================
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
}
catch {
Write-Debug "Font cache cleanup failed: $_"
Start-Service -Name "FontCache" -ErrorAction SilentlyContinue
}
}
}
# ============================================================================
# Disk Cleanup Tool Integration
# ============================================================================
function Invoke-DiskCleanupTool {
<#
.SYNOPSIS
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"
"Internet Cache Files"
"Old ChkDsk Files"
"Recycle Bin"
"Setup Log Files"
"System error memory dump files"
"System error minidump files"
"Temporary Files"
"Temporary Setup Files"
"Thumbnail Cache"
"Windows Error Reporting Archive Files"
"Windows Error Reporting Queue Files"
"Windows Error Reporting System Archive Files"
"Windows Error Reporting System Queue Files"
)
if ($Full -and (Test-IsAdmin)) {
$cleanupItems += @(
"Previous Installations"
"Temporary Windows installation files"
"Update Cleanup"
"Windows Defender"
"Windows Upgrade Log Files"
)
}
# Enable cleanup items in registry
foreach ($item in $cleanupItems) {
$itemPath = Join-Path $cleanupKey $item
if (Test-Path $itemPath) {
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
}
}
catch {
Write-Debug "Disk Cleanup failed: $_"
}
}
# ============================================================================
# Main System Cleanup Function
# ============================================================================
function Invoke-SystemCleanup {
<#
.SYNOPSIS
Run all system-level cleanup tasks (requires admin for full effect)
#>
param(
[switch]$IncludeComponentStore,
[switch]$IncludeDiskCleanup
)
Start-Section "System cleanup"
if (-not (Test-IsAdmin)) {
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
}
# ============================================================================
# Exports
# ============================================================================
# Functions: Clear-SystemTempFiles, Clear-WindowsLogs, Invoke-SystemCleanup, etc.

View File

@@ -1,352 +0,0 @@
# Mole - User Cleanup Module
# Cleans user-level temporary files, caches, and downloads
#Requires -Version 5.1
Set-StrictMode -Version Latest
# Prevent multiple sourcing
if ((Get-Variable -Name 'MOLE_CLEAN_USER_LOADED' -Scope Script -ErrorAction SilentlyContinue) -and $script:MOLE_CLEAN_USER_LOADED) { return }
$script:MOLE_CLEAN_USER_LOADED = $true
# Import dependencies
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
. "$scriptDir\..\core\base.ps1"
. "$scriptDir\..\core\log.ps1"
. "$scriptDir\..\core\file_ops.ps1"
# ============================================================================
# Windows Temp Files Cleanup
# ============================================================================
function Clear-UserTempFiles {
<#
.SYNOPSIS
Clean user temporary files
#>
param([int]$DaysOld = 7)
Start-Section "User temp files"
# User temp directory
$userTemp = $env:TEMP
if (Test-Path $userTemp) {
Remove-OldFiles -Path $userTemp -DaysOld $DaysOld -Description "User temp files"
}
# Windows Temp (if accessible)
$winTemp = "$env:WINDIR\Temp"
if ((Test-Path $winTemp) -and (Test-IsAdmin)) {
Remove-OldFiles -Path $winTemp -DaysOld $DaysOld -Description "Windows temp files"
}
Stop-Section
}
# ============================================================================
# Downloads Folder Cleanup
# ============================================================================
function Clear-OldDownloads {
<#
.SYNOPSIS
Clean old files from Downloads folder (with user confirmation pattern)
#>
param([int]$DaysOld = 30)
$downloadsPath = [Environment]::GetFolderPath('UserProfile') + '\Downloads'
if (-not (Test-Path $downloadsPath)) {
return
}
# Find old installers and archives
$patterns = @('*.exe', '*.msi', '*.zip', '*.7z', '*.rar', '*.tar.gz', '*.iso')
$cutoffDate = (Get-Date).AddDays(-$DaysOld)
$oldFiles = @()
foreach ($pattern in $patterns) {
$files = Get-ChildItem -Path $downloadsPath -Filter $pattern -File -ErrorAction SilentlyContinue |
Where-Object { $_.LastWriteTime -lt $cutoffDate }
if ($files) {
$oldFiles += $files
}
}
if ($oldFiles.Count -gt 0) {
$paths = $oldFiles | ForEach-Object { $_.FullName }
Remove-SafeItems -Paths $paths -Description "Old downloads (>${DaysOld}d)"
}
}
# ============================================================================
# Recycle Bin Cleanup
# ============================================================================
function Clear-RecycleBin {
<#
.SYNOPSIS
Empty the Recycle Bin
#>
if (Test-DryRunMode) {
Write-DryRun "Recycle Bin (would empty)"
Set-SectionActivity
return
}
try {
# Use Shell.Application COM object
$shell = New-Object -ComObject Shell.Application
$recycleBin = $shell.Namespace(0xA) # Recycle Bin
$items = $recycleBin.Items()
if ($items.Count -gt 0) {
# Calculate size
$totalSize = 0
foreach ($item in $items) {
$totalSize += $item.Size
}
# Clear using Clear-RecycleBin cmdlet (Windows 10+)
Clear-RecycleBin -Force -ErrorAction SilentlyContinue
$sizeHuman = Format-ByteSize -Bytes $totalSize
Write-Success "Recycle Bin $($script:Colors.Green)($sizeHuman)$($script:Colors.NC)"
Set-SectionActivity
}
}
catch {
Write-Debug "Could not clear Recycle Bin: $_"
}
}
# ============================================================================
# Recent Files Cleanup
# ============================================================================
function Clear-RecentFiles {
<#
.SYNOPSIS
Clean old recent file shortcuts
#>
param([int]$DaysOld = 30)
$recentPath = "$env:APPDATA\Microsoft\Windows\Recent"
if (Test-Path $recentPath) {
Remove-OldFiles -Path $recentPath -DaysOld $DaysOld -Filter "*.lnk" -Description "Old recent shortcuts"
}
# AutomaticDestinations (jump lists)
$autoDestPath = "$recentPath\AutomaticDestinations"
if (Test-Path $autoDestPath) {
Remove-OldFiles -Path $autoDestPath -DaysOld $DaysOld -Description "Old jump list entries"
}
}
# ============================================================================
# Thumbnail Cache Cleanup
# ============================================================================
function Clear-ThumbnailCache {
<#
.SYNOPSIS
Clean Windows thumbnail cache
#>
$thumbCachePath = "$env:LOCALAPPDATA\Microsoft\Windows\Explorer"
if (-not (Test-Path $thumbCachePath)) {
return
}
# Thumbnail cache files (thumbcache_*.db)
$thumbFiles = Get-ChildItem -Path $thumbCachePath -Filter "thumbcache_*.db" -File -ErrorAction SilentlyContinue
if ($thumbFiles) {
$paths = $thumbFiles | ForEach-Object { $_.FullName }
Remove-SafeItems -Paths $paths -Description "Thumbnail cache"
}
# Icon cache
$iconCache = "$env:LOCALAPPDATA\IconCache.db"
if (Test-Path $iconCache) {
Remove-SafeItem -Path $iconCache -Description "Icon cache"
}
}
# ============================================================================
# Windows Error Reports Cleanup
# ============================================================================
function Clear-ErrorReports {
<#
.SYNOPSIS
Clean Windows Error Reporting files
#>
param([int]$DaysOld = 7)
$werPaths = @(
"$env:LOCALAPPDATA\Microsoft\Windows\WER"
"$env:LOCALAPPDATA\CrashDumps"
"$env:USERPROFILE\AppData\Local\Microsoft\Windows\WER"
)
foreach ($path in $werPaths) {
if (Test-Path $path) {
$items = Get-ChildItem -Path $path -Recurse -Force -ErrorAction SilentlyContinue
if ($items) {
$paths = $items | ForEach-Object { $_.FullName }
Remove-SafeItems -Paths $paths -Description "Error reports"
}
}
}
# Memory dumps
$dumpPaths = @(
"$env:LOCALAPPDATA\CrashDumps"
"$env:USERPROFILE\*.dmp"
)
foreach ($path in $dumpPaths) {
$dumps = Get-ChildItem -Path $path -Filter "*.dmp" -ErrorAction SilentlyContinue
if ($dumps) {
$paths = $dumps | ForEach-Object { $_.FullName }
Remove-SafeItems -Paths $paths -Description "Memory dumps"
}
}
}
# ============================================================================
# Windows Prefetch Cleanup (requires admin)
# ============================================================================
function Clear-Prefetch {
<#
.SYNOPSIS
Clean Windows Prefetch files (requires admin)
#>
param([int]$DaysOld = 14)
if (-not (Test-IsAdmin)) {
Write-Debug "Skipping Prefetch cleanup - requires admin"
return
}
$prefetchPath = "$env:WINDIR\Prefetch"
if (Test-Path $prefetchPath) {
Remove-OldFiles -Path $prefetchPath -DaysOld $DaysOld -Description "Prefetch files"
}
}
# ============================================================================
# Log Files Cleanup
# ============================================================================
function Clear-UserLogs {
<#
.SYNOPSIS
Clean old log files from common locations
#>
param([int]$DaysOld = 7)
$logLocations = @(
"$env:LOCALAPPDATA\Temp\*.log"
"$env:APPDATA\*.log"
"$env:USERPROFILE\*.log"
)
foreach ($location in $logLocations) {
$parent = Split-Path -Parent $location
$filter = Split-Path -Leaf $location
if (Test-Path $parent) {
$logs = Get-ChildItem -Path $parent -Filter $filter -File -ErrorAction SilentlyContinue |
Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-$DaysOld) }
if ($logs) {
$paths = $logs | ForEach-Object { $_.FullName }
Remove-SafeItems -Paths $paths -Description "Old log files"
}
}
}
}
# ============================================================================
# Clipboard History Cleanup
# ============================================================================
function Clear-ClipboardHistory {
<#
.SYNOPSIS
Clear Windows clipboard history
#>
if (Test-DryRunMode) {
Write-DryRun "Clipboard history (would clear)"
return
}
try {
# Load Windows Forms assembly for clipboard access
Add-Type -AssemblyName System.Windows.Forms -ErrorAction SilentlyContinue
# Clear current clipboard
[System.Windows.Forms.Clipboard]::Clear()
# Clear clipboard history (Windows 10 1809+)
$clipboardPath = "$env:LOCALAPPDATA\Microsoft\Windows\Clipboard"
if (Test-Path $clipboardPath) {
Clear-DirectoryContents -Path $clipboardPath -Description "Clipboard history"
}
}
catch {
Write-Debug "Could not clear clipboard: $_"
}
}
# ============================================================================
# Main User Cleanup Function
# ============================================================================
function Invoke-UserCleanup {
<#
.SYNOPSIS
Run all user-level cleanup tasks
#>
param(
[int]$TempDaysOld = 7,
[int]$DownloadsDaysOld = 30,
[int]$LogDaysOld = 7,
[switch]$IncludeDownloads,
[switch]$IncludeRecycleBin
)
Start-Section "User essentials"
# Always clean these
Clear-UserTempFiles -DaysOld $TempDaysOld
Clear-RecentFiles -DaysOld 30
Clear-ThumbnailCache
Clear-ErrorReports -DaysOld 7
Clear-UserLogs -DaysOld $LogDaysOld
Clear-Prefetch -DaysOld 14
# Optional: Downloads cleanup
if ($IncludeDownloads) {
Clear-OldDownloads -DaysOld $DownloadsDaysOld
}
# Optional: Recycle Bin
if ($IncludeRecycleBin) {
Clear-RecycleBin
}
Stop-Section
}
# ============================================================================
# Exports
# ============================================================================
# Functions: Clear-UserTempFiles, Clear-OldDownloads, Clear-RecycleBin, etc.

View File

@@ -1,396 +0,0 @@
# Mole - Base Definitions and Utilities
# Core definitions, constants, and basic utility functions used by all modules
#Requires -Version 5.1
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
# Prevent multiple sourcing
if ((Get-Variable -Name 'MOLE_BASE_LOADED' -Scope Script -ErrorAction SilentlyContinue) -and $script:MOLE_BASE_LOADED) { return }
$script:MOLE_BASE_LOADED = $true
# ============================================================================
# Color Definitions (ANSI escape codes for modern terminals)
# ============================================================================
$script:ESC = [char]27
$script:Colors = @{
Green = "$ESC[0;32m"
Blue = "$ESC[0;34m"
Cyan = "$ESC[0;36m"
Yellow = "$ESC[0;33m"
Purple = "$ESC[0;35m"
PurpleBold = "$ESC[1;35m"
Red = "$ESC[0;31m"
Gray = "$ESC[0;90m"
White = "$ESC[0;37m"
NC = "$ESC[0m" # No Color / Reset
}
# ============================================================================
# Icon Definitions
# ============================================================================
$script:Icons = @{
Confirm = [char]0x25CE # ◎
Admin = [char]0x2699 # ⚙
Success = [char]0x2713 # ✓
Error = [char]0x263B # ☻
Warning = [char]0x25CF # ●
Empty = [char]0x25CB # ○
Solid = [char]0x25CF # ●
List = [char]0x2022 # •
Arrow = [char]0x27A4 # ➤
DryRun = [char]0x2192 # →
NavUp = [char]0x2191 # ↑
NavDown = [char]0x2193 # ↓
Folder = [char]0x25A0 # ■ (folder substitute)
File = [char]0x25A1 # □ (file substitute)
Trash = [char]0x2718 # ✘ (trash substitute)
}
# ============================================================================
# Global Configuration Constants
# ============================================================================
$script:Config = @{
TempFileAgeDays = 7 # Temp file retention (days)
OrphanAgeDays = 60 # Orphaned data retention (days)
MaxParallelJobs = 15 # Parallel job limit
LogAgeDays = 7 # Log retention (days)
CrashReportAgeDays = 7 # Crash report retention (days)
MaxIterations = 100 # Max iterations for scans
ConfigPath = "$env:USERPROFILE\.config\mole"
CachePath = "$env:USERPROFILE\.cache\mole"
WhitelistFile = "$env:USERPROFILE\.config\mole\whitelist.txt"
}
# ============================================================================
# Default Whitelist Patterns (paths to never clean)
# ============================================================================
$script:DefaultWhitelistPatterns = @(
"$env:LOCALAPPDATA\Microsoft\Windows\Explorer" # Windows Explorer cache
"$env:LOCALAPPDATA\Microsoft\Windows\Fonts" # User fonts
"$env:APPDATA\Microsoft\Windows\Recent" # Recent files (used by shell)
"$env:LOCALAPPDATA\Packages\*" # UWP app data
"$env:USERPROFILE\.vscode\extensions" # VS Code extensions
"$env:USERPROFILE\.nuget" # NuGet packages
"$env:USERPROFILE\.cargo" # Rust packages
"$env:USERPROFILE\.rustup" # Rust toolchain
"$env:USERPROFILE\.m2\repository" # Maven repository
"$env:USERPROFILE\.gradle\caches\modules-2\files-*" # Gradle modules
"$env:USERPROFILE\.ollama\models" # Ollama AI models
"$env:LOCALAPPDATA\JetBrains" # JetBrains IDEs
)
# ============================================================================
# Protected System Paths (NEVER touch these)
# ============================================================================
$script:ProtectedPaths = @(
"C:\Windows"
"C:\Windows\System32"
"C:\Windows\SysWOW64"
"C:\Program Files"
"C:\Program Files (x86)"
"C:\Program Files\Windows Defender"
"C:\Program Files (x86)\Windows Defender"
"C:\ProgramData\Microsoft\Windows Defender"
"$env:SYSTEMROOT"
"$env:WINDIR"
)
# ============================================================================
# System Utilities
# ============================================================================
function Test-IsAdmin {
<#
.SYNOPSIS
Check if running with administrator privileges
#>
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = New-Object Security.Principal.WindowsPrincipal($identity)
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}
function Get-FreeSpace {
<#
.SYNOPSIS
Get free disk space on system drive
.OUTPUTS
Human-readable string (e.g., "100GB")
#>
param([string]$Drive = $env:SystemDrive)
$disk = Get-WmiObject Win32_LogicalDisk -Filter "DeviceID='$Drive'" -ErrorAction SilentlyContinue
if ($disk) {
return Format-ByteSize -Bytes $disk.FreeSpace
}
return "Unknown"
}
function Get-WindowsVersion {
<#
.SYNOPSIS
Get Windows version information
#>
$os = Get-WmiObject Win32_OperatingSystem
return @{
Name = $os.Caption
Version = $os.Version
Build = $os.BuildNumber
Arch = $os.OSArchitecture
}
}
function Get-CPUCores {
<#
.SYNOPSIS
Get number of CPU cores
#>
return (Get-WmiObject Win32_Processor).NumberOfLogicalProcessors
}
function Get-OptimalParallelJobs {
<#
.SYNOPSIS
Get optimal number of parallel jobs based on CPU cores
#>
param(
[ValidateSet('scan', 'io', 'compute', 'default')]
[string]$OperationType = 'default'
)
$cores = Get-CPUCores
switch ($OperationType) {
'scan' { return [Math]::Min($cores * 2, 32) }
'io' { return [Math]::Min($cores * 2, 32) }
'compute' { return $cores }
default { return [Math]::Min($cores + 2, 20) }
}
}
# ============================================================================
# Path Utilities
# ============================================================================
function Test-ProtectedPath {
<#
.SYNOPSIS
Check if a path is protected and should never be modified
#>
param([string]$Path)
$normalizedPath = [System.IO.Path]::GetFullPath($Path).TrimEnd('\')
foreach ($protected in $script:ProtectedPaths) {
$normalizedProtected = [System.IO.Path]::GetFullPath($protected).TrimEnd('\')
if ($normalizedPath -eq $normalizedProtected -or
$normalizedPath.StartsWith("$normalizedProtected\", [StringComparison]::OrdinalIgnoreCase)) {
return $true
}
}
return $false
}
function Test-Whitelisted {
<#
.SYNOPSIS
Check if path matches a whitelist pattern
#>
param([string]$Path)
# Check default patterns
foreach ($pattern in $script:DefaultWhitelistPatterns) {
$expandedPattern = [Environment]::ExpandEnvironmentVariables($pattern)
if ($Path -like $expandedPattern) {
return $true
}
}
# Check user whitelist file
if (Test-Path $script:Config.WhitelistFile) {
$userPatterns = Get-Content $script:Config.WhitelistFile -ErrorAction SilentlyContinue
foreach ($pattern in $userPatterns) {
$pattern = $pattern.Trim()
if ($pattern -and -not $pattern.StartsWith('#')) {
if ($Path -like $pattern) {
return $true
}
}
}
}
return $false
}
function Resolve-SafePath {
<#
.SYNOPSIS
Resolve and validate a path for safe operations
#>
param([string]$Path)
try {
$resolved = [System.IO.Path]::GetFullPath($Path)
return $resolved
}
catch {
return $null
}
}
# ============================================================================
# Formatting Utilities
# ============================================================================
function Format-ByteSize {
<#
.SYNOPSIS
Convert bytes to human-readable format
#>
param([long]$Bytes)
if ($Bytes -ge 1TB) {
return "{0:N2}TB" -f ($Bytes / 1TB)
}
elseif ($Bytes -ge 1GB) {
return "{0:N2}GB" -f ($Bytes / 1GB)
}
elseif ($Bytes -ge 1MB) {
return "{0:N1}MB" -f ($Bytes / 1MB)
}
elseif ($Bytes -ge 1KB) {
return "{0:N0}KB" -f ($Bytes / 1KB)
}
else {
return "{0}B" -f $Bytes
}
}
function Format-Number {
<#
.SYNOPSIS
Format a number with thousands separators
#>
param([long]$Number)
return $Number.ToString("N0")
}
function Format-TimeSpan {
<#
.SYNOPSIS
Format a timespan to human-readable string
#>
param([TimeSpan]$Duration)
if ($Duration.TotalHours -ge 1) {
return "{0:N1}h" -f $Duration.TotalHours
}
elseif ($Duration.TotalMinutes -ge 1) {
return "{0:N0}m" -f $Duration.TotalMinutes
}
else {
return "{0:N0}s" -f $Duration.TotalSeconds
}
}
# ============================================================================
# Environment Detection
# ============================================================================
function Get-UserHome {
<#
.SYNOPSIS
Get the current user's home directory
#>
return $env:USERPROFILE
}
function Get-TempPath {
<#
.SYNOPSIS
Get the system temp path
#>
return [System.IO.Path]::GetTempPath()
}
function Get-ConfigPath {
<#
.SYNOPSIS
Get Mole config directory, creating it if needed
#>
$path = $script:Config.ConfigPath
if (-not (Test-Path $path)) {
New-Item -ItemType Directory -Path $path -Force | Out-Null
}
return $path
}
function Get-CachePath {
<#
.SYNOPSIS
Get Mole cache directory, creating it if needed
#>
$path = $script:Config.CachePath
if (-not (Test-Path $path)) {
New-Item -ItemType Directory -Path $path -Force | Out-Null
}
return $path
}
# ============================================================================
# Temporary File Management
# ============================================================================
$script:TempFiles = [System.Collections.ArrayList]::new()
$script:TempDirs = [System.Collections.ArrayList]::new()
function New-TempFile {
<#
.SYNOPSIS
Create a tracked temporary file
#>
param([string]$Prefix = "winmole")
$tempPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), "$Prefix-$([Guid]::NewGuid().ToString('N').Substring(0,8)).tmp")
New-Item -ItemType File -Path $tempPath -Force | Out-Null
[void]$script:TempFiles.Add($tempPath)
return $tempPath
}
function New-TempDirectory {
<#
.SYNOPSIS
Create a tracked temporary directory
#>
param([string]$Prefix = "winmole")
$tempPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), "$Prefix-$([Guid]::NewGuid().ToString('N').Substring(0,8))")
New-Item -ItemType Directory -Path $tempPath -Force | Out-Null
[void]$script:TempDirs.Add($tempPath)
return $tempPath
}
function Clear-TempFiles {
<#
.SYNOPSIS
Clean up all tracked temporary files and directories
#>
foreach ($file in $script:TempFiles) {
if (Test-Path $file) {
Remove-Item $file -Force -ErrorAction SilentlyContinue
}
}
$script:TempFiles.Clear()
foreach ($dir in $script:TempDirs) {
if (Test-Path $dir) {
Remove-Item $dir -Recurse -Force -ErrorAction SilentlyContinue
}
}
$script:TempDirs.Clear()
}
# ============================================================================
# Exports (functions and variables are available via dot-sourcing)
# ============================================================================
# Variables: Colors, Icons, Config, ProtectedPaths, DefaultWhitelistPatterns
# Functions: Test-IsAdmin, Get-FreeSpace, Get-WindowsVersion, etc.

View File

@@ -1,130 +0,0 @@
# Mole - Common Functions Library
# Main entry point that loads all core modules
#Requires -Version 5.1
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
}
$script:MOLE_COMMON_LOADED = $true
# Get the directory containing this script
$script:MOLE_CORE_DIR = Split-Path -Parent $MyInvocation.MyCommand.Path
$script:MOLE_LIB_DIR = Split-Path -Parent $script:MOLE_CORE_DIR
$script:MOLE_ROOT_DIR = Split-Path -Parent $script:MOLE_LIB_DIR
# ============================================================================
# Load Core Modules
# ============================================================================
# Base definitions (colors, icons, constants)
. "$script:MOLE_CORE_DIR\base.ps1"
# Logging functions
. "$script:MOLE_CORE_DIR\log.ps1"
# Safe file operations
. "$script:MOLE_CORE_DIR\file_ops.ps1"
# UI components
. "$script:MOLE_CORE_DIR\ui.ps1"
# ============================================================================
# Version Information
# ============================================================================
$script:MOLE_VERSION = "1.0.0"
$script:MOLE_BUILD_DATE = "2026-01-07"
function Get-MoleVersion {
<#
.SYNOPSIS
Get Mole version information
#>
return @{
Version = $script:MOLE_VERSION
BuildDate = $script:MOLE_BUILD_DATE
PowerShell = $PSVersionTable.PSVersion.ToString()
Windows = (Get-WindowsVersion).Version
}
}
# ============================================================================
# Initialization
# ============================================================================
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"
}
# ============================================================================
# Admin Elevation
# ============================================================================
function Request-AdminPrivileges {
<#
.SYNOPSIS
Request admin privileges if not already running as admin
.DESCRIPTION
Restarts the script with elevated privileges using UAC
#>
if (-not (Test-IsAdmin)) {
Write-MoleWarning "Some operations require administrator privileges."
if (Read-Confirmation -Prompt "Restart with admin privileges?" -Default $true) {
$scriptPath = $MyInvocation.PSCommandPath
if ($scriptPath) {
Start-Process powershell.exe -ArgumentList "-ExecutionPolicy Bypass -File `"$scriptPath`"" -Verb RunAs
exit
}
}
return $false
}
return $true
}
function Invoke-AsAdmin {
<#
.SYNOPSIS
Run a script block with admin privileges
#>
param(
[Parameter(Mandatory)]
[scriptblock]$ScriptBlock
)
if (Test-IsAdmin) {
& $ScriptBlock
}
else {
$command = $ScriptBlock.ToString()
Start-Process powershell.exe -ArgumentList "-ExecutionPolicy Bypass -Command `"$command`"" -Verb RunAs -Wait
}
}
# ============================================================================
# Exports (functions are available via dot-sourcing)
# ============================================================================
# All functions from base.ps1, log.ps1, file_ops.ps1, and ui.ps1 are
# automatically available when this file is dot-sourced.

View File

@@ -1,439 +0,0 @@
# Mole - Safe File Operations Module
# Provides safe file deletion and manipulation functions with protection checks
#Requires -Version 5.1
Set-StrictMode -Version Latest
# Prevent multiple sourcing
if ((Get-Variable -Name 'MOLE_FILEOPS_LOADED' -Scope Script -ErrorAction SilentlyContinue) -and $script:MOLE_FILEOPS_LOADED) { return }
$script:MOLE_FILEOPS_LOADED = $true
# Import dependencies
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
. "$scriptDir\base.ps1"
. "$scriptDir\log.ps1"
# ============================================================================
# Global State
# ============================================================================
$script:MoleDryRunMode = $env:MOLE_DRY_RUN -eq "1"
$script:TotalSizeCleaned = 0
$script:FilesCleaned = 0
$script:TotalItems = 0
# ============================================================================
# Safety Validation Functions
# ============================================================================
function Test-SafePath {
<#
.SYNOPSIS
Validate that a path is safe to operate on
.DESCRIPTION
Checks against protected paths and whitelist
.OUTPUTS
$true if safe, $false if protected
#>
param(
[Parameter(Mandatory)]
[string]$Path
)
# Must have a path
if ([string]::IsNullOrWhiteSpace($Path)) {
Write-Debug "Empty path rejected"
return $false
}
# Resolve to full path
$fullPath = Resolve-SafePath -Path $Path
if (-not $fullPath) {
Write-Debug "Could not resolve path: $Path"
return $false
}
# Check protected paths
if (Test-ProtectedPath -Path $fullPath) {
Write-Debug "Protected path rejected: $fullPath"
return $false
}
# Check whitelist
if (Test-Whitelisted -Path $fullPath) {
Write-Debug "Whitelisted path rejected: $fullPath"
return $false
}
return $true
}
function Get-PathSize {
<#
.SYNOPSIS
Get the size of a file or directory in bytes
#>
param(
[Parameter(Mandatory)]
[string]$Path
)
if (-not (Test-Path $Path)) {
return 0
}
try {
if (Test-Path $Path -PathType Container) {
$size = (Get-ChildItem -Path $Path -Recurse -Force -ErrorAction SilentlyContinue |
Measure-Object -Property Length -Sum -ErrorAction SilentlyContinue).Sum
if ($null -eq $size) { return 0 }
return [long]$size
}
else {
return (Get-Item $Path -Force -ErrorAction SilentlyContinue).Length
}
}
catch {
return 0
}
}
function Get-PathSizeKB {
<#
.SYNOPSIS
Get the size of a file or directory in kilobytes
#>
param([string]$Path)
$bytes = Get-PathSize -Path $Path
return [Math]::Ceiling($bytes / 1024)
}
# ============================================================================
# Safe Removal Functions
# ============================================================================
function Remove-SafeItem {
<#
.SYNOPSIS
Safely remove a file or directory with all protection checks
.DESCRIPTION
This is the main safe deletion function. It:
- Validates the path is not protected
- Checks against whitelist
- Supports dry-run mode
- Tracks cleanup statistics
#>
param(
[Parameter(Mandatory)]
[string]$Path,
[string]$Description = "",
[switch]$Force,
[switch]$Recurse
)
# Validate path safety
if (-not (Test-SafePath -Path $Path)) {
Write-Debug "Skipping protected/whitelisted path: $Path"
return $false
}
# Check if path exists
if (-not (Test-Path $Path)) {
Write-Debug "Path does not exist: $Path"
return $false
}
# Get size before removal
$size = Get-PathSize -Path $Path
$sizeKB = [Math]::Ceiling($size / 1024)
$sizeHuman = Format-ByteSize -Bytes $size
# Handle dry run
if ($script:MoleDryRunMode) {
$name = if ($Description) { $Description } else { Split-Path -Leaf $Path }
Write-DryRun "$name $($script:Colors.Yellow)($sizeHuman dry)$($script:Colors.NC)"
Set-SectionActivity
return $true
}
# Perform removal
try {
$isDirectory = Test-Path $Path -PathType Container
if ($isDirectory) {
Remove-Item -Path $Path -Recurse -Force -ErrorAction Stop
}
else {
Remove-Item -Path $Path -Force -ErrorAction Stop
}
# Update statistics
$script:TotalSizeCleaned += $sizeKB
$script:FilesCleaned++
$script:TotalItems++
# Log success
$name = if ($Description) { $Description } else { Split-Path -Leaf $Path }
Write-Success "$name $($script:Colors.Green)($sizeHuman)$($script:Colors.NC)"
Set-SectionActivity
return $true
}
catch {
Write-Debug "Failed to remove $Path : $_"
return $false
}
}
function Remove-SafeItems {
<#
.SYNOPSIS
Safely remove multiple items with a collective description
#>
param(
[Parameter(Mandatory)]
[string[]]$Paths,
[string]$Description = "Items"
)
$totalSize = 0
$removedCount = 0
$failedCount = 0
foreach ($path in $Paths) {
if (-not (Test-SafePath -Path $path)) {
continue
}
if (-not (Test-Path $path)) {
continue
}
$size = Get-PathSize -Path $path
if ($script:MoleDryRunMode) {
$totalSize += $size
$removedCount++
continue
}
try {
$isDirectory = Test-Path $path -PathType Container
if ($isDirectory) {
Remove-Item -Path $path -Recurse -Force -ErrorAction Stop
}
else {
Remove-Item -Path $path -Force -ErrorAction Stop
}
$totalSize += $size
$removedCount++
}
catch {
$failedCount++
Write-Debug "Failed to remove: $path - $_"
}
}
if ($removedCount -gt 0) {
$sizeKB = [Math]::Ceiling($totalSize / 1024)
$sizeHuman = Format-ByteSize -Bytes $totalSize
if ($script:MoleDryRunMode) {
Write-DryRun "$Description $($script:Colors.Yellow)($removedCount items, $sizeHuman dry)$($script:Colors.NC)"
}
else {
$script:TotalSizeCleaned += $sizeKB
$script:FilesCleaned += $removedCount
$script:TotalItems++
Write-Success "$Description $($script:Colors.Green)($removedCount items, $sizeHuman)$($script:Colors.NC)"
}
Set-SectionActivity
}
return @{
Removed = $removedCount
Failed = $failedCount
Size = $totalSize
}
}
# ============================================================================
# Pattern-Based Cleanup Functions
# ============================================================================
function Remove-OldFiles {
<#
.SYNOPSIS
Remove files older than specified days
#>
param(
[Parameter(Mandatory)]
[string]$Path,
[int]$DaysOld = 7,
[string]$Filter = "*",
[string]$Description = "Old files"
)
if (-not (Test-Path $Path)) {
return @{ Removed = 0; Size = 0 }
}
$cutoffDate = (Get-Date).AddDays(-$DaysOld)
$oldFiles = Get-ChildItem -Path $Path -Filter $Filter -File -Force -ErrorAction SilentlyContinue |
Where-Object { $_.LastWriteTime -lt $cutoffDate }
if ($oldFiles) {
$paths = $oldFiles | ForEach-Object { $_.FullName }
return Remove-SafeItems -Paths $paths -Description "$Description (>${DaysOld}d old)"
}
return @{ Removed = 0; Size = 0 }
}
function Remove-EmptyDirectories {
<#
.SYNOPSIS
Remove empty directories recursively
#>
param(
[Parameter(Mandatory)]
[string]$Path,
[string]$Description = "Empty directories"
)
if (-not (Test-Path $Path)) {
return @{ Removed = 0 }
}
$removedCount = 0
$maxIterations = 5
for ($i = 0; $i -lt $maxIterations; $i++) {
$emptyDirs = Get-ChildItem -Path $Path -Directory -Recurse -Force -ErrorAction SilentlyContinue |
Where-Object {
(Get-ChildItem -Path $_.FullName -Force -ErrorAction SilentlyContinue | Measure-Object).Count -eq 0
}
if (-not $emptyDirs -or $emptyDirs.Count -eq 0) {
break
}
foreach ($dir in $emptyDirs) {
if (Test-SafePath -Path $dir.FullName) {
if (-not $script:MoleDryRunMode) {
try {
Remove-Item -Path $dir.FullName -Force -ErrorAction Stop
$removedCount++
}
catch {
Write-Debug "Could not remove empty dir: $($dir.FullName)"
}
}
else {
$removedCount++
}
}
}
}
if ($removedCount -gt 0) {
if ($script:MoleDryRunMode) {
Write-DryRun "$Description $($script:Colors.Yellow)($removedCount dirs dry)$($script:Colors.NC)"
}
else {
Write-Success "$Description $($script:Colors.Green)($removedCount dirs)$($script:Colors.NC)"
}
Set-SectionActivity
}
return @{ Removed = $removedCount }
}
function Clear-DirectoryContents {
<#
.SYNOPSIS
Clear all contents of a directory but keep the directory itself
#>
param(
[Parameter(Mandatory)]
[string]$Path,
[string]$Description = ""
)
if (-not (Test-Path $Path)) {
return @{ Removed = 0; Size = 0 }
}
if (-not (Test-SafePath -Path $Path)) {
return @{ Removed = 0; Size = 0 }
}
$items = Get-ChildItem -Path $Path -Force -ErrorAction SilentlyContinue
if ($items) {
$paths = $items | ForEach-Object { $_.FullName }
$desc = if ($Description) { $Description } else { Split-Path -Leaf $Path }
return Remove-SafeItems -Paths $paths -Description $desc
}
return @{ Removed = 0; Size = 0 }
}
# ============================================================================
# Statistics Functions
# ============================================================================
function Get-CleanupStats {
<#
.SYNOPSIS
Get current cleanup statistics
#>
return @{
TotalSizeKB = $script:TotalSizeCleaned
TotalSizeHuman = Format-ByteSize -Bytes ($script:TotalSizeCleaned * 1024)
FilesCleaned = $script:FilesCleaned
TotalItems = $script:TotalItems
}
}
function Reset-CleanupStats {
<#
.SYNOPSIS
Reset cleanup statistics
#>
$script:TotalSizeCleaned = 0
$script:FilesCleaned = 0
$script:TotalItems = 0
}
function Set-DryRunMode {
<#
.SYNOPSIS
Enable or disable dry-run mode
#>
param([bool]$Enabled)
$script:MoleDryRunMode = $Enabled
}
function Test-DryRunMode {
<#
.SYNOPSIS
Check if dry-run mode is enabled
#>
return $script:MoleDryRunMode
}
# ============================================================================
# Exports (functions are available via dot-sourcing)
# ============================================================================
# Functions: Test-SafePath, Get-PathSize, Remove-SafeItem, etc.

View File

@@ -1,285 +0,0 @@
# Mole - Logging Module
# Provides consistent logging functions with colors and icons
#Requires -Version 5.1
Set-StrictMode -Version Latest
# Prevent multiple sourcing
if ((Get-Variable -Name 'MOLE_LOG_LOADED' -Scope Script -ErrorAction SilentlyContinue) -and $script:MOLE_LOG_LOADED) { return }
$script:MOLE_LOG_LOADED = $true
# Import base module
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
. "$scriptDir\base.ps1"
# ============================================================================
# Log Configuration
# ============================================================================
$script:LogConfig = @{
DebugEnabled = $env:MOLE_DEBUG -eq "1"
LogFile = $null
Verbose = $false
}
# ============================================================================
# Core Logging Functions
# ============================================================================
function Write-LogMessage {
<#
.SYNOPSIS
Internal function to write formatted log message
#>
param(
[string]$Message,
[string]$Level,
[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
}
}
function Write-Info {
<#
.SYNOPSIS
Write an informational message
#>
param([string]$Message)
Write-LogMessage -Message $Message -Level "INFO" -Color "Cyan" -Icon $script:Icons.List
}
function Write-Success {
<#
.SYNOPSIS
Write a success message
#>
param([string]$Message)
Write-LogMessage -Message $Message -Level "SUCCESS" -Color "Green" -Icon $script:Icons.Success
}
function Write-MoleWarning {
<#
.SYNOPSIS
Write a warning message
#>
param([string]$Message)
Write-LogMessage -Message $Message -Level "WARN" -Color "Yellow" -Icon $script:Icons.Warning
}
function Write-MoleError {
<#
.SYNOPSIS
Write an error message
#>
param([string]$Message)
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
Write-Host " ${gray}[DEBUG] $Message${nc}"
}
}
function Write-DryRun {
<#
.SYNOPSIS
Write a dry-run message (action that would be taken)
#>
param([string]$Message)
Write-LogMessage -Message $Message -Level "DRYRUN" -Color "Yellow" -Icon $script:Icons.DryRun
}
# ============================================================================
# Section Functions (for progress indication)
# ============================================================================
$script:CurrentSection = @{
Active = $false
Activity = $false
Name = ""
}
function Start-Section {
<#
.SYNOPSIS
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}"
}
function Stop-Section {
<#
.SYNOPSIS
End the current section
#>
if ($script:CurrentSection.Active -and -not $script:CurrentSection.Activity) {
Write-Success "Nothing to tidy"
}
$script:CurrentSection.Active = $false
}
function Set-SectionActivity {
<#
.SYNOPSIS
Mark that activity occurred in current section
#>
if ($script:CurrentSection.Active) {
$script:CurrentSection.Activity = $true
}
}
# ============================================================================
# Progress Spinner
# ============================================================================
$script:SpinnerFrames = @('|', '/', '-', '\')
$script:SpinnerIndex = 0
$script:SpinnerJob = $null
function Start-Spinner {
<#
.SYNOPSIS
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}"
}
function Update-Spinner {
<#
.SYNOPSIS
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} "
}
function Stop-Spinner {
<#
.SYNOPSIS
Stop the spinner and clear the line
#>
Write-Host -NoNewline "`r `r"
}
# ============================================================================
# Progress Bar
# ============================================================================
function Write-Progress {
<#
.SYNOPSIS
Write a progress bar
#>
param(
[int]$Current,
[int]$Total,
[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 "
}
function Complete-Progress {
<#
.SYNOPSIS
Clear the progress bar line
#>
Write-Host -NoNewline "`r" + (" " * 80) + "`r"
}
# ============================================================================
# Log File Management
# ============================================================================
function Set-LogFile {
<#
.SYNOPSIS
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)) {
New-Item -ItemType Directory -Path $dir -Force | Out-Null
}
}
function Enable-DebugMode {
<#
.SYNOPSIS
Enable debug logging
#>
$script:LogConfig.DebugEnabled = $true
}
function Disable-DebugMode {
<#
.SYNOPSIS
Disable debug logging
#>
$script:LogConfig.DebugEnabled = $false
}
# ============================================================================
# Exports (functions are available via dot-sourcing)
# ============================================================================
# Functions: Write-Info, Write-Success, Write-Warning, Write-Error, etc.

View File

@@ -1,449 +0,0 @@
# Mole - UI Module
# Provides interactive UI components (menus, confirmations, etc.)
#Requires -Version 5.1
Set-StrictMode -Version Latest
# Prevent multiple sourcing
if ((Get-Variable -Name 'MOLE_UI_LOADED' -Scope Script -ErrorAction SilentlyContinue) -and $script:MOLE_UI_LOADED) { return }
$script:MOLE_UI_LOADED = $true
# Import dependencies
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
. "$scriptDir\base.ps1"
. "$scriptDir\log.ps1"
# ============================================================================
# Terminal Utilities
# ============================================================================
function Get-TerminalSize {
<#
.SYNOPSIS
Get terminal width and height
#>
try {
return @{
Width = $Host.UI.RawUI.WindowSize.Width
Height = $Host.UI.RawUI.WindowSize.Height
}
}
catch {
return @{ Width = 80; Height = 24 }
}
}
function Clear-Line {
<#
.SYNOPSIS
Clear the current line
#>
$width = (Get-TerminalSize).Width
Write-Host -NoNewline ("`r" + (" " * ($width - 1)) + "`r")
}
function Move-CursorUp {
<#
.SYNOPSIS
Move cursor up N lines
#>
param([int]$Lines = 1)
Write-Host -NoNewline "$([char]27)[$Lines`A"
}
function Move-CursorDown {
<#
.SYNOPSIS
Move cursor down N lines
#>
param([int]$Lines = 1)
Write-Host -NoNewline "$([char]27)[$Lines`B"
}
# ============================================================================
# Confirmation Dialogs
# ============================================================================
function Read-Confirmation {
<#
.SYNOPSIS
Ask for yes/no confirmation
#>
param(
[Parameter(Mandatory)]
[string]$Prompt,
[bool]$Default = $false
)
$cyan = $script:Colors.Cyan
$nc = $script:Colors.NC
$hint = if ($Default) { "[Y/n]" } else { "[y/N]" }
Write-Host -NoNewline " ${cyan}$($script:Icons.Confirm)${nc} $Prompt $hint "
$response = Read-Host
if ([string]::IsNullOrWhiteSpace($response)) {
return $Default
}
return $response -match '^[Yy]'
}
function Read-ConfirmationDestructive {
<#
.SYNOPSIS
Ask for confirmation on destructive operations (requires typing 'yes')
#>
param(
[Parameter(Mandatory)]
[string]$Prompt,
[string]$ConfirmText = "yes"
)
$red = $script:Colors.Red
$nc = $script:Colors.NC
Write-Host ""
Write-Host " ${red}$($script:Icons.Warning) WARNING: $Prompt${nc}"
Write-Host " Type '$ConfirmText' to confirm: " -NoNewline
$response = Read-Host
return $response -eq $ConfirmText
}
# ============================================================================
# Menu Components
# ============================================================================
function Show-Menu {
<#
.SYNOPSIS
Display an interactive menu and return selected option
.PARAMETER Title
Menu title
.PARAMETER Options
Array of menu options (hashtables with Name and optionally Description, Action)
.PARAMETER AllowBack
Show back/exit option
#>
param(
[string]$Title = "Menu",
[Parameter(Mandatory)]
[array]$Options,
[switch]$AllowBack
)
$selected = 0
$maxIndex = $Options.Count - 1
# Add back option if allowed
if ($AllowBack) {
$Options = $Options + @{ Name = "Back"; Description = "Return to previous menu" }
$maxIndex++
}
$purple = $script:Colors.PurpleBold
$cyan = $script:Colors.Cyan
$gray = $script:Colors.Gray
$nc = $script:Colors.NC
# Hide cursor
Write-Host -NoNewline "$([char]27)[?25l"
try {
while ($true) {
# Clear screen and show menu
Clear-Host
Write-Host ""
Write-Host " ${purple}$($script:Icons.Arrow) $Title${nc}"
Write-Host ""
for ($i = 0; $i -le $maxIndex; $i++) {
$option = $Options[$i]
$name = if ($option -is [hashtable]) { $option.Name } else { $option.ToString() }
$desc = if ($option -is [hashtable] -and $option.Description) { " - $($option.Description)" } else { "" }
if ($i -eq $selected) {
Write-Host " ${cyan}> $name${nc}${gray}$desc${nc}"
}
else {
Write-Host " $name${gray}$desc${nc}"
}
}
Write-Host ""
Write-Host " ${gray}Use arrows or j/k to navigate, Enter to select, q to quit${nc}"
# Read key - handle both VirtualKeyCode and escape sequences
$key = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
# Debug: uncomment to see key codes
# Write-Host "VKey: $($key.VirtualKeyCode), Char: $([int]$key.Character)"
# Handle escape sequences for arrow keys (some terminals send these)
$moved = $false
if ($key.VirtualKeyCode -eq 0 -or $key.Character -eq [char]27) {
# Escape sequence - read the next characters
if ($Host.UI.RawUI.KeyAvailable) {
$key2 = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
if ($key2.Character -eq '[' -and $Host.UI.RawUI.KeyAvailable) {
$key3 = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
switch ($key3.Character) {
'A' { # Up arrow escape sequence
$selected = if ($selected -gt 0) { $selected - 1 } else { $maxIndex }
$moved = $true
}
'B' { # Down arrow escape sequence
$selected = if ($selected -lt $maxIndex) { $selected + 1 } else { 0 }
$moved = $true
}
}
}
}
}
if (-not $moved) {
switch ($key.VirtualKeyCode) {
38 { # Up arrow
$selected = if ($selected -gt 0) { $selected - 1 } else { $maxIndex }
}
40 { # Down arrow
$selected = if ($selected -lt $maxIndex) { $selected + 1 } else { 0 }
}
13 { # Enter
# Show cursor
Write-Host -NoNewline "$([char]27)[?25h"
if ($AllowBack -and $selected -eq $maxIndex) {
return $null # Back selected
}
return $Options[$selected]
}
default {
switch ($key.Character) {
'k' { $selected = if ($selected -gt 0) { $selected - 1 } else { $maxIndex } }
'j' { $selected = if ($selected -lt $maxIndex) { $selected + 1 } else { 0 } }
'q' {
Write-Host -NoNewline "$([char]27)[?25h"
return $null
}
}
}
}
}
}
}
finally {
# Ensure cursor is shown
Write-Host -NoNewline "$([char]27)[?25h"
}
}
function Show-SelectionList {
<#
.SYNOPSIS
Display a multi-select list
#>
param(
[string]$Title = "Select Items",
[Parameter(Mandatory)]
[array]$Items,
[switch]$MultiSelect
)
$cursor = 0
$selected = @{}
$maxIndex = $Items.Count - 1
$purple = $script:Colors.PurpleBold
$cyan = $script:Colors.Cyan
$green = $script:Colors.Green
$gray = $script:Colors.Gray
$nc = $script:Colors.NC
Write-Host -NoNewline "$([char]27)[?25l"
try {
while ($true) {
Clear-Host
Write-Host ""
Write-Host " ${purple}$($script:Icons.Arrow) $Title${nc}"
if ($MultiSelect) {
Write-Host " ${gray}Space to toggle, Enter to confirm${nc}"
}
Write-Host ""
for ($i = 0; $i -le $maxIndex; $i++) {
$item = $Items[$i]
$name = if ($item -is [hashtable]) { $item.Name } else { $item.ToString() }
$check = if ($selected[$i]) { "$($script:Icons.Success)" } else { "$($script:Icons.Empty)" }
if ($i -eq $cursor) {
Write-Host " ${cyan}> ${check} $name${nc}"
}
else {
$checkColor = if ($selected[$i]) { $green } else { $gray }
Write-Host " ${checkColor}${check}${nc} $name"
}
}
Write-Host ""
Write-Host " ${gray}j/k or arrows to navigate, space to select, Enter to confirm, q to cancel${nc}"
$key = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
# Handle escape sequences for arrow keys (some terminals send these)
$moved = $false
if ($key.VirtualKeyCode -eq 0 -or $key.Character -eq [char]27) {
# Escape sequence - read the next characters
if ($Host.UI.RawUI.KeyAvailable) {
$key2 = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
if ($key2.Character -eq '[' -and $Host.UI.RawUI.KeyAvailable) {
$key3 = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
switch ($key3.Character) {
'A' { # Up arrow escape sequence
$cursor = if ($cursor -gt 0) { $cursor - 1 } else { $maxIndex }
$moved = $true
}
'B' { # Down arrow escape sequence
$cursor = if ($cursor -lt $maxIndex) { $cursor + 1 } else { 0 }
$moved = $true
}
}
}
}
}
if (-not $moved) {
switch ($key.VirtualKeyCode) {
38 { $cursor = if ($cursor -gt 0) { $cursor - 1 } else { $maxIndex } }
40 { $cursor = if ($cursor -lt $maxIndex) { $cursor + 1 } else { 0 } }
32 { # Space
if ($MultiSelect) {
$selected[$cursor] = -not $selected[$cursor]
}
else {
$selected = @{ $cursor = $true }
}
}
13 { # Enter
Write-Host -NoNewline "$([char]27)[?25h"
$result = @()
foreach ($selKey in $selected.Keys) {
if ($selected[$selKey]) {
$result += $Items[$selKey]
}
}
return $result
}
default {
switch ($key.Character) {
'k' { $cursor = if ($cursor -gt 0) { $cursor - 1 } else { $maxIndex } }
'j' { $cursor = if ($cursor -lt $maxIndex) { $cursor + 1 } else { 0 } }
' ' {
if ($MultiSelect) {
$selected[$cursor] = -not $selected[$cursor]
}
else {
$selected = @{ $cursor = $true }
}
}
'q' {
Write-Host -NoNewline "$([char]27)[?25h"
return @()
}
}
}
}
}
}
}
finally {
Write-Host -NoNewline "$([char]27)[?25h"
}
}
# ============================================================================
# Banner / Header
# ============================================================================
function Show-Banner {
<#
.SYNOPSIS
Display the Mole ASCII banner
#>
$purple = $script:Colors.Purple
$cyan = $script:Colors.Cyan
$nc = $script:Colors.NC
Write-Host ""
Write-Host " ${purple}MOLE${nc}"
Write-Host " ${cyan}Windows System Maintenance${nc}"
Write-Host ""
}
function Show-Header {
<#
.SYNOPSIS
Display a section header
#>
param(
[Parameter(Mandatory)]
[string]$Title,
[string]$Subtitle = ""
)
$purple = $script:Colors.PurpleBold
$gray = $script:Colors.Gray
$nc = $script:Colors.NC
Write-Host ""
Write-Host " ${purple}$Title${nc}"
if ($Subtitle) {
Write-Host " ${gray}$Subtitle${nc}"
}
Write-Host ""
}
# ============================================================================
# Summary Display
# ============================================================================
function Show-Summary {
<#
.SYNOPSIS
Display cleanup summary
#>
param(
[long]$SizeBytes = 0,
[int]$ItemCount = 0,
[string]$Action = "Cleaned"
)
$green = $script:Colors.Green
$cyan = $script:Colors.Cyan
$nc = $script:Colors.NC
$sizeHuman = Format-ByteSize -Bytes $SizeBytes
Write-Host ""
Write-Host " $($green)━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━$($nc)"
Write-Host " $($green)$($script:Icons.Success)$($nc) $($Action): $($cyan)$($sizeHuman)$($nc) across $($cyan)$($ItemCount)$($nc) items"
Write-Host " $($green)━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━$($nc)"
Write-Host ""
}
# ============================================================================
# Exports (functions are available via dot-sourcing)
# ============================================================================
# Functions: Show-Menu, Show-Banner, Read-Confirmation, etc.

View File

@@ -1,320 +0,0 @@
#!/usr/bin/env pwsh
# Mole - Windows System Maintenance Toolkit
# Main CLI entry point
#Requires -Version 5.1
param(
[Parameter(Position = 0)]
[string]$Command,
[Parameter(Position = 1, ValueFromRemainingArguments)]
[string[]]$CommandArgs,
[switch]$Version,
[switch]$ShowHelp
)
$ErrorActionPreference = "Stop"
Set-StrictMode -Version Latest
# Get script directory
$script:MOLE_ROOT = Split-Path -Parent $MyInvocation.MyCommand.Path
$script:MOLE_BIN = Join-Path $script:MOLE_ROOT "bin"
$script:MOLE_LIB = Join-Path $script:MOLE_ROOT "lib"
# Import core
. "$script:MOLE_LIB\core\common.ps1"
# ============================================================================
# Version Info
# ============================================================================
$script:MOLE_VER = "1.0.0"
$script:MOLE_BUILD = "2026-01-07"
function Show-Version {
$info = Get-MoleVersion
Write-Host "Mole v$($info.Version)"
Write-Host "Built: $($info.BuildDate)"
Write-Host "PowerShell: $($info.PowerShell)"
Write-Host "Windows: $($info.Windows)"
}
# ============================================================================
# Help
# ============================================================================
function Show-MainHelp {
$cyan = $script:Colors.Cyan
$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 ""
Write-Host " ${green}COMMANDS:${nc}"
Write-Host ""
Write-Host " ${cyan}clean${nc} Deep system cleanup (caches, temp, logs)"
Write-Host " ${cyan}uninstall${nc} Smart application uninstaller"
Write-Host " ${cyan}analyze${nc} Visual disk space analyzer"
Write-Host " ${cyan}status${nc} Real-time system monitor"
Write-Host " ${cyan}optimize${nc} System optimization tasks"
Write-Host " ${cyan}purge${nc} Clean project build artifacts"
Write-Host ""
Write-Host " ${green}OPTIONS:${nc}"
Write-Host ""
Write-Host " ${cyan}-Version${nc} Show version information"
Write-Host " ${cyan}-ShowHelp${nc} Show this help message"
Write-Host ""
Write-Host " ${green}EXAMPLES:${nc}"
Write-Host ""
Write-Host " ${gray}mole${nc} ${gray}# Interactive menu${nc}"
Write-Host " ${gray}mole clean${nc} ${gray}# Deep cleanup${nc}"
Write-Host " ${gray}mole clean -DryRun${nc} ${gray}# Preview cleanup${nc}"
Write-Host " ${gray}mole uninstall${nc} ${gray}# Uninstall apps${nc}"
Write-Host " ${gray}mole analyze${nc} ${gray}# Disk analyzer${nc}"
Write-Host " ${gray}mole status${nc} ${gray}# System monitor${nc}"
Write-Host " ${gray}mole optimize${nc} ${gray}# Optimize system${nc}"
Write-Host " ${gray}mole purge${nc} ${gray}# Clean dev artifacts${nc}"
Write-Host ""
Write-Host " ${green}ENVIRONMENT:${nc}"
Write-Host ""
Write-Host " ${cyan}MOLE_DRY_RUN=1${nc} Preview without changes"
Write-Host " ${cyan}MOLE_DEBUG=1${nc} Enable debug output"
Write-Host ""
Write-Host " ${gray}Run '${nc}mole <command> -ShowHelp${gray}' for command-specific help${nc}"
Write-Host ""
}
# ============================================================================
# Interactive Menu
# ============================================================================
function Show-MainMenu {
$options = @(
@{
Name = "Clean"
Description = "Deep system cleanup"
Command = "clean"
Icon = $script:Icons.Trash
}
@{
Name = "Uninstall"
Description = "Remove applications"
Command = "uninstall"
Icon = $script:Icons.Folder
}
@{
Name = "Analyze"
Description = "Disk space analyzer"
Command = "analyze"
Icon = $script:Icons.File
}
@{
Name = "Status"
Description = "System monitor"
Command = "status"
Icon = $script:Icons.Solid
}
@{
Name = "Optimize"
Description = "System optimization"
Command = "optimize"
Icon = $script:Icons.Arrow
}
@{
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
}
# ============================================================================
# Command Router
# ============================================================================
function Invoke-MoleCommand {
param(
[string]$CommandName,
[string[]]$Arguments
)
$scriptPath = Join-Path $script:MOLE_BIN "$CommandName.ps1"
if (-not (Test-Path $scriptPath)) {
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 }
if ($argCount -gt 0) {
# 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]
$splatParams[$paramName] = $true
}
elseif ($cleanArg -match '^-(\w+)[=:](.+)$') {
# It's a named parameter with value (e.g., -Name=Value or -Name:Value)
$paramName = $Matches[1]
$paramValue = $Matches[2].Trim("'`"")
$splatParams[$paramName] = $paramValue
}
else {
# Positional argument
$positionalArgs += $cleanArg
}
}
# Execute with splatting
if ($positionalArgs.Count -gt 0) {
& $scriptPath @splatParams @positionalArgs
}
else {
& $scriptPath @splatParams
}
}
else {
& $scriptPath
}
}
# ============================================================================
# System Info Display
# ============================================================================
function Show-SystemInfo {
$cyan = $script:Colors.Cyan
$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)"
Write-Host " ${gray}Admin:${nc} $isAdmin"
Write-Host ""
}
# ============================================================================
# Main
# ============================================================================
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) {
'ShowHelp' { $effectiveShowHelp = $true; $effectiveCommand = $null }
'Help' { $effectiveShowHelp = $true; $effectiveCommand = $null }
'h' { $effectiveShowHelp = $true; $effectiveCommand = $null }
'Version' { $effectiveVersion = $true; $effectiveCommand = $null }
'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-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 ""
Write-Host " Goodbye!"
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")
Clear-Host
Show-Banner
Show-SystemInfo
}
}
# Run
try {
Main
}
catch {
Write-Host ""
Write-MoleError "An error occurred: $_"
Write-Host ""
exit 1
}
finally {
Clear-TempFiles
}

View File

@@ -1,174 +0,0 @@
# Mole Windows - Build Script
# Builds Go binaries and validates PowerShell scripts
#Requires -Version 5.1
param(
[switch]$Clean,
[switch]$Release,
[switch]$Validate,
[switch]$ShowHelp
)
$ErrorActionPreference = "Stop"
# Get script directory
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$windowsDir = Split-Path -Parent $scriptDir
$binDir = Join-Path $windowsDir "bin"
function Show-BuildHelp {
Write-Host ""
Write-Host "Mole Windows Build Script" -ForegroundColor Cyan
Write-Host ""
Write-Host "Usage: .\build.ps1 [options]"
Write-Host ""
Write-Host "Options:"
Write-Host " -Clean Clean build artifacts before building"
Write-Host " -Release Build optimized release binaries"
Write-Host " -Validate Validate PowerShell script syntax"
Write-Host " -ShowHelp Show this help message"
Write-Host ""
}
if ($ShowHelp) {
Show-BuildHelp
return
}
Write-Host ""
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " Mole Windows - Build" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
# ============================================================================
# Clean
# ============================================================================
if ($Clean) {
Write-Host "[Clean] Removing build artifacts..." -ForegroundColor Yellow
$artifacts = @(
(Join-Path $binDir "analyze.exe"),
(Join-Path $binDir "status.exe"),
(Join-Path $windowsDir "coverage-go.out"),
(Join-Path $windowsDir "coverage-pester.xml")
)
foreach ($artifact in $artifacts) {
if (Test-Path $artifact) {
Remove-Item $artifact -Force
Write-Host " Removed: $artifact" -ForegroundColor Gray
}
}
Write-Host ""
}
# ============================================================================
# Validate PowerShell Scripts
# ============================================================================
if ($Validate) {
Write-Host "[Validate] Checking PowerShell script syntax..." -ForegroundColor Yellow
$scripts = Get-ChildItem -Path $windowsDir -Filter "*.ps1" -Recurse
$errors = @()
foreach ($script in $scripts) {
try {
$null = [System.Management.Automation.Language.Parser]::ParseFile(
$script.FullName,
[ref]$null,
[ref]$null
)
Write-Host " OK: $($script.Name)" -ForegroundColor Green
}
catch {
Write-Host " ERROR: $($script.Name)" -ForegroundColor Red
$errors += $script.FullName
}
}
if ($errors.Count -gt 0) {
Write-Host ""
Write-Host " $($errors.Count) script(s) have syntax errors!" -ForegroundColor Red
exit 1
}
Write-Host ""
}
# ============================================================================
# Build Go Binaries
# ============================================================================
Write-Host "[Build] Building Go binaries..." -ForegroundColor Yellow
# Check if Go is installed
$goVersion = & go version 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Host " Error: Go is not installed" -ForegroundColor Red
Write-Host " Please install Go from https://golang.org/dl/" -ForegroundColor Gray
exit 1
}
Write-Host " $goVersion" -ForegroundColor Gray
Write-Host ""
# Create bin directory if needed
if (-not (Test-Path $binDir)) {
New-Item -ItemType Directory -Path $binDir -Force | Out-Null
}
Push-Location $windowsDir
try {
# Build flags
$ldflags = ""
if ($Release) {
$ldflags = "-s -w" # Strip debug info for smaller binaries
}
# Build analyze
Write-Host " Building analyze.exe..." -ForegroundColor Gray
if ($Release) {
& go build -ldflags "$ldflags" -o "$binDir\analyze.exe" "./cmd/analyze/"
}
else {
& go build -o "$binDir\analyze.exe" "./cmd/analyze/"
}
if ($LASTEXITCODE -ne 0) {
Write-Host " Failed to build analyze.exe" -ForegroundColor Red
exit 1
}
$analyzeSize = (Get-Item "$binDir\analyze.exe").Length / 1MB
Write-Host " Built: analyze.exe ($([math]::Round($analyzeSize, 2)) MB)" -ForegroundColor Green
# Build status
Write-Host " Building status.exe..." -ForegroundColor Gray
if ($Release) {
& go build -ldflags "$ldflags" -o "$binDir\status.exe" "./cmd/status/"
}
else {
& go build -o "$binDir\status.exe" "./cmd/status/"
}
if ($LASTEXITCODE -ne 0) {
Write-Host " Failed to build status.exe" -ForegroundColor Red
exit 1
}
$statusSize = (Get-Item "$binDir\status.exe").Length / 1MB
Write-Host " Built: status.exe ($([math]::Round($statusSize, 2)) MB)" -ForegroundColor Green
}
finally {
Pop-Location
}
Write-Host ""
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " Build complete!" -ForegroundColor Green
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""

View File

@@ -1,141 +0,0 @@
# Mole Windows - Test Runner Script
# Runs all tests (Pester for PowerShell, go test for Go)
#Requires -Version 5.1
param(
[switch]$Verbose,
[switch]$NoPester,
[switch]$NoGo,
[switch]$Coverage
)
$ErrorActionPreference = "Stop"
$script:ExitCode = 0
# Get script directory
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$windowsDir = Split-Path -Parent $scriptDir
Write-Host ""
Write-Host "========================================" -ForegroundColor Cyan
Write-Host " Mole Windows - Test Suite" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
# ============================================================================
# Pester Tests
# ============================================================================
if (-not $NoPester) {
Write-Host "[Pester] Running PowerShell tests..." -ForegroundColor Yellow
Write-Host ""
# Check if Pester is installed
$pesterModule = Get-Module -ListAvailable -Name Pester | Where-Object { $_.Version -ge "5.0.0" }
if (-not $pesterModule) {
Write-Host " Installing Pester 5.x..." -ForegroundColor Gray
Install-Module -Name Pester -MinimumVersion 5.0.0 -Force -SkipPublisherCheck -Scope CurrentUser
}
Import-Module Pester -MinimumVersion 5.0.0
$testsDir = Join-Path $windowsDir "tests"
$config = New-PesterConfiguration
$config.Run.Path = $testsDir
$config.Run.Exit = $false
$config.Output.Verbosity = if ($Verbose) { "Detailed" } else { "Normal" }
if ($Coverage) {
$config.CodeCoverage.Enabled = $true
$config.CodeCoverage.Path = @(
(Join-Path $windowsDir "lib\core\*.ps1"),
(Join-Path $windowsDir "lib\clean\*.ps1"),
(Join-Path $windowsDir "bin\*.ps1")
)
$config.CodeCoverage.OutputPath = Join-Path $windowsDir "coverage-pester.xml"
}
try {
$result = Invoke-Pester -Configuration $config
Write-Host ""
Write-Host "[Pester] Results:" -ForegroundColor Yellow
Write-Host " Passed: $($result.PassedCount)" -ForegroundColor Green
Write-Host " Failed: $($result.FailedCount)" -ForegroundColor $(if ($result.FailedCount -gt 0) { "Red" } else { "Green" })
Write-Host " Skipped: $($result.SkippedCount)" -ForegroundColor Gray
if ($result.FailedCount -gt 0) {
$script:ExitCode = 1
}
}
catch {
Write-Host " Error running Pester tests: $_" -ForegroundColor Red
$script:ExitCode = 1
}
Write-Host ""
}
# ============================================================================
# Go Tests
# ============================================================================
if (-not $NoGo) {
Write-Host "[Go] Running Go tests..." -ForegroundColor Yellow
Write-Host ""
# Check if Go is installed
$goVersion = & go version 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Host " Go is not installed, skipping Go tests" -ForegroundColor Gray
}
else {
Write-Host " $goVersion" -ForegroundColor Gray
Write-Host ""
Push-Location $windowsDir
try {
$goArgs = @("test")
if ($Verbose) {
$goArgs += "-v"
}
if ($Coverage) {
$goArgs += "-coverprofile=coverage-go.out"
}
$goArgs += "./..."
& go @goArgs
if ($LASTEXITCODE -ne 0) {
$script:ExitCode = 1
}
else {
Write-Host ""
Write-Host "[Go] All tests passed" -ForegroundColor Green
}
}
finally {
Pop-Location
}
}
Write-Host ""
}
# ============================================================================
# Summary
# ============================================================================
Write-Host "========================================" -ForegroundColor Cyan
if ($script:ExitCode -eq 0) {
Write-Host " All tests passed!" -ForegroundColor Green
}
else {
Write-Host " Some tests failed!" -ForegroundColor Red
}
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
exit $script:ExitCode

View File

@@ -1,199 +0,0 @@
# Mole Windows - Cleanup Module Tests
# Pester tests for lib/clean functionality
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 first
. "$script:LibDir\core\base.ps1"
. "$script:LibDir\core\log.ps1"
. "$script:LibDir\core\ui.ps1"
. "$script:LibDir\core\file_ops.ps1"
# Import cleanup modules
. "$script:LibDir\clean\user.ps1"
. "$script:LibDir\clean\caches.ps1"
. "$script:LibDir\clean\dev.ps1"
. "$script:LibDir\clean\apps.ps1"
. "$script:LibDir\clean\system.ps1"
# Enable dry-run mode for all tests
$env:MOLE_DRY_RUN = "1"
Set-DryRunMode -Enabled $true
}
AfterAll {
$env:MOLE_DRY_RUN = $null
Set-DryRunMode -Enabled $false
}
Describe "User Cleanup Module" {
Context "Clear-UserTempFiles" {
It "Should have Clear-UserTempFiles function" {
Get-Command Clear-UserTempFiles -ErrorAction SilentlyContinue | Should -Not -BeNullOrEmpty
}
It "Should run without error in dry-run mode" {
{ Clear-UserTempFiles } | Should -Not -Throw
}
}
Context "Clear-OldDownloads" {
It "Should have Clear-OldDownloads function" {
Get-Command Clear-OldDownloads -ErrorAction SilentlyContinue | Should -Not -BeNullOrEmpty
}
}
Context "Clear-RecycleBin" {
It "Should have Clear-RecycleBin function" {
Get-Command Clear-RecycleBin -ErrorAction SilentlyContinue | Should -Not -BeNullOrEmpty
}
}
Context "Invoke-UserCleanup" {
It "Should have main user cleanup function" {
Get-Command Invoke-UserCleanup -ErrorAction SilentlyContinue | Should -Not -BeNullOrEmpty
}
}
}
Describe "Cache Cleanup Module" {
Context "Browser Cache Functions" {
It "Should have Clear-BrowserCaches function" {
Get-Command Clear-BrowserCaches -ErrorAction SilentlyContinue | Should -Not -BeNullOrEmpty
}
It "Should run browser cache cleanup without error" {
{ Clear-BrowserCaches } | Should -Not -Throw
}
}
Context "Application Cache Functions" {
It "Should have Clear-AppCaches function" {
Get-Command Clear-AppCaches -ErrorAction SilentlyContinue | Should -Not -BeNullOrEmpty
}
}
Context "Windows Update Cache" {
It "Should have Clear-WindowsUpdateCache function" {
Get-Command Clear-WindowsUpdateCache -ErrorAction SilentlyContinue | Should -Not -BeNullOrEmpty
}
}
Context "Invoke-CacheCleanup" {
It "Should have main cache cleanup function" {
Get-Command Invoke-CacheCleanup -ErrorAction SilentlyContinue | Should -Not -BeNullOrEmpty
}
}
}
Describe "Developer Tools Cleanup Module" {
Context "Node.js Cleanup" {
It "Should have npm cache cleanup function" {
Get-Command Clear-NpmCache -ErrorAction SilentlyContinue | Should -Not -BeNullOrEmpty
}
}
Context "Python Cleanup" {
It "Should have Python cache cleanup function" {
Get-Command Clear-PythonCaches -ErrorAction SilentlyContinue | Should -Not -BeNullOrEmpty
}
}
Context "Go Cleanup" {
It "Should have Go cache cleanup function" {
Get-Command Clear-GoCaches -ErrorAction SilentlyContinue | Should -Not -BeNullOrEmpty
}
}
Context "Rust Cleanup" {
It "Should have Rust cache cleanup function" {
Get-Command Clear-RustCaches -ErrorAction SilentlyContinue | Should -Not -BeNullOrEmpty
}
}
Context "Docker Cleanup" {
It "Should have Docker cache cleanup function" {
Get-Command Clear-DockerCaches -ErrorAction SilentlyContinue | Should -Not -BeNullOrEmpty
}
}
Context "Invoke-DevToolsCleanup" {
It "Should have main dev tools cleanup function" {
Get-Command Invoke-DevToolsCleanup -ErrorAction SilentlyContinue | Should -Not -BeNullOrEmpty
}
It "Should run without error in dry-run mode" {
{ Invoke-DevToolsCleanup } | Should -Not -Throw
}
}
}
Describe "Apps Cleanup Module" {
Context "Orphan Detection" {
It "Should have Find-OrphanedAppData function" {
Get-Command Find-OrphanedAppData -ErrorAction SilentlyContinue | Should -Not -BeNullOrEmpty
}
It "Should have Clear-OrphanedAppData function" {
Get-Command Clear-OrphanedAppData -ErrorAction SilentlyContinue | Should -Not -BeNullOrEmpty
}
}
Context "Specific App Cleanup" {
It "Should have Clear-OfficeCache function" {
Get-Command Clear-OfficeCache -ErrorAction SilentlyContinue | Should -Not -BeNullOrEmpty
}
It "Should have Clear-AdobeData function" {
Get-Command Clear-AdobeData -ErrorAction SilentlyContinue | Should -Not -BeNullOrEmpty
}
}
Context "Invoke-AppCleanup" {
It "Should have main app cleanup function" {
Get-Command Invoke-AppCleanup -ErrorAction SilentlyContinue | Should -Not -BeNullOrEmpty
}
}
}
Describe "System Cleanup Module" {
Context "System Temp" {
It "Should have Clear-SystemTempFiles function" {
Get-Command Clear-SystemTempFiles -ErrorAction SilentlyContinue | Should -Not -BeNullOrEmpty
}
}
Context "Windows Logs" {
It "Should have Clear-WindowsLogs function" {
Get-Command Clear-WindowsLogs -ErrorAction SilentlyContinue | Should -Not -BeNullOrEmpty
}
}
Context "Windows Update Cleanup" {
It "Should have Clear-WindowsUpdateFiles function" {
Get-Command Clear-WindowsUpdateFiles -ErrorAction SilentlyContinue | Should -Not -BeNullOrEmpty
}
}
Context "Memory Dumps" {
It "Should have Clear-MemoryDumps function" {
Get-Command Clear-MemoryDumps -ErrorAction SilentlyContinue | Should -Not -BeNullOrEmpty
}
}
Context "Admin Requirements" {
It "Should check for admin when needed" {
# System cleanup should handle non-admin gracefully
{ Clear-SystemTempFiles } | Should -Not -Throw
}
}
Context "Invoke-SystemCleanup" {
It "Should have main system cleanup function" {
Get-Command Invoke-SystemCleanup -ErrorAction SilentlyContinue | Should -Not -BeNullOrEmpty
}
}
}

View File

@@ -1,140 +0,0 @@
# Mole Windows - Command Tests
# Pester tests for bin/ command scripts
BeforeAll {
# Get the windows directory path (tests are in windows/tests/)
$script:WindowsDir = Split-Path -Parent $PSScriptRoot
$script:BinDir = Join-Path $script:WindowsDir "bin"
}
Describe "Clean Command" {
Context "Help Display" {
It "Should show help without error" {
$result = & powershell -ExecutionPolicy Bypass -File "$script:BinDir\clean.ps1" -ShowHelp 2>&1
$result | Should -Not -BeNullOrEmpty
$LASTEXITCODE | Should -Be 0
}
It "Should mention dry-run in help" {
$result = & powershell -ExecutionPolicy Bypass -File "$script:BinDir\clean.ps1" -ShowHelp 2>&1
$result -join "`n" | Should -Match "DryRun"
}
}
Context "Dry Run Mode" {
It "Should support -DryRun parameter" {
# Just verify it starts without immediate error
$job = Start-Job -ScriptBlock {
param($binDir)
& powershell -ExecutionPolicy Bypass -File "$binDir\clean.ps1" -DryRun 2>&1
} -ArgumentList $script:BinDir
Start-Sleep -Seconds 3
Stop-Job $job -ErrorAction SilentlyContinue
Remove-Job $job -Force -ErrorAction SilentlyContinue
# If we got here without exception, test passes
$true | Should -Be $true
}
}
}
Describe "Uninstall Command" {
Context "Help Display" {
It "Should show help without error" {
$result = & powershell -ExecutionPolicy Bypass -File "$script:BinDir\uninstall.ps1" -ShowHelp 2>&1
$result | Should -Not -BeNullOrEmpty
$LASTEXITCODE | Should -Be 0
}
}
}
Describe "Optimize Command" {
Context "Help Display" {
It "Should show help without error" {
$result = & powershell -ExecutionPolicy Bypass -File "$script:BinDir\optimize.ps1" -ShowHelp 2>&1
$result | Should -Not -BeNullOrEmpty
$LASTEXITCODE | Should -Be 0
}
It "Should mention optimization options in help" {
$result = & powershell -ExecutionPolicy Bypass -File "$script:BinDir\optimize.ps1" -ShowHelp 2>&1
$result -join "`n" | Should -Match "DryRun|Disk|DNS"
}
}
}
Describe "Purge Command" {
Context "Help Display" {
It "Should show help without error" {
$result = & powershell -ExecutionPolicy Bypass -File "$script:BinDir\purge.ps1" -ShowHelp 2>&1
$result | Should -Not -BeNullOrEmpty
$LASTEXITCODE | Should -Be 0
}
It "Should list artifact types in help" {
$result = & powershell -ExecutionPolicy Bypass -File "$script:BinDir\purge.ps1" -ShowHelp 2>&1
$result -join "`n" | Should -Match "node_modules|vendor|venv"
}
}
}
Describe "Analyze Command" {
Context "Help Display" {
It "Should show help without error" {
$result = & powershell -ExecutionPolicy Bypass -File "$script:BinDir\analyze.ps1" -ShowHelp 2>&1
$result | Should -Not -BeNullOrEmpty
$LASTEXITCODE | Should -Be 0
}
It "Should mention keybindings in help" {
$result = & powershell -ExecutionPolicy Bypass -File "$script:BinDir\analyze.ps1" -ShowHelp 2>&1
$result -join "`n" | Should -Match "Navigate|Enter|Quit"
}
}
}
Describe "Status Command" {
Context "Help Display" {
It "Should show help without error" {
$result = & powershell -ExecutionPolicy Bypass -File "$script:BinDir\status.ps1" -ShowHelp 2>&1
$result | Should -Not -BeNullOrEmpty
$LASTEXITCODE | Should -Be 0
}
It "Should mention system metrics in help" {
$result = & powershell -ExecutionPolicy Bypass -File "$script:BinDir\status.ps1" -ShowHelp 2>&1
$result -join "`n" | Should -Match "CPU|Memory|Disk|health"
}
}
}
Describe "Main Entry Point" {
Context "mole.ps1" {
BeforeAll {
$script:MolePath = Join-Path $script:WindowsDir "mole.ps1"
}
It "Should show help without error" {
$result = & powershell -ExecutionPolicy Bypass -File $script:MolePath -ShowHelp 2>&1
$result | Should -Not -BeNullOrEmpty
}
It "Should show version without error" {
$result = & powershell -ExecutionPolicy Bypass -File $script:MolePath -Version 2>&1
$result | Should -Not -BeNullOrEmpty
$result -join "`n" | Should -Match "Mole|v\d+\.\d+"
}
It "Should list available commands in help" {
$result = & powershell -ExecutionPolicy Bypass -File $script:MolePath -ShowHelp 2>&1
$helpText = $result -join "`n"
$helpText | Should -Match "clean"
$helpText | Should -Match "uninstall"
$helpText | Should -Match "optimize"
$helpText | Should -Match "purge"
$helpText | Should -Match "analyze"
$helpText | Should -Match "status"
}
}
}

View File

@@ -1,242 +0,0 @@
# Mole Windows - Core Module Tests
# Pester tests for lib/core functionality
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"
. "$script:LibDir\core\ui.ps1"
. "$script:LibDir\core\file_ops.ps1"
}
Describe "Base Module" {
Context "Color Definitions" {
It "Should define color codes" {
$script:Colors | Should -Not -BeNullOrEmpty
$script:Colors.Cyan | Should -Not -BeNullOrEmpty
$script:Colors.Green | Should -Not -BeNullOrEmpty
$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
$script:Icons.Error | Should -Not -BeNullOrEmpty
$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
$result | Should -Not -BeNullOrEmpty
$result.Name | Should -Not -BeNullOrEmpty
$result.Version | Should -Not -BeNullOrEmpty
$result.Build | Should -Not -BeNullOrEmpty
}
}
Context "Get-FreeSpace" {
It "Should return free space string" {
$result = Get-FreeSpace
$result | Should -Not -BeNullOrEmpty
# 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
}
}
}
Describe "File Operations Module" {
BeforeAll {
# Create temp test directory
$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
Format-ByteSize -Bytes 0 | Should -Be "0B"
Format-ByteSize -Bytes 1024 | Should -Be "1KB"
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 {
# Reset the module's DryRun state
Set-DryRunMode -Enabled $true
$result = Remove-SafeItem -Path $script:TestFile
$result | Should -Be $true
Test-Path $script:TestFile | Should -Be $true # File should still exist
}
finally {
$env:MOLE_DRY_RUN = $null
Set-DryRunMode -Enabled $false
}
}
It "Should not remove protected paths" {
$result = Remove-SafeItem -Path "C:\Windows\System32"
$result | Should -Be $false
}
}
}
Describe "Logging Module" {
Context "Write-Log Functions" {
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-MoleWarning function" {
# Note: The actual function is Write-MoleWarning
{ Write-MoleWarning "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
{ Stop-Section } | Should -Not -Throw
}
}
}
Describe "UI Module" {
Context "Show-Banner" {
It "Should display banner without error" {
{ 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
}
}
}