diff --git a/Borealis.ps1 b/Borealis.ps1 index 6ffeb13..1419ee0 100644 --- a/Borealis.ps1 +++ b/Borealis.ps1 @@ -8,8 +8,7 @@ param( [string]$AgentAction = '', [switch]$Vite, [switch]$Flask, - [switch]$Quick, - [switch]$SilentUpdate + [switch]$Quick ) # Preselect menu choices from CLI args (optional) @@ -27,11 +26,6 @@ if ($Vite -and $Flask) { exit 1 } -if ($SilentUpdate -and ($Server -or $Agent -or $Vite -or $Flask -or $Quick -or ($AgentAction -and $AgentAction.Trim()))) { - Write-Host "-SilentUpdate cannot be combined with other options." -ForegroundColor Red - exit 1 -} - if ($Server) { # Auto-select main menu option for Server when -Server flag is provided $choice = '1' @@ -581,408 +575,6 @@ function InstallOrUpdate-BorealisAgent { } } -function Stop-AgentScheduledTasks { - param( - [string[]]$TaskNames - ) - - $stopped = @() - foreach ($name in $TaskNames) { - $taskExists = $false - try { - $null = Get-ScheduledTask -TaskName $name -ErrorAction Stop - $taskExists = $true - } catch { - try { - schtasks.exe /Query /TN "$name" 2>$null | Out-Null - if ($LASTEXITCODE -eq 0) { $taskExists = $true } - } catch {} - } - - if (-not $taskExists) { continue } - - Write-Host "Stopping scheduled task: $name" -ForegroundColor Yellow - $stopped += $name - try { Stop-ScheduledTask -TaskName $name -ErrorAction SilentlyContinue } catch {} - try { schtasks.exe /End /TN "$name" /F 2>$null | Out-Null } catch {} - try { - for ($i = 0; $i -lt 20; $i++) { - $info = Get-ScheduledTaskInfo -TaskName $name -ErrorAction Stop - if ($info.State -ne 'Running' -and $info.State -ne 'Queued') { break } - Start-Sleep -Milliseconds 500 - } - } catch {} - } - - return ,$stopped -} - -function Start-AgentScheduledTasks { - param( - [string[]]$TaskNames - ) - - foreach ($name in $TaskNames) { - Write-Host "Restarting scheduled task: $name" -ForegroundColor Green - try { - Start-ScheduledTask -TaskName $name -ErrorAction Stop | Out-Null - continue - } catch {} - - try { schtasks.exe /Run /TN "$name" 2>$null | Out-Null } catch {} - } -} - -function Get-BorealisServerUrl { - param( - [string]$AgentRoot - ) - - $serverBaseUrl = $env:BOREALIS_SERVER_URL - if (-not $serverBaseUrl) { - try { - if (-not $AgentRoot) { $AgentRoot = $scriptDir } - $settingsDir = Join-Path $AgentRoot 'Settings' - $serverUrlFile = Join-Path $settingsDir 'server_url.txt' - if (Test-Path $serverUrlFile -PathType Leaf) { - $content = Get-Content -Path $serverUrlFile -Raw -ErrorAction Stop - if ($content) { $serverBaseUrl = $content.Trim() } - } - } catch {} - } - - if (-not $serverBaseUrl) { $serverBaseUrl = 'http://localhost:5000' } - return $serverBaseUrl.Trim() -} - -function Get-AgentServiceId { - param( - [string]$AgentRoot - ) - - if (-not $AgentRoot) { $AgentRoot = $scriptDir } - $settingsDir = Join-Path $AgentRoot 'Settings' - $candidates = @( - Join-Path $settingsDir 'agent_settings_svc.json', - Join-Path $settingsDir 'agent_settings.json' - ) - - foreach ($path in $candidates) { - try { - if (Test-Path $path -PathType Leaf) { - $raw = Get-Content -Path $path -Raw -ErrorAction Stop - if (-not $raw) { continue } - $cfg = $raw | ConvertFrom-Json -ErrorAction Stop - $value = ($cfg.agent_id) - if ($value) { return ($value.ToString()).Trim() } - } - } catch {} - } - - return '' -} - -function Invoke-AgentUpdateCheck { - param( - [Parameter(Mandatory = $true)] - [string]$ServerBaseUrl, - - [Parameter(Mandatory = $true)] - [string]$AgentId - ) - - if ([string]::IsNullOrWhiteSpace($ServerBaseUrl)) { - throw 'Server URL is blank; cannot perform update check.' - } - - $base = $ServerBaseUrl.TrimEnd('/') - $uri = "$base/api/agent/update_check" - $payload = @{ agent_id = $AgentId } | ConvertTo-Json -Depth 3 - $headers = @{ 'User-Agent' = 'borealis-agent-updater' } - - $resp = Invoke-WebRequest -Uri $uri -Method Post -Headers $headers -Body $payload -ContentType 'application/json' -UseBasicParsing -ErrorAction Stop - $json = $resp.Content | ConvertFrom-Json - - if ($resp.StatusCode -ne 200) { - $message = $json.error - if (-not $message) { $message = "HTTP $($resp.StatusCode)" } - throw "Borealis server responded with an error: $message" - } - - return $json -} - -function Get-RepositoryCommitHash { - param( - [Parameter(Mandatory = $true)] - [string]$ProjectRoot - ) - - $candidates = @($ProjectRoot) - $agentRootCandidate = Join-Path $ProjectRoot 'Agent\Borealis' - if (Test-Path $agentRootCandidate -PathType Container) { $candidates += $agentRootCandidate } - - foreach ($root in $candidates) { - try { - $gitDir = Join-Path $root '.git' - $headPath = Join-Path $gitDir 'HEAD' - if (-not (Test-Path $headPath -PathType Leaf)) { continue } - $head = (Get-Content -Path $headPath -Raw -ErrorAction Stop).Trim() - if (-not $head) { continue } - - if ($head -match '^ref:\s*(.+)$') { - $ref = $Matches[1].Trim() - if ($ref) { - $refPath = $gitDir - foreach ($part in ($ref -split '/')) { - if ($part) { $refPath = Join-Path $refPath $part } - } - if (Test-Path $refPath -PathType Leaf) { - $commit = (Get-Content -Path $refPath -Raw -ErrorAction Stop).Trim() - if ($commit) { return $commit } - } - $packedRefs = Join-Path $gitDir 'packed-refs' - if (Test-Path $packedRefs -PathType Leaf) { - foreach ($line in Get-Content -Path $packedRefs -ErrorAction Stop) { - $trim = ($line).Trim() - if (-not $trim -or $trim.StartsWith('#') -or $trim.StartsWith('^')) { continue } - $parts = $trim.Split(' ', 2) - if ($parts.Count -ge 2 -and $parts[1].Trim() -eq $ref) { - $candidate = $parts[0].Trim() - if ($candidate) { return $candidate } - } - } - } - } - } else { - $detached = $head.Split([Environment]::NewLine, [StringSplitOptions]::RemoveEmptyEntries) - if ($detached.Length -gt 0) { - $candidate = $detached[0].Trim() - if ($candidate) { return $candidate } - } - } - } catch {} - } - - return '' -} - -function Submit-AgentHash { - param( - [Parameter(Mandatory = $true)] - [string]$ServerBaseUrl, - - [Parameter(Mandatory = $true)] - [string]$AgentId, - - [Parameter(Mandatory = $true)] - [string]$AgentHash - ) - - if ([string]::IsNullOrWhiteSpace($ServerBaseUrl) -or [string]::IsNullOrWhiteSpace($AgentId) -or [string]::IsNullOrWhiteSpace($AgentHash)) { - return - } - - $base = $ServerBaseUrl.TrimEnd('/') - $uri = "$base/api/agent/agent_hash" - $payload = @{ agent_id = $AgentId; agent_hash = $AgentHash } | ConvertTo-Json -Depth 3 - $headers = @{ 'User-Agent' = 'borealis-agent-updater' } - - Invoke-WebRequest -Uri $uri -Method Post -Headers $headers -Body $payload -ContentType 'application/json' -UseBasicParsing -ErrorAction Stop | Out-Null -} - -function Invoke-BorealisUpdate { - param( - [switch]$Silent - ) - - $host.UI.RawUI.WindowTitle = "Borealis Updater" - if ($Silent) { - Write-Host "Starting unattended Borealis update..." -ForegroundColor Cyan - } else { - Write-Host " " - Write-Host "Updating Borealis..." -ForegroundColor Green - } - - $updateZip = Join-Path $scriptDir "Update_Staging\main.zip" - $updateDir = Join-Path $scriptDir "Update_Staging\Borealis-main" - $preservePath = Join-Path $scriptDir "Data\Server\Python_API_Endpoints\Tesseract-OCR" - $preserveBackupPath = Join-Path $scriptDir "Update_Staging\Tesseract-OCR" - - Run-Step "Updating: Move Tesseract-OCR Folder Somewhere Safe to Restore Later" { - if (Test-Path $preservePath) { - $stagingPath = Join-Path $scriptDir "Update_Staging" - if (-not (Test-Path $stagingPath)) { New-Item -ItemType Directory -Force -Path $stagingPath | Out-Null } - Move-Item -Path $preservePath -Destination $preserveBackupPath -Force - } - } - - Run-Step "Updating: Clean Up Folders to Prepare for Update" { - Remove-Item -Recurse -Force -ErrorAction SilentlyContinue ` - (Join-Path $scriptDir "Data"), ` - (Join-Path $scriptDir "Server\web-interface\src"), ` - (Join-Path $scriptDir "Server\web-interface\build"), ` - (Join-Path $scriptDir "Server\web-interface\public"), ` - (Join-Path $scriptDir "Server\Borealis") - } - - Run-Step "Updating: Create Update Staging Folder" { - $stagingPath = Join-Path $scriptDir "Update_Staging" - if (-not (Test-Path $stagingPath)) { New-Item -ItemType Directory -Force -Path $stagingPath | Out-Null } - Set-Variable -Name updateZip -Scope 1 -Value (Join-Path $stagingPath "main.zip") - Set-Variable -Name updateDir -Scope 1 -Value (Join-Path $stagingPath "Borealis-main") - } - - Run-Step "Updating: Download Update" { Invoke-WebRequest -Uri "https://github.com/bunny-lab-io/Borealis/archive/refs/heads/main.zip" -OutFile $updateZip } - Run-Step "Updating: Extract Update Files" { Expand-Archive -Path $updateZip -DestinationPath (Join-Path $scriptDir "Update_Staging") -Force } - Run-Step "Updating: Copy Update Files into Production Borealis Root Folder" { Copy-Item "$updateDir\*" $scriptDir -Recurse -Force } - - Run-Step "Updating: Restore Tesseract-OCR Folder" { - $restorePath = Join-Path $scriptDir "Data\Server\Python_API_Endpoints" - if (Test-Path $preserveBackupPath) { - if (-not (Test-Path $restorePath)) { New-Item -ItemType Directory -Force -Path $restorePath | Out-Null } - Move-Item -Path $preserveBackupPath -Destination $restorePath -Force - } - } - - Run-Step "Updating: Clean Up Update Staging Folder" { Remove-Item -Recurse -Force -ErrorAction SilentlyContinue (Join-Path $scriptDir "Update_Staging") } - - if ($Silent) { - Write-Host "Unattended Borealis update completed." -ForegroundColor Green - return - } - - Write-Host "`nUpdate Complete! Please Re-Launch the Borealis Script." -ForegroundColor Green - Read-Host "Press any key to re-launch Borealis..." - & (Join-Path $scriptDir "Borealis.ps1") - exit 0 -} - -function Invoke-BorealisSilentUpdate { - Write-Host "Initiating Borealis silent update workflow..." -ForegroundColor DarkCyan - - $agentRootCandidate = Join-Path $scriptDir 'Agent\Borealis' - $agentRoot = $scriptDir - if (Test-Path $agentRootCandidate -PathType Container) { - try { - $agentRoot = (Resolve-Path -Path $agentRootCandidate -ErrorAction Stop).Path - } catch { - $agentRoot = $agentRootCandidate - } - } - - $serverBaseUrl = Get-BorealisServerUrl -AgentRoot $agentRoot - $agentId = Get-AgentServiceId -AgentRoot $agentRoot - - $updateMode = $env:update_mode - if ($updateMode) { $updateMode = $updateMode.ToLowerInvariant() } else { $updateMode = 'update' } - $forceUpdate = $updateMode -eq 'force_update' - - $shouldUpdate = $true - $updateInfo = $null - - if (-not $forceUpdate) { - if ($agentId) { - try { - $updateInfo = Invoke-AgentUpdateCheck -ServerBaseUrl $serverBaseUrl -AgentId $agentId - $shouldUpdate = [bool]($updateInfo.update_available) - } catch { - Write-Verbose ("Update check failed: {0}" -f $_.Exception.Message) - $shouldUpdate = $true - } - } else { - Write-Verbose 'Agent ID not found; defaulting to update.' - $shouldUpdate = $true - } - } else { - $shouldUpdate = $true - } - - if (-not $shouldUpdate) { - Write-Host "==============================================" - Write-Host "Borealis Agent Already Up-to-Date" - Write-Host "==============================================" - return - } - - $mutex = $null - $gotMutex = $false - $managedTasks = @() - try { - $mutex = New-Object System.Threading.Mutex($false, 'Global\BorealisUpdate') - $gotMutex = $mutex.WaitOne(0) - if (-not $gotMutex) { - Write-Verbose 'Another update is already running (mutex held). Exiting quietly.' - Write-Host "==============================================" - Write-Host "Borealis Agent Already Up-to-Date" - Write-Host "==============================================" - return - } - - $staging = Join-Path $scriptDir 'Update_Staging' - $zipPath = Join-Path $staging 'main.zip' - if (Test-Path $zipPath) { - Write-Verbose "Pre-flight: removing existing $zipPath" - for ($i = 1; $i -le 10; $i++) { - try { - Remove-Item -LiteralPath $zipPath -Force -ErrorAction Stop - break - } catch { - Start-Sleep -Milliseconds (100 * $i) - if ($i -eq 10) { - throw "Pre-flight delete failed; $zipPath appears locked by another process." - } - } - } - } - - $managedTasks = Stop-AgentScheduledTasks -TaskNames @('Borealis Agent','Borealis Agent (UserHelper)') - - $updateSucceeded = $false - try { - Invoke-BorealisUpdate -Silent - $updateSucceeded = $true - } finally { - if ($managedTasks.Count -gt 0) { - Start-AgentScheduledTasks -TaskNames $managedTasks - } - } - - if (-not $updateSucceeded) { - throw 'Borealis.ps1 -SilentUpdate failed.' - } - - $newHash = Get-RepositoryCommitHash -ProjectRoot $scriptDir - if ($newHash) { - try { - if ($agentId) { - Submit-AgentHash -ServerBaseUrl $serverBaseUrl -AgentId $agentId -AgentHash $newHash - } - } catch { - Write-Verbose ("Failed to submit agent hash: {0}" -f $_.Exception.Message) - } - } - - $displayHash = $newHash - if (-not $displayHash -and $updateInfo) { - $displayHash = ($updateInfo.repo_hash) - } - if (-not $displayHash) { $displayHash = 'unknown' } - - Write-Host "==============================================" - Write-Host ("Borealis Agent Updated - Repository Hash: {0}" -f $displayHash) - Write-Host "==============================================" - } finally { - if ($mutex -and $gotMutex) { $mutex.ReleaseMutex() | Out-Null } - if ($mutex) { $mutex.Dispose() } - } -} - -if ($SilentUpdate) { - Invoke-BorealisSilentUpdate - exit 0 -} - # ---------------------- Main ---------------------- Clear-Host @@ -1009,8 +601,6 @@ if (-not $choice) { Write-Host "[Experimental]" -ForegroundColor Red Write-Host " 4) Package Self-Contained EXE of Server or Agent " -NoNewline -ForegroundColor DarkGray Write-Host "[Experimental]" -ForegroundColor Red - Write-Host " 5) Update Borealis " -NoNewLine -ForegroundColor DarkGray - Write-Host "[Requires Re-Build]" -ForegroundColor Red # (Removed) AutoHotKey experimental testing Write-Host "Type a number and press " -NoNewLine Write-Host "" -ForegroundColor DarkCyan @@ -1220,11 +810,6 @@ switch ($choice) { } } - "5" { - Invoke-BorealisUpdate - break - } - # (Removed) case "6" experimental AHK test default { Write-Host "Invalid selection. Exiting..." -ForegroundColor Red; exit 1 } diff --git a/Update.ps1 b/Update.ps1 new file mode 100644 index 0000000..88f9634 --- /dev/null +++ b/Update.ps1 @@ -0,0 +1,427 @@ +[CmdletBinding()] +param() + +$scriptDir = Split-Path $MyInvocation.MyCommand.Path -Parent +$symbols = @{ + Success = [char]0x2705 + Running = [char]0x23F3 + Fail = [char]0x274C + Info = [char]0x2139 +} + +function Write-ProgressStep { + param ( + [string]$Message, + [string]$Status = $symbols["Info"] + ) + Write-Host "`r$Status $Message... " -NoNewline +} + +function Run-Step { + param ( + [string] $Message, + [scriptblock]$Script + ) + Write-ProgressStep -Message $Message -Status "$($symbols.Running)" + try { + & $Script + if ($LASTEXITCODE -eq 0 -or $?) { + Write-Host "`r$($symbols.Success) $Message " + } else { + throw "Non-zero exit code" + } + } catch { + Write-Host "`r$($symbols.Fail) $Message - Failed: $_ " -ForegroundColor Red + throw + } +} + +function Stop-AgentScheduledTasks { + param( + [string[]]$TaskNames + ) + + $stopped = @() + foreach ($name in $TaskNames) { + $taskExists = $false + try { + $null = Get-ScheduledTask -TaskName $name -ErrorAction Stop + $taskExists = $true + } catch { + try { + schtasks.exe /Query /TN "$name" 2>$null | Out-Null + if ($LASTEXITCODE -eq 0) { $taskExists = $true } + } catch {} + } + + if (-not $taskExists) { continue } + + Write-Host "Stopping scheduled task: $name" -ForegroundColor Yellow + $stopped += $name + try { Stop-ScheduledTask -TaskName $name -ErrorAction SilentlyContinue } catch {} + try { schtasks.exe /End /TN "$name" /F 2>$null | Out-Null } catch {} + try { + for ($i = 0; $i -lt 20; $i++) { + $info = Get-ScheduledTaskInfo -TaskName $name -ErrorAction Stop + if ($info.State -ne 'Running' -and $info.State -ne 'Queued') { break } + Start-Sleep -Milliseconds 500 + } + } catch {} + } + + return ,$stopped +} + +function Start-AgentScheduledTasks { + param( + [string[]]$TaskNames + ) + + foreach ($name in $TaskNames) { + Write-Host "Restarting scheduled task: $name" -ForegroundColor Green + try { + Start-ScheduledTask -TaskName $name -ErrorAction Stop | Out-Null + continue + } catch {} + + try { schtasks.exe /Run /TN "$name" 2>$null | Out-Null } catch {} + } +} + +function Get-BorealisServerUrl { + param( + [string]$AgentRoot + ) + + $serverBaseUrl = $env:BOREALIS_SERVER_URL + if (-not $serverBaseUrl) { + try { + if (-not $AgentRoot) { $AgentRoot = $scriptDir } + $settingsDir = Join-Path $AgentRoot 'Settings' + $serverUrlFile = Join-Path $settingsDir 'server_url.txt' + if (Test-Path $serverUrlFile -PathType Leaf) { + $content = Get-Content -Path $serverUrlFile -Raw -ErrorAction Stop + if ($content) { $serverBaseUrl = $content.Trim() } + } + } catch {} + } + + if (-not $serverBaseUrl) { $serverBaseUrl = 'http://localhost:5000' } + return $serverBaseUrl.Trim() +} + +function Get-AgentServiceId { + param( + [string]$AgentRoot + ) + + if (-not $AgentRoot) { $AgentRoot = $scriptDir } + $settingsDir = Join-Path $AgentRoot 'Settings' + $candidates = @( + Join-Path $settingsDir 'agent_settings_svc.json', + Join-Path $settingsDir 'agent_settings.json' + ) + + foreach ($path in $candidates) { + try { + if (Test-Path $path -PathType Leaf) { + $raw = Get-Content -Path $path -Raw -ErrorAction Stop + if (-not $raw) { continue } + $cfg = $raw | ConvertFrom-Json -ErrorAction Stop + $value = ($cfg.agent_id) + if ($value) { return ($value.ToString()).Trim() } + } + } catch {} + } + + return '' +} + +function Invoke-AgentUpdateCheck { + param( + [Parameter(Mandatory = $true)] + [string]$ServerBaseUrl, + + [Parameter(Mandatory = $true)] + [string]$AgentId + ) + + if ([string]::IsNullOrWhiteSpace($ServerBaseUrl)) { + throw 'Server URL is blank; cannot perform update check.' + } + + $base = $ServerBaseUrl.TrimEnd('/') + $uri = "$base/api/agent/update_check" + $payload = @{ agent_id = $AgentId } | ConvertTo-Json -Depth 3 + $headers = @{ 'User-Agent' = 'borealis-agent-updater' } + + $resp = Invoke-WebRequest -Uri $uri -Method Post -Headers $headers -Body $payload -ContentType 'application/json' -UseBasicParsing -ErrorAction Stop + $json = $resp.Content | ConvertFrom-Json + + if ($resp.StatusCode -ne 200) { + $message = $json.error + if (-not $message) { $message = "HTTP $($resp.StatusCode)" } + throw "Borealis server responded with an error: $message" + } + + return $json +} + +function Get-RepositoryCommitHash { + param( + [Parameter(Mandatory = $true)] + [string]$ProjectRoot + ) + + $candidates = @($ProjectRoot) + $agentRootCandidate = Join-Path $ProjectRoot 'Agent\Borealis' + if (Test-Path $agentRootCandidate -PathType Container) { $candidates += $agentRootCandidate } + + foreach ($root in $candidates) { + try { + $gitDir = Join-Path $root '.git' + $headPath = Join-Path $gitDir 'HEAD' + if (-not (Test-Path $headPath -PathType Leaf)) { continue } + $head = (Get-Content -Path $headPath -Raw -ErrorAction Stop).Trim() + if (-not $head) { continue } + + if ($head -match '^ref:\s*(.+)$') { + $ref = $Matches[1].Trim() + if ($ref) { + $refPath = $gitDir + foreach ($part in ($ref -split '/')) { + if ($part) { $refPath = Join-Path $refPath $part } + } + if (Test-Path $refPath -PathType Leaf) { + $commit = (Get-Content -Path $refPath -Raw -ErrorAction Stop).Trim() + if ($commit) { return $commit } + } + $packedRefs = Join-Path $gitDir 'packed-refs' + if (Test-Path $packedRefs -PathType Leaf) { + foreach ($line in Get-Content -Path $packedRefs -ErrorAction Stop) { + $trim = ($line).Trim() + if (-not $trim -or $trim.StartsWith('#') -or $trim.StartsWith('^')) { continue } + $parts = $trim.Split(' ', 2) + if ($parts.Count -ge 2 -and $parts[1].Trim() -eq $ref) { + $candidate = $parts[0].Trim() + if ($candidate) { return $candidate } + } + } + } + } + } else { + $detached = $head.Split([Environment]::NewLine, [StringSplitOptions]::RemoveEmptyEntries) + if ($detached.Length -gt 0) { + $candidate = $detached[0].Trim() + if ($candidate) { return $candidate } + } + } + } catch {} + } + + return '' +} + +function Submit-AgentHash { + param( + [Parameter(Mandatory = $true)] + [string]$ServerBaseUrl, + + [Parameter(Mandatory = $true)] + [string]$AgentId, + + [Parameter(Mandatory = $true)] + [string]$AgentHash + ) + + if ([string]::IsNullOrWhiteSpace($ServerBaseUrl) -or [string]::IsNullOrWhiteSpace($AgentId) -or [string]::IsNullOrWhiteSpace($AgentHash)) { + return + } + + $base = $ServerBaseUrl.TrimEnd('/') + $uri = "$base/api/agent/agent_hash" + $payload = @{ agent_id = $AgentId; agent_hash = $AgentHash } | ConvertTo-Json -Depth 3 + $headers = @{ 'User-Agent' = 'borealis-agent-updater' } + + Invoke-WebRequest -Uri $uri -Method Post -Headers $headers -Body $payload -ContentType 'application/json' -UseBasicParsing -ErrorAction Stop | Out-Null +} + +function Invoke-BorealisUpdate { + param( + [switch]$Silent + ) + + $updateZip = Join-Path $scriptDir "Update_Staging\main.zip" + $updateDir = Join-Path $scriptDir "Update_Staging\Borealis-main" + $preservePath = Join-Path $scriptDir "Data\Server\Python_API_Endpoints\Tesseract-OCR" + $preserveBackupPath = Join-Path $scriptDir "Update_Staging\Tesseract-OCR" + + Run-Step "Updating: Move Tesseract-OCR Folder Somewhere Safe to Restore Later" { + if (Test-Path $preservePath) { + $stagingPath = Join-Path $scriptDir "Update_Staging" + if (-not (Test-Path $stagingPath)) { New-Item -ItemType Directory -Force -Path $stagingPath | Out-Null } + Move-Item -Path $preservePath -Destination $preserveBackupPath -Force + } + } + + Run-Step "Updating: Clean Up Folders to Prepare for Update" { + Remove-Item -Recurse -Force -ErrorAction SilentlyContinue ` + (Join-Path $scriptDir "Data"), ` + (Join-Path $scriptDir "Server\web-interface\src"), ` + (Join-Path $scriptDir "Server\web-interface\build"), ` + (Join-Path $scriptDir "Server\web-interface\public"), ` + (Join-Path $scriptDir "Server\Borealis") + } + + Run-Step "Updating: Create Update Staging Folder" { + $stagingPath = Join-Path $scriptDir "Update_Staging" + if (-not (Test-Path $stagingPath)) { New-Item -ItemType Directory -Force -Path $stagingPath | Out-Null } + Set-Variable -Name updateZip -Scope 1 -Value (Join-Path $stagingPath "main.zip") + Set-Variable -Name updateDir -Scope 1 -Value (Join-Path $stagingPath "Borealis-main") + } + + Run-Step "Updating: Download Update" { Invoke-WebRequest -Uri "https://github.com/bunny-lab-io/Borealis/archive/refs/heads/main.zip" -OutFile $updateZip } + Run-Step "Updating: Extract Update Files" { Expand-Archive -Path $updateZip -DestinationPath (Join-Path $scriptDir "Update_Staging") -Force } + Run-Step "Updating: Copy Update Files into Production Borealis Root Folder" { Copy-Item "$updateDir\*" $scriptDir -Recurse -Force } + + Run-Step "Updating: Restore Tesseract-OCR Folder" { + $restorePath = Join-Path $scriptDir "Data\Server\Python_API_Endpoints" + if (Test-Path $preserveBackupPath) { + if (-not (Test-Path $restorePath)) { New-Item -ItemType Directory -Force -Path $restorePath | Out-Null } + Move-Item -Path $preserveBackupPath -Destination $restorePath -Force + } + } + + Run-Step "Updating: Clean Up Update Staging Folder" { Remove-Item -Recurse -Force -ErrorAction SilentlyContinue (Join-Path $scriptDir "Update_Staging") } + + if (-not $Silent) { + Write-Host "Unattended Borealis update completed." -ForegroundColor Green + } +} + +function Invoke-BorealisAgentUpdate { + Write-Host "Initiating Borealis update workflow..." -ForegroundColor DarkCyan + + $currentHash = Get-RepositoryCommitHash -ProjectRoot $scriptDir + if ($currentHash) { + Write-Host ("Current repository hash: {0}" -f $currentHash) -ForegroundColor DarkGray + } + + $agentRootCandidate = Join-Path $scriptDir 'Agent\Borealis' + $agentRoot = $scriptDir + if (Test-Path $agentRootCandidate -PathType Container) { + try { + $agentRoot = (Resolve-Path -Path $agentRootCandidate -ErrorAction Stop).Path + } catch { + $agentRoot = $agentRootCandidate + } + } + + $serverBaseUrl = Get-BorealisServerUrl -AgentRoot $agentRoot + $agentId = Get-AgentServiceId -AgentRoot $agentRoot + + $updateMode = $env:update_mode + if ($updateMode) { $updateMode = $updateMode.ToLowerInvariant() } else { $updateMode = 'update' } + $forceUpdate = $updateMode -eq 'force_update' + + $shouldUpdate = $true + $updateInfo = $null + + if (-not $forceUpdate) { + if ($agentId) { + try { + $updateInfo = Invoke-AgentUpdateCheck -ServerBaseUrl $serverBaseUrl -AgentId $agentId + $shouldUpdate = [bool]($updateInfo.update_available) + } catch { + Write-Verbose ("Update check failed: {0}" -f $_.Exception.Message) + $shouldUpdate = $true + } + } else { + Write-Verbose 'Agent ID not found; defaulting to update.' + $shouldUpdate = $true + } + } else { + $shouldUpdate = $true + } + + if (-not $shouldUpdate) { + Write-Host "==============================================" + Write-Host "Borealis Agent Already Up-to-Date" + Write-Host "==============================================" + return + } + + $mutex = $null + $gotMutex = $false + $managedTasks = @() + try { + $mutex = New-Object System.Threading.Mutex($false, 'Global\BorealisUpdate') + $gotMutex = $mutex.WaitOne(0) + if (-not $gotMutex) { + Write-Verbose 'Another update is already running (mutex held). Exiting quietly.' + Write-Host "==============================================" + Write-Host "Borealis Agent Already Up-to-Date" + Write-Host "==============================================" + return + } + + $staging = Join-Path $scriptDir 'Update_Staging' + $zipPath = Join-Path $staging 'main.zip' + if (Test-Path $zipPath) { + Write-Verbose "Pre-flight: removing existing $zipPath" + for ($i = 1; $i -le 10; $i++) { + try { + Remove-Item -LiteralPath $zipPath -Force -ErrorAction Stop + break + } catch { + Start-Sleep -Milliseconds (100 * $i) + if ($i -eq 10) { + throw "Pre-flight delete failed; $zipPath appears locked by another process." + } + } + } + } + + $managedTasks = Stop-AgentScheduledTasks -TaskNames @('Borealis Agent','Borealis Agent (UserHelper)') + + $updateSucceeded = $false + try { + Invoke-BorealisUpdate -Silent + $updateSucceeded = $true + } finally { + if ($managedTasks.Count -gt 0) { + Start-AgentScheduledTasks -TaskNames $managedTasks + } + } + + if (-not $updateSucceeded) { + throw 'Borealis update failed.' + } + + $newHash = Get-RepositoryCommitHash -ProjectRoot $scriptDir + if ($newHash) { + try { + if ($agentId) { + Submit-AgentHash -ServerBaseUrl $serverBaseUrl -AgentId $agentId -AgentHash $newHash + } + } catch { + Write-Verbose ("Failed to submit agent hash: {0}" -f $_.Exception.Message) + } + } + + $displayHash = $newHash + if (-not $displayHash -and $updateInfo) { + $displayHash = ($updateInfo.repo_hash) + } + if (-not $displayHash) { $displayHash = 'unknown' } + + Write-Host "==============================================" + Write-Host ("Borealis Agent Updated - Repository Hash: {0}" -f $displayHash) + Write-Host "==============================================" + } finally { + if ($mutex -and $gotMutex) { $mutex.ReleaseMutex() | Out-Null } + if ($mutex) { $mutex.Dispose() } + } +} + +Invoke-BorealisAgentUpdate