param( [Parameter(Mandatory=$true)][string]$SourcePath, [Parameter(Mandatory=$true)][string]$ArchivePath, [Parameter(Mandatory=$true)][string]$ExpectedVersion, [Parameter(Mandatory=$true)][string]$ExpectedSha256, [Parameter(Mandatory=$true)][int]$ParentPid, [Parameter(Mandatory=$true)][string]$LogPath ) $ErrorActionPreference = "Stop" $ProgressPreference = "SilentlyContinue" function Write-UpdateLog { param([string]$Message) $line = "$(Get-Date -Format o) $Message" New-Item -ItemType Directory -Force -Path (Split-Path -Parent $LogPath) | Out-Null Add-Content -Path $LogPath -Value $line -Encoding UTF8 } function Invoke-Robocopy { param([string]$From, [string]$To) New-Item -ItemType Directory -Force -Path $To | Out-Null & robocopy.exe $From $To /MIR /R:2 /W:1 /NFL /NDL /NJH /NJS /NP /XD node_modules .git dist | Out-Null if ($LASTEXITCODE -gt 7) { throw "robocopy failed with exit code $LASTEXITCODE" } } try { Write-UpdateLog "ForgeFlow source update started for version $ExpectedVersion." $deadline = (Get-Date).AddMinutes(2) while (Get-Process -Id $ParentPid -ErrorAction SilentlyContinue) { if ((Get-Date) -gt $deadline) { throw "ForgeFlow did not exit before the update timeout." } Start-Sleep -Milliseconds 500 } $actualHash = (Get-FileHash -Path $ArchivePath -Algorithm SHA256).Hash.ToLowerInvariant() if ($actualHash -ne $ExpectedSha256.ToLowerInvariant()) { throw "Update archive checksum mismatch." } $working = Join-Path ([IO.Path]::GetTempPath()) ("forgeflow-update-" + [guid]::NewGuid().ToString("N")) $extract = Join-Path $working "extract" $backup = Join-Path $working "backup" New-Item -ItemType Directory -Force -Path $extract | Out-Null Write-UpdateLog "Creating source backup." Invoke-Robocopy -From $SourcePath -To $backup Write-UpdateLog "Extracting update archive." Expand-Archive -LiteralPath $ArchivePath -DestinationPath $extract -Force $manifest = Get-ChildItem -Path $extract -Filter package.json -File -Recurse | Where-Object { try { $json = Get-Content $_.FullName -Raw | ConvertFrom-Json return $json.name -eq "forgeflow" -and $json.version -eq $ExpectedVersion } catch { return $false } } | Select-Object -First 1 if (-not $manifest) { throw "The update does not contain ForgeFlow version $ExpectedVersion." } $incoming = Split-Path -Parent $manifest.FullName Write-UpdateLog "Applying verified source files." Invoke-Robocopy -From $incoming -To $SourcePath Push-Location $SourcePath try { Write-UpdateLog "Installing exact dependencies." & cmd.exe /d /s /c "npm install --no-audit --no-fund" *>> $LogPath if ($LASTEXITCODE -ne 0) { throw "npm install failed with exit code $LASTEXITCODE." } Write-UpdateLog "Running ForgeFlow quality gate." & cmd.exe /d /s /c "npm run check" *>> $LogPath if ($LASTEXITCODE -ne 0) { throw "npm run check failed with exit code $LASTEXITCODE." } } finally { Pop-Location } Write-UpdateLog "Update validated successfully. Restarting ForgeFlow." Start-Process -FilePath "cmd.exe" -WorkingDirectory $SourcePath -ArgumentList "/d", "/s", "/c", "npm start" Remove-Item -LiteralPath $working -Recurse -Force -ErrorAction SilentlyContinue exit 0 } catch { Write-UpdateLog ("Update failed: " + $_.Exception.Message) try { if ($backup -and (Test-Path $backup)) { Write-UpdateLog "Restoring previous source version." Invoke-Robocopy -From $backup -To $SourcePath Push-Location $SourcePath try { & cmd.exe /d /s /c "npm install --no-audit --no-fund" *>> $LogPath } finally { Pop-Location } Start-Process -FilePath "cmd.exe" -WorkingDirectory $SourcePath -ArgumentList "/d", "/s", "/c", "npm start" } } catch { Write-UpdateLog ("Rollback failed: " + $_.Exception.Message) } exit 1 }