param( [Parameter(Mandatory = $true)] [string]$PackagePath, [string]$ProgramRoot = "", [string]$DataRoot = "", [switch]$SkipBackup = $false, [switch]$NoStart = $false ) $ErrorActionPreference = "Stop" . "$PSScriptRoot\_common.ps1" $prog = Get-NetxProgramRoot -Override $ProgramRoot $data = Get-NetxDataRoot -ProgramRoot $prog -Override $DataRoot if (-not (Test-Path $PackagePath)) { throw "package_not_found: $PackagePath" } $work = Join-Path $env:TEMP ("netx-update-" + [guid]::NewGuid().ToString("n")) New-Item -ItemType Directory -Path $work -Force | Out-Null try { Write-Host "==> Extracting update package" if ($PackagePath.ToLowerInvariant().EndsWith(".zip")) { Expand-Archive -Path $PackagePath -DestinationPath $work -Force } else { throw "unsupported_package: use a .zip built by build_release.ps1" } $src = $work # If zip has a single top-level folder, use it. $kids = @(Get-ChildItem -Path $work -Directory) if ($kids.Count -eq 1 -and (Test-Path (Join-Path $kids[0].FullName "version.json"))) { $src = $kids[0].FullName } elseif (-not (Test-Path (Join-Path $work "version.json"))) { $nested = Get-ChildItem -Path $work -Recurse -Filter "version.json" | Select-Object -First 1 if ($nested) { $src = Split-Path -Parent $nested.FullName } } $newVerFile = Join-Path $src "version.json" if (-not (Test-Path $newVerFile)) { throw "version.json missing in package" } $newVer = (Get-Content -Raw $newVerFile | ConvertFrom-Json).version $oldVer = Get-NetxVersion -ProgramRoot $prog Write-Host "==> Updating $oldVer -> $newVer" if (-not $SkipBackup) { $stamp = Get-Date -Format "yyyyMMdd_HHmmss" $bak = Join-Path $data "backups\pre-update-$stamp" New-Item -ItemType Directory -Path $bak -Force | Out-Null $envFile = Join-Path $data ".env" if (Test-Path $envFile) { Copy-Item $envFile (Join-Path $bak ".env") } Write-Host "==> Backup marker: $bak (data/pgdata left in place; optional pg_dump not run)" } Write-Host "==> Stopping services" & powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "stop_netx_app.ps1") ` -ProgramRoot $prog -DataRoot $data # Replace program layers only — never wipe data root. $replaceDirs = @("netx_api", "alembic", "web", "packaging", "scripts", "postgres", "packages") foreach ($name in $replaceDirs) { $from = Join-Path $src $name $to = Join-Path $prog $name if (-not (Test-Path $from)) { continue } Write-Host "==> Replacing $name" if (Test-Path $to) { Remove-Item -Recurse -Force $to } Copy-Item -Path $from -Destination $to -Recurse -Force } foreach ($f in @("requirements.txt", "pyproject.toml", "alembic.ini", "version.json", "README.md", "LICENSE")) { $from = Join-Path $src $f if (Test-Path $from) { Copy-Item -Path $from -Destination (Join-Path $prog $f) -Force } } # Optional runtime / .venv shipped in package if (Test-Path (Join-Path $src "runtime")) { $rt = Join-Path $prog "runtime" if (Test-Path $rt) { Remove-Item -Recurse -Force $rt } Copy-Item -Path (Join-Path $src "runtime") -Destination $rt -Recurse -Force } if (Test-Path (Join-Path $src ".venv")) { Write-Host "==> Replacing .venv from package" $venvTo = Join-Path $prog ".venv" if (Test-Path $venvTo) { Remove-Item -Recurse -Force $venvTo } Copy-Item -Path (Join-Path $src ".venv") -Destination $venvTo -Recurse -Force } Write-Host "==> Update files applied" -ForegroundColor Green if (-not $NoStart) { & powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "start_netx_app.ps1") ` -ProgramRoot $prog -DataRoot $data -SkipBrowser } } finally { if (Test-Path $work) { Remove-Item -Recurse -Force $work -ErrorAction SilentlyContinue } }