[CmdletBinding()] param( [string]$ConfigPath, [switch]$ValidateConfigOnly ) $ErrorActionPreference = "Stop" # The Go processes emit UTF-8. Without this the console decodes their output # using the ANSI code page (936 on Simplified Chinese systems) and every # non-ASCII log line arrives as mojibake. # # Setting [Console]::OutputEncoding alone is not enough: PowerShell 5.1 decodes # child process output by the console code page, so chcp has to change too. # # NOTE: keep this file pure ASCII. PowerShell 5.1 reads a .ps1 without a BOM as # ANSI, and a comment line holding an odd number of UTF-8 high bytes pairs its # last byte with the newline, swallowing it and commenting out the line below. try { $null = & chcp.com 65001 [Console]::OutputEncoding = [Text.Encoding]::UTF8 $OutputEncoding = [Text.Encoding]::UTF8 } catch { } $workspaceRoot = Split-Path -Parent $PSScriptRoot $webDirectory = Join-Path $workspaceRoot "web" function ConvertFrom-YamlScalar { param([string]$Value) $value = $Value.Trim() if ($value.Length -ge 2) { if ($value.StartsWith('"') -and $value.EndsWith('"')) { return $value.Substring(1, $value.Length - 2).Replace('\"', '"').Replace('\\', '\') } if ($value.StartsWith("'") -and $value.EndsWith("'")) { return $value.Substring(1, $value.Length - 2).Replace("''", "'") } } return $value } function Read-PortConfig { param([string]$Path) if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { throw "Local configuration file was not found: $Path. Copy config.example.yaml to config.yaml." } $values = @{} $insidePorts = $false foreach ($line in Get-Content -LiteralPath $Path) { if ($line -match '^\s*(#.*)?$') { continue } if ($line -match '^ports\s*:\s*$') { $insidePorts = $true continue } if ($insidePorts -and $line -match '^\S') { break } if ($insidePorts -and $line -match '^\s+(server|web)\s*:\s*(.*?)\s*$') { $values[$Matches[1]] = ConvertFrom-YamlScalar $Matches[2] } } foreach ($key in 'server', 'web') { $parsedPort = 0 if (-not $values.ContainsKey($key) -or -not [int]::TryParse([string]$values[$key], [ref]$parsedPort) -or $parsedPort -lt 1 -or $parsedPort -gt 65535) { throw "ports.$key must be an integer between 1 and 65535 in: $Path" } $values[$key] = $parsedPort } if ($values.server -eq $values.web) { throw "ports.server and ports.web must be different in: $Path" } return @{ Server = [int]$values.server; Web = [int]$values.web } } function Wait-ApiReady { param( [int]$Port, [int]$TimeoutSeconds = 60 ) $healthUrl = "http://127.0.0.1:${Port}/api/v1/health" $stopwatch = [Diagnostics.Stopwatch]::StartNew() Write-Host "Waiting for GoAuto API at $healthUrl ..." -ForegroundColor Cyan while ($stopwatch.Elapsed.TotalSeconds -lt $TimeoutSeconds) { try { $response = Invoke-WebRequest -Uri $healthUrl -UseBasicParsing -TimeoutSec 2 if ($response.StatusCode -eq 200) { Write-Host "GoAuto API is ready." -ForegroundColor Green return } } catch { # The API process may still be migrating or compiling. } Start-Sleep -Milliseconds 1000 } throw "GoAuto API did not become ready within ${TimeoutSeconds}s: $healthUrl" } if ([string]::IsNullOrWhiteSpace($ConfigPath)) { $ConfigPath = Join-Path $workspaceRoot "config.yaml" } elseif (-not [IO.Path]::IsPathRooted($ConfigPath)) { $ConfigPath = Join-Path $workspaceRoot $ConfigPath } $ConfigPath = [IO.Path]::GetFullPath($ConfigPath) $ports = Read-PortConfig $ConfigPath Write-Host "GoAuto web UI" -ForegroundColor Cyan Write-Host "Config: $ConfigPath" Write-Host "Ports: server=$($ports.Server), web=$($ports.Web)" if ($ValidateConfigOnly) { Write-Host "Local port configuration is valid." -ForegroundColor Green return } Wait-ApiReady -Port $ports.Server $node = Get-Command node.exe -ErrorAction SilentlyContinue if ($node) { $nodePath = $node.Source } else { $nodeCandidates = @() if (-not [string]::IsNullOrWhiteSpace($env:ProgramFiles)) { $nodeCandidates += Join-Path $env:ProgramFiles "nodejs\node.exe" } $nodeCandidates += "C:\Program Files\nodejs\node.exe" $nodePath = $nodeCandidates | Select-Object -Unique | Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } | Select-Object -First 1 if (-not $nodePath) { throw "node.exe was not found. Install Node.js or add its directory to PATH." } } $nodeDirectory = Split-Path -Parent $nodePath $env:Path = "$nodeDirectory;$env:Path" Write-Host "Node: $nodePath" $pnpm = Get-Command pnpm.cmd -ErrorAction SilentlyContinue if (-not $pnpm) { throw "pnpm was not found. Install pnpm and try again." } $pnpmCli = Join-Path (Split-Path -Parent $pnpm.Source) "node_modules\pnpm\bin\pnpm.mjs" if (-not (Test-Path -LiteralPath $pnpmCli -PathType Leaf)) { $pnpmCli = $null } Push-Location $webDirectory try { if (-not (Test-Path -LiteralPath "node_modules" -PathType Container)) { Write-Host "Installing frontend dependencies..." -ForegroundColor Cyan if ($pnpmCli) { & $nodePath $pnpmCli install --frozen-lockfile } else { & $pnpm.Source install --frozen-lockfile } if ($LASTEXITCODE -ne 0) { throw "Frontend dependency installation failed." } } # Keep browser requests same-origin. Vite proxies API/static requests to the # local backend, so a LAN client never tries to call its own 127.0.0.1. $env:VUE_APP_BASE_API = "" $env:VITE_DEV_PROXY_TARGET = "http://127.0.0.1:$($ports.Server)" Write-Host "Starting GoAuto web UI at http://0.0.0.0:$($ports.Web)" -ForegroundColor Green $viteCli = Join-Path $webDirectory "node_modules\vite\bin\vite.js" if (-not (Test-Path -LiteralPath $viteCli -PathType Leaf)) { throw "Vite was not found after dependency installation: $viteCli" } & $nodePath $viteCli --host 0.0.0.0 --port $ports.Web --strictPort if ($LASTEXITCODE -ne 0) { throw "Frontend exited with code $LASTEXITCODE." } } finally { Remove-Item Env:VUE_APP_BASE_API -ErrorAction SilentlyContinue Remove-Item Env:VITE_DEV_PROXY_TARGET -ErrorAction SilentlyContinue Pop-Location }