1
0
mirror of https://github.com/tw93/Mole.git synced 2026-02-05 03:54:44 +00:00

feat(windows): add Windows support Phase 1 - core infrastructure

Add isolated Windows support in windows/ directory with zero changes to existing macOS code.

Phase 1 includes:
- install.ps1: Windows installer with PATH and shortcut support
- mole.ps1: Main CLI entry point with menu system
- lib/core/base.ps1: Core definitions, colors, icons, constants
- lib/core/common.ps1: Common functions loader
- lib/core/file_ops.ps1: Safe file operations with protection checks
- lib/core/log.ps1: Logging functions with colors
- lib/core/ui.ps1: Interactive UI components (menus, confirmations)
- go.mod/go.sum: Go module for future TUI tools
- README.md: Windows-specific documentation

Closes #273
This commit is contained in:
Bhadra
2026-01-08 14:24:11 +05:30
parent 64a580b3a7
commit a4b5fe77cf
10 changed files with 2556 additions and 0 deletions

111
windows/README.md Normal file
View File

@@ -0,0 +1,111 @@
# 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)
- Optional: 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
```
## Environment Variables
| Variable | Description |
|----------|-------------|
| `MOLE_DRY_RUN=1` | Preview changes without making them |
| `MOLE_DEBUG=1` | Enable debug output |
## Directory Structure
```
windows/
├── mole.ps1 # Main CLI entry point
├── install.ps1 # Windows installer
├── go.mod # Go module definition
├── go.sum # Go dependencies
└── 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
```
## Configuration
Mole stores its configuration in:
- Config: `~\.config\mole\`
- Cache: `~\.cache\mole\`
- Whitelist: `~\.config\mole\whitelist.txt`
## Development
### Phase 1: Core Infrastructure (Current)
- [x] `install.ps1` - Windows installer
- [x] `mole.ps1` - Main CLI entry point
- [x] `lib/core/*` - Core utility libraries
### Phase 2: Cleanup Features (Planned)
- [ ] `bin/clean.ps1` - Deep cleanup orchestrator
- [ ] `bin/uninstall.ps1` - App removal with leftover detection
- [ ] `bin/optimize.ps1` - Cache rebuild and service refresh
- [ ] `bin/purge.ps1` - Aggressive cleanup mode
- [ ] `lib/clean/*` - Cleanup modules
### Phase 3: TUI Tools (Planned)
- [ ] `cmd/analyze/` - Disk usage analyzer (Go)
- [ ] `cmd/status/` - Real-time system monitor (Go)
- [ ] `bin/analyze.ps1` - Analyzer wrapper
- [ ] `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.

34
windows/go.mod Normal file
View File

@@ -0,0 +1,34 @@
module github.com/tw93/mole/windows
go 1.24.0
require (
github.com/charmbracelet/bubbletea v0.25.0
github.com/charmbracelet/lipgloss v0.9.1
github.com/shirou/gopsutil/v3 v3.24.5
)
require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
github.com/mattn/go-isatty v0.0.18 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/reflow v0.3.0 // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/shoenig/go-m1cpu v0.1.7 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/term v0.6.0 // indirect
golang.org/x/text v0.3.8 // indirect
)

73
windows/go.sum Normal file
View File

@@ -0,0 +1,73 @@
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=

420
windows/install.ps1 Normal file
View File

@@ -0,0 +1,420 @@
#!/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]$Help
)
$ErrorActionPreference = "Stop"
Set-StrictMode -Version Latest
# ============================================================================
# Configuration
# ============================================================================
$script:VERSION = "1.0.0"
$script:SourceDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$script:ShortcutName = "Mole"
# Colors
$script:Colors = @{
Red = "`e[31m"
Green = "`e[32m"
Yellow = "`e[33m"
Blue = "`e[34m"
Cyan = "`e[36m"
Gray = "`e[90m"
NC = "`e[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-Warning {
param([string]$Message)
$c = $script:Colors
Write-Host " $($c.Yellow)WARN$($c.NC) $Message"
}
function Write-Error {
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)╔╦╗╔═╗╦ ╔═╗$($c.NC)"
Write-Host " $($c.Cyan)║║║║ ║║ ║╣ $($c.NC)"
Write-Host " $($c.Cyan)╩ ╩╚═╝╩═╝╚═╝$($c.NC)"
Write-Host " $($c.Gray)Windows System Maintenance$($c.NC)"
Write-Host ""
}
function Show-Help {
Show-Banner
$c = $script:Colors
Write-Host " $($c.Green)USAGE:$($c.NC)"
Write-Host ""
Write-Host " .\install.ps1 [options]"
Write-Host ""
Write-Host " $($c.Green)OPTIONS:$($c.NC)"
Write-Host ""
Write-Host " $($c.Cyan)-InstallDir <path>$($c.NC) Installation directory"
Write-Host " Default: $env:LOCALAPPDATA\Mole"
Write-Host ""
Write-Host " $($c.Cyan)-AddToPath$($c.NC) Add Mole to user PATH"
Write-Host ""
Write-Host " $($c.Cyan)-CreateShortcut$($c.NC) Create Start Menu shortcut"
Write-Host ""
Write-Host " $($c.Cyan)-Uninstall$($c.NC) Remove Mole from system"
Write-Host ""
Write-Host " $($c.Cyan)-Force$($c.NC) Overwrite existing installation"
Write-Host ""
Write-Host " $($c.Cyan)-Help$($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-Error "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-Error "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-Error "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-Error "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-Error "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-Error "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) {
Copy-Item -Path $src -Destination $dst -Recurse -Force
}
else {
Copy-Item -Path $src -Destination $dst -Force
}
Write-Success "Copied: $item"
}
catch {
Write-Error "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
$batchContent = @"
@echo off
powershell.exe -ExecutionPolicy Bypass -NoLogo -File "%~dp0mole.ps1" %*
"@
$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-Warning "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-Error "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-Warning "Failed to remove config: $_"
}
}
}
Write-Host ""
Write-Success "Mole uninstalled successfully!"
Write-Host ""
return $true
}
# ============================================================================
# Main
# ============================================================================
function Main {
if ($Help) {
Show-Help
return
}
Show-Banner
if ($Uninstall) {
Uninstall-Mole
}
else {
Install-Mole
}
}
# Run
try {
Main
}
catch {
Write-Host ""
Write-Error "Installation failed: $_"
Write-Host ""
exit 1
}

394
windows/lib/core/base.ps1 Normal file
View File

@@ -0,0 +1,394 @@
# 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\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.

130
windows/lib/core/common.ps1 Normal file
View File

@@ -0,0 +1,130 @@
# 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-Warning "Some operations require administrator privileges."
if (Read-Confirmation -Prompt "Restart with admin privileges?" -Default $true) {
$scriptPath = $MyInvocation.PSCommandPath
if ($scriptPath) {
Start-Process powershell.exe -ArgumentList "-ExecutionPolicy Bypass -File `"$scriptPath`"" -Verb RunAs
exit
}
}
return $false
}
return $true
}
function Invoke-AsAdmin {
<#
.SYNOPSIS
Run a script block with admin privileges
#>
param(
[Parameter(Mandatory)]
[scriptblock]$ScriptBlock
)
if (Test-IsAdmin) {
& $ScriptBlock
}
else {
$command = $ScriptBlock.ToString()
Start-Process powershell.exe -ArgumentList "-ExecutionPolicy Bypass -Command `"$command`"" -Verb RunAs -Wait
}
}
# ============================================================================
# Exports (functions are available via dot-sourcing)
# ============================================================================
# All functions from base.ps1, log.ps1, file_ops.ps1, and ui.ps1 are
# automatically available when this file is dot-sourced.

View File

@@ -0,0 +1,439 @@
# 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:DryRun = $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:DryRun) {
$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:DryRun) {
$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:DryRun) {
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:DryRun) {
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:DryRun) {
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:DryRun = $Enabled
}
function Test-DryRunMode {
<#
.SYNOPSIS
Check if dry-run mode is enabled
#>
return $script:DryRun
}
# ============================================================================
# Exports (functions are available via dot-sourcing)
# ============================================================================
# Functions: Test-SafePath, Get-PathSize, Remove-SafeItem, etc.

283
windows/lib/core/log.ps1 Normal file
View File

@@ -0,0 +1,283 @@
# 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-Warning {
<#
.SYNOPSIS
Write a warning message
#>
param([string]$Message)
Write-LogMessage -Message $Message -Level "WARN" -Color "Yellow" -Icon $script:Icons.Warning
}
function Write-Error {
<#
.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.

407
windows/lib/core/ui.ps1 Normal file
View File

@@ -0,0 +1,407 @@
# 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
$key = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
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")
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 ($key in $selected.Keys) {
if ($selected[$key]) {
$result += $Items[$key]
}
}
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
$banner = @"
${purple} ${nc}
${purple} ${nc}
${purple} ${nc}
${purple} ${nc}
${purple} ${nc}
${purple} ${nc}
${cyan}Windows System Maintenance${nc}
"@
Write-Host $banner
}
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.

265
windows/mole.ps1 Normal file
View File

@@ -0,0 +1,265 @@
#!/usr/bin/env pwsh
# Mole - Windows System Maintenance Toolkit
# Main CLI entry point
#Requires -Version 5.1
param(
[Parameter(Position = 0)]
[string]$Command,
[Parameter(Position = 1, ValueFromRemainingArguments)]
[string[]]$CommandArgs,
[switch]$Version,
[switch]$ShowHelp
)
$ErrorActionPreference = "Stop"
Set-StrictMode -Version Latest
# Get script directory
$script:MOLE_ROOT = Split-Path -Parent $MyInvocation.MyCommand.Path
$script:MOLE_BIN = Join-Path $script:MOLE_ROOT "bin"
$script:MOLE_LIB = Join-Path $script:MOLE_ROOT "lib"
# Import core
. "$script:MOLE_LIB\core\common.ps1"
# ============================================================================
# Version Info
# ============================================================================
$script:MOLE_VER = "1.0.0"
$script:MOLE_BUILD = "2026-01-07"
function Show-Version {
$info = Get-MoleVersion
Write-Host "Mole v$($info.Version)"
Write-Host "Built: $($info.BuildDate)"
Write-Host "PowerShell: $($info.PowerShell)"
Write-Host "Windows: $($info.Windows)"
}
# ============================================================================
# Help
# ============================================================================
function Show-MainHelp {
$cyan = $script:Colors.Cyan
$gray = $script:Colors.Gray
$green = $script:Colors.Green
$nc = $script:Colors.NC
Show-Banner
Write-Host " ${cyan}Windows System Maintenance Toolkit${nc}"
Write-Host " ${gray}Clean, optimize, and maintain your Windows system${nc}"
Write-Host ""
Write-Host " ${green}COMMANDS:${nc}"
Write-Host ""
Write-Host " ${cyan}clean${nc} Deep system cleanup (caches, temp, logs)"
Write-Host " ${cyan}uninstall${nc} Smart application uninstaller"
Write-Host " ${cyan}analyze${nc} Visual disk space analyzer"
Write-Host " ${cyan}status${nc} Real-time system monitor"
Write-Host " ${cyan}optimize${nc} System optimization tasks"
Write-Host " ${cyan}purge${nc} Clean project build artifacts"
Write-Host ""
Write-Host " ${green}OPTIONS:${nc}"
Write-Host ""
Write-Host " ${cyan}-Version${nc} Show version information"
Write-Host " ${cyan}-ShowHelp${nc} Show this help message"
Write-Host ""
Write-Host " ${green}EXAMPLES:${nc}"
Write-Host ""
Write-Host " ${gray}mole${nc} ${gray}# Interactive menu${nc}"
Write-Host " ${gray}mole clean${nc} ${gray}# Deep cleanup${nc}"
Write-Host " ${gray}mole clean -DryRun${nc} ${gray}# Preview cleanup${nc}"
Write-Host " ${gray}mole uninstall${nc} ${gray}# Uninstall apps${nc}"
Write-Host " ${gray}mole analyze${nc} ${gray}# Disk analyzer${nc}"
Write-Host " ${gray}mole status${nc} ${gray}# System monitor${nc}"
Write-Host " ${gray}mole optimize${nc} ${gray}# Optimize system${nc}"
Write-Host " ${gray}mole purge${nc} ${gray}# Clean dev artifacts${nc}"
Write-Host ""
Write-Host " ${green}ENVIRONMENT:${nc}"
Write-Host ""
Write-Host " ${cyan}MOLE_DRY_RUN=1${nc} Preview without changes"
Write-Host " ${cyan}MOLE_DEBUG=1${nc} Enable debug output"
Write-Host ""
Write-Host " ${gray}Run '${nc}mole <command> -ShowHelp${gray}' for command-specific help${nc}"
Write-Host ""
}
# ============================================================================
# Interactive Menu
# ============================================================================
function Show-MainMenu {
$options = @(
@{
Name = "Clean"
Description = "Deep system cleanup"
Command = "clean"
Icon = $script:Icons.Trash
}
@{
Name = "Uninstall"
Description = "Remove applications"
Command = "uninstall"
Icon = $script:Icons.Folder
}
@{
Name = "Analyze"
Description = "Disk space analyzer"
Command = "analyze"
Icon = $script:Icons.File
}
@{
Name = "Status"
Description = "System monitor"
Command = "status"
Icon = $script:Icons.Solid
}
@{
Name = "Optimize"
Description = "System optimization"
Command = "optimize"
Icon = $script:Icons.Arrow
}
@{
Name = "Purge"
Description = "Clean dev artifacts"
Command = "purge"
Icon = $script:Icons.List
}
)
$selected = Show-Menu -Title "What would you like to do?" -Options $options -AllowBack
if ($null -eq $selected) {
return $null
}
return $selected.Command
}
# ============================================================================
# Command Router
# ============================================================================
function Invoke-Command {
param(
[string]$CommandName,
[string[]]$Arguments
)
$scriptPath = Join-Path $script:MOLE_BIN "$CommandName.ps1"
if (-not (Test-Path $scriptPath)) {
Write-Error "Unknown command: $CommandName"
Write-Host ""
Write-Host "Run 'mole -ShowHelp' for available commands"
return
}
# Execute the command script with arguments
& $scriptPath @Arguments
}
# ============================================================================
# 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 version flag
if ($Version) {
Show-Version
return
}
# Handle help flag
if ($ShowHelp -and -not $Command) {
Show-MainHelp
return
}
# If command specified, route to it
if ($Command) {
$validCommands = @("clean", "uninstall", "analyze", "status", "optimize", "purge")
if ($Command -in $validCommands) {
Invoke-Command -CommandName $Command -Arguments $CommandArgs
}
else {
Write-Error "Unknown command: $Command"
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-Command -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-Error "An error occurred: $_"
Write-Host ""
exit 1
}
finally {
Clear-TempFiles
}