diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml deleted file mode 100644 index 69484db..0000000 --- a/.github/workflows/windows.yml +++ /dev/null @@ -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 - } diff --git a/windows/.gitignore b/windows/.gitignore deleted file mode 100644 index 0a24ad3..0000000 --- a/windows/.gitignore +++ /dev/null @@ -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 diff --git a/windows/Makefile b/windows/Makefile deleted file mode 100644 index b2904ba..0000000 --- a/windows/Makefile +++ /dev/null @@ -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 ./... diff --git a/windows/README.md b/windows/README.md deleted file mode 100644 index 044fdcb..0000000 --- a/windows/README.md +++ /dev/null @@ -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. diff --git a/windows/bin/analyze.ps1 b/windows/bin/analyze.ps1 deleted file mode 100644 index a792172..0000000 --- a/windows/bin/analyze.ps1 +++ /dev/null @@ -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 diff --git a/windows/bin/clean.ps1 b/windows/bin/clean.ps1 deleted file mode 100644 index ce66194..0000000 --- a/windows/bin/clean.ps1 +++ /dev/null @@ -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 diff --git a/windows/bin/optimize.ps1 b/windows/bin/optimize.ps1 deleted file mode 100644 index 5973ee9..0000000 --- a/windows/bin/optimize.ps1 +++ /dev/null @@ -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 diff --git a/windows/bin/purge.ps1 b/windows/bin/purge.ps1 deleted file mode 100644 index 2d1f4ba..0000000 --- a/windows/bin/purge.ps1 +++ /dev/null @@ -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 diff --git a/windows/bin/status.ps1 b/windows/bin/status.ps1 deleted file mode 100644 index ed3e3c1..0000000 --- a/windows/bin/status.ps1 +++ /dev/null @@ -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 diff --git a/windows/bin/uninstall.ps1 b/windows/bin/uninstall.ps1 deleted file mode 100644 index 5c4ca67..0000000 --- a/windows/bin/uninstall.ps1 +++ /dev/null @@ -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 diff --git a/windows/cmd/analyze/analyze.exe b/windows/cmd/analyze/analyze.exe deleted file mode 100644 index 88661c1..0000000 Binary files a/windows/cmd/analyze/analyze.exe and /dev/null differ diff --git a/windows/cmd/analyze/main.go b/windows/cmd/analyze/main.go deleted file mode 100644 index 60cc9bc..0000000 --- a/windows/cmd/analyze/main.go +++ /dev/null @@ -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) - } -} diff --git a/windows/cmd/analyze/main_test.go b/windows/cmd/analyze/main_test.go deleted file mode 100644 index e62b5a0..0000000 --- a/windows/cmd/analyze/main_test.go +++ /dev/null @@ -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 -} diff --git a/windows/cmd/status/main.go b/windows/cmd/status/main.go deleted file mode 100644 index 39afa4f..0000000 --- a/windows/cmd/status/main.go +++ /dev/null @@ -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) - } -} diff --git a/windows/cmd/status/main_test.go b/windows/cmd/status/main_test.go deleted file mode 100644 index bda451f..0000000 --- a/windows/cmd/status/main_test.go +++ /dev/null @@ -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") - } -} diff --git a/windows/go.mod b/windows/go.mod deleted file mode 100644 index 5cb9f41..0000000 --- a/windows/go.mod +++ /dev/null @@ -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 -) diff --git a/windows/go.sum b/windows/go.sum deleted file mode 100644 index 1fce446..0000000 --- a/windows/go.sum +++ /dev/null @@ -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= diff --git a/windows/install.ps1 b/windows/install.ps1 deleted file mode 100644 index f4efd12..0000000 --- a/windows/install.ps1 +++ /dev/null @@ -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 $($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 -} diff --git a/windows/lib/clean/apps.ps1 b/windows/lib/clean/apps.ps1 deleted file mode 100644 index 36ed9c7..0000000 --- a/windows/lib/clean/apps.ps1 +++ /dev/null @@ -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. diff --git a/windows/lib/clean/caches.ps1 b/windows/lib/clean/caches.ps1 deleted file mode 100644 index bba2550..0000000 --- a/windows/lib/clean/caches.ps1 +++ /dev/null @@ -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. diff --git a/windows/lib/clean/dev.ps1 b/windows/lib/clean/dev.ps1 deleted file mode 100644 index 5e85d5e..0000000 --- a/windows/lib/clean/dev.ps1 +++ /dev/null @@ -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. diff --git a/windows/lib/clean/system.ps1 b/windows/lib/clean/system.ps1 deleted file mode 100644 index f84593b..0000000 --- a/windows/lib/clean/system.ps1 +++ /dev/null @@ -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. diff --git a/windows/lib/clean/user.ps1 b/windows/lib/clean/user.ps1 deleted file mode 100644 index 448f1e5..0000000 --- a/windows/lib/clean/user.ps1 +++ /dev/null @@ -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. diff --git a/windows/lib/core/base.ps1 b/windows/lib/core/base.ps1 deleted file mode 100644 index 9f6b777..0000000 --- a/windows/lib/core/base.ps1 +++ /dev/null @@ -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. diff --git a/windows/lib/core/common.ps1 b/windows/lib/core/common.ps1 deleted file mode 100644 index 2cef321..0000000 --- a/windows/lib/core/common.ps1 +++ /dev/null @@ -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. diff --git a/windows/lib/core/file_ops.ps1 b/windows/lib/core/file_ops.ps1 deleted file mode 100644 index 7be9188..0000000 --- a/windows/lib/core/file_ops.ps1 +++ /dev/null @@ -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. diff --git a/windows/lib/core/log.ps1 b/windows/lib/core/log.ps1 deleted file mode 100644 index b1849bf..0000000 --- a/windows/lib/core/log.ps1 +++ /dev/null @@ -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. diff --git a/windows/lib/core/ui.ps1 b/windows/lib/core/ui.ps1 deleted file mode 100644 index 9f1a1d7..0000000 --- a/windows/lib/core/ui.ps1 +++ /dev/null @@ -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. diff --git a/windows/mole.ps1 b/windows/mole.ps1 deleted file mode 100644 index f5c5eab..0000000 --- a/windows/mole.ps1 +++ /dev/null @@ -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 -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 -} diff --git a/windows/scripts/build.ps1 b/windows/scripts/build.ps1 deleted file mode 100644 index dca6617..0000000 --- a/windows/scripts/build.ps1 +++ /dev/null @@ -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 "" diff --git a/windows/scripts/test.ps1 b/windows/scripts/test.ps1 deleted file mode 100644 index 10ec682..0000000 --- a/windows/scripts/test.ps1 +++ /dev/null @@ -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 diff --git a/windows/tests/Clean.Tests.ps1 b/windows/tests/Clean.Tests.ps1 deleted file mode 100644 index ac625a2..0000000 --- a/windows/tests/Clean.Tests.ps1 +++ /dev/null @@ -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 - } - } -} diff --git a/windows/tests/Commands.Tests.ps1 b/windows/tests/Commands.Tests.ps1 deleted file mode 100644 index 8eea4dd..0000000 --- a/windows/tests/Commands.Tests.ps1 +++ /dev/null @@ -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" - } - } -} diff --git a/windows/tests/Core.Tests.ps1 b/windows/tests/Core.Tests.ps1 deleted file mode 100644 index 1eebc31..0000000 --- a/windows/tests/Core.Tests.ps1 +++ /dev/null @@ -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 - } - } -}