diff --git a/.github/workflows/release-windows.yml b/.github/workflows/release-windows.yml index 019aef7..8e2b66e 100644 --- a/.github/workflows/release-windows.yml +++ b/.github/workflows/release-windows.yml @@ -1,10 +1,8 @@ -name: Build Windows Release +name: Build Windows Prerelease on: push: tags: - - 'v*.*.*' # Trigger on version tags (e.g., v1.0.0) - - 'V*.*.*' # Also support uppercase V (e.g., V1.0.0) - 'v*.*.*-windows' # Windows-specific releases - 'V*.*.*-windows' # Windows-specific releases (uppercase) workflow_dispatch: # Allow manual trigger @@ -19,7 +17,7 @@ permissions: jobs: build-windows: - name: Build Windows Release Artifacts + name: Build Windows Prerelease Artifacts runs-on: windows-latest steps: @@ -31,7 +29,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v5 with: - go-version: '1.21' + go-version: '1.24.6' - name: Setup PowerShell shell: pwsh @@ -47,6 +45,7 @@ jobs: $version = "${{ github.event.inputs.version }}" } elseif ("${{ github.ref }}" -like "refs/tags/*") { $version = "${{ github.ref }}" -replace '^refs/tags/[Vv]', '' + $version = $version -replace '-windows$', '' } else { $content = Get-Content mole.ps1 -Raw if ($content -match '\$script:MOLE_VER\s*=\s*"([^"]+)"') { @@ -79,6 +78,12 @@ jobs: shell: pwsh run: | & scripts\build-release.ps1 -Version ${{ steps.version.outputs.VERSION }} + + - name: Prepare raw TUI binary assets + shell: pwsh + run: | + Copy-Item "bin\analyze.exe" "release\analyze-windows-x64.exe" -Force + Copy-Item "bin\status.exe" "release\status-windows-x64.exe" -Force - name: Build standalone EXE @@ -117,22 +122,47 @@ jobs: name: checksums path: release/SHA256SUMS.txt if-no-files-found: error + + - name: Upload TUI binaries + uses: actions/upload-artifact@v4 + with: + name: tui-binaries + path: | + release/analyze-windows-x64.exe + release/status-windows-x64.exe + if-no-files-found: error + + - name: Collect release files + id: release_files + shell: pwsh + run: | + $files = @( + "release/mole-${{ steps.version.outputs.VERSION }}-x64.zip", + "release/analyze-windows-x64.exe", + "release/status-windows-x64.exe", + "release/SHA256SUMS.txt" + ) + $optionalExe = "release/mole-${{ steps.version.outputs.VERSION }}-x64.exe" + if (Test-Path $optionalExe) { + $files += $optionalExe + } + "files<&1 - if ($LASTEXITCODE -ne 0) { - Write-Host "mole.ps1 -ShowHelp failed" -ForegroundColor Red - exit 1 - } - Write-Host "✓ mole.ps1 works on ${{ matrix.os }}" - - - name: Test command scripts - shell: powershell - run: | - $commands = @("clean", "uninstall", "optimize", "purge", "analyze", "status") - foreach ($cmd in $commands) { - $scriptPath = "windows\bin\$cmd.ps1" - if (Test-Path $scriptPath) { - $result = & powershell -ExecutionPolicy Bypass -File $scriptPath -ShowHelp 2>&1 - if ($LASTEXITCODE -ne 0) { - Write-Host "✗ $cmd.ps1 failed" -ForegroundColor Red - exit 1 - } - Write-Host "✓ $cmd.ps1 works" - } - } - - security: - name: Security Checks - runs-on: windows-latest - steps: - - uses: actions/checkout@v4 - - - name: Check for unsafe patterns - shell: pwsh - run: | - Write-Host "Checking for unsafe removal patterns..." - - $unsafePatterns = @( - "Remove-Item.*-Recurse.*-Force.*\\\$env:SystemRoot", - "Remove-Item.*-Recurse.*-Force.*C:\\Windows", - "Remove-Item.*-Recurse.*-Force.*C:\\Program Files" - ) - - $files = Get-ChildItem -Path windows -Filter "*.ps1" -Recurse - $issues = @() - - foreach ($file in $files) { - $content = Get-Content $file.FullName -Raw - foreach ($pattern in $unsafePatterns) { - if ($content -match $pattern) { - $issues += "$($file.Name): matches unsafe pattern" - } - } - } - - if ($issues.Count -gt 0) { - Write-Host "Unsafe patterns found:" -ForegroundColor Red - $issues | ForEach-Object { Write-Host " $_" -ForegroundColor Red } - exit 1 - } - - Write-Host "✓ No unsafe patterns found" -ForegroundColor Green - - - name: Verify protection checks - shell: pwsh - run: | - Write-Host "Verifying protection logic..." - - # Source file_ops to get Test-IsProtectedPath - . windows\lib\core\base.ps1 - . windows\lib\core\file_ops.ps1 - - $protectedPaths = @( - "C:\Windows", - "C:\Windows\System32", - "C:\Program Files", - "C:\Program Files (x86)" - ) - - foreach ($path in $protectedPaths) { - if (-not (Test-ProtectedPath -Path $path)) { - Write-Host "✗ $path should be protected!" -ForegroundColor Red - exit 1 - } - Write-Host "✓ $path is protected" -ForegroundColor Green - } diff --git a/.gitignore b/.gitignore index 3ee0085..91d1853 100644 --- a/.gitignore +++ b/.gitignore @@ -1,21 +1,86 @@ # Windows Mole - .gitignore -# Build artifacts -bin/*.exe +# OS files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db -# Go build cache -.gocache/ - -# IDE files +# Editor files +*~ +*.swp +*.swo .idea/ .vscode/ *.code-workspace +# Logs +*.log +logs/ + +# Temporary files +tmp/ +temp/ +*.tmp +*.temp + +# Cache +.cache/ +*.cache +.gocache/ +.gomod/ + +# Backup files +*.bak +*.backup + +# System files +*.pid +*.lock + +# AI / local assistant files +.agents/ +.claude/ +.gemini/ +.kiro/ +ANTIGRAVITY.md +AGENTS.md +CLAUDE.md +GEMINI.md +WARP.md +.cursorrules +cmd/AGENTS.md +lib/AGENTS.md +tests/AGENTS.md + +# Build artifacts +bin/*.exe +cmd/analyze/*.exe +cmd/status/*.exe +release/ +mole-windows.zip +mole-*-x64.zip +mole-*-x64.exe +mole-*-x64.msi +SHA256SUMS.txt + # Test artifacts *.test +*.out coverage.out +coverage.html +coverage-go.out +coverage-pester.xml +test-results.xml +tests/tmp-*/ +tests/*.tmp +tests/*.log -# Main branch specific files -ANTIGRAVITY.md -CLAUDE.md +# Branch-specific local notes windows-readme-update.md +mole_guidelines.md +run_tests.ps1 +session.json diff --git a/README.md b/README.md index c1c9bae..e64deb2 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@

Stars - Version + Channel License Commits Twitter @@ -23,6 +23,7 @@ - **Smart uninstaller**: Thoroughly removes apps along with AppData, preferences, and hidden remnants - **Disk insights**: Visualizes usage, manages large files, and refreshes system services - **Live monitoring**: Real-time stats for CPU, memory, disk, and network to diagnose performance issues +- **Source channel updates**: Install from the `windows` branch and refresh to the latest source with `mo update` ## Platform Support @@ -32,7 +33,8 @@ Mole is designed for Windows 10/11. This is the native Windows version ported fr - Windows 10/11 - PowerShell 5.1 or later (pre-installed on Windows 10/11) -- Go 1.24+ (for building TUI tools) +- Git (required for source-channel install and `mo update`) +- Go 1.24+ (optional, only needed when building TUI tools locally) ## Quick Start @@ -44,25 +46,23 @@ Mole is designed for Windows 10/11. This is the native Windows version ported fr iwr -useb https://raw.githubusercontent.com/tw93/Mole/windows/quick-install.ps1 | iex ``` -This will automatically download and install Mole with PATH configuration. +This will clone the latest `windows` branch into your install directory and configure PATH. ### Manual Installation If you prefer to review the code first or customize the installation: ```powershell -# Clone the repository -git clone https://github.com/tw93/Mole.git -cd Mole +# Clone the windows branch into your install directory +$installDir = "$env:LOCALAPPDATA\Mole" +git clone --branch windows https://github.com/tw93/Mole.git $installDir +cd $installDir -# Switch to windows branch -git checkout windows - -# Run the installer -.\install.ps1 -AddToPath +# Run the installer in place (keeps .git for mo update) +.\install.ps1 -InstallDir $installDir -AddToPath # Optional: Create Start Menu shortcut -.\install.ps1 -AddToPath -CreateShortcut +.\install.ps1 -InstallDir $installDir -AddToPath -CreateShortcut ``` Run: @@ -74,6 +74,8 @@ mo uninstall # Remove apps + leftovers mo optimize # Refresh caches & services mo analyze # Visual disk explorer mo status # Live system health dashboard +mo update # Pull the latest windows source +mo remove # Remove Mole from this system mo purge # Clean project build artifacts mo --help # Show help @@ -88,6 +90,38 @@ mo optimize --debug # Run with detailed operation logs mo purge --paths # Configure project scan directories ``` +Source-channel installs can later be refreshed with: + +```powershell +mo update +``` + +If a matching Windows prerelease exists for the installed version, Mole will reuse/download prebuilt `analyze` and `status` binaries before falling back to a local Go build. + +## macOS Parity + +Windows is closest to macOS on these commands: + +- `clean` +- `uninstall` +- `optimize` +- `analyze` +- `status` +- `purge` +- `update` +- `remove` + +Still missing or intentionally platform-specific compared with `main`: + +- `installer`: no dedicated Windows installer-file cleanup command yet +- `completion`: no PowerShell completion setup command yet +- `touchid`: macOS-only, not applicable on Windows +- Release channels: Windows currently uses a git source channel, not Homebrew/stable release installs +- Update options: `mo update --nightly` is not implemented on Windows +- Optimization controls: `mo optimize --whitelist` is not implemented on Windows +- Some UI depth: macOS `status` and `analyze` expose richer device-specific details than Windows today +- Windows prereleases use `Vx.y.z-windows` tags so they stay isolated from the macOS stable release channel + ## Tips - **Safety**: Built with strict protections. Preview changes with `mo clean --dry-run`. @@ -245,14 +279,14 @@ Custom scan paths can be configured with `mo purge --paths`. ### Manual Installation ```powershell -# Install to custom location +# Install to custom location from a cloned windows branch .\install.ps1 -InstallDir C:\Tools\Mole -AddToPath # Create Start Menu shortcut -.\install.ps1 -AddToPath -CreateShortcut +.\install.ps1 -InstallDir C:\Tools\Mole -AddToPath -CreateShortcut -# Optional: Custom install location -.\install.ps1 -InstallDir C:\Tools\Mole -AddToPath +# Refresh the source channel later +mo update ``` ### Uninstall @@ -280,12 +314,14 @@ mole/ (windows branch) ├── go.mod # Go module definition ├── go.sum # Go dependencies ├── bin/ -301: │ ├── clean.ps1 # Deep cleanup orchestrator -302: │ ├── uninstall.ps1 # Interactive app uninstaller -303: │ ├── optimize.ps1 # System optimization -304: │ ├── purge.ps1 # Project artifact cleanup -305: │ ├── analyze.ps1 # Disk analyzer wrapper -306: │ └── status.ps1 # Status monitor wrapper +│ ├── clean.ps1 # Deep cleanup orchestrator +│ ├── uninstall.ps1 # Interactive app uninstaller +│ ├── optimize.ps1 # System optimization +│ ├── purge.ps1 # Project artifact cleanup +│ ├── analyze.ps1 # Disk analyzer wrapper +│ ├── status.ps1 # Status monitor wrapper +│ ├── update.ps1 # Source channel updater +│ └── remove.ps1 # Self-uninstall wrapper ├── cmd/ │ ├── analyze/ # Disk analyzer (Go TUI) │ │ └── main.go @@ -297,6 +333,7 @@ mole/ (windows branch) │ ├── common.ps1 # Common functions loader │ ├── file_ops.ps1 # Safe file operations │ ├── log.ps1 # Logging functions + │ ├── tui_binaries.ps1 # TUI binary restore/build helpers │ └── ui.ps1 # Interactive UI components └── clean/ ├── user.ps1 # User cleanup (temp, downloads, etc.) @@ -308,7 +345,7 @@ mole/ (windows branch) ## Building TUI Tools -The analyze and status commands require Go to be installed: +Install Go if you want to build the analyze and status tools locally: ```powershell # From the repository root @@ -320,7 +357,8 @@ make build go build -o bin/analyze.exe ./cmd/analyze/ go build -o bin/status.exe ./cmd/status/ -# The wrapper scripts will auto-build if Go is available +# The wrapper scripts try bin/ first, then Windows prerelease assets, +# then auto-build if Go is available ``` ## Support @@ -351,6 +389,8 @@ go build -o bin/status.exe ./cmd/status/ - [x] `cmd/status/` - Real-time system monitor (Go) - [x] `bin/analyze.ps1` - Analyzer wrapper - [x] `bin/status.ps1` - Status wrapper +- [x] `bin/update.ps1` - Source channel updater +- [x] `bin/remove.ps1` - Self-uninstall wrapper ### Phase 4: Testing & CI (Planned) diff --git a/bin/analyze.ps1 b/bin/analyze.ps1 index adfd48f..67faa42 100644 --- a/bin/analyze.ps1 +++ b/bin/analyze.ps1 @@ -15,7 +15,8 @@ $ErrorActionPreference = "Stop" # Script location $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path $windowsDir = Split-Path -Parent $scriptDir -$binPath = Join-Path $windowsDir "bin\analyze.exe" +$defaultBinPath = Join-Path $windowsDir "bin\analyze.exe" +. (Join-Path $windowsDir "lib\core\tui_binaries.ps1") # Help function Show-AnalyzeHelp { @@ -47,29 +48,11 @@ if ($ShowHelp) { return } -# Check if binary exists -if (-not (Test-Path $binPath)) { - Write-Host "Building analyze tool..." -ForegroundColor Cyan - - $cmdDir = Join-Path $windowsDir "cmd\analyze" - $binDir = Join-Path $windowsDir "bin" - - if (-not (Test-Path $binDir)) { - New-Item -ItemType Directory -Path $binDir -Force | Out-Null - } - - Push-Location $windowsDir - try { - $result = & go build -o "$binPath" "./cmd/analyze/" 2>&1 - if ($LASTEXITCODE -ne 0) { - Write-Host "Failed to build analyze tool: $result" -ForegroundColor Red - Pop-Location - return - } - } - finally { - Pop-Location - } +$binPath = Ensure-TuiBinary -Name "analyze" -WindowsDir $windowsDir -DestinationPath $defaultBinPath -SourcePath "./cmd/analyze/" +if (-not $binPath) { + Write-Host "Analyze binary not found, no prerelease asset was available, and Go 1.24+ is not installed." -ForegroundColor Red + Write-Host "Install Go or wait for a Windows prerelease asset that includes analyze.exe." -ForegroundColor Yellow + exit 1 } # Set path environment variable if provided diff --git a/bin/remove.ps1 b/bin/remove.ps1 new file mode 100644 index 0000000..f4d6ebf --- /dev/null +++ b/bin/remove.ps1 @@ -0,0 +1,40 @@ +# Mole - Remove Command +# Removes Mole from the current installation directory. + +#Requires -Version 5.1 +param( + [Alias('h')] + [switch]$ShowHelp +) + +$ErrorActionPreference = "Stop" + +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$windowsDir = Split-Path -Parent $scriptDir +$installScript = Join-Path $windowsDir "install.ps1" + +function Show-RemoveHelp { + $esc = [char]27 + Write-Host "" + Write-Host "$esc[1;35mmo remove$esc[0m - Remove Mole from this system" + Write-Host "" + Write-Host "$esc[33mUsage:$esc[0m mo remove" + Write-Host "" + Write-Host "$esc[33mBehavior:$esc[0m" + Write-Host " - Removes the current Mole installation directory" + Write-Host " - Removes PATH entries created for this install" + Write-Host " - Prompts before deleting Mole config files" + Write-Host "" +} + +if ($ShowHelp) { + Show-RemoveHelp + return +} + +if (-not (Test-Path $installScript)) { + Write-Host "Installer not found at: $installScript" -ForegroundColor Red + exit 1 +} + +& $installScript -InstallDir $windowsDir -Uninstall diff --git a/bin/status.ps1 b/bin/status.ps1 index bddac52..857185c 100644 --- a/bin/status.ps1 +++ b/bin/status.ps1 @@ -12,7 +12,8 @@ $ErrorActionPreference = "Stop" # Script location $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path $windowsDir = Split-Path -Parent $scriptDir -$binPath = Join-Path $windowsDir "bin\status.exe" +$defaultBinPath = Join-Path $windowsDir "bin\status.exe" +. (Join-Path $windowsDir "lib\core\tui_binaries.ps1") # Help function Show-StatusHelp { @@ -45,29 +46,11 @@ if ($ShowHelp) { return } -# Check if binary exists -if (-not (Test-Path $binPath)) { - Write-Host "Building status tool..." -ForegroundColor Cyan - - $cmdDir = Join-Path $windowsDir "cmd\status" - $binDir = Join-Path $windowsDir "bin" - - if (-not (Test-Path $binDir)) { - New-Item -ItemType Directory -Path $binDir -Force | Out-Null - } - - Push-Location $windowsDir - try { - $result = & go build -o "$binPath" "./cmd/status/" 2>&1 - if ($LASTEXITCODE -ne 0) { - Write-Host "Failed to build status tool: $result" -ForegroundColor Red - Pop-Location - return - } - } - finally { - Pop-Location - } +$binPath = Ensure-TuiBinary -Name "status" -WindowsDir $windowsDir -DestinationPath $defaultBinPath -SourcePath "./cmd/status/" +if (-not $binPath) { + Write-Host "Status binary not found, no prerelease asset was available, and Go 1.24+ is not installed." -ForegroundColor Red + Write-Host "Install Go or wait for a Windows prerelease asset that includes status.exe." -ForegroundColor Yellow + exit 1 } # Run the binary diff --git a/bin/update.ps1 b/bin/update.ps1 new file mode 100644 index 0000000..fdd6ba1 --- /dev/null +++ b/bin/update.ps1 @@ -0,0 +1,126 @@ +# Mole - Update Command +# Updates a source-channel installation from the windows branch. + +#Requires -Version 5.1 +param( + [Alias('h')] + [switch]$ShowHelp +) + +$ErrorActionPreference = "Stop" + +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path +$windowsDir = Split-Path -Parent $scriptDir +$installScript = Join-Path $windowsDir "install.ps1" +$gitDir = Join-Path $windowsDir ".git" + +function Show-UpdateHelp { + $esc = [char]27 + Write-Host "" + Write-Host "$esc[1;35mmo update$esc[0m - Update the Windows source channel" + Write-Host "" + Write-Host "$esc[33mUsage:$esc[0m mo update" + Write-Host "" + Write-Host "$esc[33mBehavior:$esc[0m" + Write-Host " - Pulls the latest commit from origin/windows" + Write-Host " - Re-runs the local installer in-place" + Write-Host " - Rebuilds analyze/status if Go is available" + Write-Host "" + Write-Host "$esc[33mNotes:$esc[0m" + Write-Host " - Works only for git-based source installs" + Write-Host " - Legacy copied installs should be reinstalled with quick-install" + Write-Host "" +} + +function Test-InstallDirOnUserPath { + param([string]$Path) + + $currentPath = [Environment]::GetEnvironmentVariable("PATH", "User") + if (-not $currentPath) { + return $false + } + + return $currentPath -split ";" | Where-Object { $_ -eq $Path } +} + +if ($ShowHelp) { + Show-UpdateHelp + return +} + +if (-not (Get-Command git -ErrorAction SilentlyContinue)) { + Write-Host "Git is not installed. Install Git to use 'mo update'." -ForegroundColor Red + exit 1 +} + +if (-not (Test-Path $installScript)) { + Write-Host "Installer not found at: $installScript" -ForegroundColor Red + exit 1 +} + +if (-not (Test-Path $gitDir)) { + Write-Host "This installation is not a git-based source install." -ForegroundColor Red + Write-Host "Reinstall with quick-install to enable 'mo update'." -ForegroundColor Yellow + exit 1 +} + +$dirtyOutput = & git -C $windowsDir status --porcelain --untracked-files=no 2>&1 +if ($LASTEXITCODE -ne 0) { + Write-Host "Failed to inspect git status: $dirtyOutput" -ForegroundColor Red + exit 1 +} + +if ($dirtyOutput) { + Write-Host "Local tracked changes detected in the installation directory." -ForegroundColor Red + Write-Host "Commit or discard them before running 'mo update'." -ForegroundColor Yellow + exit 1 +} + +$remote = (& git -C $windowsDir remote get-url origin 2>&1).Trim() +if ($LASTEXITCODE -ne 0 -or -not $remote) { + Write-Host "Git remote 'origin' is not configured for this install." -ForegroundColor Red + exit 1 +} + +$branch = (& git -C $windowsDir branch --show-current 2>&1).Trim() +if ($LASTEXITCODE -ne 0 -or -not $branch) { + $branch = "windows" +} + +$before = (& git -C $windowsDir rev-parse --short HEAD 2>&1).Trim() +if ($LASTEXITCODE -ne 0 -or -not $before) { + Write-Host "Failed to read current revision." -ForegroundColor Red + exit 1 +} + +Write-Host "Updating source from $remote ($branch)..." -ForegroundColor Cyan + +$pullOutput = & git -C $windowsDir pull --ff-only origin $branch 2>&1 +if ($LASTEXITCODE -ne 0) { + Write-Host "Failed to update source: $pullOutput" -ForegroundColor Red + exit 1 +} + +$after = (& git -C $windowsDir rev-parse --short HEAD 2>&1).Trim() +if ($LASTEXITCODE -ne 0 -or -not $after) { + Write-Host "Updated source, but failed to read the new revision." -ForegroundColor Red + exit 1 +} + +if ($after -eq $before) { + Write-Host "Already up to date at $after." -ForegroundColor Green +} +else { + Write-Host "Updated source: $before -> $after" -ForegroundColor Green +} + +$installArgs = @( + "-InstallDir", $windowsDir +) + +if (Test-InstallDirOnUserPath -Path $windowsDir) { + $installArgs += "-AddToPath" +} + +Write-Host "Refreshing local installation..." -ForegroundColor Cyan +& $installScript @installArgs diff --git a/cmd/analyze/analyze.exe b/cmd/analyze/analyze.exe deleted file mode 100644 index 88661c1..0000000 Binary files a/cmd/analyze/analyze.exe and /dev/null differ diff --git a/install.ps1 b/install.ps1 index 6ff6903..e74bdbe 100644 --- a/install.ps1 +++ b/install.ps1 @@ -39,6 +39,8 @@ $script:Colors = @{ NC = "$($script:ESC)[0m" } +. (Join-Path $script:SourceDir "lib\core\tui_binaries.ps1") + # ============================================================================ # Helpers # ============================================================================ @@ -67,6 +69,21 @@ function Write-MoleError { Write-Host " $($c.Red)ERROR$($c.NC) $Message" } +function Get-NormalizedPath { + param([string]$Path) + + return [System.IO.Path]::GetFullPath($Path).TrimEnd('\', '/') +} + +function Test-SamePath { + param( + [string]$PathA, + [string]$PathB + ) + + return (Get-NormalizedPath -Path $PathA) -eq (Get-NormalizedPath -Path $PathB) +} + function Show-Banner { $c = $script:Colors Write-Host "" @@ -227,12 +244,38 @@ function Remove-StartMenuShortcut { # Install # ============================================================================ +function Ensure-OptionalTuiTools { + param([string]$RootDir) + + Write-Info "Ensuring optional TUI tools..." + + $tools = @( + @{ Name = "analyze"; Output = "bin\analyze.exe"; Source = "./cmd/analyze/" }, + @{ Name = "status"; Output = "bin\status.exe"; Source = "./cmd/status/" } + ) + + $version = Get-MoleVersionFromScriptFile -WindowsDir $RootDir + + foreach ($tool in $tools) { + $destination = Join-Path $RootDir $tool.Output + $binPath = Ensure-TuiBinary -Name $tool.Name -WindowsDir $RootDir -DestinationPath $destination -SourcePath $tool.Source -Version $version + if ($binPath) { + Write-Success "Ready: $($tool.Name).exe" + } + else { + Write-MoleWarning "Could not prepare $($tool.Name).exe. Install Go or wait for a Windows prerelease asset." + } + } +} + function Install-Mole { Write-Info "Installing Mole v$script:VERSION..." Write-Host "" + $inPlaceInstall = Test-SamePath -PathA $script:SourceDir -PathB $InstallDir + # Check if already installed - if ((Test-Path $InstallDir) -and -not $Force) { + if ((Test-Path $InstallDir) -and -not $Force -and -not $inPlaceInstall) { Write-MoleError "Mole is already installed at: $InstallDir" Write-Host "" Write-Host " Use -Force to overwrite or -Uninstall to remove first" @@ -252,39 +295,44 @@ function Install-Mole { } } - # Copy files - Write-Info "Copying files..." + if ($inPlaceInstall) { + Write-Info "Using in-place source installation in: $InstallDir" + } + else { + # Copy files + Write-Info "Copying files..." - $filesToCopy = @( - "mole.ps1" - "go.mod" - "go.sum" - "bin" - "lib" - "cmd" - ) + $filesToCopy = @( + "mole.ps1" + "go.mod" + "go.sum" + "bin" + "lib" + "cmd" + ) - foreach ($item in $filesToCopy) { - $src = Join-Path $script:SourceDir $item - $dst = Join-Path $InstallDir $item + foreach ($item in $filesToCopy) { + $src = Join-Path $script:SourceDir $item + $dst = Join-Path $InstallDir $item - if (Test-Path $src) { - try { - if ((Get-Item $src).PSIsContainer) { - # For directories, remove destination first if exists to avoid nesting - if (Test-Path $dst) { - Remove-Item -Path $dst -Recurse -Force + if (Test-Path $src) { + try { + if ((Get-Item $src).PSIsContainer) { + # For directories, remove destination first if exists to avoid nesting + if (Test-Path $dst) { + Remove-Item -Path $dst -Recurse -Force + } + Copy-Item -Path $src -Destination $dst -Recurse -Force } - Copy-Item -Path $src -Destination $dst -Recurse -Force + else { + Copy-Item -Path $src -Destination $dst -Force + } + Write-Success "Copied: $item" } - else { - Copy-Item -Path $src -Destination $dst -Force + catch { + Write-MoleError "Failed to copy $item`: $_" + return $false } - Write-Success "Copied: $item" - } - catch { - Write-MoleError "Failed to copy $item`: $_" - return $false } } } @@ -298,6 +346,9 @@ function Install-Mole { } } + Write-Host "" + Ensure-OptionalTuiTools -RootDir $InstallDir + # Create launcher batch file for easier access # Note: Store %~dp0 immediately to avoid issues with delayed expansion in the parse loop $batchContent = @" @@ -355,6 +406,8 @@ powershell.exe -ExecutionPolicy Bypass -NoLogo -NoProfile -Command "& '%MOLE_DIR Write-Host " .\install.ps1 -AddToPath" } + Write-Host "" + Write-Host " Run 'mo update' to pull the latest source from the windows branch" Write-Host "" return $true } diff --git a/lib/core/tui_binaries.ps1 b/lib/core/tui_binaries.ps1 new file mode 100644 index 0000000..2874501 --- /dev/null +++ b/lib/core/tui_binaries.ps1 @@ -0,0 +1,184 @@ +# Mole - TUI binary helper +# Resolves, downloads, or builds analyze/status executables on Windows. + +#Requires -Version 5.1 +Set-StrictMode -Version Latest + +if ((Get-Variable -Name 'MOLE_TUI_BINARIES_LOADED' -Scope Script -ErrorAction SilentlyContinue) -and $script:MOLE_TUI_BINARIES_LOADED) { + return +} +$script:MOLE_TUI_BINARIES_LOADED = $true + +$script:MoleGitHubRepo = "tw93/Mole" +$script:MoleGitHubApiRoot = "https://api.github.com/repos/$($script:MoleGitHubRepo)" +$script:MoleGitHubHeaders = @{ + "User-Agent" = "Mole-Windows" + "Accept" = "application/vnd.github+json" +} + +function Get-MoleVersionFromScriptFile { + param([string]$WindowsDir) + + $moleScript = Join-Path $WindowsDir "mole.ps1" + if (-not (Test-Path $moleScript)) { + return $null + } + + $content = Get-Content $moleScript -Raw + if ($content -match '\$script:MOLE_VER\s*=\s*"([^"]+)"') { + return $Matches[1] + } + + return $null +} + +function Get-TuiBinaryAssetName { + param([string]$Name) + + return "$Name-windows-x64.exe" +} + +function Resolve-TuiBinaryPath { + param( + [string]$WindowsDir, + [string]$Name + ) + + $candidates = @( + (Join-Path $WindowsDir "bin\$Name.exe"), + (Join-Path $WindowsDir "$Name.exe") + ) + + foreach ($candidate in $candidates) { + if (Test-Path $candidate) { + return $candidate + } + } + + return $null +} + +function Get-WindowsPrereleaseReleaseInfo { + param([string]$Version) + + if (-not $Version) { + return $null + } + + $tagCandidates = @( + "V$Version-windows", + "v$Version-windows" + ) + + foreach ($tag in $tagCandidates) { + $uri = "$($script:MoleGitHubApiRoot)/releases/tags/$tag" + try { + return Invoke-RestMethod -Uri $uri -Headers $script:MoleGitHubHeaders -Method Get + } + catch { + continue + } + } + + return $null +} + +function Restore-PrebuiltTuiBinary { + param( + [string]$Name, + [string]$WindowsDir, + [string]$DestinationPath, + [string]$Version + ) + + $releaseInfo = Get-WindowsPrereleaseReleaseInfo -Version $Version + if (-not $releaseInfo) { + return $false + } + + $assetName = Get-TuiBinaryAssetName -Name $Name + $asset = $releaseInfo.assets | Where-Object { $_.name -eq $assetName } | Select-Object -First 1 + if (-not $asset) { + return $false + } + + $binDir = Split-Path -Parent $DestinationPath + if (-not (Test-Path $binDir)) { + New-Item -ItemType Directory -Path $binDir -Force | Out-Null + } + + Write-Host "Downloading prebuilt $Name tool..." -ForegroundColor Cyan + + try { + Invoke-WebRequest -Uri $asset.browser_download_url -Headers $script:MoleGitHubHeaders -OutFile $DestinationPath -UseBasicParsing + return (Test-Path $DestinationPath) + } + catch { + if (Test-Path $DestinationPath) { + Remove-Item $DestinationPath -Force -ErrorAction SilentlyContinue + } + return $false + } +} + +function Build-TuiBinary { + param( + [string]$Name, + [string]$WindowsDir, + [string]$DestinationPath, + [string]$SourcePath + ) + + $binDir = Split-Path -Parent $DestinationPath + if (-not (Test-Path $binDir)) { + New-Item -ItemType Directory -Path $binDir -Force | Out-Null + } + + Write-Host "Building $Name tool..." -ForegroundColor Cyan + + Push-Location $WindowsDir + try { + $result = & go build -o "$DestinationPath" $SourcePath 2>&1 + if ($LASTEXITCODE -ne 0) { + Write-Host "Failed to build $Name tool: $result" -ForegroundColor Red + return $false + } + } + finally { + Pop-Location + } + + return $true +} + +function Ensure-TuiBinary { + param( + [string]$Name, + [string]$WindowsDir, + [string]$DestinationPath, + [string]$SourcePath, + [string]$Version + ) + + $existingBin = Resolve-TuiBinaryPath -WindowsDir $WindowsDir -Name $Name + if ($existingBin) { + return $existingBin + } + + if (-not $Version) { + $Version = Get-MoleVersionFromScriptFile -WindowsDir $WindowsDir + } + + if (Restore-PrebuiltTuiBinary -Name $Name -WindowsDir $WindowsDir -DestinationPath $DestinationPath -Version $Version) { + return $DestinationPath + } + + if (Get-Command go -ErrorAction SilentlyContinue) { + if (Build-TuiBinary -Name $Name -WindowsDir $WindowsDir -DestinationPath $DestinationPath -SourcePath $SourcePath) { + return $DestinationPath + } + return $null + } + + return $null +} diff --git a/mole.ps1 b/mole.ps1 index ccbb9eb..53c0914 100644 --- a/mole.ps1 +++ b/mole.ps1 @@ -65,6 +65,8 @@ function Show-MainHelp { Write-Host " ${cyan}optimize${nc} System optimization and repairs" Write-Host " ${cyan}analyze${nc} Disk space analyzer" Write-Host " ${cyan}status${nc} System monitor" + Write-Host " ${cyan}update${nc} Update the source channel" + Write-Host " ${cyan}remove${nc} Remove Mole from this system" Write-Host " ${cyan}purge${nc} Clean project artifacts" Write-Host "" Write-Host " ${green}OPTIONS:${nc}" @@ -80,15 +82,11 @@ function Show-MainHelp { Write-Host " ${gray}mo uninstall${nc} ${gray}# Uninstall apps${nc}" Write-Host " ${gray}mo analyze${nc} ${gray}# Disk analyzer${nc}" Write-Host " ${gray}mo status${nc} ${gray}# System monitor${nc}" + Write-Host " ${gray}mo update${nc} ${gray}# Pull latest windows source${nc}" + Write-Host " ${gray}mo remove${nc} ${gray}# Remove Mole from this system${nc}" Write-Host " ${gray}mo optimize${nc} ${gray}# Optimize system (includes repairs)${nc}" Write-Host " ${gray}mo optimize --dry-run${nc} ${gray}# Preview optimizations${nc}" Write-Host " ${gray}mo purge${nc} ${gray}# Clean dev artifacts${nc}" - Write-Host " ${gray}mo${nc} ${gray}# Interactive menu${nc}" - Write-Host " ${gray}mo clean${nc} ${gray}# Deep cleanup${nc}" - Write-Host " ${gray}mo clean --dry-run${nc} ${gray}# Preview cleanup${nc}" - Write-Host " ${gray}mo optimize${nc} ${gray}# Optimize system${nc}" - Write-Host " ${gray}mo optimize --dry-run${nc} ${gray}# Preview optimization${nc}" - Write-Host " ${gray}mo uninstall${nc} ${gray}# Uninstall apps${nc}" Write-Host "" Write-Host " ${green}ENVIRONMENT:${nc}" Write-Host "" @@ -135,6 +133,18 @@ function Show-MainMenu { Command = "status" Icon = $script:Icons.Solid } + @{ + Name = "Update" + Description = "Pull latest source" + Command = "update" + Icon = $script:Icons.Arrow + } + @{ + Name = "Remove" + Description = "Uninstall Mole" + Command = "remove" + Icon = $script:Icons.Trash + } @{ Name = "Purge" Description = "Clean dev artifacts" @@ -273,7 +283,7 @@ function Main { # If command specified, route to it if ($effectiveCommand) { - $validCommands = @("clean", "uninstall", "analyze", "status", "optimize", "purge") + $validCommands = @("clean", "uninstall", "analyze", "status", "optimize", "update", "remove", "purge") if ($effectiveCommand -in $validCommands) { Invoke-MoleCommand -CommandName $effectiveCommand -Arguments $CommandArgs diff --git a/quick-install.ps1 b/quick-install.ps1 index b659912..f8be772 100644 --- a/quick-install.ps1 +++ b/quick-install.ps1 @@ -4,6 +4,10 @@ #Requires -Version 5.1 +param( + [string]$InstallDir = "$env:LOCALAPPDATA\Mole" +) + $ErrorActionPreference = "Stop" Set-StrictMode -Version Latest @@ -32,11 +36,17 @@ function Write-ErrorMsg { Write-Host " $($Colors.Red)✗$($Colors.NC) $Message" } +function Test-SourceInstall { + param([string]$Path) + + return (Test-Path (Join-Path $Path ".git")) +} + # Main installation try { Write-Host "" Write-Host " $($Colors.Cyan)Mole Quick Installer$($Colors.NC)" - Write-Host " $($Colors.Yellow)Installing experimental Windows version...$($Colors.NC)" + Write-Host " $($Colors.Yellow)Installing experimental Windows source channel...$($Colors.NC)" Write-Host "" # Check prerequisites @@ -50,38 +60,61 @@ try { Write-Success "Git found" - # Create temp directory - $TempDir = Join-Path $env:TEMP "mole-install-$(Get-Random)" - Write-Step "Downloading Mole..." + if (Test-Path $InstallDir) { + if (Test-SourceInstall -Path $InstallDir) { + Write-Step "Existing source install found, refreshing..." - # Clone windows branch - git clone --quiet --depth 1 --branch windows https://github.com/tw93/Mole.git $TempDir 2>&1 | Out-Null + Push-Location $InstallDir + try { + git fetch --quiet origin windows 2>&1 | Out-Null + if ($LASTEXITCODE -ne 0) { + Write-ErrorMsg "Failed to fetch latest source" + exit 1 + } - if (-not (Test-Path "$TempDir\install.ps1")) { - Write-ErrorMsg "Failed to download installer" - exit 1 + git pull --ff-only origin windows 2>&1 | Out-Null + if ($LASTEXITCODE -ne 0) { + Write-ErrorMsg "Failed to fast-forward source install" + exit 1 + } + } + finally { + Pop-Location + } + } + else { + Write-ErrorMsg "Install directory already exists and is not a source install: $InstallDir" + Write-Host " Remove it first or reinstall with the latest quick installer." + exit 1 + } } + else { + Write-Step "Cloning Mole source..." - Write-Success "Downloaded to temp directory" + git clone --quiet --depth 1 --branch windows https://github.com/tw93/Mole.git $InstallDir 2>&1 | Out-Null + + if (-not (Test-Path (Join-Path $InstallDir "install.ps1"))) { + Write-ErrorMsg "Failed to clone source installer" + exit 1 + } + + Write-Success "Cloned source to $InstallDir" + } # Run installer Write-Step "Running installer..." Write-Host "" - & "$TempDir\install.ps1" -AddToPath + & (Join-Path $InstallDir "install.ps1") -InstallDir $InstallDir -AddToPath Write-Host "" Write-Success "Installation complete!" Write-Host "" Write-Host " Run ${Colors.Green}mole$($Colors.NC) to get started" + Write-Host " Run ${Colors.Green}mo update$($Colors.NC) to pull the latest windows source later" Write-Host "" } catch { Write-ErrorMsg "Installation failed: $_" exit 1 -} finally { - # Cleanup - if (Test-Path $TempDir) { - Remove-Item $TempDir -Recurse -Force -ErrorAction SilentlyContinue - } } diff --git a/scripts/build-release.ps1 b/scripts/build-release.ps1 index 3b769b5..1fbf7ea 100644 --- a/scripts/build-release.ps1 +++ b/scripts/build-release.ps1 @@ -211,6 +211,8 @@ New-Item -ItemType Directory -Path $tempBuildDir -Force | Out-Null $filesToInclude = @( @{Source = "$projectRoot\mole.ps1"; Dest = "$tempBuildDir\mole.ps1"}, @{Source = "$projectRoot\install.ps1"; Dest = "$tempBuildDir\install.ps1"}, + @{Source = "$projectRoot\go.mod"; Dest = "$tempBuildDir\go.mod"}, + @{Source = "$projectRoot\go.sum"; Dest = "$tempBuildDir\go.sum"}, @{Source = "$projectRoot\LICENSE"; Dest = "$tempBuildDir\LICENSE"}, @{Source = "$projectRoot\README.md"; Dest = "$tempBuildDir\README.md"} ) diff --git a/tests/Commands.Tests.ps1 b/tests/Commands.Tests.ps1 index 82f5b91..6536cb8 100644 --- a/tests/Commands.Tests.ps1 +++ b/tests/Commands.Tests.ps1 @@ -109,6 +109,36 @@ Describe "Status Command" { } } +Describe "Update Command" { + Context "Help Display" { + It "Should show help without error" { + $result = & powershell -ExecutionPolicy Bypass -File "$script:BinDir\update.ps1" -ShowHelp 2>&1 + $result | Should -Not -BeNullOrEmpty + $LASTEXITCODE | Should -Be 0 + } + + It "Should explain the source channel" { + $result = & powershell -ExecutionPolicy Bypass -File "$script:BinDir\update.ps1" -ShowHelp 2>&1 + $result -join "`n" | Should -Match "source|origin/windows|git" + } + } +} + +Describe "Remove Command" { + Context "Help Display" { + It "Should show help without error" { + $result = & powershell -ExecutionPolicy Bypass -File "$script:BinDir\remove.ps1" -ShowHelp 2>&1 + $result | Should -Not -BeNullOrEmpty + $LASTEXITCODE | Should -Be 0 + } + + It "Should mention uninstall behavior" { + $result = & powershell -ExecutionPolicy Bypass -File "$script:BinDir\remove.ps1" -ShowHelp 2>&1 + $result -join "`n" | Should -Match "Remove Mole|PATH|config" + } + } +} + Describe "Main Entry Point" { Context "mole.ps1" { BeforeAll { @@ -135,6 +165,8 @@ Describe "Main Entry Point" { $helpText | Should -Match "purge" $helpText | Should -Match "analyze" $helpText | Should -Match "status" + $helpText | Should -Match "update" + $helpText | Should -Match "remove" } } }