현재 작업 상태 저장
This commit is contained in:
parent
07cc67761e
commit
6bd91b0d5e
674 changed files with 8726 additions and 298 deletions
|
|
@ -1,4 +1,4 @@
|
|||
<#
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Vignette 로컬 개발 스택(게이트웨이 9099 + API 8000 + 웹 5173)을 정리한다.
|
||||
.DESCRIPTION
|
||||
|
|
@ -6,6 +6,9 @@
|
|||
.EXAMPLE
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\dev-down.ps1
|
||||
#>
|
||||
param(
|
||||
[int]$ApiPort = 8000
|
||||
)
|
||||
$ErrorActionPreference = 'SilentlyContinue'
|
||||
|
||||
function Stop-Stale([string]$pattern, [string]$label) {
|
||||
|
|
@ -21,5 +24,9 @@ Write-Host "로컬 dev 스택 종료..."
|
|||
Stop-Stale 'engine_gateway\.gateway' 'gateway'
|
||||
Stop-Stale 'app\.main:app' 'api'
|
||||
Stop-Stale 'vite' 'web'
|
||||
Get-NetTCPConnection -LocalPort $ApiPort -State Listen -ErrorAction SilentlyContinue |
|
||||
Select-Object -ExpandProperty OwningProcess -Unique |
|
||||
Where-Object { $_ -and $_ -ne 0 } |
|
||||
ForEach-Object { Stop-Process -Id $_ -Force }
|
||||
Start-Sleep -Seconds 1
|
||||
Write-Host "완료."
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
<#
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Vignette 로컬 개발 스택을 깔끔하게 (재)기동한다: 엔진 게이트웨이(9099) + API(8000) + 웹(5173).
|
||||
|
||||
.DESCRIPTION
|
||||
- 기존(고아 포함) 프로세스를 커맨드라인 기준으로 정확히 정리한 뒤 새로 띄운다.
|
||||
API는 --reload(watchfiles)로 핫리로드 — app/ 수정 시 자동 재기동(수동 재시작 불필요).
|
||||
API는 Windows reloader 고아 프로세스/포트 잔류를 피하려고 단일 프로세스로 띄운다.
|
||||
게이트웨이는 no-reload 유지(상주 claude -p 세션 보존; Windows에선 reload 시 자식 claude 고아화).
|
||||
웹(vite)은 npm run dev 기본 HMR.
|
||||
- API는 DB 미가용 시 in-memory degraded로 기동된다(Postgres 불필요). dev-login + seed 페르소나 활성.
|
||||
|
|
@ -18,6 +18,7 @@
|
|||
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\dev-up.ps1
|
||||
#>
|
||||
param(
|
||||
[int]$ApiPort = 8000,
|
||||
[switch]$NoGateway,
|
||||
[switch]$NoWeb,
|
||||
[switch]$NoDb
|
||||
|
|
@ -58,6 +59,16 @@ function Stop-Stale([string]$pattern, [string]$label) {
|
|||
}
|
||||
}
|
||||
|
||||
function Stop-ListenerByPort([int]$port, [string]$label) {
|
||||
Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue |
|
||||
Select-Object -ExpandProperty OwningProcess -Unique |
|
||||
Where-Object { $_ -and $_ -ne 0 } |
|
||||
ForEach-Object {
|
||||
Write-Host (" stop {0,-8} PID {1} (:${port})" -f $label, $_)
|
||||
Stop-Process -Id $_ -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
function Wait-Health([string]$url, [string]$label, [int]$timeoutSec = 40) {
|
||||
$deadline = (Get-Date).AddSeconds($timeoutSec)
|
||||
while ((Get-Date) -lt $deadline) {
|
||||
|
|
@ -70,70 +81,112 @@ function Wait-Health([string]$url, [string]$label, [int]$timeoutSec = 40) {
|
|||
}
|
||||
|
||||
function Ensure-DevDb {
|
||||
# Docker Postgres(pgvector)를 apps/api/.env 의 DATABASE_URL 자격증명에 맞춰 보장한다.
|
||||
# DB가 있어야 페르소나 source=database 가 되어 UI 회기 시작이 열린다(무DB면 degraded로 막힘).
|
||||
docker ps *> $null 2>&1
|
||||
if ($LASTEXITCODE -ne 0) { Write-Host " WARN Docker 데몬 미응답 — DB 없이 degraded(UI 세션 시작 제한). Docker Desktop 실행 필요."; return }
|
||||
$running = docker ps --filter name=vignette-dev-db --format "{{.Names}}" 2>$null
|
||||
if ("$running" -match 'vignette-dev-db') { Write-Host " OK db (vignette-dev-db 실행 중)"; return }
|
||||
docker rm -f vignette-dev-db *> $null 2>&1
|
||||
$envLines = Get-Content (Join-Path $api '.env')
|
||||
$dbUrl = (($envLines | Where-Object { $_ -match '^DATABASE_URL=' }) -replace '^DATABASE_URL=','').Trim()
|
||||
if ($dbUrl -notmatch 'postgresql://([^:]+):([^@]+)@[^:]+:([0-9]+)/(\S+)') { Write-Host " WARN DATABASE_URL 파싱 실패 — DB 스킵"; return }
|
||||
$u=$Matches[1]; $p=$Matches[2]; $port=$Matches[3]; $db=$Matches[4]
|
||||
$initPath = Join-Path $repo 'infra\db\init'
|
||||
docker run -d --name vignette-dev-db -p "$port`:5432" -e POSTGRES_USER=$u -e POSTGRES_PASSWORD=$p -e POSTGRES_DB=$db -e APP_DB_USER=vignette_app -e APP_DB_PASSWORD=vignette_app -v "$initPath`:/docker-entrypoint-initdb.d:ro" pgvector/pgvector:pg16 *> $null
|
||||
for ($i=0; $i -lt 24; $i++) {
|
||||
Start-Sleep -Seconds 2
|
||||
docker exec vignette-dev-db pg_isready -U $u *> $null 2>&1
|
||||
if ($LASTEXITCODE -eq 0) { Write-Host " OK db (Postgres 준비됨, init 스키마 적용)"; Start-Sleep -Seconds 1; return }
|
||||
if (!(Get-Command docker -ErrorAction SilentlyContinue)) {
|
||||
Write-Host ' WARN docker CLI 없음 - DB 없이 degraded(UI 세션 시작 제한).'
|
||||
return
|
||||
}
|
||||
Write-Host " WARN db 준비 타임아웃"
|
||||
if (!(Test-Path -LiteralPath '\\.\pipe\dockerDesktopLinuxEngine')) {
|
||||
Write-Host ' WARN Docker Desktop Linux engine 미실행 - DB 없이 degraded(UI 세션 시작 제한).'
|
||||
return
|
||||
}
|
||||
docker ps 1>$null 2>$null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host ' WARN Docker 데몬 미응답 - DB 없이 degraded(UI 세션 시작 제한). Docker Desktop 실행 필요.'
|
||||
return
|
||||
}
|
||||
|
||||
$running = docker ps --filter name=vignette-dev-db --format '{{.Names}}' 2>$null
|
||||
if ($running -match 'vignette-dev-db') {
|
||||
Write-Host ' OK db (vignette-dev-db 실행 중)'
|
||||
return
|
||||
}
|
||||
|
||||
docker rm -f vignette-dev-db 1>$null 2>$null
|
||||
$envLines = Get-Content -LiteralPath (Join-Path $api '.env')
|
||||
$dbUrl = (($envLines | Where-Object { $_ -match '^DATABASE_URL=' }) -replace '^DATABASE_URL=', '').Trim()
|
||||
if ($dbUrl -notmatch 'postgresql://([^:]+):([^@]+)@[^:]+:([0-9]+)/(\S+)') {
|
||||
Write-Host ' WARN DATABASE_URL 파싱 실패 - DB 스킵'
|
||||
return
|
||||
}
|
||||
|
||||
$u = $Matches[1]
|
||||
$p = $Matches[2]
|
||||
$port = $Matches[3]
|
||||
$db = $Matches[4]
|
||||
$initPath = Join-Path $repo 'infra\db\init'
|
||||
$dockerArgs = @(
|
||||
'run', '-d',
|
||||
'--name', 'vignette-dev-db',
|
||||
'-p', ('{0}:5432' -f $port),
|
||||
'-e', ('POSTGRES_USER={0}' -f $u),
|
||||
'-e', ('POSTGRES_PASSWORD={0}' -f $p),
|
||||
'-e', ('POSTGRES_DB={0}' -f $db),
|
||||
'-e', 'APP_DB_USER=vignette_app',
|
||||
'-e', 'APP_DB_PASSWORD=vignette_app',
|
||||
'-v', ('{0}:/docker-entrypoint-initdb.d:ro' -f $initPath),
|
||||
'pgvector/pgvector:pg16'
|
||||
)
|
||||
& docker @dockerArgs 1>$null
|
||||
|
||||
for ($i = 0; $i -lt 24; $i++) {
|
||||
Start-Sleep -Seconds 2
|
||||
docker exec vignette-dev-db pg_isready -U $u 1>$null 2>$null
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Host ' OK db (Postgres 준비됨, init 스키마 적용)'
|
||||
Start-Sleep -Seconds 1
|
||||
return
|
||||
}
|
||||
}
|
||||
Write-Host ' WARN db 준비 타임아웃'
|
||||
}
|
||||
|
||||
Write-Host "[1/4] 기존 스택 정리..."
|
||||
Write-Host '[1/4] 기존 스택 정리...'
|
||||
Stop-Stale 'engine_gateway\.gateway' 'gateway'
|
||||
Stop-Stale 'app\.main:app' 'api'
|
||||
Stop-Stale 'vite' 'web'
|
||||
Stop-Stale 'app\.main:app' 'api'
|
||||
Stop-Stale 'vite' 'web'
|
||||
Stop-ListenerByPort 9099 'gateway'
|
||||
Stop-ListenerByPort $ApiPort 'api'
|
||||
Stop-ListenerByPort 5173 'web'
|
||||
Start-Sleep -Seconds 2
|
||||
|
||||
if (-not $NoDb) {
|
||||
Write-Host "[DB] Postgres(pgvector) 보장..."
|
||||
Write-Host '[DB] Postgres(pgvector) 보장...'
|
||||
Ensure-DevDb
|
||||
}
|
||||
|
||||
if (-not $NoGateway) {
|
||||
Write-Host "[2/4] 엔진 게이트웨이 :9099 (claude_cli)..."
|
||||
Start-Process -FilePath $Python `
|
||||
-ArgumentList (@($PyPre) + @('-m','uvicorn','engine_gateway.gateway:app','--host','127.0.0.1','--port','9099')) `
|
||||
-WorkingDirectory $api -WindowStyle Hidden `
|
||||
-RedirectStandardOutput (Join-Path $logs 'gateway.out.log') `
|
||||
-RedirectStandardError (Join-Path $logs 'gateway.err.log')
|
||||
} else { Write-Host "[2/4] (게이트웨이 건너뜀)" }
|
||||
Write-Host '[2/4] 엔진 게이트웨이 :9099 (claude_cli)...'
|
||||
$gatewayArgs = @($PyPre) + @('-m', 'uvicorn', 'engine_gateway.gateway:app', '--host', '127.0.0.1', '--port', '9099')
|
||||
Start-Process -FilePath $Python -ArgumentList $gatewayArgs -WorkingDirectory $api -WindowStyle Hidden -RedirectStandardOutput (Join-Path $logs 'gateway.out.log') -RedirectStandardError (Join-Path $logs 'gateway.err.log')
|
||||
} else {
|
||||
Write-Host '[2/4] (게이트웨이 건너뜀)'
|
||||
}
|
||||
|
||||
Write-Host "[3/4] API :8000 (핫리로드 --reload, degraded in-memory OK, seed 페르소나)..."
|
||||
Write-Host ('[3/4] API :{0} (단일 프로세스, degraded in-memory OK, seed 페르소나)...' -f $ApiPort)
|
||||
$env:AUTO_SEED_PERSONAS = 'true'
|
||||
$env:ALLOW_SEED_PERSONA_FALLBACK = 'true'
|
||||
# --reload + --reload-dir app: app/ 하위 .py 저장 시 watchfiles 가 감지해 자동 재기동.
|
||||
Start-Process -FilePath $Python `
|
||||
-ArgumentList (@($PyPre) + @('-m','uvicorn','app.main:app','--host','127.0.0.1','--port','8000','--reload','--reload-dir','app')) `
|
||||
-WorkingDirectory $api -WindowStyle Hidden `
|
||||
-RedirectStandardOutput (Join-Path $logs 'api.out.log') `
|
||||
-RedirectStandardError (Join-Path $logs 'api.err.log')
|
||||
$env:VITE_API_PROXY_TARGET = "http://127.0.0.1:$ApiPort"
|
||||
$apiArgs = @($PyPre) + @('-m', 'uvicorn', 'app.main:app', '--host', '127.0.0.1', '--port', "$ApiPort")
|
||||
Start-Process -FilePath $Python -ArgumentList $apiArgs -WorkingDirectory $api -WindowStyle Hidden -RedirectStandardOutput (Join-Path $logs 'api.out.log') -RedirectStandardError (Join-Path $logs 'api.err.log')
|
||||
|
||||
if (-not $NoWeb) {
|
||||
Write-Host "[4/4] 웹 vite :5173..."
|
||||
Start-Process -FilePath 'cmd.exe' -ArgumentList '/c','npm run dev' `
|
||||
-WorkingDirectory $web -WindowStyle Hidden `
|
||||
-RedirectStandardOutput (Join-Path $logs 'web.out.log') `
|
||||
-RedirectStandardError (Join-Path $logs 'web.err.log')
|
||||
} else { Write-Host "[4/4] (웹 건너뜀)" }
|
||||
Write-Host ('[4/4] 웹 vite :5173... API proxy {0}' -f $env:VITE_API_PROXY_TARGET)
|
||||
Start-Process -FilePath 'cmd.exe' -ArgumentList @('/c', 'npm run dev -- --host 127.0.0.1 --port 5173') -WorkingDirectory $web -WindowStyle Hidden -RedirectStandardOutput (Join-Path $logs 'web.out.log') -RedirectStandardError (Join-Path $logs 'web.err.log')
|
||||
} else {
|
||||
Write-Host '[4/4] (웹 건너뜀)'
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds 3
|
||||
Write-Host "`n헬스 체크:"
|
||||
if (-not $NoGateway) { Wait-Health 'http://127.0.0.1:9099/health' 'gateway' | Out-Null }
|
||||
Wait-Health 'http://127.0.0.1:8000/health' 'api' | Out-Null
|
||||
if (-not $NoWeb) { Wait-Health 'http://localhost:5173/' 'web' | Out-Null }
|
||||
Write-Host ''
|
||||
Write-Host '헬스 체크:'
|
||||
if (-not $NoGateway) {
|
||||
Wait-Health 'http://127.0.0.1:9099/health' 'gateway' | Out-Null
|
||||
}
|
||||
Wait-Health "http://127.0.0.1:$ApiPort/health" 'api' | Out-Null
|
||||
if (-not $NoWeb) {
|
||||
Wait-Health 'http://localhost:5173/' 'web' | Out-Null
|
||||
}
|
||||
|
||||
Write-Host "`n준비 완료. 진입점: http://localhost:5173 (로그인 페이지에서 dev-login)"
|
||||
Write-Host "종료: powershell -NoProfile -ExecutionPolicy Bypass -File scripts\dev-down.ps1"
|
||||
Write-Host ''
|
||||
Write-Host '준비 완료. 진입점: http://localhost:5173 (로그인 페이지에서 dev-login)'
|
||||
Write-Host '종료: powershell -NoProfile -ExecutionPolicy Bypass -File scripts\dev-down.ps1'
|
||||
|
|
|
|||
|
|
@ -1,21 +1,28 @@
|
|||
param(
|
||||
param(
|
||||
[string]$Workspace = "D:\workspace\vignette",
|
||||
[int]$ApiPort = 8001,
|
||||
[int]$WebPort = 5174,
|
||||
[int]$EnginePort = 9099,
|
||||
[string]$Python = "$env:LOCALAPPDATA\Programs\Python\Python311\python.exe",
|
||||
[string]$Cloudflared = "$env:LOCALAPPDATA\Microsoft\WinGet\Links\cloudflared.exe",
|
||||
[string]$CloudflaredConfig = "$env:USERPROFILE\.cloudflared\vignette-config.yml",
|
||||
[switch]$SkipEngineRestart,
|
||||
[switch]$SkipWebRestart,
|
||||
[switch]$RouteCloudflareDns,
|
||||
[string]$CloudflareTunnelName = "vignette",
|
||||
[switch]$SkipCloudflaredRestart
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$ApiDir = Join-Path $Workspace "apps\api"
|
||||
$WebDir = Join-Path $Workspace "apps\web"
|
||||
$OutLog = Join-Path $ApiDir "api.public.out.log"
|
||||
$ErrLog = Join-Path $ApiDir "api.public.err.log"
|
||||
$EngineOutLog = Join-Path $ApiDir "engine.public.out.log"
|
||||
$EngineErrLog = Join-Path $ApiDir "engine.public.err.log"
|
||||
$WebOutLog = Join-Path $Workspace "web.public.out.log"
|
||||
$WebErrLog = Join-Path $Workspace "web.public.err.log"
|
||||
|
||||
function Get-JsonHealth {
|
||||
param(
|
||||
|
|
@ -49,6 +56,27 @@ function Wait-JsonHealth {
|
|||
throw "Timed out waiting for healthy response from $Uri"
|
||||
}
|
||||
|
||||
function Wait-HttpStatus {
|
||||
param(
|
||||
[string]$Uri,
|
||||
[int]$TimeoutSec = 30
|
||||
)
|
||||
|
||||
$deadline = (Get-Date).AddSeconds($TimeoutSec)
|
||||
do {
|
||||
try {
|
||||
$response = Invoke-WebRequest -UseBasicParsing -Uri $Uri -TimeoutSec 5
|
||||
if ($response.StatusCode -ge 200 -and $response.StatusCode -lt 500) {
|
||||
return $response
|
||||
}
|
||||
} catch {
|
||||
Start-Sleep -Seconds 1
|
||||
}
|
||||
} while ((Get-Date) -lt $deadline)
|
||||
|
||||
throw "Timed out waiting for HTTP response from $Uri"
|
||||
}
|
||||
|
||||
function Stop-UvicornByPort {
|
||||
param(
|
||||
[string]$AppImport,
|
||||
|
|
@ -64,12 +92,72 @@ function Stop-UvicornByPort {
|
|||
ForEach-Object { Stop-Process -Id $_.ProcessId -Force }
|
||||
}
|
||||
|
||||
function Stop-NodeByPortHint {
|
||||
param([int]$Port)
|
||||
|
||||
Get-CimInstance Win32_Process |
|
||||
Where-Object {
|
||||
$_.Name -eq "node.exe" -and
|
||||
$_.CommandLine -and
|
||||
$_.CommandLine -like "*vite*preview*" -and
|
||||
$_.CommandLine -like "*$Port*"
|
||||
} |
|
||||
ForEach-Object { Stop-Process -Id $_.ProcessId -Force }
|
||||
}
|
||||
|
||||
function ConvertTo-CompactJson {
|
||||
param([object]$Value)
|
||||
ConvertTo-Json -InputObject $Value -Compress
|
||||
}
|
||||
|
||||
function Set-CloudflaredIngress {
|
||||
param(
|
||||
[string]$ConfigPath,
|
||||
[string[]]$ApiHostnames,
|
||||
[string[]]$WebHostnames,
|
||||
[int]$ApiPortValue,
|
||||
[int]$WebPortValue
|
||||
)
|
||||
|
||||
$lines = Get-Content -Encoding UTF8 -Path $ConfigPath
|
||||
$ingressIndex = -1
|
||||
for ($i = 0; $i -lt $lines.Count; $i++) {
|
||||
if ($lines[$i] -match "^\s*ingress:\s*$") {
|
||||
$ingressIndex = $i
|
||||
break
|
||||
}
|
||||
}
|
||||
if ($ingressIndex -lt 0) {
|
||||
throw "Could not find ingress: in $ConfigPath"
|
||||
}
|
||||
|
||||
$nextLines = @()
|
||||
if ($ingressIndex -gt 0) {
|
||||
$nextLines += $lines[0..($ingressIndex - 1)]
|
||||
}
|
||||
$nextLines += "ingress:"
|
||||
foreach ($hostname in $WebHostnames) {
|
||||
$nextLines += " - hostname: $hostname"
|
||||
$nextLines += " service: http://127.0.0.1:$WebPortValue"
|
||||
}
|
||||
foreach ($hostname in $ApiHostnames) {
|
||||
$nextLines += " - hostname: $hostname"
|
||||
$nextLines += " service: http://127.0.0.1:$ApiPortValue"
|
||||
}
|
||||
$nextLines += " - service: http_status:404"
|
||||
|
||||
Set-Content -Encoding UTF8 -Path $ConfigPath -Value $nextLines
|
||||
}
|
||||
|
||||
if (!(Test-Path $Python)) {
|
||||
throw "Python 3.11 not found at $Python"
|
||||
}
|
||||
if (!(Test-Path $ApiDir)) {
|
||||
throw "API directory not found at $ApiDir"
|
||||
}
|
||||
if (!(Test-Path $WebDir)) {
|
||||
throw "Web directory not found at $WebDir"
|
||||
}
|
||||
|
||||
$engineHealth = Get-JsonHealth -Uri "http://127.0.0.1:$EnginePort/health"
|
||||
if ($SkipEngineRestart) {
|
||||
|
|
@ -101,7 +189,17 @@ $env:AUTH_DEV_LOGIN_ENABLED = "false"
|
|||
$env:AUTO_SEED_PERSONAS = "false"
|
||||
$env:ALLOW_SEED_PERSONA_FALLBACK = "false"
|
||||
$env:FRONTEND_BASE_URL = "https://vignette.chanpaca.net"
|
||||
$env:CORS_ORIGINS = '["https://vignette.chanpaca.net","https://vignette-b1q.pages.dev","http://localhost:5170","http://localhost:5171","http://localhost:5172","http://localhost:5173","http://localhost:5174","http://localhost:5175","http://localhost:5176","http://localhost:5177","http://localhost:5178","http://localhost:5179","http://localhost:5180","http://127.0.0.1:5170","http://127.0.0.1:5171","http://127.0.0.1:5172","http://127.0.0.1:5173","http://127.0.0.1:5174","http://127.0.0.1:5175","http://127.0.0.1:5176","http://127.0.0.1:5177","http://127.0.0.1:5178","http://127.0.0.1:5179","http://127.0.0.1:5180"]'
|
||||
$frontendOrigins = @("https://vignette.chanpaca.net", "https://vnet.18ka.net", "https://vignette-b1q.pages.dev")
|
||||
$localViteOrigins = @()
|
||||
foreach ($port in 5170..5180) {
|
||||
$localViteOrigins += "http://localhost:$port"
|
||||
$localViteOrigins += "http://127.0.0.1:$port"
|
||||
}
|
||||
$env:CORS_ORIGINS = ConvertTo-CompactJson -Value ($frontendOrigins + $localViteOrigins)
|
||||
$env:FRONTEND_ORIGIN_MAP = ConvertTo-CompactJson -Value ([ordered]@{
|
||||
"api-vignette.chanpaca.net" = "https://vignette.chanpaca.net"
|
||||
"api-vnet.18ka.net" = "https://vnet.18ka.net"
|
||||
})
|
||||
|
||||
$proc = Start-Process -WindowStyle Hidden -FilePath $Python `
|
||||
-ArgumentList @("-m", "uvicorn", "app.main:app", "--host", "127.0.0.1", "--port", "$ApiPort") `
|
||||
|
|
@ -119,6 +217,29 @@ if ($health.environment -ne "prod" -or -not $health.db -or -not $health.engine)
|
|||
throw "Public API health is not production-safe: $($health | ConvertTo-Json -Compress)"
|
||||
}
|
||||
|
||||
if (!$SkipWebRestart) {
|
||||
Stop-NodeByPortHint -Port $WebPort
|
||||
|
||||
$build = Start-Process -FilePath "cmd.exe" `
|
||||
-ArgumentList @("/c", "npm run build") `
|
||||
-WorkingDirectory $WebDir `
|
||||
-NoNewWindow `
|
||||
-Wait `
|
||||
-PassThru
|
||||
if ($build.ExitCode -ne 0) {
|
||||
throw "Web build failed with exit code $($build.ExitCode)"
|
||||
}
|
||||
|
||||
Start-Process -WindowStyle Hidden -FilePath "cmd.exe" `
|
||||
-ArgumentList @("/c", "npm run preview -- --host 127.0.0.1 --port $WebPort") `
|
||||
-WorkingDirectory $WebDir `
|
||||
-RedirectStandardOutput $WebOutLog `
|
||||
-RedirectStandardError $WebErrLog `
|
||||
-PassThru | Out-Null
|
||||
|
||||
Wait-HttpStatus -Uri "http://127.0.0.1:$WebPort/" -TimeoutSec 30 | Out-Null
|
||||
}
|
||||
|
||||
if (!$SkipCloudflaredRestart) {
|
||||
if (!(Test-Path $Cloudflared)) {
|
||||
throw "cloudflared not found at $Cloudflared"
|
||||
|
|
@ -127,30 +248,24 @@ if (!$SkipCloudflaredRestart) {
|
|||
throw "cloudflared config not found at $CloudflaredConfig"
|
||||
}
|
||||
|
||||
$lines = Get-Content $CloudflaredConfig
|
||||
$insideApiIngress = $false
|
||||
$updated = $false
|
||||
$nextLines = foreach ($line in $lines) {
|
||||
if ($line -match "hostname:\s*api-vignette\.chanpaca\.net") {
|
||||
$insideApiIngress = $true
|
||||
$line
|
||||
continue
|
||||
$apiHostnames = @("api-vignette.chanpaca.net", "api-vnet.18ka.net")
|
||||
$webHostnames = @("vnet.18ka.net")
|
||||
|
||||
if ($RouteCloudflareDns) {
|
||||
foreach ($hostname in ($webHostnames + $apiHostnames)) {
|
||||
& $Cloudflared tunnel route dns $CloudflareTunnelName $hostname
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Warning "cloudflared DNS route failed for $hostname"
|
||||
}
|
||||
}
|
||||
if ($insideApiIngress -and $line -match "^\s*service:\s*http://127\.0\.0\.1:\d+\s*$") {
|
||||
$updated = $true
|
||||
$insideApiIngress = $false
|
||||
" service: http://127.0.0.1:$ApiPort"
|
||||
continue
|
||||
}
|
||||
if ($insideApiIngress -and $line -match "^\s*-\s+") {
|
||||
$insideApiIngress = $false
|
||||
}
|
||||
$line
|
||||
}
|
||||
if (!$updated) {
|
||||
throw "Could not find api-vignette.chanpaca.net localhost service in $CloudflaredConfig"
|
||||
}
|
||||
Set-Content -Encoding UTF8 -Path $CloudflaredConfig -Value $nextLines
|
||||
|
||||
Set-CloudflaredIngress `
|
||||
-ConfigPath $CloudflaredConfig `
|
||||
-ApiHostnames $apiHostnames `
|
||||
-WebHostnames $webHostnames `
|
||||
-ApiPortValue $ApiPort `
|
||||
-WebPortValue $WebPort
|
||||
|
||||
Get-CimInstance Win32_Process |
|
||||
Where-Object { $_.Name -eq "cloudflared.exe" -and $_.CommandLine -like "*vignette-config.yml*" } |
|
||||
|
|
@ -166,4 +281,7 @@ if (!$SkipCloudflaredRestart) {
|
|||
|
||||
Write-Output "Engine gateway healthy on http://127.0.0.1:$EnginePort"
|
||||
Write-Output "Public API running on http://127.0.0.1:$ApiPort with PID $($proc.Id)"
|
||||
if (!$SkipWebRestart) {
|
||||
Write-Output "Public vnet web preview running on http://127.0.0.1:$WebPort"
|
||||
}
|
||||
Write-Output "Health: $($health | ConvertTo-Json -Compress)"
|
||||
|
|
|
|||
137
scripts/start-tailscale-runtime.ps1
Normal file
137
scripts/start-tailscale-runtime.ps1
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
<#
|
||||
.SYNOPSIS
|
||||
Tailnet 전용 Vignette 접속점을 켠다.
|
||||
|
||||
.DESCRIPTION
|
||||
- 현재 Tailscale 노드의 MagicDNS URL을 읽는다.
|
||||
- dev 스택(API 8000 + web 5173 + gateway 9099)을 기동하면서 Tailnet URL에서 dev-login을 허용한다.
|
||||
- tailscale serve 루트 HTTPS를 로컬 web 포트로 연결한다.
|
||||
|
||||
공개 인터넷 노출이 아니라 같은 Tailnet에 로그인한 PC/모바일 전용이다.
|
||||
#>
|
||||
param(
|
||||
[string]$Workspace = "D:\workspace\vignette",
|
||||
[int]$ApiPort = 8010,
|
||||
[int]$WebPort = 5173,
|
||||
[switch]$SkipStackRestart,
|
||||
[switch]$SkipServeUpdate
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Get-TailnetOrigin {
|
||||
$raw = & tailscale status --json
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "tailscale status failed"
|
||||
}
|
||||
$status = $raw | ConvertFrom-Json
|
||||
$dnsName = [string]$status.Self.DNSName
|
||||
if (!$dnsName) {
|
||||
throw "Tailscale MagicDNS name was not reported. Enable MagicDNS or check tailscale status."
|
||||
}
|
||||
"https://$($dnsName.TrimEnd('.'))"
|
||||
}
|
||||
|
||||
function Wait-HttpOk {
|
||||
param(
|
||||
[string]$Uri,
|
||||
[int]$TimeoutSec = 45
|
||||
)
|
||||
|
||||
$deadline = (Get-Date).AddSeconds($TimeoutSec)
|
||||
do {
|
||||
try {
|
||||
$response = Invoke-WebRequest -UseBasicParsing -Uri $Uri -TimeoutSec 8
|
||||
if ($response.StatusCode -ge 200 -and $response.StatusCode -lt 500) {
|
||||
return $response
|
||||
}
|
||||
} catch {
|
||||
Start-Sleep -Seconds 1
|
||||
}
|
||||
} while ((Get-Date) -lt $deadline)
|
||||
|
||||
throw "Timed out waiting for $Uri"
|
||||
}
|
||||
|
||||
function Set-EnvFileValue {
|
||||
param(
|
||||
[string]$EnvPath,
|
||||
[string]$Key,
|
||||
[string]$Value
|
||||
)
|
||||
|
||||
$line = "$Key=$Value"
|
||||
if (Test-Path -LiteralPath $EnvPath) {
|
||||
$lines = Get-Content -LiteralPath $EnvPath -Encoding UTF8
|
||||
} else {
|
||||
$lines = @()
|
||||
}
|
||||
|
||||
$updated = $false
|
||||
$next = foreach ($existing in $lines) {
|
||||
if ($existing -match "^$([regex]::Escape($Key))=") {
|
||||
$updated = $true
|
||||
$line
|
||||
} else {
|
||||
$existing
|
||||
}
|
||||
}
|
||||
if (!$updated) {
|
||||
$next += $line
|
||||
}
|
||||
Set-Content -LiteralPath $EnvPath -Encoding UTF8 -Value $next
|
||||
}
|
||||
|
||||
if (!(Get-Command tailscale -ErrorAction SilentlyContinue)) {
|
||||
throw "tailscale CLI not found"
|
||||
}
|
||||
if (!(Test-Path -LiteralPath $Workspace)) {
|
||||
throw "Workspace not found: $Workspace"
|
||||
}
|
||||
|
||||
$tailnetOrigin = Get-TailnetOrigin
|
||||
$tailnetHost = ([uri]$tailnetOrigin).Host
|
||||
$env:AUTH_DEV_LOGIN_EXTRA_ORIGINS = ConvertTo-Json -InputObject @($tailnetOrigin) -Compress
|
||||
$env:VITE_ALLOWED_HOSTS = $tailnetHost
|
||||
$env:VITE_API_PROXY_TARGET = "http://127.0.0.1:$ApiPort"
|
||||
Set-EnvFileValue `
|
||||
-EnvPath (Join-Path $Workspace "apps\api\.env") `
|
||||
-Key "AUTH_DEV_LOGIN_EXTRA_ORIGINS" `
|
||||
-Value $env:AUTH_DEV_LOGIN_EXTRA_ORIGINS
|
||||
|
||||
Write-Output "Tailnet origin: $tailnetOrigin"
|
||||
Write-Output "AUTH_DEV_LOGIN_EXTRA_ORIGINS=$env:AUTH_DEV_LOGIN_EXTRA_ORIGINS"
|
||||
Write-Output "VITE_ALLOWED_HOSTS=$env:VITE_ALLOWED_HOSTS"
|
||||
Write-Output "VITE_API_PROXY_TARGET=$env:VITE_API_PROXY_TARGET"
|
||||
|
||||
if (!$SkipStackRestart) {
|
||||
$devUp = Join-Path $Workspace "scripts\dev-up.ps1"
|
||||
if (!(Test-Path -LiteralPath $devUp)) {
|
||||
throw "dev-up script not found: $devUp"
|
||||
}
|
||||
& $devUp -ApiPort $ApiPort
|
||||
}
|
||||
|
||||
if (!$SkipServeUpdate) {
|
||||
$target = "http://127.0.0.1:$WebPort"
|
||||
Write-Output "Updating tailscale serve root -> $target"
|
||||
& tailscale serve --bg --yes $target
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "tailscale serve update failed"
|
||||
}
|
||||
}
|
||||
|
||||
Wait-HttpOk -Uri $tailnetOrigin | Out-Null
|
||||
$authConfig = Invoke-RestMethod -Uri "$tailnetOrigin/api/auth/config" -TimeoutSec 15
|
||||
if ($authConfig.dev_login_enabled -ne $true) {
|
||||
throw "Tailnet auth config is reachable, but dev_login_enabled is not true"
|
||||
}
|
||||
|
||||
Write-Output "Tailscale Vignette URL: $tailnetOrigin"
|
||||
Write-Output "Login path: $tailnetOrigin/login"
|
||||
Write-Output "Serve status:"
|
||||
& tailscale serve status
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
param(
|
||||
param(
|
||||
[int]$ApiPort = 8001,
|
||||
[int]$WebPort = 5174,
|
||||
[switch]$StopCloudflared
|
||||
)
|
||||
|
||||
|
|
@ -9,10 +10,19 @@ Get-CimInstance Win32_Process |
|
|||
Where-Object { $_.CommandLine -like "*uvicorn app.main:app*--port $ApiPort*" } |
|
||||
ForEach-Object { Stop-Process -Id $_.ProcessId -Force }
|
||||
|
||||
Get-CimInstance Win32_Process |
|
||||
Where-Object {
|
||||
$_.Name -eq "node.exe" -and
|
||||
$_.CommandLine -and
|
||||
$_.CommandLine -like "*vite*preview*" -and
|
||||
$_.CommandLine -like "*$WebPort*"
|
||||
} |
|
||||
ForEach-Object { Stop-Process -Id $_.ProcessId -Force }
|
||||
|
||||
if ($StopCloudflared) {
|
||||
Get-CimInstance Win32_Process |
|
||||
Where-Object { $_.Name -eq "cloudflared.exe" -and $_.CommandLine -like "*vignette-config.yml*" } |
|
||||
ForEach-Object { Stop-Process -Id $_.ProcessId -Force }
|
||||
}
|
||||
|
||||
Write-Host "Stopped public API processes on port $ApiPort"
|
||||
Write-Host "Stopped public API processes on port $ApiPort and public web preview on port $WebPort"
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
param(
|
||||
param(
|
||||
[string]$Workspace = "D:\workspace\vignette",
|
||||
[int]$ApiPort = 8001,
|
||||
[int]$WebPort = 5174,
|
||||
[int]$EnginePort = 9099,
|
||||
[string]$Python = "$env:LOCALAPPDATA\Programs\Python\Python311\python.exe",
|
||||
[string]$Cloudflared = "$env:LOCALAPPDATA\Microsoft\WinGet\Links\cloudflared.exe",
|
||||
[string]$CloudflaredConfig = "$env:USERPROFILE\.cloudflared\vignette-config.yml",
|
||||
[string]$PublicHealthUrl = "https://api-vignette.chanpaca.net/health",
|
||||
[string[]]$AdditionalPublicHealthUrls = @("https://api-vnet.18ka.net/health"),
|
||||
[string]$LogPath = "",
|
||||
[switch]$CheckOnly,
|
||||
[switch]$SkipPublicHealth,
|
||||
|
|
@ -91,6 +93,10 @@ $checks = @(
|
|||
-Name "api" `
|
||||
-Uri "http://127.0.0.1:$ApiPort/health" `
|
||||
-IsHealthy { param($health) $health.environment -eq "prod" -and $health.db -and $health.engine }),
|
||||
(Test-JsonHealth `
|
||||
-Name "web-preview" `
|
||||
-Uri "http://127.0.0.1:$WebPort/" `
|
||||
-IsHealthy { param($body) $true }),
|
||||
(Test-CloudflaredProcess)
|
||||
)
|
||||
|
||||
|
|
@ -100,6 +106,13 @@ if (!$SkipPublicHealth) {
|
|||
-Uri $PublicHealthUrl `
|
||||
-IsHealthy { param($health) $health.environment -eq "prod" -and $health.db -and $health.engine } `
|
||||
-TimeoutSec 20
|
||||
foreach ($url in $AdditionalPublicHealthUrls) {
|
||||
$checks += Test-JsonHealth `
|
||||
-Name "public-api:$url" `
|
||||
-Uri $url `
|
||||
-IsHealthy { param($health) $health.environment -eq "prod" -and $health.db -and $health.engine } `
|
||||
-TimeoutSec 20
|
||||
}
|
||||
}
|
||||
|
||||
$failed = @($checks | Where-Object { -not $_.Ok })
|
||||
|
|
@ -116,6 +129,7 @@ if ($CheckOnly) {
|
|||
$startArgs = @{
|
||||
Workspace = $Workspace
|
||||
ApiPort = $ApiPort
|
||||
WebPort = $WebPort
|
||||
EnginePort = $EnginePort
|
||||
Python = $Python
|
||||
Cloudflared = $Cloudflared
|
||||
|
|
@ -143,6 +157,15 @@ if (!$apiAfter.Ok) {
|
|||
throw "Public API still unhealthy after restart: $($apiAfter.Detail)"
|
||||
}
|
||||
|
||||
$webAfter = Test-JsonHealth `
|
||||
-Name "web-preview" `
|
||||
-Uri "http://127.0.0.1:$WebPort/" `
|
||||
-IsHealthy { param($body) $true } `
|
||||
-TimeoutSec 20
|
||||
if (!$webAfter.Ok) {
|
||||
throw "Public web preview still unhealthy after restart: $($webAfter.Detail)"
|
||||
}
|
||||
|
||||
if (!$SkipPublicHealth) {
|
||||
$publicAfter = Test-JsonHealth `
|
||||
-Name "public-api" `
|
||||
|
|
@ -152,6 +175,16 @@ if (!$SkipPublicHealth) {
|
|||
if (!$publicAfter.Ok) {
|
||||
throw "Public API tunnel still unhealthy after restart: $($publicAfter.Detail)"
|
||||
}
|
||||
foreach ($url in $AdditionalPublicHealthUrls) {
|
||||
$publicExtraAfter = Test-JsonHealth `
|
||||
-Name "public-api:$url" `
|
||||
-Uri $url `
|
||||
-IsHealthy { param($health) $health.environment -eq "prod" -and $health.db -and $health.engine } `
|
||||
-TimeoutSec 20
|
||||
if (!$publicExtraAfter.Ok) {
|
||||
throw "Public API tunnel still unhealthy after restart: $($publicExtraAfter.Detail)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Write-WatchdogLog "restart verified"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue