1
0
mirror of https://github.com/tw93/Mole.git synced 2026-03-22 16:45:07 +00:00

fix(windows): align source channel and prerelease binaries

Refs #538
This commit is contained in:
Tw93
2026-03-06 08:34:10 +08:00
parent 7c9b420a22
commit d0f627892d
17 changed files with 732 additions and 453 deletions

View File

@@ -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<<EOF" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
$files | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
"EOF" | Out-File -FilePath $env:GITHUB_OUTPUT -Encoding utf8 -Append
- name: Create GitHub Release
- name: Create GitHub Prerelease
if: startsWith(github.ref, 'refs/tags/')
uses: softprops/action-gh-release@v1
with:
files: |
release/mole-${{ steps.version.outputs.VERSION }}-x64.zip
release/mole-${{ steps.version.outputs.VERSION }}-x64.exe
release/SHA256SUMS.txt
files: ${{ steps.release_files.outputs.files }}
draft: false
prerelease: false
prerelease: true
generate_release_notes: true
body: |
## Windows System Maintenance Toolkit
## Windows System Maintenance Toolkit (Prerelease)
**Download:** `mole-${{ steps.version.outputs.VERSION }}-x64.zip` (portable) or `.exe` (launcher)
**TUI assets:** `analyze-windows-x64.exe` and `status-windows-x64.exe`
**Install:**
```powershell
@@ -140,7 +170,9 @@ jobs:
cd C:\mole
.\install.ps1
```
These assets are prerelease-only and do not affect the macOS stable release channel.
**Verify:** Check SHA256SUMS.txt for file integrity.
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,106 +0,0 @@
name: Release
on:
push:
tags:
- 'W*' # Windows releases use W prefix (e.g., W1.0.0)
permissions:
contents: write
jobs:
build:
name: Build Windows
runs-on: windows-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.24.6"
- name: Build Binaries
run: |
cd cmd/analyze
go build -ldflags="-s -w" -o analyze.exe .
cd ../status
go build -ldflags="-s -w" -o status.exe .
shell: pwsh
- name: Create release package
run: |
# Create release directory
New-Item -ItemType Directory -Force -Path release
# Copy binaries
Copy-Item cmd/analyze/analyze.exe release/
Copy-Item cmd/status/status.exe release/
# Copy PowerShell scripts
Copy-Item mole.ps1 release/
Copy-Item install.ps1 release/
Copy-Item -Recurse bin release/
Copy-Item -Recurse lib release/
# Copy docs
Copy-Item README.md release/
Copy-Item LICENSE release/
# Create zip
Compress-Archive -Path release/* -DestinationPath mole-windows.zip
shell: pwsh
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: windows-release
path: mole-windows.zip
retention-days: 5
release:
name: Publish Release
needs: build
runs-on: ubuntu-latest
steps:
- name: Download artifacts
uses: actions/download-artifact@v4
with:
name: windows-release
- name: Display downloaded files
run: ls -la
- name: Create Release
uses: softprops/action-gh-release@v2
if: startsWith(github.ref, 'refs/tags/')
with:
files: mole-windows.zip
generate_release_notes: true
draft: false
prerelease: false
name: "Mole for Windows ${{ github.ref_name }}"
body: |
## Mole for Windows
Windows port of the Mole system maintenance toolkit.
### Installation
**Quick install:**
```powershell
irm https://raw.githubusercontent.com/tw93/Mole/windows/install.ps1 | iex
```
**Manual install:**
1. Download and extract `mole-windows.zip`
2. Run `install.ps1`
### Features
- Deep system cleanup (temp files, caches, logs)
- Smart app uninstaller with leftover detection
- Disk space analyzer (TUI)
- System status monitor (TUI)
- Developer artifact cleanup
- System optimization

View File

@@ -54,21 +54,19 @@ jobs:
- name: Build Go binaries
run: |
cd cmd/analyze
go build -o analyze.exe .
cd ../status
go build -o status.exe .
go build -o bin/analyze.exe ./cmd/analyze/
go build -o bin/status.exe ./cmd/status/
shell: pwsh
- name: Verify binaries
run: |
if (Test-Path cmd/analyze/analyze.exe) {
if (Test-Path bin/analyze.exe) {
Write-Host "analyze.exe built successfully"
} else {
Write-Host "Failed to build analyze.exe"
exit 1
}
if (Test-Path cmd/status/status.exe) {
if (Test-Path bin/status.exe) {
Write-Host "status.exe built successfully"
} else {
Write-Host "Failed to build status.exe"

View File

@@ -1,196 +0,0 @@
name: Windows CI
on:
push:
branches: [main, dev]
paths:
- 'windows/**'
- '.github/workflows/windows.yml'
pull_request:
branches: [main, dev]
paths:
- 'windows/**'
- '.github/workflows/windows.yml'
jobs:
build:
name: Build & Test
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.24"
cache-dependency-path: windows/go.sum
- name: Build Go binaries
working-directory: windows
run: |
go build -o bin/analyze.exe ./cmd/analyze/
go build -o bin/status.exe ./cmd/status/
- name: Run Go tests
working-directory: windows
run: go test -v ./...
- name: Validate PowerShell syntax
shell: pwsh
run: |
$scripts = Get-ChildItem -Path windows -Filter "*.ps1" -Recurse
$errors = @()
foreach ($script in $scripts) {
$parseErrors = $null
$null = [System.Management.Automation.Language.Parser]::ParseFile(
$script.FullName,
[ref]$null,
[ref]$parseErrors
)
if ($parseErrors) {
Write-Host "ERROR: $($script.FullName)" -ForegroundColor Red
foreach ($err in $parseErrors) {
Write-Host " $($err.Message)" -ForegroundColor Red
}
$errors += $script.FullName
} else {
Write-Host "OK: $($script.Name)" -ForegroundColor Green
}
}
if ($errors.Count -gt 0) {
Write-Host "`n$($errors.Count) script(s) have syntax errors!" -ForegroundColor Red
exit 1
}
pester:
name: Pester Tests
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Install Pester
shell: pwsh
run: |
Set-PSRepository -Name PSGallery -InstallationPolicy Trusted
Install-Module -Name Pester -MinimumVersion 5.0.0 -Force -SkipPublisherCheck
- name: Run Pester tests
shell: pwsh
run: |
Import-Module Pester -MinimumVersion 5.0.0
$config = New-PesterConfiguration
$config.Run.Path = "windows/tests"
$config.Run.Exit = $true
$config.Output.Verbosity = "Detailed"
$config.TestResult.Enabled = $true
$config.TestResult.OutputPath = "windows/test-results.xml"
$config.TestResult.OutputFormat = "NUnitXml"
Invoke-Pester -Configuration $config
- name: Upload test results
uses: actions/upload-artifact@v4
if: always()
with:
name: pester-results
path: windows/test-results.xml
compatibility:
name: Windows Compatibility
strategy:
matrix:
os: [windows-2022, windows-2019]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- name: Test PowerShell 5.1
shell: powershell
run: |
Write-Host "Testing on ${{ matrix.os }} with PowerShell $($PSVersionTable.PSVersion)"
# Test main entry point
$result = & powershell -ExecutionPolicy Bypass -File "windows\mole.ps1" -ShowHelp 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Host "mole.ps1 -ShowHelp failed" -ForegroundColor Red
exit 1
}
Write-Host "✓ mole.ps1 works on ${{ matrix.os }}"
- name: Test command scripts
shell: powershell
run: |
$commands = @("clean", "uninstall", "optimize", "purge", "analyze", "status")
foreach ($cmd in $commands) {
$scriptPath = "windows\bin\$cmd.ps1"
if (Test-Path $scriptPath) {
$result = & powershell -ExecutionPolicy Bypass -File $scriptPath -ShowHelp 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Host "✗ $cmd.ps1 failed" -ForegroundColor Red
exit 1
}
Write-Host "✓ $cmd.ps1 works"
}
}
security:
name: Security Checks
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Check for unsafe patterns
shell: pwsh
run: |
Write-Host "Checking for unsafe removal patterns..."
$unsafePatterns = @(
"Remove-Item.*-Recurse.*-Force.*\\\$env:SystemRoot",
"Remove-Item.*-Recurse.*-Force.*C:\\Windows",
"Remove-Item.*-Recurse.*-Force.*C:\\Program Files"
)
$files = Get-ChildItem -Path windows -Filter "*.ps1" -Recurse
$issues = @()
foreach ($file in $files) {
$content = Get-Content $file.FullName -Raw
foreach ($pattern in $unsafePatterns) {
if ($content -match $pattern) {
$issues += "$($file.Name): matches unsafe pattern"
}
}
}
if ($issues.Count -gt 0) {
Write-Host "Unsafe patterns found:" -ForegroundColor Red
$issues | ForEach-Object { Write-Host " $_" -ForegroundColor Red }
exit 1
}
Write-Host "✓ No unsafe patterns found" -ForegroundColor Green
- name: Verify protection checks
shell: pwsh
run: |
Write-Host "Verifying protection logic..."
# Source file_ops to get Test-IsProtectedPath
. windows\lib\core\base.ps1
. windows\lib\core\file_ops.ps1
$protectedPaths = @(
"C:\Windows",
"C:\Windows\System32",
"C:\Program Files",
"C:\Program Files (x86)"
)
foreach ($path in $protectedPaths) {
if (-not (Test-ProtectedPath -Path $path)) {
Write-Host "✗ $path should be protected!" -ForegroundColor Red
exit 1
}
Write-Host "✓ $path is protected" -ForegroundColor Green
}

83
.gitignore vendored
View File

@@ -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

View File

@@ -6,7 +6,7 @@
<p align="center">
<a href="https://github.com/tw93/mole/stargazers"><img src="https://img.shields.io/github/stars/tw93/mole?style=flat-square" alt="Stars"></a>
<a href="https://github.com/tw93/mole/releases"><img src="https://img.shields.io/github/v/tag/tw93/mole?label=version&style=flat-square" alt="Version"></a>
<img src="https://img.shields.io/badge/channel-windows%20source-orange?style=flat-square" alt="Channel">
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square" alt="License"></a>
<a href="https://github.com/tw93/mole/commits"><img src="https://img.shields.io/github/commit-activity/m/tw93/mole?style=flat-square" alt="Commits"></a>
<a href="https://twitter.com/HiTw93"><img src="https://img.shields.io/badge/follow-Tw93-red?style=flat-square&logo=Twitter" alt="Twitter"></a>
@@ -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)

View File

@@ -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

40
bin/remove.ps1 Normal file
View File

@@ -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

View File

@@ -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

126
bin/update.ps1 Normal file
View File

@@ -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

Binary file not shown.

View File

@@ -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
}

184
lib/core/tui_binaries.ps1 Normal file
View File

@@ -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
}

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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"}
)

View File

@@ -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"
}
}
}