diff --git a/README.md b/README.md index 38cef32..303ba77 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,22 @@ bash install.sh --local Requires Node.js 22.x (LTS, recommended) or 24.x, and pnpm (auto-installed via corepack if missing). +### One-Command Install (Windows PowerShell) + +```powershell +git clone https://github.com/builderz-labs/mission-control.git +cd mission-control +.\install.ps1 -Mode local +``` + +Or with Docker: + +```powershell +.\install.ps1 -Mode docker +``` + +Additional options: `-Port 8080`, `-SkipOpenClaw`. Requires Node.js 22+ and pnpm (auto-installed via corepack if missing). + ### Manual Setup > **Requires [pnpm](https://pnpm.io/installation)** and **Node.js 22.x (LTS, recommended) or 24.x**. diff --git a/install.ps1 b/install.ps1 new file mode 100644 index 0000000..31f7527 --- /dev/null +++ b/install.ps1 @@ -0,0 +1,423 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Mission Control — Windows Installer + The mothership for your OpenClaw fleet. + +.DESCRIPTION + Installs Mission Control on Windows via local Node.js deployment. + Mirrors the behaviour of install.sh for Linux/macOS. + +.PARAMETER Mode + Deployment mode: "local" (default) or "docker". + +.PARAMETER Port + Port the Next.js server listens on (default: 3000). + +.PARAMETER DataDir + Custom data directory path (default: .data/ in project root). + +.PARAMETER InstallDir + Target directory when cloning from GitHub (default: .\mission-control). + +.PARAMETER SkipOpenClaw + Skip OpenClaw fleet checks. + +.EXAMPLE + .\install.ps1 + .\install.ps1 -Mode local -Port 8080 + .\install.ps1 -Mode docker + +.NOTES + PowerShell uses single-dash parameters: -Mode local, -Port 8080 + Bash-style --local / --docker flags are NOT supported by PowerShell syntax. +#> + +[CmdletBinding()] +param( + [ValidateSet("local", "docker")] + [string]$Mode = "", + + [int]$Port = 3000, + + [string]$DataDir = "", + + [string]$InstallDir = "", + + [switch]$SkipOpenClaw +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +# ── Defaults ────────────────────────────────────────────────────────────────── +if (-not $InstallDir) { + $InstallDir = if ($env:MC_INSTALL_DIR) { $env:MC_INSTALL_DIR } else { Join-Path (Get-Location) "mission-control" } +} +$RepoUrl = "https://github.com/builderz-labs/mission-control.git" + +# ── Helpers ─────────────────────────────────────────────────────────────────── +function Write-MC { param([string]$Msg) Write-Host "[MC] $Msg" -ForegroundColor Blue } +function Write-Ok { param([string]$Msg) Write-Host "[OK] $Msg" -ForegroundColor Green } +function Write-Warn { param([string]$Msg) Write-Host "[!!] $Msg" -ForegroundColor Yellow } +function Write-Err { param([string]$Msg) Write-Host "[ERR] $Msg" -ForegroundColor Red } +function Stop-WithError { param([string]$Msg) Write-Err $Msg; exit 1 } + +function Test-Command { param([string]$Name) $null -ne (Get-Command $Name -ErrorAction SilentlyContinue) } + +function Get-RandomPassword { + param([int]$Length = 24) + $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' + -join (1..$Length | ForEach-Object { $chars[(Get-Random -Maximum $chars.Length)] }) +} + +function Get-RandomHex { + param([int]$Length = 32) + $bytes = New-Object byte[] ($Length / 2) + [System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($bytes) + ($bytes | ForEach-Object { $_.ToString("x2") }) -join '' +} + +# ── Prerequisites ───────────────────────────────────────────────────────────── +function Test-Prerequisites { + $hasDocker = $false + $hasNode = $false + + if (Test-Command "docker") { + docker info *>$null + if ($LASTEXITCODE -eq 0) { + $hasDocker = $true + $dockerVersion = (docker --version) -split "`n" | Select-Object -First 1 + Write-Ok "Docker available ($dockerVersion)" + } else { + Write-Warn "Docker found but daemon is not running" + } + } + + if (Test-Command "node") { + $nodeVersion = (node -v).TrimStart('v') + $nodeMajor = [int]($nodeVersion -split '\.')[0] + if ($nodeMajor -ge 22) { + $hasNode = $true + Write-Ok "Node.js v$nodeVersion available" + } else { + Write-Warn "Node.js v$nodeVersion found but v22+ required (LTS recommended)" + } + } + + if (-not $hasDocker -and -not $hasNode) { + Stop-WithError "Either Docker or Node.js 22+ is required. Install one and retry." + } + + # Auto-select deploy mode if not specified + if (-not $script:Mode) { + if ($hasDocker) { + $script:Mode = "docker" + Write-MC "Auto-selected Docker deployment (use -Mode local to override)" + } else { + $script:Mode = "local" + Write-MC "Auto-selected local deployment (Docker not available)" + } + } + + # Validate chosen mode + if ($script:Mode -eq "docker" -and -not $hasDocker) { + Stop-WithError "Docker deployment requested but Docker is not available" + } + if ($script:Mode -eq "local" -and -not $hasNode) { + Stop-WithError "Local deployment requested but Node.js 22+ is not available" + } + if ($script:Mode -eq "local" -and -not (Test-Command "pnpm")) { + Write-MC "Installing pnpm via corepack..." + corepack enable + corepack prepare pnpm@latest --activate + Write-Ok "pnpm installed" + } +} + +# ── Clone or update repo ───────────────────────────────────────────────────── +function Get-Source { + if (Test-Path (Join-Path $script:InstallDir ".git")) { + Write-MC "Updating existing installation at $($script:InstallDir)..." + Push-Location $script:InstallDir + try { + git fetch --tags + $latestTag = git describe --tags --abbrev=0 origin/main 2>$null + if ($latestTag) { + git checkout $latestTag + Write-Ok "Checked out $latestTag" + } else { + git pull origin main + Write-Ok "Updated to latest main" + } + } finally { + Pop-Location + } + } else { + Write-MC "Cloning Mission Control..." + if (-not (Test-Command "git")) { + Stop-WithError "git is required to clone the repository" + } + git clone --depth 1 $RepoUrl $script:InstallDir + Write-Ok "Cloned to $($script:InstallDir)" + } +} + +# ── Generate .env ───────────────────────────────────────────────────────────── +function New-EnvFile { + $envPath = Join-Path $script:InstallDir ".env" + $examplePath = Join-Path $script:InstallDir ".env.example" + + if (Test-Path $envPath) { + Write-MC "Existing .env found - keeping current configuration" + return + } + + if (-not (Test-Path $examplePath)) { + Stop-WithError ".env.example not found at $examplePath" + } + + Write-MC "Generating secure .env configuration..." + + $authPass = Get-RandomPassword 24 + $apiKey = Get-RandomHex 32 + $authSecret = Get-RandomPassword 32 + + $content = Get-Content $examplePath -Raw + $content = $content -replace '(?m)^# AUTH_USER=.*', "AUTH_USER=admin" + $content = $content -replace '(?m)^# AUTH_PASS=.*', "AUTH_PASS=$authPass" + $content = $content -replace '(?m)^# API_KEY=.*', "API_KEY=$apiKey" + $content = $content -replace '(?m)^# AUTH_SECRET=.*', "AUTH_SECRET=$authSecret" + + # Set port if non-default + if ($script:Port -ne 3000) { + $content = $content -replace '(?m)^# PORT=3000', "PORT=$($script:Port)" + } + + $content | Set-Content $envPath -NoNewline + Write-Ok "Secure .env generated" + + Write-Host "" + Write-Host " AUTH_USER: admin" -ForegroundColor Cyan + Write-Host " AUTH_PASS: $authPass" -ForegroundColor Cyan + Write-Host " API_KEY: $apiKey" -ForegroundColor Cyan + Write-Host "" + Write-Host " Save these credentials - they are not stored elsewhere." -ForegroundColor Yellow + Write-Host "" +} + +# ── Docker deployment ───────────────────────────────────────────────────────── +function Deploy-Docker { + Write-MC "Starting Docker deployment..." + + Push-Location $script:InstallDir + try { + $env:MC_PORT = $script:Port + docker compose up -d --build + + Write-MC "Waiting for Mission Control to become healthy..." + $retries = 30 + while ($retries -gt 0) { + try { + $response = Invoke-WebRequest -Uri "http://localhost:$($script:Port)/login" -UseBasicParsing -TimeoutSec 2 -ErrorAction SilentlyContinue + if ($response.StatusCode -eq 200) { break } + } catch { } + Start-Sleep -Seconds 2 + $retries-- + } + + if ($retries -eq 0) { + Write-Warn "Timeout waiting for health check - container may still be starting" + docker compose logs --tail 20 + } else { + Write-Ok "Mission Control is running in Docker" + } + } finally { + Pop-Location + } +} + +# ── Local deployment ────────────────────────────────────────────────────────── +function Deploy-Local { + Write-MC "Starting local deployment..." + + Push-Location $script:InstallDir + try { + pnpm install --frozen-lockfile 2>$null + if ($LASTEXITCODE -ne 0) { pnpm install } + Write-Ok "Dependencies installed" + + Write-MC "Building Mission Control..." + pnpm build + if ($LASTEXITCODE -ne 0) { Stop-WithError "Build failed" } + Write-Ok "Build complete" + + # Copy static assets into standalone directory (required by Next.js standalone mode) + $standaloneDir = Join-Path $script:InstallDir ".next" | Join-Path -ChildPath "standalone" + $standaloneNextDir = Join-Path $standaloneDir ".next" + $sourceStatic = Join-Path $script:InstallDir ".next" | Join-Path -ChildPath "static" + $destStatic = Join-Path $standaloneNextDir "static" + $sourcePublic = Join-Path $script:InstallDir "public" + $destPublic = Join-Path $standaloneDir "public" + + if (Test-Path $sourceStatic) { + if (Test-Path $destStatic) { Remove-Item $destStatic -Recurse -Force } + Copy-Item $sourceStatic $destStatic -Recurse + } + if (Test-Path $sourcePublic) { + if (Test-Path $destPublic) { Remove-Item $destPublic -Recurse -Force } + Copy-Item $sourcePublic $destPublic -Recurse + } + Write-Ok "Static assets copied to standalone directory" + + Write-MC "Starting Mission Control..." + $env:PORT = $script:Port + $env:NODE_ENV = "production" + $env:HOSTNAME = "0.0.0.0" + $dataPath = Join-Path $script:InstallDir ".data" + $logPath = Join-Path $dataPath "mc.log" + $errLogPath = Join-Path $dataPath "mc-err.log" + $pidPath = Join-Path $dataPath "mc.pid" + + $serverJs = Join-Path $standaloneDir "server.js" + $process = Start-Process -FilePath "cmd.exe" ` + -ArgumentList "/c node `"$serverJs`" > `"$logPath`" 2> `"$errLogPath`"" ` + -WorkingDirectory $script:InstallDir ` + -WindowStyle Hidden ` + -PassThru + $process.Id | Set-Content $pidPath + + Start-Sleep -Seconds 3 + if (-not $process.HasExited) { + Write-Ok "Mission Control running (PID $($process.Id))" + } else { + Write-Err "Failed to start. Check logs: $logPath" + exit 1 + } + } finally { + Pop-Location + } +} + +# ── OpenClaw fleet check ───────────────────────────────────────────────────── +function Test-OpenClaw { + if ($SkipOpenClaw) { + Write-MC "Skipping OpenClaw checks (-SkipOpenClaw)" + return + } + + Write-Host "" + Write-MC "=== OpenClaw Fleet Check ===" + + if (Test-Command "openclaw") { + $ocVersion = try { openclaw --version 2>$null } catch { "unknown" } + Write-Ok "OpenClaw binary found: $ocVersion" + } elseif (Test-Command "clawdbot") { + $cbVersion = try { clawdbot --version 2>$null } catch { "unknown" } + Write-Ok "ClawdBot binary found: $cbVersion (legacy)" + Write-Warn "Consider upgrading to openclaw CLI" + } else { + Write-MC "OpenClaw CLI not found - install it to enable agent orchestration" + Write-MC " See: https://github.com/builderz-labs/openclaw" + return + } + + # Check OpenClaw home directory + $ocHome = if ($env:OPENCLAW_HOME) { $env:OPENCLAW_HOME } else { Join-Path $HOME ".openclaw" } + if (Test-Path $ocHome) { + Write-Ok "OpenClaw home: $ocHome" + + $ocConfig = Join-Path $ocHome "openclaw.json" + if (Test-Path $ocConfig) { + Write-Ok "Config found: $ocConfig" + } else { + Write-Warn "No openclaw.json found at $ocConfig" + Write-MC "Mission Control will create a default config on first gateway connection" + } + } else { + Write-MC "OpenClaw home not found at $ocHome" + Write-MC "Set OPENCLAW_HOME in .env to point to your OpenClaw state directory" + } + + # Check gateway port + $gwHost = if ($env:OPENCLAW_GATEWAY_HOST) { $env:OPENCLAW_GATEWAY_HOST } else { "127.0.0.1" } + $gwPort = if ($env:OPENCLAW_GATEWAY_PORT) { [int]$env:OPENCLAW_GATEWAY_PORT } else { 18789 } + try { + $tcp = New-Object System.Net.Sockets.TcpClient + $tcp.Connect($gwHost, $gwPort) + $tcp.Close() + Write-Ok "Gateway reachable at ${gwHost}:${gwPort}" + } catch { + Write-MC "Gateway not reachable at ${gwHost}:${gwPort} (start it with: openclaw gateway start)" + } +} + +# ── Main ────────────────────────────────────────────────────────────────────── +function Main { + Write-Host "" + Write-Host " +======================================+" -ForegroundColor Magenta + Write-Host " | Mission Control Installer |" -ForegroundColor Magenta + Write-Host " | The mothership for your fleet |" -ForegroundColor Magenta + Write-Host " +======================================+" -ForegroundColor Magenta + Write-Host "" + + $arch = if ([Environment]::Is64BitOperatingSystem) { "x64" } else { "x86" } + Write-Ok "Detected Windows/$arch" + + Test-Prerequisites + + # If running from within an existing clone, use current dir + $packageJson = Join-Path (Get-Location) "package.json" + if ((Test-Path $packageJson) -and (Select-String -Path $packageJson -Pattern '"mission-control"' -Quiet)) { + $script:InstallDir = (Get-Location).Path + Write-MC "Running from existing clone at $($script:InstallDir)" + } else { + Get-Source + } + + # Ensure data directory exists + $dataDir = Join-Path $script:InstallDir ".data" + if (-not (Test-Path $dataDir)) { + New-Item -ItemType Directory -Path $dataDir -Force | Out-Null + } + + New-EnvFile + + switch ($Mode) { + "docker" { Deploy-Docker } + "local" { Deploy-Local } + default { Stop-WithError "Unknown deploy mode: $Mode" } + } + + Test-OpenClaw + + # ── Print summary ── + Write-Host "" + Write-Host " +======================================+" -ForegroundColor Green + Write-Host " | Installation Complete |" -ForegroundColor Green + Write-Host " +======================================+" -ForegroundColor Green + Write-Host "" + Write-MC "Dashboard: http://localhost:$Port" + Write-MC "Mode: $Mode" + Write-MC "Data: $(Join-Path $script:InstallDir '.data')" + Write-Host "" + Write-MC "Credentials are in: $(Join-Path $script:InstallDir '.env')" + Write-Host "" + + if ($Mode -eq "docker") { + Write-MC "Manage:" + Write-MC " docker compose logs -f # view logs" + Write-MC " docker compose restart # restart" + Write-MC " docker compose down # stop" + } else { + $mcDataPath = Join-Path $script:InstallDir ".data" + $pidPath = Join-Path $mcDataPath "mc.pid" + $logPath = Join-Path $mcDataPath "mc.log" + Write-MC "Manage:" + Write-MC " Get-Content '$logPath' -Tail 50 # view logs" + Write-MC " Stop-Process -Id (Get-Content '$pidPath') # stop" + } + Write-Host "" +} + +Main \ No newline at end of file