190 lines
6.5 KiB
PowerShell
190 lines
6.5 KiB
PowerShell
[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
|
|
}
|