wip: Windows desktop app scaffold (WinUI 3/.NET 8) + NSIS installer + docs
Owner decision pending (STATE.md DC-100 tick): adopt/ship/park. Preserved from fragile git stash to named branch 2026-08-23. NOTE: requires a Windows build machine (WinUI XAML compiler + MSIX do not cross-build on Linux) — see WINDOWS_APP_BUILD.md in this tree. Secret-scanned clean 2026-08-23 (no keys/tokens/PEM in tree).
This commit is contained in:
@@ -0,0 +1,532 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
DashCaddy Bootstrap Script - Runs after NSIS install to set up Docker, WSL, and services
|
||||
|
||||
.DESCRIPTION
|
||||
This script handles the heavy lifting:
|
||||
1. Installs Docker Desktop via winget (if not present)
|
||||
2. Enables WSL2 (if needed, handles reboot)
|
||||
3. Pulls DashCaddy Docker images
|
||||
4. Starts services via docker compose
|
||||
5. Registers auto-start
|
||||
6. Creates config.yaml
|
||||
|
||||
.PARAMETER Action
|
||||
Install (default), Stop, Start, Restart, Uninstall
|
||||
|
||||
.EXAMPLE
|
||||
.\bootstrap.ps1
|
||||
.\bootstrap.ps1 -Action Stop
|
||||
#>
|
||||
|
||||
param(
|
||||
[ValidateSet('Install', 'Stop', 'Start', 'Restart', 'Uninstall')]
|
||||
[string]$Action = 'Install'
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
|
||||
$InstallDir = $PSScriptRoot
|
||||
$AppDataDir = Join-Path $env:LOCALAPPDATA 'DashCaddy'
|
||||
$DataDir = Join-Path $AppDataDir 'data'
|
||||
$ConfigDir = Join-Path $AppDataDir 'config'
|
||||
$LogDir = Join-Path $AppDataDir 'logs'
|
||||
$ComposeFile = Join-Path $InstallDir 'docker-compose.yml'
|
||||
$ConfigFile = Join-Path $ConfigDir 'config.yaml'
|
||||
|
||||
function Write-Log {
|
||||
param([string]$Message, [string]$Level = 'INFO')
|
||||
$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
|
||||
$logMessage = "[$timestamp] [$Level] $Message"
|
||||
Write-Host $logMessage
|
||||
|
||||
$logFile = Join-Path $LogDir "bootstrap-$(Get-Date -Format 'yyyyMMdd').log"
|
||||
Add-Content -Path $logFile -Value $logMessage
|
||||
}
|
||||
|
||||
function Ensure-Directories {
|
||||
Write-Log "Creating directories..."
|
||||
@($DataDir, $ConfigDir, $LogDir) | ForEach-Object {
|
||||
if (-not (Test-Path $_)) {
|
||||
New-Item -ItemType Directory -Path $_ -Force | Out-Null
|
||||
Write-Log "Created: $_"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Check-Docker {
|
||||
Write-Log "Checking Docker Desktop..."
|
||||
try {
|
||||
$dockerVersion = docker version --format '{{.Server.Version}}' 2>$null
|
||||
if ($dockerVersion) {
|
||||
Write-Log "Docker Desktop found: v$dockerVersion"
|
||||
return $true
|
||||
}
|
||||
} catch {}
|
||||
return $false
|
||||
}
|
||||
|
||||
function Install-DockerDesktop {
|
||||
Write-Log "Installing Docker Desktop via winget..."
|
||||
|
||||
# Check if winget is available
|
||||
if (-not (Get-Command winget -ErrorAction SilentlyContinue)) {
|
||||
Write-Log "winget not found, trying Microsoft Store..." 'WARN'
|
||||
# Fallback: direct download
|
||||
$dockerUrl = 'https://desktop.docker.com/win/main/amd64/Docker%20Desktop%20Installer.exe'
|
||||
$installerPath = Join-Path $env:TEMP 'DockerDesktopInstaller.exe'
|
||||
Invoke-WebRequest -Uri $dockerUrl -OutFile $installerPath
|
||||
Write-Log "Downloaded Docker Desktop installer"
|
||||
Start-Process -FilePath $installerPath -ArgumentList 'install', '--quiet' -Wait
|
||||
return
|
||||
}
|
||||
|
||||
# Install via winget
|
||||
try {
|
||||
winget install --id Docker.DockerDesktop --accept-source-agreements --accept-package-agreements --silent
|
||||
Write-Log "Docker Desktop installed successfully"
|
||||
} catch {
|
||||
Write-Log "winget install failed, trying direct download..." 'WARN'
|
||||
$dockerUrl = 'https://desktop.docker.com/win/main/amd64/Docker%20Desktop%20Installer.exe'
|
||||
$installerPath = Join-Path $env:TEMP 'DockerDesktopInstaller.exe'
|
||||
Invoke-WebRequest -Uri $dockerUrl -OutFile $installerPath
|
||||
Start-Process -FilePath $installerPath -ArgumentList 'install', '--quiet' -Wait
|
||||
}
|
||||
}
|
||||
|
||||
function Enable-WSL2 {
|
||||
Write-Log "Checking WSL2..."
|
||||
|
||||
$wslStatus = wsl --status 2>&1
|
||||
if ($wslStatus -match 'WSL 2') {
|
||||
Write-Log "WSL2 already enabled"
|
||||
return
|
||||
}
|
||||
|
||||
Write-Log "Enabling WSL2..."
|
||||
try {
|
||||
# Enable WSL and Virtual Machine Platform
|
||||
dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart | Out-Null
|
||||
dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart | Out-Null
|
||||
|
||||
# Set WSL2 as default
|
||||
wsl --set-default-version 2 | Out-Null
|
||||
|
||||
# Install Ubuntu if not present
|
||||
if (-not (wsl -l -q | Where-Object { $_ -eq 'Ubuntu' })) {
|
||||
Write-Log "Installing Ubuntu..."
|
||||
wsl --install -d Ubuntu | Out-Null
|
||||
}
|
||||
|
||||
Write-Log "WSL2 enabled. A reboot is required."
|
||||
$global:RebootRequired = $true
|
||||
} catch {
|
||||
Write-Log "Failed to enable WSL2: $_" 'ERROR'
|
||||
throw
|
||||
}
|
||||
}
|
||||
|
||||
function Ensure-DockerRunning {
|
||||
Write-Log "Ensuring Docker is running..."
|
||||
|
||||
$maxAttempts = 30
|
||||
$attempt = 0
|
||||
|
||||
while ($attempt -lt $maxAttempts) {
|
||||
if (Check-Docker) {
|
||||
Write-Log "Docker is running"
|
||||
return
|
||||
}
|
||||
|
||||
$attempt++
|
||||
Write-Log "Waiting for Docker... (attempt $attempt/$maxAttempts)"
|
||||
Start-Sleep -Seconds 5
|
||||
}
|
||||
|
||||
throw "Docker failed to start after $maxAttempts attempts"
|
||||
}
|
||||
|
||||
function Create-DockerCompose {
|
||||
Write-Log "Creating docker-compose.yml..."
|
||||
|
||||
# Read DNS provider from config
|
||||
$dnsProvider = "coredns"
|
||||
if (Test-Path $ConfigFile) {
|
||||
$config = Get-Content $ConfigFile | ForEach-Object {
|
||||
if ($_ -match '^(\w+):\s*(.*)$') {
|
||||
@{$matches[1] = $matches[2].Trim()}
|
||||
}
|
||||
} | ForEach-Object { $_ }
|
||||
if ($config.ContainsKey("dns_provider")) {
|
||||
$dnsProvider = $config["dns_provider"]
|
||||
}
|
||||
}
|
||||
|
||||
$composeContent = @"
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
dashcaddy-api:
|
||||
image: dashcaddy/dashcaddy-api:latest
|
||||
container_name: dashcaddy-api
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "127.0.0.1:3001:3001"
|
||||
volumes:
|
||||
- ${DataDir}:/opt/dashcaddy/data
|
||||
- ${AppDataDir}/config.yaml:/opt/dashcaddy/config.yaml:ro
|
||||
- //./pipe/docker_engine:/var/run/docker.sock:ro
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- DATA_DIR=/opt/dashcaddy/data
|
||||
- CONFIG_FILE=/opt/dashcaddy/config.yaml
|
||||
healthcheck:
|
||||
test: ["CMD", "node", "-e", "require('http').get('http://localhost:3001/health', (r) => { process.exit(r.statusCode === 200 ? 0 : 1); }).on('error', () => process.exit(1))"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 15s
|
||||
networks:
|
||||
- dashcaddy-net
|
||||
|
||||
caddy:
|
||||
image: caddy:2.10-alpine
|
||||
container_name: dashcaddy-caddy
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ${DataDir}/caddy/Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
- ${DataDir}/caddy/data:/data
|
||||
- ${DataDir}/caddy/config:/config
|
||||
networks:
|
||||
- dashcaddy-net
|
||||
depends_on:
|
||||
- dashcaddy-api
|
||||
|
||||
$(Get-DnsServiceCompose $dnsProvider $DataDir)
|
||||
|
||||
networks:
|
||||
dashcaddy-net:
|
||||
driver: bridge
|
||||
"@
|
||||
|
||||
$composeContent | Set-Content -Path $ComposeFile -Encoding UTF8
|
||||
Write-Log "docker-compose.yml created at $ComposeFile (DNS: $dnsProvider)"
|
||||
}
|
||||
|
||||
function Get-DnsServiceCompose {
|
||||
param(
|
||||
[string]$Provider,
|
||||
[string]$DataDir
|
||||
)
|
||||
|
||||
switch ($Provider.ToLower()) {
|
||||
"technitium" {
|
||||
return @"
|
||||
technitium:
|
||||
image: technitium/dns-server:latest
|
||||
container_name: dashcaddy-technitium
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "53:53/udp"
|
||||
- "53:53/tcp"
|
||||
- "5380:5380"
|
||||
volumes:
|
||||
- ${DataDir}/technitium:/etc/dns
|
||||
environment:
|
||||
- DNS_SERVER_DOMAIN=local
|
||||
networks:
|
||||
- dashcaddy-net
|
||||
"
|
||||
}
|
||||
"coredns" {
|
||||
return @"
|
||||
coredns:
|
||||
image: coredns/coredns:latest
|
||||
container_name: dashcaddy-coredns
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "53:53/udp"
|
||||
- "53:53/tcp"
|
||||
volumes:
|
||||
- ${DataDir}/coredns/Corefile:/etc/coredns/Corefile:ro
|
||||
networks:
|
||||
- dashcaddy-net
|
||||
"
|
||||
}
|
||||
"cloudflare" {
|
||||
return "" # Cloudflare is external, no local container needed
|
||||
}
|
||||
"route53" {
|
||||
return "" # Route53 is external, no local container needed
|
||||
}
|
||||
default {
|
||||
return @"
|
||||
coredns:
|
||||
image: coredns/coredns:latest
|
||||
container_name: dashcaddy-coredns
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "53:53/udp"
|
||||
- "53:53/tcp"
|
||||
volumes:
|
||||
- ${DataDir}/coredns/Corefile:/etc/coredns/Corefile:ro
|
||||
networks:
|
||||
- dashcaddy-net
|
||||
"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Create-Config {
|
||||
Write-Log "Creating config.yaml..."
|
||||
|
||||
if (-not (Test-Path $ConfigFile)) {
|
||||
$configContent = @"
|
||||
domain: local
|
||||
email: admin@local
|
||||
docker_data: $DataDir
|
||||
dns_provider: coredns
|
||||
auto_start: true
|
||||
auto_update: false
|
||||
"@
|
||||
$configContent | Set-Content -Path $ConfigFile -Encoding UTF8
|
||||
Write-Log "config.yaml created"
|
||||
}
|
||||
}
|
||||
|
||||
function Create-Caddyfile {
|
||||
Write-Log "Creating Caddyfile..."
|
||||
|
||||
$caddyDir = Join-Path $DataDir 'caddy'
|
||||
if (-not (Test-Path $caddyDir)) {
|
||||
New-Item -ItemType Directory -Path $caddyDir -Force | Out-Null
|
||||
}
|
||||
|
||||
$caddyfilePath = Join-Path $caddyDir 'Caddyfile'
|
||||
if (-not (Test-Path $caddyfilePath)) {
|
||||
$caddyContent = @"
|
||||
{
|
||||
admin :2019
|
||||
email admin@local
|
||||
}
|
||||
|
||||
# Dashboard
|
||||
status.local {
|
||||
reverse_proxy dashcaddy-api:3001
|
||||
tls internal
|
||||
}
|
||||
|
||||
# API
|
||||
api.local {
|
||||
reverse_proxy dashcaddy-api:3001
|
||||
tls internal
|
||||
}
|
||||
|
||||
# Catch-all for other services (configured via API)
|
||||
import dashcaddy_services
|
||||
"@
|
||||
$caddyContent | Set-Content -Path $caddyfilePath -Encoding UTF8
|
||||
Write-Log "Caddyfile created"
|
||||
}
|
||||
}
|
||||
|
||||
function Create-Corefile {
|
||||
Write-Log "Creating Corefile..."
|
||||
|
||||
$corednsDir = Join-Path $DataDir 'coredns'
|
||||
if (-not (Test-Path $corednsDir)) {
|
||||
New-Item -ItemType Directory -Path $corednsDir -Force | Out-Null
|
||||
}
|
||||
|
||||
$corefilePath = Join-Path $corednsDir 'Corefile'
|
||||
if (-not (Test-Path $corefilePath)) {
|
||||
$corefileContent = @"
|
||||
.:53 {
|
||||
forward . 1.1.1.1 8.8.8.8
|
||||
log
|
||||
errors
|
||||
cache 30
|
||||
}
|
||||
|
||||
local:53 {
|
||||
file /etc/coredns/db.local
|
||||
log
|
||||
errors
|
||||
}
|
||||
"@
|
||||
$corefileContent | Set-Content -Path $corefilePath -Encoding UTF8
|
||||
Write-Log "Corefile created"
|
||||
}
|
||||
}
|
||||
|
||||
function Create-TechnitiumConfig {
|
||||
Write-Log "Creating Technitium config..."
|
||||
|
||||
$techDir = Join-Path $DataDir 'technitium'
|
||||
if (-not (Test-Path $techDir)) {
|
||||
New-Item -ItemType Directory -Path $techDir -Force | Out-Null
|
||||
}
|
||||
|
||||
# Technitium uses a config file - minimal setup, most config via API
|
||||
Write-Log "Technitium config directory created"
|
||||
}
|
||||
|
||||
function Register-AutoStart {
|
||||
Write-Log "Registering auto-start..."
|
||||
|
||||
$runKey = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Run'
|
||||
$appPath = Join-Path $InstallDir 'DashCaddy.exe'
|
||||
|
||||
Set-ItemProperty -Path $runKey -Name 'DashCaddy' -Value "`"$appPath`" --minimized" -Force
|
||||
Write-Log "Auto-start registered"
|
||||
}
|
||||
|
||||
function Unregister-AutoStart {
|
||||
Write-Log "Unregistering auto-start..."
|
||||
|
||||
$runKey = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Run'
|
||||
Remove-ItemProperty -Path $runKey -Name 'DashCaddy' -ErrorAction SilentlyContinue
|
||||
Write-Log "Auto-start unregistered"
|
||||
}
|
||||
|
||||
function Pull-Images {
|
||||
Write-Log "Pulling Docker images..."
|
||||
|
||||
# Read DNS provider to know which images to pull
|
||||
$dnsProvider = "coredns"
|
||||
if (Test-Path $ConfigFile) {
|
||||
$config = Get-Content $ConfigFile | ForEach-Object {
|
||||
if ($_ -match '^(\w+):\s*(.*)$') {
|
||||
@{$matches[1] = $matches[2].Trim()}
|
||||
}
|
||||
} | ForEach-Object { $_ }
|
||||
if ($config.ContainsKey("dns_provider")) {
|
||||
$dnsProvider = $config["dns_provider"]
|
||||
}
|
||||
}
|
||||
|
||||
$images = @(
|
||||
'dashcaddy/dashcaddy-api:latest',
|
||||
'caddy:2.10-alpine'
|
||||
)
|
||||
|
||||
# Add DNS provider image
|
||||
switch ($dnsProvider.ToLower()) {
|
||||
"technitium" { $images += 'technitium/dns-server:latest' }
|
||||
"coredns" { $images += 'coredns/coredns:latest' }
|
||||
default { $images += 'coredns/coredns:latest' }
|
||||
}
|
||||
|
||||
foreach ($image in $images) {
|
||||
Write-Log "Pulling $image..."
|
||||
docker pull $image 2>&1 | ForEach-Object { Write-Log $_ 'DEBUG' }
|
||||
}
|
||||
|
||||
Write-Log "All images pulled"
|
||||
}
|
||||
|
||||
function Start-Services {
|
||||
Write-Log "Starting services..."
|
||||
|
||||
Ensure-DockerRunning
|
||||
Create-DockerCompose
|
||||
Create-Config
|
||||
Create-Caddyfile
|
||||
|
||||
# Create DNS-specific configs
|
||||
$dnsProvider = "coredns"
|
||||
if (Test-Path $ConfigFile) {
|
||||
$config = Get-Content $ConfigFile | ForEach-Object {
|
||||
if ($_ -match '^(\w+):\s*(.*)$') {
|
||||
@{$matches[1] = $matches[2].Trim()}
|
||||
}
|
||||
} | ForEach-Object { $_ }
|
||||
if ($config.ContainsKey("dns_provider")) {
|
||||
$dnsProvider = $config["dns_provider"]
|
||||
}
|
||||
}
|
||||
|
||||
switch ($dnsProvider.ToLower()) {
|
||||
"coredns" { Create-Corefile }
|
||||
"technitium" { Create-TechnitiumConfig }
|
||||
default { Create-Corefile }
|
||||
}
|
||||
|
||||
Pull-Images
|
||||
|
||||
Write-Log "Running docker compose up..."
|
||||
docker compose -f $ComposeFile up -d 2>&1 | ForEach-Object { Write-Log $_ 'DEBUG' }
|
||||
|
||||
# Wait for health
|
||||
Write-Log "Waiting for services to be healthy..."
|
||||
$maxWait = 120
|
||||
$waited = 0
|
||||
while ($waited -lt $maxWait) {
|
||||
$status = docker compose -f $ComposeFile ps --format json 2>$null | ConvertFrom-Json
|
||||
$healthy = $status | Where-Object { $_.Health -eq 'healthy' -or $_.State -eq 'running' }
|
||||
if ($healthy.Count -eq $status.Count) {
|
||||
Write-Log "All services healthy"
|
||||
break
|
||||
}
|
||||
Start-Sleep -Seconds 5
|
||||
$waited += 5
|
||||
}
|
||||
|
||||
Register-AutoStart
|
||||
Write-Log "Services started successfully"
|
||||
}
|
||||
|
||||
function Stop-Services {
|
||||
Write-Log "Stopping services..."
|
||||
|
||||
if (Test-Path $ComposeFile) {
|
||||
docker compose -f $ComposeFile down 2>&1 | ForEach-Object { Write-Log $_ 'DEBUG' }
|
||||
}
|
||||
|
||||
Unregister-AutoStart
|
||||
Write-Log "Services stopped"
|
||||
}
|
||||
|
||||
function Restart-Services {
|
||||
Stop-Services
|
||||
Start-Services
|
||||
}
|
||||
|
||||
# ─── Main ───
|
||||
|
||||
Write-Log "=== DashCaddy Bootstrap Started (Action: $Action) ==="
|
||||
|
||||
Ensure-Directories
|
||||
|
||||
switch ($Action) {
|
||||
'Install' {
|
||||
if (-not (Check-Docker)) {
|
||||
Install-DockerDesktop
|
||||
}
|
||||
Enable-WSL2
|
||||
|
||||
if ($global:RebootRequired) {
|
||||
Write-Log "REBOOT REQUIRED - WSL2 was enabled. Please reboot and run bootstrap again." 'WARN'
|
||||
Write-Host ""
|
||||
Write-Host "==========================================" -ForegroundColor Yellow
|
||||
Write-Host " REBOOT REQUIRED" -ForegroundColor Yellow
|
||||
Write-Host "==========================================" -ForegroundColor Yellow
|
||||
Write-Host "WSL2 was enabled. Please reboot your computer"
|
||||
Write-Host "and then run DashCaddy from Start Menu."
|
||||
Write-Host "==========================================" -ForegroundColor Yellow
|
||||
exit 0
|
||||
}
|
||||
|
||||
Start-Services
|
||||
}
|
||||
'Stop' { Stop-Services }
|
||||
'Start' { Start-Services }
|
||||
'Restart' { Restart-Services }
|
||||
'Uninstall' { Stop-Services }
|
||||
}
|
||||
|
||||
Write-Log "=== DashCaddy Bootstrap Completed ==="
|
||||
@@ -0,0 +1,176 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Build script for DashCaddy Windows Desktop App
|
||||
|
||||
.DESCRIPTION
|
||||
This script:
|
||||
1. Builds the WinUI 3 desktop app (Release)
|
||||
2. Packages as MSIX
|
||||
3. Creates NSIS installer
|
||||
4. Outputs: DashCaddy-Setup-1.15.0.exe
|
||||
|
||||
.REQUIREMENTS
|
||||
- Visual Studio 2022 with Windows App SDK workload
|
||||
- NSIS 3.08+
|
||||
- .NET 8 SDK
|
||||
#>
|
||||
|
||||
param(
|
||||
[string]$Version = "1.15.0",
|
||||
[string]$Configuration = "Release",
|
||||
[string]$Platform = "x64"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$RootDir = Split-Path $PSScriptRoot -Parent
|
||||
$DesktopDir = Join-Path $RootDir 'desktop'
|
||||
$InstallerDir = Join-Path $RootDir 'installer\windows'
|
||||
$OutputDir = Join-Path $RootDir 'artifacts'
|
||||
$BuildDir = Join-Path $OutputDir 'build'
|
||||
|
||||
Write-Host "=== DashCaddy Windows Build v$Version ===" -ForegroundColor Cyan
|
||||
Write-Host "Configuration: $Configuration"
|
||||
Write-Host "Platform: $Platform"
|
||||
|
||||
# ─── Clean ───
|
||||
if (Test-Path $OutputDir) {
|
||||
Write-Host "Cleaning previous build..."
|
||||
Remove-Item -Recurse -Force $OutputDir
|
||||
}
|
||||
New-Item -ItemType Directory -Path $BuildDir -Force | Out-Null
|
||||
|
||||
# ─── Build .NET App ───
|
||||
Write-Host "`n[1/5] Building WinUI 3 Desktop App..." -ForegroundColor Green
|
||||
$csproj = Join-Path $DesktopDir 'DashCaddy.Desktop.csproj'
|
||||
$publishDir = Join-Path $BuildDir 'app'
|
||||
|
||||
dotnet publish $csproj `
|
||||
-c $Configuration `
|
||||
-r win10-$Platform `
|
||||
--self-contained true `
|
||||
-p:PublishSingleFile=true `
|
||||
-p:PublishTrimmed=true `
|
||||
-p:TrimMode=partial `
|
||||
-p:EnableMsixTooling=true `
|
||||
-p:AppxPackageDir=$OutputDir `
|
||||
-o $publishDir
|
||||
|
||||
if (-not (Test-Path (Join-Path $publishDir 'DashCaddy.exe'))) {
|
||||
throw "Build failed - DashCaddy.exe not found"
|
||||
}
|
||||
Write-Host "App built to: $publishDir"
|
||||
|
||||
# ─── Copy Assets ───
|
||||
Write-Host "`n[2/5] Copying assets..." -ForegroundColor Green
|
||||
$assetsSrc = Join-Path $InstallerDir 'assets'
|
||||
$assetsDst = Join-Path $BuildDir 'assets'
|
||||
if (Test-Path $assetsSrc) {
|
||||
Copy-Item -Recurse $assetsSrc $assetsDst
|
||||
} else {
|
||||
# Create minimal assets
|
||||
New-Item -ItemType Directory -Path $assetsDst -Force | Out-Null
|
||||
# Create a simple icon placeholder
|
||||
Write-Host "Warning: No assets found, creating placeholders"
|
||||
}
|
||||
|
||||
# ─── Copy Bootstrap ───
|
||||
Write-Host "`n[3/5] Copying bootstrap script..." -ForegroundColor Green
|
||||
Copy-Item (Join-Path $InstallerDir 'bootstrap.ps1') (Join-Path $BuildDir 'bootstrap.ps1')
|
||||
|
||||
# ─── Create docker-compose.yml for installer ───
|
||||
Write-Host "`n[4/5] Creating installer docker-compose.yml..." -ForegroundColor Green
|
||||
$composeContent = @"
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
dashcaddy-api:
|
||||
image: dashcaddy/dashcaddy-api:latest
|
||||
container_name: dashcaddy-api
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "127.0.0.1:3001:3001"
|
||||
volumes:
|
||||
- \${DATA_DIR}:/opt/dashcaddy/data
|
||||
- \${APPDATA_DIR}/config.yaml:/opt/dashcaddy/config.yaml:ro
|
||||
- //./pipe/docker_engine:/var/run/docker.sock:ro
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- DATA_DIR=/opt/dashcaddy/data
|
||||
- CONFIG_FILE=/opt/dashcaddy/config.yaml
|
||||
healthcheck:
|
||||
test: ["CMD", "node", "-e", "require('http').get('http://localhost:3001/health', (r) => { process.exit(r.statusCode === 200 ? 0 : 1); }).on('error', () => process.exit(1))"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 15s
|
||||
networks:
|
||||
- dashcaddy-net
|
||||
|
||||
caddy:
|
||||
image: caddy:2.10-alpine
|
||||
container_name: dashcaddy-caddy
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- \${DATA_DIR}/caddy/Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
- \${DATA_DIR}/caddy/data:/data
|
||||
- \${DATA_DIR}/caddy/config:/config
|
||||
networks:
|
||||
- dashcaddy-net
|
||||
depends_on:
|
||||
- dashcaddy-api
|
||||
|
||||
coredns:
|
||||
image: coredns/coredns:latest
|
||||
container_name: dashcaddy-coredns
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "53:53/udp"
|
||||
- "53:53/tcp"
|
||||
volumes:
|
||||
- \${DATA_DIR}/coredns/Corefile:/etc/coredns/Corefile:ro
|
||||
networks:
|
||||
- dashcaddy-net
|
||||
|
||||
networks:
|
||||
dashcaddy-net:
|
||||
driver: bridge
|
||||
"@
|
||||
|
||||
$composeContent | Set-Content -Path (Join-Path $BuildDir 'docker-compose.yml') -Encoding UTF8
|
||||
|
||||
# ─── Build NSIS Installer ───
|
||||
Write-Host "`n[5/5] Building NSIS Installer..." -ForegroundColor Green
|
||||
|
||||
$nsisPath = "C:\Program Files (x86)\NSIS\makensis.exe"
|
||||
if (-not (Test-Path $nsisPath)) {
|
||||
$nsisPath = "C:\Program Files\NSIS\makensis.exe"
|
||||
}
|
||||
if (-not (Test-Path $nsisPath)) {
|
||||
throw "NSIS not found. Install NSIS 3.08+ from https://nsis.sourceforge.io/"
|
||||
}
|
||||
|
||||
$nsiFile = Join-Path $InstallerDir 'dashcaddy.nsi'
|
||||
$installerOutput = Join-Path $OutputDir "DashCaddy-Setup-$Version.exe"
|
||||
|
||||
& $nsisPath `
|
||||
"/DVERSION=$Version" `
|
||||
"/DOUTPUT=$installerOutput" `
|
||||
"/DINSTALLER_DIR=$BuildDir" `
|
||||
$nsiFile
|
||||
|
||||
if (Test-Path $installerOutput) {
|
||||
$size = [math]::Round((Get-Item $installerOutput).Length / 1MB, 1)
|
||||
Write-Host "`n✅ Build Complete!" -ForegroundColor Green
|
||||
Write-Host "Installer: $installerOutput ($size MB)"
|
||||
Write-Host ""
|
||||
Write-Host "Next steps:"
|
||||
Write-Host " 1. Test installer on clean Windows VM"
|
||||
Write-Host " 2. Code sign: signtool sign /fd sha256 /tr http://timestamp.digicert.com $installerOutput"
|
||||
Write-Host " 3. Upload to dashcaddy.net/downloads"
|
||||
} else {
|
||||
throw "NSIS build failed - installer not created"
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
; DashCaddy Windows Installer (NSIS)
|
||||
; Build: makensis /DVERSION=1.15.0 installer/windows/dashcaddy.nsi
|
||||
; Output: DashCaddy-Setup-1.15.0.exe
|
||||
;
|
||||
; This installer:
|
||||
; 1. Installs DashCaddy Desktop app (WinUI 3, MSIX-packaged)
|
||||
; 2. Installs Docker Desktop via winget (if not present)
|
||||
; 3. Enables WSL2 (if needed, with reboot handling)
|
||||
; 4. Pulls Docker images
|
||||
; 5. Starts services
|
||||
; 6. Creates Start Menu shortcuts
|
||||
; 7. Registers for auto-updates via MSIX
|
||||
|
||||
!include "MUI2.nsh"
|
||||
!include "LogicLib.nsh"
|
||||
!include "FileFunc.nsh"
|
||||
!include "x64.nsh"
|
||||
!include "WinShell.nsh"
|
||||
!include "nsProcess.nsh"
|
||||
|
||||
; ─────────────────────────────────────────────────────────────────
|
||||
; Product Information
|
||||
; ─────────────────────────────────────────────────────────────────
|
||||
!define PRODUCT_NAME "DashCaddy"
|
||||
!define PRODUCT_VERSION "1.15.0"
|
||||
!define PRODUCT_PUBLISHER "Sami Ahmed"
|
||||
!define PRODUCT_WEB_SITE "https://dashcaddy.net"
|
||||
!define PRODUCT_UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}"
|
||||
!define PRODUCT_UNINST_ROOT_KEY "HKCU" ; Per-user install (no admin required)
|
||||
|
||||
; ─────────────────────────────────────────────────────────────────
|
||||
; Installer Configuration
|
||||
; ─────────────────────────────────────────────────────────────────
|
||||
Name "${PRODUCT_NAME} ${PRODUCT_VERSION}"
|
||||
OutFile "DashCaddy-Setup-${PRODUCT_VERSION}.exe"
|
||||
InstallDir "$LOCALAPPDATA\DashCaddy"
|
||||
RequestExecutionLevel user
|
||||
ShowInstDetails show
|
||||
XPStyle on
|
||||
|
||||
; ─────────────────────────────────────────────────────────────────
|
||||
; Modern UI
|
||||
; ─────────────────────────────────────────────────────────────────
|
||||
!define MUI_ABORTWARNING
|
||||
!define MUI_ICON "assets\dashcaddy.ico"
|
||||
!define MUI_UNICON "assets\dashcaddy.ico"
|
||||
!define MUI_WELCOMEFINISHPAGE_BITMAP "assets\welcome.bmp"
|
||||
|
||||
!insertmacro MUI_PAGE_WELCOME
|
||||
!insertmacro MUI_PAGE_LICENSE "assets\LICENSE.txt"
|
||||
!insertmacro MUI_PAGE_DIRECTORY
|
||||
!insertmacro MUI_PAGE_COMPONENTS
|
||||
!insertmacro MUI_PAGE_INSTFILES
|
||||
!define MUI_FINISHPAGE_RUN "$INSTDIR\DashCaddy.exe"
|
||||
!define MUI_FINISHPAGE_RUN_TEXT "Launch DashCaddy"
|
||||
!insertmacro MUI_PAGE_FINISH
|
||||
|
||||
!insertmacro MUI_UNPAGE_WELCOME
|
||||
!insertmacro MUI_UNPAGE_CONFIRM
|
||||
!insertmacro MUI_UNPAGE_INSTFILES
|
||||
!insertmacro MUI_UNPAGE_FINISH
|
||||
|
||||
!insertmacro MUI_LANGUAGE "English"
|
||||
|
||||
; ─────────────────────────────────────────────────────────────────
|
||||
; Variables
|
||||
; ─────────────────────────────────────────────────────────────────
|
||||
Var /GLOBAL DockerInstalled
|
||||
Var /GLOBAL WSLEnabled
|
||||
Var /GLOBAL RebootRequired
|
||||
|
||||
; ─────────────────────────────────────────────────────────────────
|
||||
; Sections
|
||||
; ─────────────────────────────────────────────────────────────────
|
||||
Section "DashCaddy Application (Required)" SecApp
|
||||
SectionIn RO
|
||||
SetOutPath "$INSTDIR"
|
||||
File /r "app\*"
|
||||
File "DashCaddy.exe"
|
||||
File "config.yaml.example"
|
||||
File "README.md"
|
||||
File "LICENSE"
|
||||
|
||||
; Write uninstaller
|
||||
WriteUninstaller "$INSTDIR\Uninstall.exe"
|
||||
|
||||
; Register in Add/Remove Programs (HKCU)
|
||||
WriteRegStr HKCU "${PRODUCT_UNINST_KEY}" "DisplayName" "${PRODUCT_NAME} ${PRODUCT_VERSION}"
|
||||
WriteRegStr HKCU "${PRODUCT_UNINST_KEY}" "UninstallString" "$INSTDIR\Uninstall.exe"
|
||||
WriteRegStr HKCU "${PRODUCT_UNINST_KEY}" "DisplayVersion" "${PRODUCT_VERSION}"
|
||||
WriteRegStr HKCU "${PRODUCT_UNINST_KEY}" "Publisher" "${PRODUCT_PUBLISHER}"
|
||||
WriteRegStr HKCU "${PRODUCT_UNINST_KEY}" "URLInfoAbout" "${PRODUCT_WEB_SITE}"
|
||||
WriteRegStr HKCU "${PRODUCT_UNINST_KEY}" "InstallLocation" "$INSTDIR"
|
||||
WriteRegStr HKCU "${PRODUCT_UNINST_KEY}" "DisplayIcon" "$INSTDIR\assets\dashcaddy.ico"
|
||||
WriteRegDWORD HKCU "${PRODUCT_UNINST_KEY}" "NoModify" 1
|
||||
WriteRegDWORD HKCU "${PRODUCT_UNINST_KEY}" "NoRepair" 1
|
||||
|
||||
; Start Menu
|
||||
CreateDirectory "$SMPROGRAMS\DashCaddy"
|
||||
CreateShortCut "$SMPROGRAMS\DashCaddy\DashCaddy.lnk" "$INSTDIR\DashCaddy.exe" "" "$INSTDIR\assets\dashcaddy.ico" 0
|
||||
CreateShortCut "$SMPROGRAMS\DashCaddy\Uninstall.lnk" "$INSTDIR\Uninstall.exe" "" "$INSTDIR\assets\dashcaddy.ico" 0
|
||||
SectionEnd
|
||||
|
||||
Section "Docker Desktop (Auto-Install)" SecDocker
|
||||
SectionIn 1
|
||||
; Docker Desktop installed via bootstrap.ps1
|
||||
SectionEnd
|
||||
|
||||
Section "WSL 2 Support (Required for Docker)" SecWSL
|
||||
SectionIn 1
|
||||
; WSL2 enabled via bootstrap.ps1
|
||||
SectionEnd
|
||||
|
||||
Section "Desktop Shortcut" SecShortcut
|
||||
SectionIn 1
|
||||
CreateShortCut "$DESKTOP\DashCaddy.lnk" "$INSTDIR\DashCaddy.exe" "" "$INSTDIR\assets\dashcaddy.ico" 0
|
||||
SectionEnd
|
||||
|
||||
Section "Auto-Start on Login" SecAutostart
|
||||
SectionIn 1
|
||||
; Registered via bootstrap.ps1
|
||||
SectionEnd
|
||||
|
||||
; ─────────────────────────────────────────────────────────────────
|
||||
; Installer Functions
|
||||
; ─────────────────────────────────────────────────────────────────
|
||||
Function .onInit
|
||||
; Check Windows 10/11
|
||||
${If} ${AtLeastWin10} == 0
|
||||
MessageBox MB_ICONSTOP "DashCaddy requires Windows 10 (build 19041) or later.$\n$\nCurrent OS: Windows $0"
|
||||
Abort
|
||||
${EndIf}
|
||||
|
||||
; Check if already running
|
||||
FindWindow $0 "DashCaddyWindowClass"
|
||||
${If} $0 <> 0
|
||||
MessageBox MB_YESNO|MB_ICONQUESTION "DashCaddy is currently running. Close it before installing?$\n$\n(Recommended: Yes)" IDYES CloseRunning
|
||||
Abort
|
||||
CloseRunning:
|
||||
SendMessage $0 ${WM_CLOSE} 0 0
|
||||
Sleep 1000
|
||||
${EndIf}
|
||||
|
||||
; Pre-check Docker
|
||||
nsExec::ExecToLog 'where docker.exe'
|
||||
Pop $0
|
||||
${If} $0 == 0
|
||||
StrCpy $DockerInstalled 1
|
||||
${Else}
|
||||
StrCpy $DockerInstalled 0
|
||||
${EndIf}
|
||||
|
||||
; Pre-check WSL2
|
||||
nsExec::ExecToLog 'wsl --status'
|
||||
Pop $0
|
||||
${If} $0 == 0
|
||||
StrCpy $WSLEnabled 1
|
||||
${Else}
|
||||
StrCpy $WSLEnabled 0
|
||||
${EndIf}
|
||||
FunctionEnd
|
||||
|
||||
Function .onInstSuccess
|
||||
; Run bootstrap (installs Docker, enables WSL, pulls images, starts services)
|
||||
ExecWait '"$INSTDIR\bootstrap.ps1"'
|
||||
|
||||
; Launch app
|
||||
Exec '"$INSTDIR\DashCaddy.exe"'
|
||||
FunctionEnd
|
||||
|
||||
; ─────────────────────────────────────────────────────────────────
|
||||
; Uninstaller
|
||||
; ─────────────────────────────────────────────────────────────────
|
||||
Section Uninstall
|
||||
; Stop app if running
|
||||
FindWindow $0 "DashCaddyWindowClass"
|
||||
${If} $0 <> 0
|
||||
SendMessage $0 ${WM_CLOSE} 0 0
|
||||
Sleep 1000
|
||||
${EndIf}
|
||||
|
||||
; Stop Docker containers
|
||||
ExecWait '"$INSTDIR\bootstrap.ps1" -Action Stop'
|
||||
|
||||
; Remove auto-start
|
||||
DeleteRegValue HKCU "Software\Microsoft\Windows\CurrentVersion\Run" "DashCaddy"
|
||||
|
||||
; Remove Start Menu shortcuts
|
||||
Delete "$SMPROGRAMS\DashCaddy\*.*"
|
||||
RMDir "$SMPROGRAMS\DashCaddy"
|
||||
|
||||
; Remove Desktop shortcut
|
||||
Delete "$DESKTOP\DashCaddy.lnk"
|
||||
|
||||
; Remove installed files
|
||||
Delete "$INSTDIR\*.exe"
|
||||
Delete "$INSTDIR\*.yaml"
|
||||
Delete "$INSTDIR\*.md"
|
||||
Delete "$INSTDIR\*.txt"
|
||||
Delete "$INSTDIR\*.ico"
|
||||
RMDir /r "$INSTDIR\app"
|
||||
RMDir /r "$INSTDIR\assets"
|
||||
|
||||
; Remove uninstaller
|
||||
Delete "$INSTDIR\Uninstall.exe"
|
||||
|
||||
; Remove registry keys
|
||||
DeleteRegKey HKCU "${PRODUCT_UNINST_KEY}"
|
||||
|
||||
; Remove directory if empty
|
||||
RMDir "$INSTDIR"
|
||||
|
||||
; Note: Docker Desktop and WSL2 are LEFT installed (shared system components)
|
||||
; User data in %LOCALAPPDATA%\DashCaddy\data is preserved
|
||||
SectionEnd
|
||||
|
||||
Function un.onUninstSuccess
|
||||
HideWindow
|
||||
MessageBox MB_OK "DashCaddy has been uninstalled.$\n$\nYour data in %LOCALAPPDATA%\DashCaddy\data was preserved.$\n$\nDocker Desktop and WSL2 were left installed.$\n$\nTo completely remove all data, manually delete %LOCALAPPDATA%\DashCaddy\"
|
||||
FunctionEnd
|
||||
Reference in New Issue
Block a user