param( [switch]$DryRun ) $ErrorActionPreference = "Stop" $repoRoot = Split-Path -Parent $PSScriptRoot $overlayDir = Join-Path $repoRoot "overlay" $dataDir = Join-Path $repoRoot "data" $launcherLog = Join-Path $dataDir "startup-launcher.log" $backendUrl = "http://127.0.0.1:8000/api/state" if (-not (Test-Path $dataDir)) { New-Item -ItemType Directory -Path $dataDir | Out-Null } function Write-LauncherLog { param([string]$Message) $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" Add-Content -Path $launcherLog -Value "[$timestamp] $Message" } function Resolve-PythonPath { $venvPython = Join-Path $repoRoot ".venv\Scripts\python.exe" if (-not (Test-Path $venvPython)) { throw "Project Python environment not found: $venvPython. Run: py -3.14 -m venv .venv" } return (Resolve-Path $venvPython).Path } function Resolve-NpmPath { $npmCmd = Get-Command npm.cmd -ErrorAction SilentlyContinue if ($null -ne $npmCmd) { return $npmCmd.Source } throw "Could not find npm.cmd. Install Node.js/npm first." } function Test-PythonRuntime { param([string]$PythonPath) $output = & $PythonPath -c "import uvicorn, backend.main; print('backend imports OK')" 2>&1 if ($LASTEXITCODE -ne 0) { $details = ($output | Out-String).Trim() throw "Backend Python runtime validation failed: $details" } Write-LauncherLog "Python runtime verified: $PythonPath" } function Start-HiddenProcess { param( [string]$Name, [string]$FilePath, [string[]]$Arguments, [string]$WorkingDirectory, [string]$StdOutPath, [string]$StdErrPath ) if ($DryRun) { Write-LauncherLog "DRY RUN -> $Name | $FilePath $($Arguments -join ' ')" return $null } $process = Start-Process ` -FilePath $FilePath ` -ArgumentList $Arguments ` -WorkingDirectory $WorkingDirectory ` -WindowStyle Hidden ` -RedirectStandardOutput $StdOutPath ` -RedirectStandardError $StdErrPath ` -PassThru Write-LauncherLog "Started $Name process (PID $($process.Id))." return $process } function Test-BackendReady { try { $response = Invoke-WebRequest -UseBasicParsing -Uri $backendUrl -Method Get -TimeoutSec 2 -ErrorAction Stop return $response.StatusCode -eq 200 } catch { return $false } } function Wait-ForBackendReady { param( [System.Diagnostics.Process]$Process, [int]$TimeoutSeconds = 30 ) $deadline = (Get-Date).AddSeconds($TimeoutSeconds) do { if (Test-BackendReady) { return $true } if ($null -ne $Process) { $Process.Refresh() if ($Process.HasExited) { return $false } } Start-Sleep -Milliseconds 500 } while ((Get-Date) -lt $deadline) return $false } function Is-BackendRunning { $matches = Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | Where-Object { $_.Name -like "python*" -and $_.CommandLine -and $_.CommandLine -like "*backend.main:app*" } return ($matches | Measure-Object).Count -gt 0 } function Is-OverlayRunning { $matches = Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | Where-Object { $_.CommandLine -and $_.CommandLine -like "*custom-streamdeck*overlay*" } return ($matches | Measure-Object).Count -gt 0 } try { $pythonPath = Resolve-PythonPath $npmPath = Resolve-NpmPath Write-LauncherLog "Launcher using Python: $pythonPath" if (-not $DryRun) { Test-PythonRuntime $pythonPath } if (-not (Test-Path (Join-Path $repoRoot "frontend\dist\index.html"))) { Write-LauncherLog "Warning: frontend/dist is missing. Build frontend for production view." } $backendReady = Test-BackendReady if ($backendReady) { Write-LauncherLog "Backend already ready at $backendUrl. Skipping backend launch." } elseif (Is-BackendRunning) { Write-LauncherLog "Backend process exists but is not ready. Waiting for $backendUrl." if (-not $DryRun -and -not (Wait-ForBackendReady -TimeoutSeconds 30)) { throw "Existing backend process did not become ready at $backendUrl. See data/backend.stderr.log." } $backendReady = $true Write-LauncherLog "Backend ready at $backendUrl." } else { $backendOut = Join-Path $dataDir "backend.stdout.log" $backendErr = Join-Path $dataDir "backend.stderr.log" $backendProcess = Start-HiddenProcess ` -Name "backend" ` -FilePath $pythonPath ` -Arguments @("-m", "uvicorn", "backend.main:app", "--host", "127.0.0.1", "--port", "8000") ` -WorkingDirectory $repoRoot ` -StdOutPath $backendOut ` -StdErrPath $backendErr if (-not $DryRun -and -not (Wait-ForBackendReady -Process $backendProcess -TimeoutSeconds 30)) { $stderrTail = if (Test-Path $backendErr) { (Get-Content $backendErr -Tail 10 | Out-String).Trim() } else { "No backend stderr log was created." } Write-LauncherLog "Backend failed readiness check. stderr tail: $stderrTail" throw "Backend did not become ready at $backendUrl." } $backendReady = $true if (-not $DryRun) { Write-LauncherLog "Backend ready at $backendUrl." } } if (-not $backendReady -and -not $DryRun) { throw "Overlay launch skipped because the backend is not ready." } if (Is-OverlayRunning) { Write-LauncherLog "Overlay already running. Skipping overlay launch." } else { $overlayOut = Join-Path $dataDir "overlay.stdout.log" $overlayErr = Join-Path $dataDir "overlay.stderr.log" Start-HiddenProcess ` -Name "overlay" ` -FilePath $npmPath ` -Arguments @("run", "start") ` -WorkingDirectory $overlayDir ` -StdOutPath $overlayOut ` -StdErrPath $overlayErr } } catch { Write-LauncherLog "Launcher failed: $($_.Exception.Message)" throw }