studio-ui/server.ps1
|
<# .SYNOPSIS Standalone HTTP server for the dotbot studio. .DESCRIPTION Lightweight PowerShell HTTP server using System.Net.HttpListener. Serves the REST API via StudioAPI.psm1 and static client files from the static/ directory. Port selection: tries ports starting from BasePort (default 9001) until an available one is found. Writes the chosen port to ~/dotbot/.studio-port so the CLI can discover a running studio. .PARAMETER Port Base port to start searching from (default: 9001) .EXAMPLE pwsh server.ps1 pwsh server.ps1 -Port 4000 #> param( [Parameter(Mandatory = $false)] [ValidateRange(1024, 65535)] [int]$Port = 9001 ) Set-StrictMode -Version 1.0 # --------------------------------------------------------------------------- # Resolve paths # --------------------------------------------------------------------------- $scriptDir = $PSScriptRoot $staticRoot = Join-Path $scriptDir 'static' # Resolve workflows directory: walk up to find dotbot root, fallback ~/dotbot function Find-DotbotRoot { $dir = $scriptDir while ($dir -ne [System.IO.Path]::GetPathRoot($dir)) { if ((Test-Path (Join-Path $dir 'workflows')) -and (Test-Path (Join-Path $dir 'scripts'))) { return $dir } $dir = Split-Path $dir -Parent } return Join-Path $HOME 'dotbot' } $dotbotRoot = Find-DotbotRoot $workflowsDir = Join-Path $dotbotRoot 'workflows' # Import the API module Import-Module (Join-Path $scriptDir 'StudioAPI.psm1') -Force Initialize-StudioAPI -WorkflowsDir $workflowsDir -StaticRoot $staticRoot # --------------------------------------------------------------------------- # Port file management (defined early so trap/cleanup can reference them) # --------------------------------------------------------------------------- $portFile = Join-Path $HOME 'dotbot' '.studio-port' function Write-PortFile { param([int]$Port) $dir = Split-Path $portFile -Parent if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Force -Path $dir | Out-Null } @{ port = $Port; pid = $PID } | ConvertTo-Json -Compress | Set-Content -Path $portFile -Encoding UTF8 -NoNewline } function Remove-PortFile { if (Test-Path $portFile) { Remove-Item -Path $portFile -Force -ErrorAction SilentlyContinue } } # --------------------------------------------------------------------------- # Port discovery # --------------------------------------------------------------------------- $maxAttempts = 20 function Find-AvailablePort { param([int]$StartPort) for ($p = $StartPort; $p -lt ($StartPort + $maxAttempts); $p++) { $http = [System.Net.HttpListener]::new() try { $http.Prefixes.Add("http://localhost:$p/") $http.Start() # Port is available — stop the test listener and return $http.Stop() $http.Close() return $p } catch { try { $http.Close() } catch { } continue } } return $null } $selectedPort = Find-AvailablePort -StartPort $Port if (-not $selectedPort) { $endPort = $Port + $maxAttempts - 1 Write-Host "" Write-Host " Could not find an available port to start the studio-ui server." -ForegroundColor Red Write-Host " Tried ports ${Port}-${endPort}, all are in use or blocked." -ForegroundColor Red Write-Host " Try specifying a different port: pwsh server.ps1 -Port 9100" -ForegroundColor Yellow Write-Host "" exit 1 } # --------------------------------------------------------------------------- # Start server # --------------------------------------------------------------------------- $listener = [System.Net.HttpListener]::new() $listener.Prefixes.Add("http://localhost:$selectedPort/") try { $listener.Start() } catch { Write-Host "Failed to start listener on port $selectedPort : $_" -ForegroundColor Red exit 1 } Write-PortFile -Port $selectedPort $url = "http://localhost:$selectedPort" Write-Host "dotbot studio running at $url" -ForegroundColor Green # Auto-open browser (skip if DEV_MODE env is set — Vite handles the UI in dev) if (-not $env:DEV_MODE) { try { Start-Process $url } catch { Write-Host "Open $url in your browser" -ForegroundColor Yellow } } # Cleanup on exit $null = Register-EngineEvent -SourceIdentifier PowerShell.Exiting -Action { Remove-PortFile } trap { Remove-PortFile if ($listener.IsListening) { $listener.Stop() } $listener.Close() } # --------------------------------------------------------------------------- # Request loop # --------------------------------------------------------------------------- try { while ($listener.IsListening) { try { $context = $listener.GetContext() $handled = Invoke-StudioRequest -Context $context if (-not $handled) { $context.Response.StatusCode = 404 $buffer = [System.Text.Encoding]::UTF8.GetBytes('{"error":"Not found"}') $context.Response.ContentType = 'application/json' $context.Response.ContentLength64 = $buffer.Length $context.Response.OutputStream.Write($buffer, 0, $buffer.Length) $context.Response.Close() } } catch [System.Net.HttpListenerException] { # Listener was stopped (Ctrl+C) — exit cleanly break } catch { Write-Host "Request error: $_" -ForegroundColor Yellow } } } finally { Remove-PortFile if ($listener.IsListening) { $listener.Stop() } $listener.Close() Write-Host "Server stopped." -ForegroundColor Yellow } |