mirror of
				https://github.com/bunny-lab-io/Borealis.git
				synced 2025-10-26 15:41:58 -06:00 
			
		
		
		
	
		
			
				
	
	
		
			1012 lines
		
	
	
		
			35 KiB
		
	
	
	
		
			PowerShell
		
	
	
	
	
	
			
		
		
	
	
			1012 lines
		
	
	
		
			35 KiB
		
	
	
	
		
			PowerShell
		
	
	
	
	
	
| [CmdletBinding()]
 | |
| param()
 | |
| 
 | |
| $scriptDir = Split-Path $MyInvocation.MyCommand.Path -Parent
 | |
| $symbols = @{
 | |
|     Success = [char]0x2705
 | |
|     Running = [char]0x23F3
 | |
|     Fail    = [char]0x274C
 | |
|     Info    = [char]0x2139
 | |
| }
 | |
| 
 | |
| $repositoryUrl = 'https://github.com/bunny-lab-io/Borealis.git'
 | |
| 
 | |
| 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 Get-GitExecutablePath {
 | |
|     param(
 | |
|         [string]$ProjectRoot
 | |
|     )
 | |
| 
 | |
|     $candidates = @()
 | |
|     if ($ProjectRoot) {
 | |
|         $candidates += (Join-Path $ProjectRoot 'Dependencies\git\cmd\git.exe')
 | |
|         $candidates += (Join-Path $ProjectRoot 'Dependencies\git\bin\git.exe')
 | |
|     }
 | |
| 
 | |
|     foreach ($candidate in ($candidates | Select-Object -Unique)) {
 | |
|         try {
 | |
|             if (Test-Path $candidate -PathType Leaf) { return $candidate }
 | |
|         } catch {}
 | |
|     }
 | |
| 
 | |
|     return ''
 | |
| }
 | |
| 
 | |
| function Invoke-GitCommand {
 | |
|     param(
 | |
|         [Parameter(Mandatory = $true)]
 | |
|         [string]$GitExe,
 | |
| 
 | |
|         [Parameter(Mandatory = $true)]
 | |
|         [string]$WorkingDirectory,
 | |
| 
 | |
|         [Parameter(Mandatory = $true)]
 | |
|         [string[]]$Arguments
 | |
|     )
 | |
| 
 | |
|     if ([string]::IsNullOrWhiteSpace($GitExe) -or -not (Test-Path $GitExe -PathType Leaf)) {
 | |
|         throw "Git executable not found at '$GitExe'"
 | |
|     }
 | |
| 
 | |
|     if (-not (Test-Path $WorkingDirectory -PathType Container)) {
 | |
|         throw "Working directory '$WorkingDirectory' does not exist."
 | |
|     }
 | |
| 
 | |
|     $fullArgs = @('-C', $WorkingDirectory) + $Arguments
 | |
|     $output = & $GitExe @fullArgs 2>&1
 | |
|     $exitCode = $LASTEXITCODE
 | |
|     if ($exitCode -ne 0) {
 | |
|         $joined = ($Arguments -join ' ')
 | |
|         $message = "git $joined failed with exit code $exitCode."
 | |
|         if ($output) {
 | |
|             $message = "$message Output: $output"
 | |
|         }
 | |
|         throw $message
 | |
|     }
 | |
| 
 | |
|     return $output
 | |
| }
 | |
| 
 | |
| 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 Stop-AgentPythonProcesses {
 | |
|     param(
 | |
|         [string[]]$ProcessNames = @('python', 'pythonw')
 | |
|     )
 | |
| 
 | |
|     foreach ($name in ($ProcessNames | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique)) {
 | |
|         $name = $name.Trim()
 | |
|         if (-not $name) { continue }
 | |
| 
 | |
|         $processes = @()
 | |
|         try {
 | |
|             $processes = Get-Process -Name $name -ErrorAction Stop
 | |
|         } catch {
 | |
|             $processes = @()
 | |
|         }
 | |
| 
 | |
|         foreach ($proc in $processes) {
 | |
|             $procId = $null
 | |
|             $procName = $null
 | |
|             try {
 | |
|                 $procId = $proc.Id
 | |
|                 $procName = $proc.ProcessName
 | |
|             } catch {}
 | |
| 
 | |
|             if ($procId -eq $null) { continue }
 | |
| 
 | |
|             if (-not $procName) { $procName = $name }
 | |
| 
 | |
|             $stopped = $false
 | |
|             Write-Host "Stopping process: $procName (PID $procId)" -ForegroundColor Yellow
 | |
| 
 | |
|             try {
 | |
|                 Stop-Process -Id $procId -Force -ErrorAction Stop
 | |
|                 $stopped = $true
 | |
|             } catch {
 | |
|                 Write-Host "Unable to stop process via Stop-Process: $procName (PID $procId). $_" -ForegroundColor DarkYellow
 | |
|             }
 | |
| 
 | |
|             if (-not $stopped) {
 | |
|                 try {
 | |
|                     $taskkillOutput = taskkill.exe /PID $procId /F 2>&1
 | |
|                     if ($LASTEXITCODE -eq 0) {
 | |
|                         $stopped = $true
 | |
|                     } else {
 | |
|                         if ($taskkillOutput) {
 | |
|                             Write-Host "taskkill.exe returned exit code ${LASTEXITCODE} for PID ${procId}: $taskkillOutput" -ForegroundColor DarkYellow
 | |
|                         }
 | |
|                     }
 | |
|                 } catch {
 | |
|                     Write-Host "Unable to stop process via taskkill.exe: $procName (PID $procId). $_" -ForegroundColor DarkYellow
 | |
|                 }
 | |
|             }
 | |
| 
 | |
|             if (-not $stopped) {
 | |
|                 Write-Host "Process still running after termination attempts: $procName (PID $procId)" -ForegroundColor DarkYellow
 | |
|             }
 | |
|         }
 | |
|     }
 | |
| }
 | |
| 
 | |
| 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_SYSTEM.json')
 | |
|         (Join-Path $settingsDir 'agent_settings_CURRENTUSER.json')
 | |
|         (Join-Path $settingsDir 'agent_settings_svc.json')
 | |
|         (Join-Path $settingsDir 'agent_settings_user.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 Get-AgentGuid {
 | |
|     param(
 | |
|         [string]$AgentRoot
 | |
|     )
 | |
| 
 | |
|     if (-not $AgentRoot) { $AgentRoot = $scriptDir }
 | |
|     $candidates = @()
 | |
|     if ($AgentRoot) {
 | |
|         $settingsDir = Join-Path $AgentRoot 'Settings'
 | |
|         if ($settingsDir) {
 | |
|             $settingsGuid = Join-Path $settingsDir 'Agent_GUID.txt'
 | |
|             if ($candidates -notcontains $settingsGuid) { $candidates += $settingsGuid }
 | |
|         }
 | |
|         $legacyPath = Join-Path $AgentRoot 'agent_GUID'
 | |
|         if ($candidates -notcontains $legacyPath) { $candidates += $legacyPath }
 | |
|     }
 | |
| 
 | |
|     $projectSettingsGuid = Join-Path $scriptDir 'Agent\Borealis\Settings\Agent_GUID.txt'
 | |
|     if ($candidates -notcontains $projectSettingsGuid) { $candidates += $projectSettingsGuid }
 | |
|     $projectLegacyGuid = Join-Path $scriptDir 'Agent\Borealis\agent_GUID'
 | |
|     if ($candidates -notcontains $projectLegacyGuid) { $candidates += $projectLegacyGuid }
 | |
| 
 | |
|     foreach ($path in ($candidates | Select-Object -Unique)) {
 | |
|         try {
 | |
|             if (Test-Path $path -PathType Leaf) {
 | |
|                 $value = (Get-Content -Path $path -Raw -ErrorAction Stop)
 | |
|                 if ($value) { return $value.Trim() }
 | |
|             }
 | |
|         } catch {}
 | |
|     }
 | |
| 
 | |
|     return ''
 | |
| }
 | |
| 
 | |
| function Get-AgentSettingsDirectory {
 | |
|     param(
 | |
|         [string]$AgentRoot
 | |
|     )
 | |
| 
 | |
|     if (-not $AgentRoot) { $AgentRoot = $scriptDir }
 | |
|     $settingsDir = Join-Path $AgentRoot 'Settings'
 | |
|     if ($settingsDir -and (Test-Path $settingsDir -PathType Container)) {
 | |
|         return $settingsDir
 | |
|     }
 | |
|     return ''
 | |
| }
 | |
| 
 | |
| function Get-ProtectedTokenString {
 | |
|     param(
 | |
|         [string]$Path
 | |
|     )
 | |
| 
 | |
|     if (-not $Path -or -not (Test-Path $Path -PathType Leaf)) {
 | |
|         return ''
 | |
|     }
 | |
| 
 | |
|     try {
 | |
|         $protected = [System.IO.File]::ReadAllBytes($Path)
 | |
|         if (-not $protected -or $protected.Length -eq 0) { return '' }
 | |
|     } catch {
 | |
|         return ''
 | |
|     }
 | |
| 
 | |
|     $scopes = @(
 | |
|         [System.Security.Cryptography.DataProtectionScope]::CurrentUser,
 | |
|         [System.Security.Cryptography.DataProtectionScope]::LocalMachine
 | |
|     )
 | |
| 
 | |
|     foreach ($scope in $scopes) {
 | |
|         try {
 | |
|             $unprotected = [System.Security.Cryptography.ProtectedData]::Unprotect($protected, $null, $scope)
 | |
|             if ($unprotected -and $unprotected.Length -gt 0) {
 | |
|                 return [System.Text.Encoding]::UTF8.GetString($unprotected)
 | |
|             }
 | |
|         } catch {
 | |
|             continue
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     return ''
 | |
| }
 | |
| 
 | |
| function Invoke-AgentTokenRefresh {
 | |
|     param(
 | |
|         [Parameter(Mandatory = $true)]
 | |
|         [string]$ServerBaseUrl,
 | |
| 
 | |
|         [Parameter(Mandatory = $true)]
 | |
|         [string]$AgentGuid,
 | |
| 
 | |
|         [Parameter(Mandatory = $true)]
 | |
|         [string]$RefreshToken
 | |
|     )
 | |
| 
 | |
|     if ([string]::IsNullOrWhiteSpace($ServerBaseUrl) -or [string]::IsNullOrWhiteSpace($AgentGuid) -or [string]::IsNullOrWhiteSpace($RefreshToken)) {
 | |
|         return $null
 | |
|     }
 | |
| 
 | |
|     $base = $ServerBaseUrl.TrimEnd('/')
 | |
|     $uri = "$base/api/agent/token/refresh"
 | |
|     $payload = @{
 | |
|         guid = $AgentGuid
 | |
|         refresh_token = $RefreshToken
 | |
|     } | ConvertTo-Json
 | |
|     $headers = @{
 | |
|         'User-Agent'    = 'borealis-agent-updater'
 | |
|         'Content-Type'  = 'application/json'
 | |
|     }
 | |
| 
 | |
|     try {
 | |
|         $resp = Invoke-WebRequest -Uri $uri -Method Post -Body $payload -Headers $headers -UseBasicParsing -ErrorAction Stop
 | |
|         $json = $resp.Content | ConvertFrom-Json
 | |
|         if ($json -and $json.access_token) {
 | |
|             $expiresIn = 900
 | |
|             try {
 | |
|                 if ($json.expires_in) {
 | |
|                     $expiresIn = [int]$json.expires_in
 | |
|                 }
 | |
|             } catch {}
 | |
|             $now = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
 | |
|             $expiresAt = $now + [Math]::Max(0, $expiresIn - 5)
 | |
|             return [pscustomobject]@{
 | |
|                 AccessToken = ($json.access_token).Trim()
 | |
|                 ExpiresAt   = $expiresAt
 | |
|             }
 | |
|         }
 | |
|     } catch {
 | |
|         return $null
 | |
|     }
 | |
| 
 | |
|     return $null
 | |
| }
 | |
| 
 | |
| function Get-AgentAccessTokenContext {
 | |
|     param(
 | |
|         [string]$AgentRoot,
 | |
|         [string]$ServerBaseUrl,
 | |
|         [string]$AgentGuid
 | |
|     )
 | |
| 
 | |
|     $settingsDir = Get-AgentSettingsDirectory -AgentRoot $AgentRoot
 | |
|     if (-not $settingsDir) { return $null }
 | |
| 
 | |
|     $accessPath = Join-Path $settingsDir 'access.jwt'
 | |
|     $metaPath   = Join-Path $settingsDir 'access.meta.json'
 | |
|     $refreshPath = Join-Path $settingsDir 'refresh.token'
 | |
| 
 | |
|     $accessToken = ''
 | |
|     $expiresAt = 0
 | |
| 
 | |
|     if (Test-Path $accessPath -PathType Leaf) {
 | |
|         try {
 | |
|             $accessToken = (Get-Content -Path $accessPath -Raw -ErrorAction Stop).Trim()
 | |
|         } catch {
 | |
|             $accessToken = ''
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     if (Test-Path $metaPath -PathType Leaf) {
 | |
|         try {
 | |
|             $metaRaw = Get-Content -Path $metaPath -Raw -ErrorAction Stop
 | |
|             if ($metaRaw) {
 | |
|                 $metaJson = $metaRaw | ConvertFrom-Json -ErrorAction Stop
 | |
|                 if ($metaJson -and $metaJson.access_expires_at) {
 | |
|                     $expiresAt = [int]$metaJson.access_expires_at
 | |
|                 }
 | |
|             }
 | |
|         } catch {
 | |
|             $expiresAt = 0
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     $now = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
 | |
|     if ($accessToken -and $expiresAt -gt ($now + 30)) {
 | |
|         return [pscustomobject]@{
 | |
|             AccessToken = $accessToken
 | |
|             ExpiresAt   = $expiresAt
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     $refreshToken = Get-ProtectedTokenString -Path $refreshPath
 | |
|     if (-not $refreshToken) {
 | |
|         return $null
 | |
|     }
 | |
| 
 | |
|     $refreshResult = Invoke-AgentTokenRefresh -ServerBaseUrl $ServerBaseUrl -AgentGuid $AgentGuid -RefreshToken $refreshToken
 | |
|     if ($refreshResult -and $refreshResult.AccessToken) {
 | |
|         return $refreshResult
 | |
|     }
 | |
| 
 | |
|     return $null
 | |
| }
 | |
| function Get-RepositoryCommitHash {
 | |
|     param(
 | |
|         [Parameter(Mandatory = $true)]
 | |
|         [string]$ProjectRoot,
 | |
| 
 | |
|         [string]$AgentRoot,
 | |
| 
 | |
|         [string]$GitExe
 | |
|     )
 | |
| 
 | |
|     $candidates = @()
 | |
|     if ($ProjectRoot -and ($candidates -notcontains $ProjectRoot)) { $candidates += $ProjectRoot }
 | |
|     if ($AgentRoot -and ($candidates -notcontains $AgentRoot)) { $candidates += $AgentRoot }
 | |
|     if ($ProjectRoot) {
 | |
|         $agentRootCandidate = Join-Path $ProjectRoot 'Agent\Borealis'
 | |
|         if ((Test-Path $agentRootCandidate -PathType Container) -and ($candidates -notcontains $agentRootCandidate)) {
 | |
|             $candidates += $agentRootCandidate
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     if ($GitExe -and (Test-Path $GitExe -PathType Leaf)) {
 | |
|         foreach ($root in $candidates) {
 | |
|             try {
 | |
|                 if (-not (Test-Path (Join-Path $root '.git') -PathType Container)) { continue }
 | |
|                 $revParse = Invoke-GitCommand -GitExe $GitExe -WorkingDirectory $root -Arguments @('rev-parse','HEAD')
 | |
|                 if ($revParse) {
 | |
|                     $candidate = ($revParse | Select-Object -Last 1)
 | |
|                     if ($candidate) { return ($candidate.Trim()) }
 | |
|                 }
 | |
|             } catch {}
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     foreach ($root in $candidates) {
 | |
|         try {
 | |
|             $gitDir = Join-Path $root '.git'
 | |
|             $fetchHead = Join-Path $gitDir 'FETCH_HEAD'
 | |
|             if (-not (Test-Path $fetchHead -PathType Leaf)) { continue }
 | |
|             foreach ($line in Get-Content -Path $fetchHead -ErrorAction Stop) {
 | |
|                 $trim = ($line).Trim()
 | |
|                 if (-not $trim -or $trim.StartsWith('#')) { continue }
 | |
|                 $split = $trim.Split(@("`t", ' '), [StringSplitOptions]::RemoveEmptyEntries)
 | |
|                 if ($split.Count -gt 0) {
 | |
|                     $candidate = $split[0].Trim()
 | |
|                     if ($candidate) { return $candidate }
 | |
|                 }
 | |
|             }
 | |
|         } catch {}
 | |
|     }
 | |
| 
 | |
|     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 {}
 | |
|     }
 | |
| 
 | |
|     if ($AgentRoot) {
 | |
|         $stored = Get-StoredAgentHash -AgentRoot $AgentRoot
 | |
|         if ($stored) { return $stored }
 | |
|     }
 | |
| 
 | |
|     return ''
 | |
| }
 | |
| 
 | |
| function Get-StoredAgentHash {
 | |
|     param(
 | |
|         [string]$AgentRoot
 | |
|     )
 | |
| 
 | |
|     if (-not $AgentRoot) { return '' }
 | |
| 
 | |
|     try {
 | |
|         $settingsDir = Join-Path $AgentRoot 'Settings'
 | |
|         $hashFile = Join-Path $settingsDir 'agent_hash.txt'
 | |
|         if (Test-Path $hashFile -PathType Leaf) {
 | |
|             $value = (Get-Content -Path $hashFile -Raw -ErrorAction Stop).Trim()
 | |
|             return $value
 | |
|         }
 | |
|     } catch {}
 | |
| 
 | |
|     return ''
 | |
| }
 | |
| 
 | |
| function Set-StoredAgentHash {
 | |
|     param(
 | |
|         [string]$AgentRoot,
 | |
|         [string]$AgentHash
 | |
|     )
 | |
| 
 | |
|     if ([string]::IsNullOrWhiteSpace($AgentRoot) -or [string]::IsNullOrWhiteSpace($AgentHash)) { return }
 | |
| 
 | |
|     try {
 | |
|         $settingsDir = Join-Path $AgentRoot 'Settings'
 | |
|         if (-not (Test-Path $settingsDir -PathType Container)) {
 | |
|             New-Item -ItemType Directory -Force -Path $settingsDir | Out-Null
 | |
|         }
 | |
|         $hashFile = Join-Path $settingsDir 'agent_hash.txt'
 | |
|         Set-Content -Path $hashFile -Value $AgentHash.Trim() -Encoding UTF8
 | |
|     } catch {}
 | |
| }
 | |
| 
 | |
| function Set-GitFetchHeadHash {
 | |
|     param(
 | |
|         [string]$ProjectRoot,
 | |
|         [string]$CommitHash,
 | |
|         [string]$BranchName = 'main'
 | |
|     )
 | |
| 
 | |
|     if ([string]::IsNullOrWhiteSpace($ProjectRoot) -or [string]::IsNullOrWhiteSpace($CommitHash)) { return }
 | |
| 
 | |
|     try {
 | |
|         $gitDir = Join-Path $ProjectRoot '.git'
 | |
|         if (-not (Test-Path $gitDir -PathType Container)) {
 | |
|             New-Item -ItemType Directory -Force -Path $gitDir | Out-Null
 | |
|         }
 | |
|         $fetchHead = Join-Path $gitDir 'FETCH_HEAD'
 | |
|         $branchSegment = if ([string]::IsNullOrWhiteSpace($BranchName)) { '' } else { "`tbranch '$BranchName'" }
 | |
|         $content = "{0}{1}" -f ($CommitHash.Trim()), $branchSegment
 | |
|         Set-Content -Path $fetchHead -Value $content -Encoding UTF8
 | |
|     } catch {}
 | |
| }
 | |
| 
 | |
| function Get-ServerCurrentRepoHash {
 | |
|     param(
 | |
|         [Parameter(Mandatory = $true)]
 | |
|         [string]$ServerBaseUrl,
 | |
|         [string]$AuthToken
 | |
|     )
 | |
| 
 | |
|     if ([string]::IsNullOrWhiteSpace($ServerBaseUrl)) { return $null }
 | |
| 
 | |
|     $base = $ServerBaseUrl.TrimEnd('/')
 | |
|     $uri = "$base/api/repo/current_hash"
 | |
|     $headers = @{ 'User-Agent' = 'borealis-agent-updater' }
 | |
|     if ($AuthToken -and $AuthToken.Trim()) {
 | |
|         $headers['Authorization'] = "Bearer $AuthToken"
 | |
|     }
 | |
| 
 | |
|     try {
 | |
|         $resp = Invoke-WebRequest -Uri $uri -Method Get -Headers $headers -UseBasicParsing -ErrorAction Stop
 | |
|         $json = $resp.Content | ConvertFrom-Json
 | |
|         return $json
 | |
|     } catch {
 | |
|         return $null
 | |
|     }
 | |
| }
 | |
| 
 | |
| function Submit-AgentHash {
 | |
|     param(
 | |
|         [Parameter(Mandatory = $true)]
 | |
|         [string]$ServerBaseUrl,
 | |
| 
 | |
|         [Parameter(Mandatory = $true)]
 | |
|         [string]$AgentId,
 | |
| 
 | |
|         [Parameter(Mandatory = $true)]
 | |
|         [string]$AgentHash,
 | |
| 
 | |
|         [string]$AgentGuid,
 | |
| 
 | |
|         [string]$AuthToken
 | |
|     )
 | |
| 
 | |
|     if ([string]::IsNullOrWhiteSpace($ServerBaseUrl) -or [string]::IsNullOrWhiteSpace($AgentHash)) {
 | |
|         return
 | |
|     }
 | |
| 
 | |
|     $base = $ServerBaseUrl.TrimEnd('/')
 | |
|     $uri = "$base/api/agent/hash"
 | |
|     $payloadBody = @{ agent_hash = $AgentHash }
 | |
|     if (-not [string]::IsNullOrWhiteSpace($AgentId)) { $payloadBody.agent_id = $AgentId }
 | |
|     if (-not [string]::IsNullOrWhiteSpace($AgentGuid)) { $payloadBody.agent_guid = $AgentGuid }
 | |
|     $payload = $payloadBody | ConvertTo-Json -Depth 3
 | |
|     $headers = @{ 'User-Agent' = 'borealis-agent-updater' }
 | |
|     if ($AuthToken -and $AuthToken.Trim()) {
 | |
|         $headers['Authorization'] = "Bearer $AuthToken"
 | |
|     }
 | |
| 
 | |
|     $resp = Invoke-WebRequest -Uri $uri -Method Post -Headers $headers -Body $payload -ContentType 'application/json' -UseBasicParsing -ErrorAction Stop
 | |
|     try {
 | |
|         $json = $resp.Content | ConvertFrom-Json
 | |
|         return $json
 | |
|     } catch {
 | |
|         return $null
 | |
|     }
 | |
| }
 | |
| 
 | |
| function Sync-AgentHashRecord {
 | |
|     param(
 | |
|         [string]$ProjectRoot,
 | |
|         [string]$AgentRoot,
 | |
|         [string]$AgentHash,
 | |
|         [string]$ServerBaseUrl,
 | |
|         [string]$AgentId,
 | |
|         [string]$AgentGuid,
 | |
|         [string]$AuthToken = '',
 | |
|         [string]$BranchName = 'main'
 | |
|     )
 | |
| 
 | |
|     if ([string]::IsNullOrWhiteSpace($AgentHash)) { return }
 | |
| 
 | |
|     if ($ProjectRoot) {
 | |
|         Set-GitFetchHeadHash -ProjectRoot $ProjectRoot -CommitHash $AgentHash -BranchName $BranchName
 | |
|     }
 | |
|     if ($AgentRoot) {
 | |
|         Set-StoredAgentHash -AgentRoot $AgentRoot -AgentHash $AgentHash
 | |
|     }
 | |
| 
 | |
|     if ([string]::IsNullOrWhiteSpace($ServerBaseUrl)) { return }
 | |
| 
 | |
|     Write-Host ("Submitting agent hash to server: {0}" -f $AgentHash)
 | |
| 
 | |
|     if ([string]::IsNullOrWhiteSpace($AgentId) -and [string]::IsNullOrWhiteSpace($AgentGuid)) {
 | |
|         Write-Host "Agent identifier unavailable; skipping agent hash submission." -ForegroundColor DarkYellow
 | |
|         return
 | |
|     }
 | |
| 
 | |
|     try {
 | |
|         $submitResult = Submit-AgentHash -ServerBaseUrl $ServerBaseUrl -AgentId $AgentId -AgentHash $AgentHash -AgentGuid $AgentGuid -AuthToken $AuthToken
 | |
|         if ($submitResult -and ($submitResult.status -eq 'ok')) {
 | |
|             Write-Host "The server-side agent hash database record was updated successfully."
 | |
|         } elseif ($submitResult -and ($submitResult.status -eq 'ignored')) {
 | |
|             Write-Host "Server ignored the agent hash update (the agent is not enrolled with the server)." -ForegroundColor DarkYellow
 | |
|         } elseif ($submitResult) {
 | |
|             Write-Host "Server agent_hash update response unrecognized.  We don't know what to do here. (Panic)" -ForegroundColor DarkYellow
 | |
|         }
 | |
|     } catch {
 | |
|         Write-Verbose ("Failed to Submit Agent Hash: {0}" -f $_.Exception.Message)
 | |
|     }
 | |
| }
 | |
| 
 | |
| function Invoke-BorealisUpdate {
 | |
|     param(
 | |
|         [Parameter(Mandatory = $true)]
 | |
|         [string]$GitExe,
 | |
| 
 | |
|         [Parameter(Mandatory = $true)]
 | |
|         [string]$RepositoryUrl,
 | |
| 
 | |
|         [Parameter(Mandatory = $true)]
 | |
|         [string]$TargetHash,
 | |
| 
 | |
|         [string]$BranchName = 'main',
 | |
| 
 | |
|         [switch]$Silent
 | |
|     )
 | |
| 
 | |
|     if ([string]::IsNullOrWhiteSpace($TargetHash)) {
 | |
|         throw 'Target commit hash is required for Borealis update.'
 | |
|     }
 | |
| 
 | |
|     $preservePath = Join-Path $scriptDir "Data\Server\Python_API_Endpoints\Tesseract-OCR"
 | |
|     $preserveBackupPath = Join-Path $scriptDir "Update_Staging\Tesseract-OCR"
 | |
|     $ansibleEePath = Join-Path $scriptDir "Agent\Ansible_EE"
 | |
|     $ansibleEeBackupPath = Join-Path $scriptDir "Update_Staging\Ansible_EE"
 | |
| 
 | |
|     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: Preserve Ansible Execution Environment" {
 | |
|         if (Test-Path $ansibleEePath) {
 | |
|             $stagingPath = Join-Path $scriptDir "Update_Staging"
 | |
|             if (-not (Test-Path $stagingPath)) { New-Item -ItemType Directory -Force -Path $stagingPath | Out-Null }
 | |
|             if (Test-Path $ansibleEeBackupPath) {
 | |
|                 Remove-Item -Path $ansibleEeBackupPath -Recurse -Force -ErrorAction SilentlyContinue
 | |
|             }
 | |
|             Move-Item -Path $ansibleEePath -Destination $ansibleEeBackupPath -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"), `
 | |
|             (Join-Path $scriptDir '.git')
 | |
|     }
 | |
| 
 | |
|     $stagingPath = Join-Path $scriptDir "Update_Staging"
 | |
|     $cloneDir = Join-Path $stagingPath 'repo'
 | |
| 
 | |
|     Run-Step "Updating: Create Update Staging Folder" {
 | |
|         if (-not (Test-Path $stagingPath)) { New-Item -ItemType Directory -Force -Path $stagingPath | Out-Null }
 | |
|         if (Test-Path $cloneDir) {
 | |
|             Remove-Item -Recurse -Force -ErrorAction SilentlyContinue $cloneDir
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     Run-Step "Updating: Clone Repository Source" {
 | |
|         $cloneArgs = @('clone','--no-tags','--depth','1')
 | |
|         if (-not [string]::IsNullOrWhiteSpace($BranchName)) {
 | |
|             $cloneArgs += @('--branch', $BranchName)
 | |
|         }
 | |
|         $cloneArgs += @($RepositoryUrl, $cloneDir)
 | |
|         Invoke-GitCommand -GitExe $GitExe -WorkingDirectory $stagingPath -Arguments $cloneArgs | Out-Null
 | |
|     }
 | |
| 
 | |
|     Run-Step "Updating: Checkout Target Revision" {
 | |
|         $normalizedHash = $TargetHash.Trim()
 | |
|         $haveHash = $false
 | |
|         try {
 | |
|             Invoke-GitCommand -GitExe $GitExe -WorkingDirectory $cloneDir -Arguments @('rev-parse', $normalizedHash) | Out-Null
 | |
|             $haveHash = $true
 | |
|         } catch {
 | |
|             $haveHash = $false
 | |
|         }
 | |
| 
 | |
|         if (-not $haveHash) {
 | |
|             Invoke-GitCommand -GitExe $GitExe -WorkingDirectory $cloneDir -Arguments @('fetch','origin',$normalizedHash) | Out-Null
 | |
|         }
 | |
| 
 | |
|         if ([string]::IsNullOrWhiteSpace($BranchName)) {
 | |
|             Invoke-GitCommand -GitExe $GitExe -WorkingDirectory $cloneDir -Arguments @('checkout', $normalizedHash) | Out-Null
 | |
|         } else {
 | |
|             Invoke-GitCommand -GitExe $GitExe -WorkingDirectory $cloneDir -Arguments @('checkout','-B',$BranchName,$normalizedHash) | Out-Null
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     Run-Step "Updating: Copy Update Files into Production Borealis Root Folder" {
 | |
|         Get-ChildItem -Path $cloneDir -Force | ForEach-Object {
 | |
|             $destination = Join-Path $scriptDir $_.Name
 | |
|             if ($_.PSIsContainer) {
 | |
|                 Copy-Item -Path $_.FullName -Destination $destination -Recurse -Force
 | |
|             } else {
 | |
|                 Copy-Item -Path $_.FullName -Destination $scriptDir -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: Restore Ansible Execution Environment" {
 | |
|         $restorePath = Join-Path $scriptDir "Agent"
 | |
|         if (Test-Path $ansibleEeBackupPath) {
 | |
|             if (-not (Test-Path $restorePath)) { New-Item -ItemType Directory -Force -Path $restorePath | Out-Null }
 | |
|             Move-Item -Path $ansibleEeBackupPath -Destination $restorePath -Force
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     Run-Step "Updating: Clean Up Update Staging Folder" {
 | |
|         Remove-Item -Recurse -Force -ErrorAction SilentlyContinue $stagingPath
 | |
|     }
 | |
| 
 | |
|     if (-not $Silent) {
 | |
|         Write-Host "Unattended Borealis update completed." -ForegroundColor Green
 | |
|     }
 | |
| }
 | |
| 
 | |
| function Invoke-BorealisAgentUpdate {
 | |
|     Write-Host "==============================================="
 | |
|     Write-Host " Borealis - Automation Platform Updater Script "
 | |
|     Write-Host "==============================================="
 | |
| 
 | |
|     $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
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     $agentGuid = Get-AgentGuid -AgentRoot $agentRoot
 | |
|     if ($agentGuid) {
 | |
|         Write-Host ("Agent GUID: {0}" -f $agentGuid)
 | |
|     } else {
 | |
|         Write-Host "Warning: No agent GUID detected - Please deploy the agent, associating it with a Borealis server then try running the updater script again." -ForegroundColor Yellow
 | |
|         Write-Host "⚠️ Borealis update aborted."
 | |
|         return
 | |
|     }
 | |
| 
 | |
|     $gitExe = Get-GitExecutablePath -ProjectRoot $scriptDir
 | |
|     $currentHash = Get-RepositoryCommitHash -ProjectRoot $scriptDir -AgentRoot $agentRoot -GitExe $gitExe
 | |
|     $serverBaseUrl = Get-BorealisServerUrl -AgentRoot $agentRoot
 | |
|     $agentId = Get-AgentServiceId -AgentRoot $agentRoot
 | |
| 
 | |
|     $authContext = Get-AgentAccessTokenContext -AgentRoot $agentRoot -ServerBaseUrl $serverBaseUrl -AgentGuid $agentGuid
 | |
|     if (-not $authContext -or -not $authContext.AccessToken) {
 | |
|         Write-Host "Unable to obtain agent authentication token. Ensure the agent is running and enrolled, then rerun the updater." -ForegroundColor Yellow
 | |
|         Write-Host "⚠️ Borealis update aborted."
 | |
|         return
 | |
|     }
 | |
|     $authToken = $authContext.AccessToken
 | |
| 
 | |
|     $serverRepoInfo = Get-ServerCurrentRepoHash -ServerBaseUrl $serverBaseUrl -AuthToken $authToken
 | |
|     $serverHash = ''
 | |
|     $serverBranch = 'main'
 | |
|     if ($serverRepoInfo) {
 | |
|         try { $serverHash = (($serverRepoInfo.sha) -as [string]).Trim() } catch { $serverHash = '' }
 | |
|         try {
 | |
|             $branchCandidate = (($serverRepoInfo.branch) -as [string]).Trim()
 | |
|             if ($branchCandidate) { $serverBranch = $branchCandidate }
 | |
|         } catch { $serverBranch = 'main' }
 | |
|     }
 | |
| 
 | |
|     $updateMode = $env:update_mode
 | |
|     if ($updateMode) { $updateMode = $updateMode.ToLowerInvariant() } else { $updateMode = 'update' }
 | |
|     $forceUpdate = $updateMode -eq 'force_update'
 | |
| 
 | |
|     if ($currentHash) {
 | |
|         Write-Host ("Local Agent Hash: {0}" -f $currentHash)
 | |
|     } else {
 | |
|         Write-Host "Local Agent Hash: unavailable"
 | |
|     }
 | |
| 
 | |
|     if ($serverHash) {
 | |
|         Write-Host ("Borealis Server Hash: {0}" -f $serverHash)
 | |
|     } else {
 | |
|         Write-Host "Borealis Server Hash: unavailable"
 | |
|     }
 | |
| 
 | |
|     $normalizedLocalHash = if ($currentHash) { $currentHash.Trim().ToLowerInvariant() } else { '' }
 | |
|     $normalizedServerHash = if ($serverHash) { $serverHash.Trim().ToLowerInvariant() } else { '' }
 | |
|     $hashesMatch = ($normalizedLocalHash -and $normalizedServerHash -and ($normalizedLocalHash -eq $normalizedServerHash))
 | |
|     $needsUpdate = $forceUpdate -or (-not $hashesMatch)
 | |
| 
 | |
|     if ($forceUpdate) {
 | |
|         Write-Host "Force update requested; skipping hash comparison." -ForegroundColor Yellow
 | |
|     } elseif (-not $serverHash) {
 | |
|         Write-Host "Borealis server hash unavailable; cannot continue." -ForegroundColor Yellow
 | |
|         Write-Host "⚠️ Borealis update aborted."
 | |
|         return
 | |
|     } elseif (-not $needsUpdate) {
 | |
|         Write-Host "Local agent files already match the server repository hash." -ForegroundColor Green
 | |
|         Sync-AgentHashRecord -ProjectRoot $scriptDir -AgentRoot $agentRoot -AgentHash $serverHash -ServerBaseUrl $serverBaseUrl -AgentId $agentId -AgentGuid $agentGuid -AuthToken $authToken -BranchName $serverBranch
 | |
|         Write-Host "✅ Borealis - Automation Platform Already Up-to-Date"
 | |
|         return
 | |
|     } else {
 | |
|         Write-Host "Repository hash mismatch detected; update required."
 | |
|     }
 | |
| 
 | |
|     if (-not ($gitExe) -or -not (Test-Path $gitExe -PathType Leaf)) {
 | |
|         Write-Host "Bundled Git dependency not found. Run '.\\Borealis.ps1 -Agent -AgentAction repair' to bootstrap dependencies and try again." -ForegroundColor Yellow
 | |
|         Write-Host "⚠️ Borealis update aborted."
 | |
|         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 "⚠️ Borealis update already in progress on this device."
 | |
|             return
 | |
|         }
 | |
| 
 | |
|         $staging = Join-Path $scriptDir 'Update_Staging'
 | |
| 
 | |
|         $managedTasks = Stop-AgentScheduledTasks -TaskNames @('Borealis Agent','Borealis Agent (UserHelper)')
 | |
|         Run-Step "Updating: Terminate Running Python Processes" { Stop-AgentPythonProcesses }
 | |
| 
 | |
|         $updateSucceeded = $false
 | |
|         try {
 | |
|             Invoke-BorealisUpdate -GitExe $gitExe -RepositoryUrl $repositoryUrl -TargetHash $serverHash -BranchName $serverBranch -Silent
 | |
|             $updateSucceeded = $true
 | |
|         } finally {
 | |
|             if ($managedTasks.Count -gt 0) {
 | |
|                 Start-AgentScheduledTasks -TaskNames $managedTasks
 | |
|             }
 | |
|         }
 | |
| 
 | |
|         if (-not $updateSucceeded) {
 | |
|             throw 'Borealis update failed.'
 | |
|         }
 | |
| 
 | |
|         $refreshedContext = Get-AgentAccessTokenContext -AgentRoot $agentRoot -ServerBaseUrl $serverBaseUrl -AgentGuid $agentGuid
 | |
|         if ($refreshedContext -and $refreshedContext.AccessToken) {
 | |
|             $authToken = $refreshedContext.AccessToken
 | |
|         }
 | |
|         $postUpdateInfo = Get-ServerCurrentRepoHash -ServerBaseUrl $serverBaseUrl -AuthToken $authToken
 | |
|         if ($postUpdateInfo) {
 | |
|             try {
 | |
|                 $refreshedSha = (($postUpdateInfo.sha) -as [string]).Trim()
 | |
|                 if ($refreshedSha) { $serverHash = $refreshedSha }
 | |
|             } catch {}
 | |
|             try {
 | |
|                 $branchCandidate = (($postUpdateInfo.branch) -as [string]).Trim()
 | |
|                 if ($branchCandidate) { $serverBranch = $branchCandidate }
 | |
|             } catch {}
 | |
|         }
 | |
| 
 | |
|         $newHash = Get-RepositoryCommitHash -ProjectRoot $scriptDir -AgentRoot $agentRoot -GitExe $gitExe
 | |
| 
 | |
|         $normalizedNewHash = if ($newHash) { $newHash.Trim().ToLowerInvariant() } else { '' }
 | |
|         $normalizedServerHash = if ($serverHash) { $serverHash.Trim().ToLowerInvariant() } else { '' }
 | |
| 
 | |
|         if ($normalizedServerHash -and (-not $normalizedNewHash -or $normalizedNewHash -ne $normalizedServerHash)) {
 | |
|             $newHash = $serverHash
 | |
|             $normalizedNewHash = $normalizedServerHash
 | |
|         } elseif (-not $newHash -and $serverHash) {
 | |
|             $newHash = $serverHash
 | |
|         }
 | |
| 
 | |
|         if ($newHash) {
 | |
|             Sync-AgentHashRecord -ProjectRoot $scriptDir -AgentRoot $agentRoot -AgentHash $newHash -ServerBaseUrl $serverBaseUrl -AgentId $agentId -AgentGuid $agentGuid -AuthToken $authToken -BranchName $serverBranch
 | |
|         } else {
 | |
|             Write-Host "Unable to determine repository hash for submission; server hash not updated." -ForegroundColor DarkYellow
 | |
|         }
 | |
| 
 | |
|         Write-Host "✅ Borealis - Automation Platform Successfully Updated"
 | |
|     } finally {
 | |
|         if ($mutex -and $gotMutex) { $mutex.ReleaseMutex() | Out-Null }
 | |
|         if ($mutex) { $mutex.Dispose() }
 | |
|     }
 | |
| }
 | |
| 
 | |
| Invoke-BorealisAgentUpdate
 |