2026-06-16 16:35:19 +01:00
param (
[ string ] $RavenHost = " raven " ,
[ string ] $LocalViliRoot = " E:\Projects\Vili " ,
[ string ] $LocalCultLibRoot = " E:\Projects\CultLib " ,
[ string ] $RemoteViliRoot = " E:\Projects\Vili " ,
[ int ] $Port = 8824 ,
[ string ] $HostName = " 0.0.0.0 " ,
[ string ] $StateRoot = " E:\Projects\Vili\.vili " ,
2026-07-04 22:38:43 +01:00
[ string ] $IdunnRudpHealth = $ ( if ( $env:VILI_IDUNN_RUDP_HEALTH ) { $env:VILI_IDUNN_RUDP_HEALTH } else { $env:IDUNN_RUDP_HEALTH } ) ,
2026-06-16 16:35:19 +01:00
[ string ] $IdunnDaemon = " vili " ,
[ string ] $IdunnHealthContract = " vili.cultnet-rudp-animation-health " ,
[ int ] $IdunnHealthIntervalSeconds = 15
)
$ErrorActionPreference = " Stop "
2026-07-04 22:38:43 +01:00
if ( $env:IDUNN_ACTUATOR -ne " 1 " -or $env:IDUNN_COMMAND_AUTHORITY -ne " idunn-daemon " ) {
throw " restart-vili.ps1 is an Idunn actuator body. Redeploy by poking Idunn; direct service restart is not an owned path. "
}
if ( [ string ] :: IsNullOrWhiteSpace ( $IdunnRudpHealth ) ) {
throw " Idunn RUDP health endpoint must be supplied by VILI_IDUNN_RUDP_HEALTH, IDUNN_RUDP_HEALTH, or -IdunnRudpHealth; no WireGuard endpoint default is allowed. "
}
2026-06-16 16:35:19 +01:00
$localFiles = @ (
( Join-Path $LocalViliRoot " package.json " ) ,
( Join-Path $LocalViliRoot " package-lock.json " ) ,
( Join-Path $LocalViliRoot " scripts\idunn-rudp.cjs " ) ,
( Join-Path $LocalViliRoot " scripts\install-vili-task.ps1 " ) ,
( Join-Path $LocalViliRoot " scripts\restart-vili-daemon.cmd " ) ,
( Join-Path $LocalViliRoot " scripts\restart-vili-daemon.ps1 " ) ,
( Join-Path $LocalViliRoot " scripts\run-hidden-powershell.vbs " ) ,
( Join-Path $LocalViliRoot " scripts\run-vili-daemon.ps1 " ) ,
( Join-Path $LocalViliRoot " scripts\start-vili-daemon.ps1 " ) ,
( Join-Path $LocalViliRoot " scripts\vili-daemon.mjs " ) ,
( Join-Path $LocalCultLibRoot " packages\cultnet-ts " ) ,
( Join-Path $LocalCultLibRoot " packages\cultcache-ts " ) ,
( Join-Path $LocalCultLibRoot " node_modules\ajv " ) ,
( Join-Path $LocalCultLibRoot " node_modules\fast-deep-equal " ) ,
( Join-Path $LocalCultLibRoot " node_modules\fast-uri " ) ,
( Join-Path $LocalCultLibRoot " node_modules\json-schema-traverse " ) ,
( Join-Path $LocalCultLibRoot " node_modules\require-from-string " ) ,
2026-07-04 22:38:43 +01:00
( Join-Path $LocalViliRoot " node_modules\@msgpack " )
2026-06-16 16:35:19 +01:00
)
foreach ( $path in $localFiles ) {
if ( -not ( Test-Path -LiteralPath $path ) ) {
throw " Required Vili runtime path not found: $path "
}
}
$syncId = [ guid ] :: NewGuid ( ) . ToString ( " N " )
$remoteSyncRootPs = " $RemoteViliRoot \.odin-vili-sync- $syncId "
$remoteSyncRootSftp = ( $remoteSyncRootPs -replace " \\ " , " / " )
$remoteTempPs = " C:\Windows\Temp\odin-raven-vili-restart- $syncId .ps1 "
$remoteTempSftp = ( $remoteTempPs -replace " \\ " , " / " )
$remoteBundlePs = " C:\Windows\Temp\odin-raven-vili-bundle- $syncId .zip "
$remoteBundleSftp = ( $remoteBundlePs -replace " \\ " , " / " )
$localRemoteScript = Join-Path $env:TEMP " odin-raven-vili-restart- $syncId .ps1 "
$localSftpBatch = Join-Path $env:TEMP " odin-raven-vili-restart- $syncId .sftp "
$localBundleRoot = Join-Path $env:TEMP " odin-raven-vili-bundle- $syncId "
$localBundleScripts = Join-Path $localBundleRoot " scripts "
$localBundleNodeModules = Join-Path $localBundleRoot " node_modules "
$localBundleZip = Join-Path $env:TEMP " odin-raven-vili-bundle- $syncId .zip "
$remoteScript = @"
`$ ErrorActionPreference = " Stop "
`$ ProgressPreference = " SilentlyContinue "
`$ remoteRoot = " $RemoteViliRoot "
`$ syncRoot = " $remoteSyncRootPs "
`$ scriptsRoot = Join-Path `$ remoteRoot " scripts "
`$ stateRoot = " $StateRoot "
`$ nodeModulesRoot = Join-Path `$ remoteRoot " node_modules "
`$ pidPath = Join-Path `$ stateRoot " vili-daemon.pid "
try {
Expand-Archive -LiteralPath " $remoteBundlePs " -DestinationPath `$ syncRoot -Force
if ( -not ( Test-Path -LiteralPath `$ syncRoot ) ) {
throw " Uploaded sync root not found at `$ syncRoot "
}
if ( Test-Path -LiteralPath `$ pidPath ) {
`$ existingPid = ( Get-Content -LiteralPath `$ pidPath -Raw -ErrorAction SilentlyContinue ) . Trim ( )
if ( `$ existingPid -match '^\d+$' ) {
`$ process = Get-Process -Id ( [ int ] `$ existingPid ) -ErrorAction SilentlyContinue
if ( `$ null -ne `$ process ) {
Stop-Process -Id `$ process . Id -Force
Start-Sleep -Seconds 1
}
}
Remove-Item -LiteralPath `$ pidPath -Force -ErrorAction SilentlyContinue
}
New-Item -ItemType Directory -Force -Path `$ scriptsRoot | Out-Null
New-Item -ItemType Directory -Force -Path `$ stateRoot | Out-Null
Copy-Item -LiteralPath ( Join-Path `$ syncRoot " package.json " ) -Destination ( Join-Path `$ remoteRoot " package.json " ) -Force
Copy-Item -LiteralPath ( Join-Path `$ syncRoot " package-lock.json " ) -Destination ( Join-Path `$ remoteRoot " package-lock.json " ) -Force
`$ scriptFiles = @ (
" idunn-rudp.cjs " ,
" install-vili-task.ps1 " ,
" restart-vili-daemon.cmd " ,
" restart-vili-daemon.ps1 " ,
" run-hidden-powershell.vbs " ,
" run-vili-daemon.ps1 " ,
" start-vili-daemon.ps1 " ,
" vili-daemon.mjs "
)
foreach ( `$ name in `$ scriptFiles ) {
Copy-Item -LiteralPath ( Join-Path `$ syncRoot " scripts\ `$ name " ) -Destination ( Join-Path `$ scriptsRoot `$ name ) -Force
}
if ( Test-Path -LiteralPath `$ nodeModulesRoot ) {
Remove-Item -LiteralPath `$ nodeModulesRoot -Recurse -Force
}
Move-Item -LiteralPath ( Join-Path `$ syncRoot " node_modules " ) -Destination `$ nodeModulesRoot -Force
& powershell . exe @ (
" -NoProfile " ,
" -NonInteractive " ,
" -ExecutionPolicy " , " Bypass " ,
" -File " , ( Join-Path `$ scriptsRoot " install-vili-task.ps1 " ) ,
" -TaskName " , " GameCult\Vili " ,
" -Port " , " $Port " ,
" -HostName " , " $HostName " ,
" -StateRoot " , " $StateRoot " ,
" -IdunnRudpHealth " , " $IdunnRudpHealth " ,
" -IdunnDaemon " , " $IdunnDaemon " ,
" -IdunnHealthContract " , " $IdunnHealthContract " ,
" -IdunnHealthIntervalSeconds " , " $IdunnHealthIntervalSeconds "
)
& powershell . exe @ (
" -NoProfile " ,
" -NonInteractive " ,
" -ExecutionPolicy " , " Bypass " ,
" -File " , ( Join-Path `$ scriptsRoot " restart-vili-daemon.ps1 " ) ,
" -Port " , " $Port " ,
" -HostName " , " $HostName " ,
" -StateRoot " , " $StateRoot " ,
" -IdunnRudpHealth " , " $IdunnRudpHealth " ,
" -IdunnDaemon " , " $IdunnDaemon " ,
" -IdunnHealthContract " , " $IdunnHealthContract " ,
" -IdunnHealthIntervalSeconds " , " $IdunnHealthIntervalSeconds "
)
`$ task = Get-ScheduledTask -TaskName " Vili " -TaskPath " \GameCult\ " -ErrorAction Stop
`$ action = @ ( `$ task . Actions ) [ 0 ]
if ( `$ action . Execute -notmatch '(^|\\)wscript\.exe$' ) {
throw " GameCult\Vili action executes `$ ( `$ action.Execute), expected wscript.exe "
}
if ( `$ action . Arguments -notlike " *run-hidden-powershell.vbs* " ) {
throw " GameCult\Vili action arguments do not reference run-hidden-powershell.vbs "
}
`$ taskHealthChecks = @ (
( `$ action . Arguments -like " *-IdunnRudpHealth* " ) ,
( `$ action . Arguments -like " * $IdunnRudpHealth * " ) ,
( `$ action . Arguments -like " *-IdunnDaemon* " ) ,
( `$ action . Arguments -like " * $IdunnDaemon * " ) ,
( `$ action . Arguments -like " *-IdunnHealthContract* " ) ,
( `$ action . Arguments -like " * $IdunnHealthContract * " )
)
if ( `$ taskHealthChecks -contains `$ false ) {
throw " GameCult\Vili action arguments do not carry the Idunn RUDP health contract. "
}
`$ process = Get-CimInstance Win32_Process |
Where-Object { `$ _ . CommandLine -like '*vili-daemon.mjs*' } |
Select-Object -First 1
if ( `$ null -eq `$ process ) {
throw " Vili process is not running after restart. "
}
`$ processHealthChecks = @ (
( `$ process . CommandLine -like " *--idunn-rudp-health* " ) ,
( `$ process . CommandLine -like " * $IdunnRudpHealth * " ) ,
( `$ process . CommandLine -like " *--idunn-daemon* " ) ,
( `$ process . CommandLine -like " * $IdunnDaemon * " ) ,
( `$ process . CommandLine -like " *--idunn-health-contract* " ) ,
( `$ process . CommandLine -like " * $IdunnHealthContract * " )
)
if ( `$ processHealthChecks -contains `$ false ) {
throw " Vili process command line does not include the Idunn RUDP health arguments. "
}
2026-07-04 22:38:43 +01:00
`$ operatorStatePath = Join-Path `$ stateRoot " operator-state.json "
2026-06-16 16:35:19 +01:00
`$ deadline = ( Get-Date ) . AddSeconds ( 30 )
`$ state = `$ null
while ( ( Get-Date ) -lt `$ deadline ) {
2026-07-04 22:38:43 +01:00
if ( Test-Path -LiteralPath `$ operatorStatePath ) {
2026-06-16 16:35:19 +01:00
try {
2026-07-04 22:38:43 +01:00
`$ state = Get-Content -LiteralPath `$ operatorStatePath -Raw | ConvertFrom-Json
2026-06-16 16:35:19 +01:00
if ( `$ null -ne `$ state . idunnRudpHealth -and `$ state . idunnRudpHealth . status -eq " published " ) {
break
}
} catch {
}
}
Start-Sleep -Seconds 1
}
if ( `$ null -eq `$ state -or `$ null -eq `$ state . idunnRudpHealth ) {
2026-07-04 22:38:43 +01:00
throw " Vili daemon state file did not report idunnRudpHealth after restart. "
2026-06-16 16:35:19 +01:00
}
if ( `$ state . idunnRudpHealth . status -ne " published " ) {
throw " Vili idunnRudpHealth status is `$ ( `$ state.idunnRudpHealth.status): `$ ( `$ state.idunnRudpHealth.error) "
}
Write-Host " Vili Raven runtime repaired and restarted with Idunn RUDP health. "
Write-Host " ProcessId= `$ ( `$ process.ProcessId) "
Write-Host " TaskArguments= `$ ( `$ action.Arguments) "
Write-Host " HealthStatus= `$ ( `$ state.idunnRudpHealth.status) "
Write-Host " HealthContract= `$ ( `$ state.idunnRudpHealth.contract) "
} finally {
Remove-Item -LiteralPath `$ syncRoot -Recurse -Force -ErrorAction SilentlyContinue
Remove-Item -LiteralPath " $remoteBundlePs " -Force -ErrorAction SilentlyContinue
}
" @
$batchLines = @ (
" put "" $localBundleZip "" "" $remoteBundleSftp "" " ,
" put "" $localRemoteScript "" "" $remoteTempSftp "" "
)
try {
$moduleCopies = @ (
@ { Source = ( Join-Path $LocalCultLibRoot " packages\cultnet-ts " ) ; Destination = ( Join-Path $localBundleNodeModules " cultnet-ts " ) } ,
@ { Source = ( Join-Path $LocalCultLibRoot " packages\cultcache-ts " ) ; Destination = ( Join-Path $localBundleNodeModules " cultcache-ts " ) } ,
@ { Source = ( Join-Path $LocalCultLibRoot " node_modules\ajv " ) ; Destination = ( Join-Path $localBundleNodeModules " ajv " ) } ,
@ { Source = ( Join-Path $LocalCultLibRoot " node_modules\fast-deep-equal " ) ; Destination = ( Join-Path $localBundleNodeModules " fast-deep-equal " ) } ,
@ { Source = ( Join-Path $LocalCultLibRoot " node_modules\fast-uri " ) ; Destination = ( Join-Path $localBundleNodeModules " fast-uri " ) } ,
@ { Source = ( Join-Path $LocalCultLibRoot " node_modules\json-schema-traverse " ) ; Destination = ( Join-Path $localBundleNodeModules " json-schema-traverse " ) } ,
@ { Source = ( Join-Path $LocalCultLibRoot " node_modules\require-from-string " ) ; Destination = ( Join-Path $localBundleNodeModules " require-from-string " ) } ,
2026-07-04 22:38:43 +01:00
@ { Source = ( Join-Path $LocalViliRoot " node_modules\@msgpack " ) ; Destination = ( Join-Path $localBundleNodeModules " @msgpack " ) }
2026-06-16 16:35:19 +01:00
)
New-Item -ItemType Directory -Force -Path $localBundleScripts , $localBundleNodeModules | Out-Null
Copy-Item -LiteralPath ( Join-Path $LocalViliRoot " package.json " ) -Destination ( Join-Path $localBundleRoot " package.json " ) -Force
Copy-Item -LiteralPath ( Join-Path $LocalViliRoot " package-lock.json " ) -Destination ( Join-Path $localBundleRoot " package-lock.json " ) -Force
$scriptCopies = @ (
" idunn-rudp.cjs " ,
" install-vili-task.ps1 " ,
" restart-vili-daemon.cmd " ,
" restart-vili-daemon.ps1 " ,
" run-hidden-powershell.vbs " ,
" run-vili-daemon.ps1 " ,
" start-vili-daemon.ps1 " ,
" vili-daemon.mjs "
)
foreach ( $scriptName in $scriptCopies ) {
Copy-Item -LiteralPath ( Join-Path $LocalViliRoot " scripts\ $scriptName " ) -Destination ( Join-Path $localBundleScripts $scriptName ) -Force
}
foreach ( $copy in $moduleCopies ) {
New-Item -ItemType Directory -Force -Path ( Split-Path -Parent $copy . Destination ) | Out-Null
Copy-Item -LiteralPath $copy . Source -Destination $copy . Destination -Recurse -Force
}
if ( Test-Path -LiteralPath $localBundleZip ) {
Remove-Item -LiteralPath $localBundleZip -Force
}
Compress-Archive -Path ( Join-Path $localBundleRoot " * " ) -DestinationPath $localBundleZip -CompressionLevel Optimal
Set-Content -LiteralPath $localRemoteScript -Encoding ASCII -Value $remoteScript
Set-Content -LiteralPath $localSftpBatch -Encoding ASCII -Value ( $batchLines -join " `r `n " )
& sftp . exe -b $localSftpBatch $RavenHost
if ( $LASTEXITCODE -ne 0 ) {
exit $LASTEXITCODE
}
$remoteRunner = @"
`$ ErrorActionPreference = " Stop "
try {
& " $remoteTempPs "
`$ code = `$ LASTEXITCODE
} finally {
Remove-Item -LiteralPath " $remoteTempPs " -Force -ErrorAction SilentlyContinue
}
exit `$ code
" @
$encodedRunner = [ Convert ] :: ToBase64String ( [ Text.Encoding ] :: Unicode . GetBytes ( $remoteRunner ) )
& ssh . exe -o BatchMode = yes -o ConnectTimeout = 10 $RavenHost " powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand $encodedRunner "
} finally {
Remove-Item -LiteralPath $localRemoteScript , $localSftpBatch -Force -ErrorAction SilentlyContinue
Remove-Item -LiteralPath $localBundleZip -Force -ErrorAction SilentlyContinue
Remove-Item -LiteralPath $localBundleRoot -Recurse -Force -ErrorAction SilentlyContinue
}
$restartExit = $LASTEXITCODE
exit $restartExit