WinMole.ps1

#Requires -Version 7.0
<#
.SYNOPSIS
    WinMole - Deep clean and optimize your Windows PC.
 
.DESCRIPTION
    WinMole is a PowerShell-based system maintenance toolkit for Windows,
    inspired by the macOS Mole tool. It provides deep cleaning, smart
    uninstalling, disk analysis, system optimization, live status monitoring,
    and project artifact cleanup — all from a single command.
 
.PARAMETER Command
    The subcommand to run: clean, uninstall, optimize, analyze, status, purge,
    update, remove, or help. Omit to show the interactive menu.
 
.PARAMETER DryRun
    Preview what would be done without making any changes.
 
.PARAMETER Debug
    Show detailed operation logs.
 
.PARAMETER Json
    Output results as JSON (supported by: analyze, status).
 
.PARAMETER Path
    Path for the analyze command.
 
.PARAMETER Whitelist
    Manage the protection whitelist (supported by: clean).
 
.PARAMETER Paths
    Configure scan paths (supported by: purge).
 
.PARAMETER Version
    Show the installed version and exit.
 
.PARAMETER Help
    Show this help message and exit.
 
.EXAMPLE
    mo # Interactive menu
    mo clean # Deep system cleanup
    mo clean --dry-run # Preview cleanup
    mo uninstall # Remove apps + leftovers
    mo optimize # Refresh caches & services
    mo analyze # Visual disk explorer
    mo analyze C:\Users # Analyze specific path
    mo status # Live system dashboard
    mo status --json # Machine-readable output
    mo purge # Clean project build artifacts
    mo purge --paths # Configure scan directories
 
.NOTES
    Version : 0.1.3
    Author : WinMole contributors
    License : MIT
#>

param(
    [Parameter(Position = 0)]
    [string]$Command = '',

    [Parameter(Position = 1)]
    [string]$Arg1 = '',

    [Alias('dry-run')]
    [switch]$DryRun,

    [switch]$DebugMode,

    [switch]$Json,

    [string]$Path = '',

    [switch]$Whitelist,
    [switch]$Paths,
    [switch]$Version,

    [Alias('h')]
    [switch]$Help,

    # status options
    [double]$ProcCpuThreshold = 50.0,
    [int]$ProcCpuWindow       = 10,
    [switch]$NoProcCpuAlerts,

    [Parameter(ValueFromRemainingArguments = $true)]
    [string[]]$RemainingArgs
)

$ErrorActionPreference = 'SilentlyContinue'
Set-StrictMode -Off   # relax for interactive use

# ── Locate WinMole root ───────────────────────────────────────────────────────
$script:WINMOLE_ROOT = $PSScriptRoot

# ── Load core library ─────────────────────────────────────────────────────────
$corePath = Join-Path $script:WINMOLE_ROOT 'lib\core.ps1'
if (-not (Test-Path $corePath)) {
    Write-Error "WinMole core library not found at: $corePath"
    exit 1
}
. $corePath

# ── Map 'analyse' alias ───────────────────────────────────────────────────────
function Resolve-LongOptionValue {
    param(
        [string]$OptionName,
        [string]$InlineValue,
        [System.Collections.Generic.List[string]]$Tokens,
        [int]$CurrentIndex
    )
    if ($null -ne $InlineValue) {
        return [pscustomobject]@{ Value = $InlineValue; NextIndex = $CurrentIndex }
    }
    if (($CurrentIndex + 1) -ge $Tokens.Count) {
        Write-Err "Missing value for option: --$OptionName"
        exit 1
    }
    return [pscustomobject]@{
        Value     = [string]$Tokens[$CurrentIndex + 1]
        NextIndex = $CurrentIndex + 1
    }
}

function Normalize-CommandArguments {
    $tokens = [System.Collections.Generic.List[string]]::new()
    foreach ($token in @($Command, $Arg1) + @($RemainingArgs)) {
        if (-not [string]::IsNullOrWhiteSpace($token)) {
            $tokens.Add([string]$token)
        }
    }

    $positionals = [System.Collections.Generic.List[string]]::new()

    for ($i = 0; $i -lt $tokens.Count; $i++) {
        $token = $tokens[$i]
        if ($token -notlike '--*') {
            $positionals.Add($token)
            continue
        }

        $optionSpec = $token.Substring(2)
        $optionName = $optionSpec
        $optionValue = $null
        if ($optionSpec -match '^(?<name>[^=]+)=(?<value>.*)$') {
            $optionName = $Matches.name
            $optionValue = $Matches.value
        }

        switch ($optionName) {
            'dry-run' { $script:DryRun = $true }
            'debug' { $script:DebugMode = $true }
            'json' { $script:Json = $true }
            'whitelist' { $script:Whitelist = $true }
            'paths' { $script:Paths = $true }
            'version' { $script:Version = $true }
            'help' { $script:Help = $true }
            'no-proc-cpu-alerts' { $script:NoProcCpuAlerts = $true }
            'path' {
                $resolved = Resolve-LongOptionValue -OptionName $optionName -InlineValue $optionValue -Tokens $tokens -CurrentIndex $i
                $script:Path = $resolved.Value
                $i = $resolved.NextIndex
            }
            'proc-cpu-threshold' {
                $resolved = Resolve-LongOptionValue -OptionName $optionName -InlineValue $optionValue -Tokens $tokens -CurrentIndex $i
                try {
                    $script:ProcCpuThreshold = [double]$resolved.Value
                } catch {
                    Write-Err "Invalid value for --proc-cpu-threshold: $($resolved.Value)"
                    exit 1
                }
                $i = $resolved.NextIndex
            }
            'proc-cpu-window' {
                $resolved = Resolve-LongOptionValue -OptionName $optionName -InlineValue $optionValue -Tokens $tokens -CurrentIndex $i
                try {
                    $script:ProcCpuWindow = [int]$resolved.Value
                } catch {
                    Write-Err "Invalid value for --proc-cpu-window: $($resolved.Value)"
                    exit 1
                }
                $i = $resolved.NextIndex
            }
            default {
                Write-Err "Unknown option: --$optionName"
                exit 1
            }
        }
    }

    $script:Command = if ($positionals.Count -gt 0) { $positionals[0] } else { '' }
    $script:Arg1    = if ($positionals.Count -gt 1) { $positionals[1] } else { '' }
}

Normalize-CommandArguments
if ($Command -eq 'analyse') { $Command = 'analyze' }

# ── Dispatch table ────────────────────────────────────────────────────────────
function Invoke-Command {
    param([string]$Cmd)

    $binDir = Join-Path $script:WINMOLE_ROOT 'bin'

    switch ($Cmd.ToLower()) {
        'clean' {
            & "$binDir\clean.ps1" `
                -DryRun:$DryRun `
                -Debug:$DebugMode `
                -Whitelist:$Whitelist
        }
        'uninstall' {
            & "$binDir\uninstall.ps1" `
                -DryRun:$DryRun
        }
        'optimize' {
            & "$binDir\optimize.ps1" `
                -DryRun:$DryRun `
                -Debug:$DebugMode
        }
        'analyze' {
            $analyzePath = if ($Arg1) { $Arg1 } elseif ($Path) { $Path } else { '' }
            & "$binDir\analyze.ps1" `
                -Path $analyzePath `
                -Json:$Json
        }
        'status' {
            & "$binDir\status.ps1" `
                -Json:$Json `
                -ProcCpuThreshold $ProcCpuThreshold `
                -ProcCpuWindow $ProcCpuWindow `
                -NoProcCpuAlerts:$NoProcCpuAlerts
        }
        'purge' {
            & "$binDir\purge.ps1" `
                -DryRun:$DryRun `
                -Paths:$Paths
        }
        'update' {
            Invoke-Update
        }
        'remove' {
            Invoke-Remove -DryRun:$DryRun
        }
        default {
            Write-Err "Unknown command: $Cmd"
            Write-SpectreHost " Run [deepskyblue1]mo --help[/] to see available commands."
            exit 1
        }
    }
}

# ── Interactive main menu ─────────────────────────────────────────────────────
function Show-MainMenu {
    . (Join-Path $script:WINMOLE_ROOT 'lib\ui.ps1')

    $info = Get-SysInfoSummary

    Write-Host ""
    Write-SpectreHost " [bold deepskyblue1]WinMole[/] [grey]v$script:WINMOLE_VERSION — $(Esc $info.MachineName) · $(Esc ($info.OSCaption -replace 'Microsoft ',''))[/]"
    Write-SpectreHost " [grey]RAM: $($info.RamUsedGB)/$($info.RamTotalGB)GB | Disk: $($info.DiskUsedGB)/$($info.DiskTotalGB)GB | Uptime: $($info.Uptime)[/]"
    Write-Host ""

    $options = @(
        'clean — Deep system cleanup',
        'uninstall — Remove apps + leftovers',
        'optimize — Refresh caches & services',
        'analyze — Visual disk explorer',
        'status — Live system dashboard',
        'purge — Clean project artifacts',
        '──────────────────────────────────',
        'update — Update WinMole',
        'remove — Uninstall WinMole',
        'help — Show all commands'
    )

    $selected = Show-SelectMenu -Title 'Choose a command' -Options $options
    if ($selected -lt 0) { return }

    # Map selection index to command, skipping separator
    $cmdMap = @('clean','uninstall','optimize','analyze','status','purge',$null,'update','remove','help')
    $cmd    = $cmdMap[$selected]

    if (-not $cmd) { Show-MainMenu; return }
    if ($cmd -eq 'help') { Show-Help; return }

    Write-Host ""
    Invoke-Command -Cmd $cmd
}

# ── Help text ─────────────────────────────────────────────────────────────────
function Show-Help {
    Write-Host ""
    Write-SpectreHost " [bold deepskyblue1]WinMole[/] v$script:WINMOLE_VERSION — Deep clean and optimize your Windows PC"
    Write-Host ""
    Write-SpectreHost " [bold]USAGE[/]"
    Write-SpectreHost " mo [grey][[command]] [[options]][/]"
    Write-Host ""
    Write-SpectreHost " [bold]COMMANDS[/]"
    $cmds = @(
        @('mo ', 'Interactive menu'),
        @('mo clean ', 'Deep system cleanup (temp, browser, dev caches, Recycle Bin)'),
        @('mo uninstall ', 'Smart app uninstaller with leftover cleanup'),
        @('mo optimize ', 'System optimization (DNS, WU cache, event logs, etc.)'),
        @('mo analyze [[path]] ', 'Interactive disk space explorer'),
        @('mo status ', 'Live system health dashboard'),
        @('mo purge ', 'Clean project build artifacts'),
        @('mo update ', 'Update WinMole to latest version'),
        @('mo remove ', 'Remove WinMole from system')
    )
    foreach ($c in $cmds) {
        Write-SpectreHost " [deepskyblue1]$($c[0])[/][grey]$($c[1])[/]"
    }
    Write-Host ""
    Write-SpectreHost " [bold]OPTIONS[/]"
    $opts = @(
        @('--dry-run ', 'Preview changes without deleting anything'),
        @('--debug ', 'Show detailed operation logs'),
        @('--json ', 'Machine-readable JSON output (analyze, status)'),
        @('--whitelist ', 'Manage protected paths (clean)'),
        @('--paths ', 'Configure scan directories (purge)'),
        @('--version ', 'Show installed version'),
        @('--help ', 'Show this help message')
    )
    foreach ($o in $opts) {
        Write-SpectreHost " [yellow]$($o[0])[/][grey]$($o[1])[/]"
    }
    Write-Host ""
    Write-SpectreHost " [bold]EXAMPLES[/]"
    Write-SpectreHost " [grey]mo clean --dry-run # Preview cleanup plan[/]"
    Write-SpectreHost " [grey]mo clean --dry-run --debug # Preview with risk detail[/]"
    Write-SpectreHost " [grey]mo analyze C:\Users\me # Analyze specific path[/]"
    Write-SpectreHost " [grey]mo purge --paths # Configure project dirs[/]"
    Write-SpectreHost " [grey]mo clean --whitelist # Manage protected paths[/]"
    Write-Host ""
    Write-SpectreHost " [grey]Logs:[/] $(Esc $script:LOG_FILE)"
    Write-SpectreHost " [grey]Config:[/] $(Esc $script:WINMOLE_CONFIG)"
    Write-Host ""
}

# ── Self-update ───────────────────────────────────────────────────────────────
function Invoke-Update {
    Write-Header -Title 'WinMole Update'

    $currentVersion = $script:WINMOLE_VERSION
    Write-Info "Current version: v$currentVersion"

    $isGalleryInstall = $null -ne (Get-InstalledModule -Name WinMole -ErrorAction SilentlyContinue)
    if (-not $isGalleryInstall) {
        Write-Warn "WinMole was not installed from the PowerShell Gallery."
        Write-Info "Install from the gallery to enable updates:"
        Write-SpectreHost " [deepskyblue1]Install-Module -Name WinMole -Scope CurrentUser[/]"
        return
    }

    Write-Info "Checking PowerShell Gallery for updates..."

    try {
        $gallery = Find-Module -Name WinMole -ErrorAction Stop
        $latestVersion = $gallery.Version

        if ([version]$latestVersion -le [version]$currentVersion) {
            Write-Success "Already up to date (v$currentVersion)."
            return
        }

        Write-Info "Updating v$currentVersion → v$latestVersion..."
        Update-Module -Name WinMole -Force -Scope CurrentUser
        Write-Success "Updated to v$latestVersion."
        Write-SpectreHost " [grey]Restart your terminal to use the new version.[/]"
    } catch {
        Write-Err "Update failed: $_"
        Write-Info "Try manually: Update-Module -Name WinMole -Force"
    }
}

# ── Self-remove ───────────────────────────────────────────────────────────────
function Invoke-Remove {
    param([switch]$DryRun)
    Write-Header -Title 'WinMole Remove'

    $isGalleryInstall = $null -ne (Get-InstalledModule -Name WinMole -ErrorAction SilentlyContinue)

    if ($isGalleryInstall) {
        $installed = Get-InstalledModule -Name WinMole
        Write-SpectreHost " Installed via PowerShell Gallery: [gold1]v$($installed.Version)[/]"
    } else {
        Write-SpectreHost " Installed at: [gold1]$(Esc $script:WINMOLE_ROOT)[/]"
    }
    Write-Host ""

    if ($DryRun) {
        if ($isGalleryInstall) {
            Write-Info "Dry run — would run: Uninstall-Module -Name WinMole -AllVersions -Force"
        } else {
            Write-Info "Dry run — would remove: $($script:WINMOLE_ROOT)"
        }
        Write-Info "Would clean up any legacy PATH and profile entries."
        return
    }

    if (-not (Confirm-Action -Message 'Proceed with removal?')) {
        Write-SpectreHost " [grey]Cancelled.[/]"
        Write-Host ""
        return
    }

    if ($isGalleryInstall) {
        try {
            Remove-Module -Name WinMole -Force -ErrorAction SilentlyContinue
            Uninstall-Module -Name WinMole -AllVersions -Force
            Write-Success "WinMole module uninstalled."
        } catch {
            Write-Warn "Could not uninstall module: $_"
            Write-Info "Try manually: Uninstall-Module -Name WinMole -AllVersions -Force"
        }
    } else {
        try {
            Remove-Item -LiteralPath $script:WINMOLE_ROOT -Recurse -Force
            Write-Success "Removed $($script:WINMOLE_ROOT)"
        } catch {
            Write-Warn "Could not remove $($script:WINMOLE_ROOT) — $_"
            Write-Info "You may need to manually delete: $($script:WINMOLE_ROOT)"
        }
    }

    # Clean up legacy PATH and profile entries from git-based installs
    $currentPath = [System.Environment]::GetEnvironmentVariable('PATH', 'User')
    if ($currentPath -like '*WinMole*') {
        $newPath = ($currentPath -split ';' | Where-Object { $_ -notlike '*WinMole*' }) -join ';'
        [System.Environment]::SetEnvironmentVariable('PATH', $newPath, 'User')
        Write-Success "Removed legacy PATH entry"
    }

    foreach ($prof in @($PROFILE.CurrentUserAllHosts, $PROFILE.CurrentUserCurrentHost)) {
        if (Test-Path $prof) {
            $content = Get-Content $prof -Raw -ErrorAction SilentlyContinue
            if ($content -match 'WinMole BEGIN') {
                $newContent = $content -replace '(?ms)# WinMole BEGIN.*?# WinMole END\r?\n?', ''
                Set-Content -Path $prof -Value $newContent.TrimEnd()
                Write-Success "Removed legacy profile entry"
            }
        }
    }

    Write-Host ""
    Write-Success "WinMole removed successfully."
    Write-SpectreHost " [grey]Restart your terminal to complete removal.[/]"
    Write-Host ""
}

# ── Entry point ───────────────────────────────────────────────────────────────
if ($Version) {
    Write-Host "WinMole $script:WINMOLE_VERSION"
    exit 0
}

if ($Help -or $Command -eq '--help' -or $Command -eq 'help') {
    Show-Help
    exit 0
}

if (-not $Command) {
    Show-MainMenu
} else {
    Invoke-Command -Cmd $Command
}