| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261 |
- param(
- [switch]$Restart,
- [switch]$ForceKillPorts,
- [switch]$InstallDependencies,
- [switch]$InstallBrowsers,
- [switch]$InitDatabase,
- [switch]$SeedDatabase
- )
- $ErrorActionPreference = "Stop"
- Set-StrictMode -Version Latest
- $RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
- $RuntimeDir = Join-Path $RepoRoot ".runtime"
- $Services = @(
- @{
- Name = "api"
- Port = 3001
- Command = "npm.cmd run dev:api"
- ReadyUrl = "http://localhost:3001/health"
- ReadyText = '"ok":true'
- },
- @{
- Name = "web"
- Port = 3000
- Command = "npm.cmd run dev:web"
- ReadyUrl = "http://localhost:3000"
- ReadyText = ""
- }
- )
- function Write-Step([string]$Message) {
- Write-Host "==> $Message" -ForegroundColor Cyan
- }
- function Write-WarnLine([string]$Message) {
- Write-Host "WARN: $Message" -ForegroundColor Yellow
- }
- function Get-PidFilePath([string]$Name) {
- Join-Path $RuntimeDir "$Name.pid"
- }
- function Get-LogPath([string]$Name) {
- Join-Path $RuntimeDir "$Name.log"
- }
- function Get-ErrLogPath([string]$Name) {
- Join-Path $RuntimeDir "$Name.err.log"
- }
- function Get-ManagedPid([string]$Name) {
- $pidFile = Get-PidFilePath $Name
- if (-not (Test-Path $pidFile)) {
- return $null
- }
- $raw = Get-Content $pidFile -ErrorAction SilentlyContinue | Select-Object -First 1
- if (-not $raw) {
- return $null
- }
- $parsed = 0
- if ([int]::TryParse($raw, [ref]$parsed)) {
- return $parsed
- }
- return $null
- }
- function Test-ProcessRunning([int]$ProcessId) {
- try {
- $null = Get-Process -Id $ProcessId -ErrorAction Stop
- return $true
- } catch {
- return $false
- }
- }
- function Get-ProcessCommandLine([int]$ProcessId) {
- try {
- $process = Get-CimInstance Win32_Process -Filter "ProcessId = $ProcessId"
- if ($null -eq $process -or $null -eq $process.CommandLine) {
- return ""
- }
- return [string]$process.CommandLine
- } catch {
- return ""
- }
- }
- function Test-SentAIProcess([int]$ProcessId) {
- $commandLine = Get-ProcessCommandLine $ProcessId
- if (-not $commandLine) {
- return $false
- }
- return $commandLine -like "*SentAI*" -or
- $commandLine -like "*@sentai*" -or
- $commandLine -like "*npm.cmd run dev:api*" -or
- $commandLine -like "*npm.cmd run dev:web*"
- }
- function Stop-ProcessTree([int]$ProcessId) {
- & taskkill /PID $ProcessId /T /F | Out-Null
- }
- function Get-ListenerOwners([int]$Port) {
- try {
- return @(Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction Stop |
- Select-Object -ExpandProperty OwningProcess -Unique)
- } catch {
- return @()
- }
- }
- function Test-UrlReady([string]$Url, [string]$ExpectedText) {
- try {
- $response = Invoke-WebRequest -Uri $Url -UseBasicParsing -TimeoutSec 3
- if ($response.StatusCode -lt 200 -or $response.StatusCode -ge 400) {
- return $false
- }
- if ([string]::IsNullOrWhiteSpace($ExpectedText)) {
- return $true
- }
- $content = [string]$response.Content
- return $content.Contains($ExpectedText)
- } catch {
- return $false
- }
- }
- function Wait-ForReady([hashtable]$Service, [int]$TimeoutSeconds = 90) {
- $deadline = (Get-Date).AddSeconds($TimeoutSeconds)
- while ((Get-Date) -lt $deadline) {
- if (Test-UrlReady -Url $Service.ReadyUrl -ExpectedText $Service.ReadyText) {
- return $true
- }
- Start-Sleep -Seconds 2
- }
- return $false
- }
- function Ensure-PortAvailable([hashtable]$Service) {
- [array]$owners = @(Get-ListenerOwners $Service.Port)
- if ($owners.Count -eq 0) {
- return
- }
- foreach ($owner in $owners) {
- if ($ForceKillPorts -or (Test-SentAIProcess $owner)) {
- Write-WarnLine "Stopping process $owner on port $($Service.Port)."
- Stop-ProcessTree $owner
- continue
- }
- $commandLine = Get-ProcessCommandLine $owner
- throw "Port $($Service.Port) is already used by process $owner. Use -ForceKillPorts if you want the script to clear it. CommandLine: $commandLine"
- }
- Start-Sleep -Seconds 2
- [array]$remaining = @(Get-ListenerOwners $Service.Port)
- if ($remaining.Count -gt 0) {
- throw "Port $($Service.Port) is still occupied after cleanup."
- }
- }
- function Ensure-ManagedProcessState([hashtable]$Service) {
- $managedPid = Get-ManagedPid $Service.Name
- if (-not $managedPid) {
- return
- }
- if (-not (Test-ProcessRunning $managedPid)) {
- Remove-Item (Get-PidFilePath $Service.Name) -ErrorAction SilentlyContinue
- return
- }
- if ($Restart) {
- Write-WarnLine "Restart requested. Stopping existing managed $($Service.Name) process $managedPid."
- Stop-ProcessTree $managedPid
- Remove-Item (Get-PidFilePath $Service.Name) -ErrorAction SilentlyContinue
- Start-Sleep -Seconds 2
- return
- }
- if (Test-UrlReady -Url $Service.ReadyUrl -ExpectedText $Service.ReadyText) {
- Write-Step "$($Service.Name) is already running on port $($Service.Port)."
- $Service.SkipStart = $true
- return
- }
- Write-WarnLine "Managed $($Service.Name) process $managedPid exists but is not healthy. Restarting it."
- Stop-ProcessTree $managedPid
- Remove-Item (Get-PidFilePath $Service.Name) -ErrorAction SilentlyContinue
- Start-Sleep -Seconds 2
- }
- function Start-ServiceProcess([hashtable]$Service) {
- if ($Service.ContainsKey("SkipStart") -and $Service.SkipStart) {
- return
- }
- $stdout = Get-LogPath $Service.Name
- $stderr = Get-ErrLogPath $Service.Name
- Remove-Item $stdout, $stderr -ErrorAction SilentlyContinue
- Write-Step "Starting $($Service.Name) with '$($Service.Command)'."
- $process = Start-Process `
- -FilePath "cmd.exe" `
- -ArgumentList "/c", $Service.Command `
- -WorkingDirectory $RepoRoot `
- -RedirectStandardOutput $stdout `
- -RedirectStandardError $stderr `
- -PassThru
- Set-Content -Path (Get-PidFilePath $Service.Name) -Value $process.Id
- if (-not (Wait-ForReady -Service $Service)) {
- throw "$($Service.Name) did not become ready in time. Check $stdout and $stderr"
- }
- Write-Step "$($Service.Name) is ready at $($Service.ReadyUrl)."
- }
- New-Item -ItemType Directory -Path $RuntimeDir -Force | Out-Null
- Set-Location $RepoRoot
- if ($InstallDependencies) {
- Write-Step "Installing npm dependencies."
- & npm.cmd install
- }
- if ($InitDatabase) {
- Write-Step "Initializing database schema."
- & npm.cmd run db:init
- }
- if ($SeedDatabase) {
- Write-Step "Seeding database."
- & npm.cmd run db:seed
- }
- if ($InstallBrowsers) {
- Write-Step "Installing Playwright browsers."
- & npm.cmd run install:browsers
- }
- foreach ($service in $Services) {
- Ensure-ManagedProcessState $service
- Ensure-PortAvailable $service
- Start-ServiceProcess $service
- }
- Write-Host ""
- Write-Host "SentAI local environment is running." -ForegroundColor Green
- Write-Host " Web: http://localhost:3000"
- Write-Host " API: http://localhost:3001/health"
|