feat(windows): add Windows support Phase 1 - core infrastructure#275
feat(windows): add Windows support Phase 1 - core infrastructure#275
Conversation
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 tw93#273
- Simplified banner in install.ps1 and ui.ps1 to avoid Unicode box-drawing characters that caused parsing errors on some Windows systems - Fixed PowerShell 5.1 compatibility for ANSI color codes using [char]27 - Renamed Show-Help to Show-InstallerHelp to avoid potential conflicts - Changed -Help parameter to -ShowHelp for consistency with mole.ps1
There was a problem hiding this comment.
Pull request overview
This PR introduces Windows support for Mole as Phase 1 of a multi-phase implementation. The changes add a complete PowerShell-based infrastructure for Windows system maintenance, isolated entirely in a new windows/ directory with zero impact on existing macOS code.
Key Changes:
- Complete PowerShell-based CLI and library infrastructure
- Windows installer with PATH integration and shortcut creation
- Core utility libraries for safe file operations, logging, and interactive UI
- Go module setup for planned TUI tools in future phases
Reviewed changes
Copilot reviewed 9 out of 10 changed files in this pull request and generated 14 comments.
Show a summary per file
| File | Description |
|---|---|
| windows/README.md | Documentation covering installation, usage, directory structure, and development roadmap |
| windows/install.ps1 | Windows installer supporting custom paths, PATH integration, shortcuts, and uninstallation |
| windows/mole.ps1 | Main CLI entry point with command routing, interactive menu, and help system |
| windows/go.mod | Go module definition for future TUI tool dependencies |
| windows/go.sum | Dependency checksums for Go modules |
| windows/lib/core/base.ps1 | Core definitions including colors, icons, constants, path utilities, and formatting functions |
| windows/lib/core/common.ps1 | Module loader and initialization logic |
| windows/lib/core/log.ps1 | Logging infrastructure with multiple log levels and progress indicators |
| windows/lib/core/ui.ps1 | Interactive UI components including menus, confirmations, and banners |
| windows/lib/core/file_ops.ps1 | Safe file operations with protection checks, whitelisting, and cleanup statistics |
| function Write-Error { | ||
| param([string]$Message) | ||
| $c = $script:Colors | ||
| Write-Host " $($c.Red)ERROR$($c.NC) $Message" | ||
| } |
There was a problem hiding this comment.
The function redefines PowerShell's built-in "Write-Error" cmdlet, which is a fundamental part of PowerShell's error handling. This can cause significant conflicts and break error handling throughout the script and other modules. Consider renaming to "Write-ErrorMessage" or "Write-InstallerError" to avoid shadowing the built-in cmdlet.
| function Write-Error { | ||
| <# | ||
| .SYNOPSIS | ||
| Write an error message | ||
| #> | ||
| param([string]$Message) | ||
| Write-LogMessage -Message $Message -Level "ERROR" -Color "Red" -Icon $script:Icons.Error | ||
| } |
There was a problem hiding this comment.
The function redefines PowerShell's built-in "Write-Error" cmdlet, which can cause conflicts and unexpected behavior throughout the script. This shadows the standard error handling mechanism in PowerShell. Consider renaming to "Write-ErrorMessage" or "Write-MoleError" to avoid conflicts with the built-in cmdlet.
| function Write-Warning { | ||
| <# | ||
| .SYNOPSIS | ||
| Write a warning message | ||
| #> | ||
| param([string]$Message) | ||
| Write-LogMessage -Message $Message -Level "WARN" -Color "Yellow" -Icon $script:Icons.Warning | ||
| } |
There was a problem hiding this comment.
The function redefines PowerShell's built-in "Write-Warning" cmdlet, which can cause conflicts and unexpected behavior. This shadows the standard warning mechanism in PowerShell. Consider renaming to "Write-WarningMessage" or "Write-MoleWarning" to avoid conflicts with the built-in cmdlet.
| 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}" | ||
| } | ||
| } |
There was a problem hiding this comment.
The function redefines PowerShell's built-in "Write-Debug" cmdlet, which can cause conflicts with PowerShell's native debug output functionality. Consider renaming to "Write-DebugMessage" or "Write-MoleDebug" to avoid shadowing the built-in cmdlet.
| 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 " | ||
| } |
There was a problem hiding this comment.
The function redefines PowerShell's built-in "Write-Progress" cmdlet, which is widely used for progress bars throughout PowerShell. This can cause conflicts with other modules and scripts that rely on the standard Write-Progress cmdlet. Consider renaming to "Write-ProgressBar" or "Write-MoleProgress" to avoid shadowing the built-in cmdlet.
| if (-not (Test-Path $scriptPath)) { | ||
| Write-Error "Unknown command: $CommandName" | ||
| Write-Host "" | ||
| Write-Host "Run 'mole -ShowHelp' for available commands" |
There was a problem hiding this comment.
The error message incorrectly states to use "mole -ShowHelp" when the correct syntax is "mole.ps1 -ShowHelp" (since this is within a context where the command may not be in PATH yet). The message should be consistent with how users would actually invoke the command at this point in the code.
| Write-Host "Run 'mole -ShowHelp' for available commands" | |
| Write-Host "Run 'mole.ps1 -ShowHelp' for available commands" |
| 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 | ||
| } |
There was a problem hiding this comment.
The function uses "$MyInvocation.PSCommandPath" which may be null in certain execution contexts (like when dot-sourcing). This could cause the restart to fail silently. Consider adding a fallback or validation check to ensure the script path is available before attempting to restart.
| 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 | |
| } | |
| if (Read-Confirmation -Prompt "Restart with admin privileges?" -Default $true) { | |
| # Try to get the current script path from PSCommandPath; this may be null in some contexts | |
| $scriptPath = $MyInvocation.PSCommandPath | |
| # Fallback to the automatic $PSCommandPath variable if available | |
| if (-not $scriptPath -and $PSCommandPath) { | |
| $scriptPath = $PSCommandPath | |
| } | |
| if ($scriptPath) { | |
| Start-Process powershell.exe -ArgumentList "-ExecutionPolicy Bypass -File `"$scriptPath`"" -Verb RunAs | |
| exit | |
| } | |
| else { | |
| Write-Error "Cannot restart with admin privileges because the script path is not available in this execution context." | |
| } |
| .SYNOPSIS | ||
| Get number of CPU cores | ||
| #> | ||
| return (Get-WmiObject Win32_Processor).NumberOfLogicalProcessors |
There was a problem hiding this comment.
Using Get-WmiObject is deprecated in PowerShell. Consider using Get-CimInstance instead, which is the modern replacement and performs better. For example, replace "Get-WmiObject Win32_Processor" with "Get-CimInstance Win32_Processor".
| "C:\ProgramData\Microsoft\Windows Defender" | ||
| "$env:SYSTEMROOT" | ||
| "$env:WINDIR" | ||
| ) |
There was a problem hiding this comment.
The protected paths list includes environment variables that may not be set in all Windows configurations, which could lead to null or empty paths being added to the protection list. Consider adding validation to ensure these environment variables are defined before adding them to the protected paths array.
| ) | |
| ) | Where-Object { $_ -and $_.ToString().Trim() } |
windows/lib/core/ui.ps1
Outdated
| foreach ($key in $selected.Keys) { | ||
| if ($selected[$key]) { | ||
| $result += $Items[$key] |
There was a problem hiding this comment.
The variable name "$key" shadows the loop variable in the outer foreach loop (line 291). This creates confusion and potential bugs. Consider renaming the inner variable to something like "$selectedKey" or "$index" to avoid shadowing.
| foreach ($key in $selected.Keys) { | |
| if ($selected[$key]) { | |
| $result += $Items[$key] | |
| foreach ($selectedKey in $selected.Keys) { | |
| if ($selected[$selectedKey]) { | |
| $result += $Items[$selectedKey] |
Add cleanup modules and command scripts for Windows: - lib/clean: user, caches, dev, apps, system cleanup modules - bin/clean: deep cleanup orchestrator with dry-run and whitelist - bin/uninstall: interactive app uninstaller - bin/optimize: system optimization and health checks - bin/purge: project artifact cleanup All scripts support dry-run mode and follow safe deletion practices.
|
Add Go-based TUI tools for Windows: - cmd/analyze: Interactive disk space analyzer with Bubble Tea - cmd/status: Real-time system health monitor using gopsutil/WMI - bin/analyze.ps1: Wrapper script for analyze tool - bin/status.ps1: Wrapper script for status tool - Makefile: Build automation for Go tools - Updated README.md with Phase 3 documentation
- Pester test suite for PowerShell scripts (Core, Clean, Commands) - Go tests for TUI tools (analyze, status) - GitHub Actions workflow for Windows CI - Build and test automation scripts (test.ps1, build.ps1)
- Fix mole.cmd batch launcher to properly pass switch arguments - Store %~dp0 before parse loop to avoid expansion issues - Use PowerShell splatting in Invoke-MoleCommand for proper switch handling - Rename $script:DryRun to $script:MoleDryRunMode in file_ops.ps1 to avoid variable shadowing when dot-sourcing - Handle switches passed as strings (e.g., '-ShowHelp') in mole.ps1 Main() Fixes issue where 'mole clean -DryRun' would run cleanup instead of preview.
- Fix analyze: Add timeout and depth limits to calculateDirSize() to prevent indefinite scanning on large directories like user profile - Fix purge: Add null checks for .Count property access under StrictMode - Fix uninstall: Wrap registry property access in try/catch for items without DisplayName, and add null checks throughout - Fix mole.ps1: Add null check for Arguments.Count - Add try/catch around CursorVisible calls for non-interactive terminals All 73 Pester tests and all Go tests pass.
- Reduce timeout from 5s to 500ms per directory - Limit scan depth to 3 levels for quick size estimation - Reduce max files scanned from 50k to 10k - Results now appear in 1-2 seconds instead of minutes
|
We may want to put this in a separate branch until the complete Windows implementation is complete |
- Remove existing directories before copying to prevent nested folder structures (e.g., bin/bin) - This fixes 'Unknown command' errors after installation
- Fix openInExplorer to actually execute explorer.exe (was a no-op) - Load System.Windows.Forms assembly before using Clipboard - Use Remove-SafeItem in purge for consistent safety checks - Fix Dropbox typo (was DroplboxCache)
- Fix Test-IsProtectedPath -> Test-ProtectedPath in CI workflow - Fix purge.ps1 to handle boolean return from Remove-SafeItem - Add C:\Program Files and C:\Program Files (x86) to ProtectedPaths
- Add protection checks to Go analyze tool before delete operations - Use try/finally to ensure Windows Update service restarts after cleanup - Don't count interactive uninstall as automatic success - Tighten orphaned app detection with stricter prefix matching
Some terminals send escape sequences (ESC [ A/B) instead of VirtualKeyCode for arrow keys. Now handles both methods for better terminal compatibility.
|
@NetChirp Thank you for your feedback. I will discuss with the contributors whether having a separate Windows branch would be better to avoid inconvenience for everyone. |

Adds Windows support for Mole as proposed in #273. All Windows code is isolated in the
windows/directory with zero changes to existing macOS code.This PR implements Phase 1: Core Infrastructure.
Changes
New Files in
windows/mole.ps1install.ps1README.mdgo.mod/go.sumlib/core/base.ps1lib/core/common.ps1lib/core/file_ops.ps1lib/core/log.ps1lib/core/ui.ps1Architecture