Files
Borealis-Github-Replica/Borealis.ps1

1024 lines
46 KiB
PowerShell

#////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: <ProjectRoot>/Borealis.ps1
[CmdletBinding()]
param(
[switch]$Server,
[switch]$Agent,
[ValidateSet('install','repair','remove','launch','')]
[string]$AgentAction = '',
[switch]$Vite,
[switch]$Flask,
[switch]$Quick
)
# Preselect menu choices from CLI args (optional)
$choice = $null
$modeChoice = $null
$agentSubChoice = $null
if ($Server -and $Agent) {
Write-Host "Cannot use -Server and -Agent together." -ForegroundColor Red
exit 1
}
if ($Vite -and $Flask) {
Write-Host "Cannot combine -Vite and -Flask." -ForegroundColor Red
exit 1
}
if ($Server) {
# Auto-select main menu option for Server when -Server flag is provided
$choice = '1'
} elseif ($Agent) {
$choice = '2'
switch ($AgentAction) {
'install' { $agentSubChoice = '1' }
'repair' { $agentSubChoice = '2' }
'remove' { $agentSubChoice = '3' }
'launch' { $agentSubChoice = '4' }
default { $agentSubChoice = '1' }
}
}
if ($Server) {
if ($Vite) { $modeChoice = '3' }
elseif ($Flask -and $Quick){ $modeChoice = '2' }
elseif ($Flask) { $modeChoice = '1' }
}
$host.UI.RawUI.WindowTitle = "Borealis"
Clear-Host
## Note: Heavy dependency downloads are deferred until selecting Server (option 1)
# ---------------------- ASCII Art Terminal Required Changes ----------------------
# Set the .NET Console output encoding to UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# Change the Windows OEM code page to 65001 (UTF-8)
chcp.com 65001 > $null
# ---------------------- Add Common Functions Used Throughout Script ----------------------
$symbols = @{
Success = [char]0x2705
Running = [char]0x23F3
Fail = [char]0x274C
Info = [char]0x2139
}
# Admin/Elevation helpers for Agent deployment
function Test-IsAdmin {
try {
$id = [Security.Principal.WindowsIdentity]::GetCurrent()
$p = New-Object Security.Principal.WindowsPrincipal($id)
return $p.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
} catch { return $false }
}
function Request-AgentElevation {
param(
[string]$ScriptPath,
[string]$AgentActionParam,
[switch]$Auto
)
if (Test-IsAdmin) { return $true }
if (-not $Auto) {
Write-Host "" # spacer
Write-Host "Agent requires Administrator permissions to register scheduled tasks and run reliably." -ForegroundColor Yellow -BackgroundColor Black
Write-Host "Grant elevated permissions now? (Y/N)" -ForegroundColor Yellow -BackgroundColor Black
$resp = Read-Host
if ($resp -notin @('y','Y','yes','YES')) { return $false }
}
$args = @('-NoProfile','-ExecutionPolicy','Bypass','-File', '"' + $ScriptPath + '"', '-Agent')
if ($AgentActionParam -and $AgentActionParam.Trim()) {
$args += @('-AgentAction', $AgentActionParam)
}
try {
Start-Process -FilePath 'powershell.exe' -Verb RunAs -ArgumentList $args -WindowStyle Normal | Out-Null
return $false # stop current non-elevated instance
} catch {
Write-Host "Elevation was denied or failed." -ForegroundColor Red
return $false
}
}
# Ensure log directories
function Ensure-AgentLogDir {
$logRoot = Join-Path $scriptDir 'Logs'
$agentLogDir = Join-Path $logRoot 'Agent'
if (-not (Test-Path $agentLogDir)) { New-Item -ItemType Directory -Path $agentLogDir -Force | Out-Null }
return $agentLogDir
}
function Write-AgentLog {
param(
[string]$FileName,
[string]$Message
)
$dir = Ensure-AgentLogDir
$path = Join-Path $dir $FileName
$ts = Get-Date -Format s
"[$ts] $Message" | Out-File -FilePath $path -Append -Encoding UTF8
}
$script:Utf8CodePageChanged = $false
function Ensure-SystemUtf8CodePage {
param([string]$LogName = 'Install.log')
$codePageKey = 'HKLM:\SYSTEM\CurrentControlSet\Control\Nls\CodePage'
$target = '65001'
try {
$props = Get-ItemProperty -Path $codePageKey -ErrorAction Stop
$currentAcp = ($props.ACP | ForEach-Object { $_.ToString() })
$currentOem = ($props.OEMCP | ForEach-Object { $_.ToString() })
Write-AgentLog -FileName $LogName -Message ("[UTF8] Detected ACP={0} OEMCP={1}" -f $currentAcp,$currentOem)
} catch {
Write-AgentLog -FileName $LogName -Message ("[UTF8] Failed to read code page info: {0}" -f $_.Exception.Message)
return
}
if ($currentAcp -eq $target -and $currentOem -eq $target) {
Write-AgentLog -FileName $LogName -Message '[UTF8] System code pages already set to 65001 (UTF-8).'
return
}
Write-AgentLog -FileName $LogName -Message '[UTF8] Updating system code pages to UTF-8 (65001). Requires reboot to finalize.'
try {
Set-ItemProperty -Path $codePageKey -Name 'ACP' -Value $target -Force
Set-ItemProperty -Path $codePageKey -Name 'OEMCP' -Value $target -Force
try { Set-ItemProperty -Path $codePageKey -Name 'MACCP' -Value $target -Force } catch {}
$script:Utf8CodePageChanged = $true
Write-AgentLog -FileName $LogName -Message '[UTF8] Code page registry values updated successfully.'
} catch {
Write-AgentLog -FileName $LogName -Message ("[UTF8] Failed to update code pages: {0}" -f $_.Exception.Message)
}
}
# Forcefully remove legacy and current Borealis services and tasks
function Remove-BorealisServicesAndTasks {
param([string]$LogName)
$svcNames = @('BorealisAgent','BorealisScriptService','BorealisScriptAgent')
foreach ($n in $svcNames) {
Write-AgentLog -FileName $LogName -Message "Attempting to stop service: $n"
try { sc.exe stop $n 2>$null | Out-Null } catch {}
Start-Sleep -Milliseconds 300
Write-AgentLog -FileName $LogName -Message "Attempting to delete service: $n"
try { sc.exe delete $n 2>$null | Out-Null } catch {}
}
# Remove all Borealis scheduled tasks (supervisor/watchdog/legacy/user helper)
try {
$tasks = @()
try { $tasks = Get-ScheduledTask -ErrorAction SilentlyContinue | Where-Object { $_.TaskName -like 'Borealis Agent*' -or $_.TaskName -like 'Borealis*Supervisor*' -or $_.TaskName -like 'Borealis*Watchdog*' } } catch {}
foreach ($t in $tasks) {
Write-AgentLog -FileName $LogName -Message ("Deleting scheduled task: {0}" -f $t.TaskName)
try { Unregister-ScheduledTask -TaskName $t.TaskName -Confirm:$false -ErrorAction SilentlyContinue } catch {}
}
# Fallback to schtasks for machines without the ScheduledTasks module
foreach ($tn in @('Borealis Agent','Borealis Agent (UserHelper)','Borealis Agent - Supervisor','Borealis Agent - Watchdog')) {
try { schtasks.exe /Delete /TN "$tn" /F 2>$null | Out-Null } catch {}
}
} catch {}
# Gracefully stop only Agent venv Python processes (avoid killing dev web UI/node)
Write-Host "Stopping Agent Python processes scoped to Agent venv..." -ForegroundColor Yellow
Write-AgentLog -FileName $LogName -Message "Stopping Agent Python processes in Agent\\*"
try {
Get-Process python,pythonw -ErrorAction SilentlyContinue |
Where-Object { $_.Path -like (Join-Path $scriptDir 'Agent\*') } |
ForEach-Object { try { $_ | Stop-Process -Force } catch {} }
} catch {}
# Remove legacy watchdog script if present
try { Remove-Item -Force -ErrorAction SilentlyContinue (Join-Path $env:ProgramData 'Borealis\watchdog.ps1') } catch {}
}
# Repair routine: cleans services, ensures venv files, reinstalls and starts BorealisAgent
function Repair-BorealisAgent {
$logName = 'Repair.log'
Write-AgentLog -FileName $logName -Message "=== Repair start ==="
Remove-BorealisServicesAndTasks -LogName $logName
InstallOrUpdate-BorealisAgent
Write-AgentLog -FileName $logName -Message "=== Repair end ==="
}
function Remove-BorealisAgent {
$logName = 'Removal.log'
Write-AgentLog -FileName $logName -Message "=== Removal start ==="
Remove-BorealisServicesAndTasks -LogName $logName
# Kill stray helpers
Write-AgentLog -FileName $logName -Message "Terminating stray helper processes"
Get-Process python,pythonw -ErrorAction SilentlyContinue | Where-Object { $_.Path -like (Join-Path $scriptDir 'Agent\*') } | ForEach-Object {
try { $_ | Stop-Process -Force } catch {}
}
# Remove venv folder
$venvFolder = Join-Path $scriptDir 'Agent'
Write-AgentLog -FileName $logName -Message "Removing folder: $venvFolder"
try { Remove-Item $venvFolder -Recurse -Force -ErrorAction SilentlyContinue } catch {}
Write-AgentLog -FileName $logName -Message "=== Removal end ==="
}
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
}
}
# ---------------------- Server Deployment / Operation Mode Variables ----------------------
# Define the default operation mode: production | developer
[string]$borealis_operation_mode = 'production'
# ---------------------- Bundle Executables Setup ----------------------
$scriptDir = Split-Path $MyInvocation.MyCommand.Path -Parent
$depsRoot = Join-Path $scriptDir 'Dependencies'
$pythonExe = Join-Path $depsRoot 'Python\python.exe'
$nodeExe = Join-Path $depsRoot 'NodeJS\node.exe'
$sevenZipExe = Join-Path $depsRoot "7zip\7z.exe"
$npmCmd = Join-Path (Split-Path $nodeExe) 'npm.cmd'
$npxCmd = Join-Path (Split-Path $nodeExe) 'npx.cmd'
$ansibleEeRequirementsPath = Join-Path $scriptDir 'Data\Agent\ansible-ee-requirements.txt'
$ansibleEeVersionFile = Join-Path $scriptDir 'Data\Agent\ansible-ee-version.txt'
$script:AnsibleExecutionEnvironmentVersion = '1.0.0'
if (Test-Path $ansibleEeVersionFile -PathType Leaf) {
try {
$rawVersion = (Get-Content -Path $ansibleEeVersionFile -Raw -ErrorAction Stop)
if ($rawVersion) {
$script:AnsibleExecutionEnvironmentVersion = ($rawVersion.Split("`n")[0]).Trim()
}
} catch {
# Leave default version value
}
}
$node7zUrl = "https://nodejs.org/dist/v23.11.0/node-v23.11.0-win-x64.7z"
$nodeInstallDir = Join-Path $depsRoot "NodeJS"
$node7zPath = Join-Path $depsRoot "node-v23.11.0-win-x64.7z"
$gitVersionTag = 'v2.47.1.windows.1'
$gitPackageName = 'MinGit-2.47.1-64-bit.zip'
$gitZipUrl = "https://github.com/git-for-windows/git/releases/download/$gitVersionTag/$gitPackageName"
$gitZipPath = Join-Path $depsRoot $gitPackageName
$gitInstallDir = Join-Path $depsRoot 'git'
$gitExePath = Join-Path $gitInstallDir 'cmd\git.exe'
# ---------------------- Dependency Installation Functions ----------------------
function Install_Shared_Dependencies {
# Python (shared by Server and Agent)
Run-Step "Dependency: Python" {
$pythonInstallDir = Join-Path $scriptDir "Dependencies\Python"
$localPythonExe = Join-Path $pythonInstallDir "python.exe"
$pythonMsiBaseUrl = "https://www.python.org/ftp/python/3.13.3/amd64/"
$pythonMsiFiles = @(
"core.msi",
"exe.msi",
"lib.msi",
"pip.msi",
"dev.msi"
)
if (-not (Test-Path $localPythonExe)) {
if (-not (Test-Path $pythonInstallDir)) {
New-Item -ItemType Directory -Path $pythonInstallDir | Out-Null
}
foreach ($file in $pythonMsiFiles) {
$url = "$pythonMsiBaseUrl$file"
$localPath = Join-Path $scriptDir "Dependencies\$file"
# Download if missing
if (-not (Test-Path $localPath)) {
Invoke-WebRequest -Uri $url -OutFile $localPath
}
# Extract MSI into install directory
Start-Process -Wait -NoNewWindow -FilePath "msiexec.exe" `
-ArgumentList "/a `"$localPath`" /qn TARGETDIR=`"$pythonInstallDir`""
}
# Clean up downloaded MSIs
foreach ($file in $pythonMsiFiles) {
$localPath = Join-Path $scriptDir "Dependencies\$file"
Remove-Item $localPath -Force -ErrorAction SilentlyContinue
}
# Validate success
if (-not (Test-Path $localPythonExe)) {
throw "Python executable not found after MSI extraction."
}
}
}
}
function Install_Server_Dependencies {
# Tesseract OCR Engine
Run-Step "Dependency: Tesseract-OCR" {
$tessExeUrl = "https://github.com/tesseract-ocr/tesseract/releases/download/5.5.0/tesseract-ocr-w64-setup-5.5.0.20241111.exe"
$tessExePath = Join-Path $depsRoot "tesseract-installer.exe"
$tessInstallDir = Join-Path $scriptDir "Data\Server\Python_API_Endpoints\Tesseract-OCR"
if (-not (Test-Path (Join-Path $tessInstallDir "tesseract.exe"))) {
# Download the installer if it doesn't exist
if (-not (Test-Path $tessExePath)) {
Invoke-WebRequest -Uri $tessExeUrl -OutFile $tessExePath
}
# Extract using 7-Zip
if (-not (Test-Path $sevenZipExe)) {
throw "7-Zip CLI not found at: $sevenZipExe"
}
if (Test-Path $tessInstallDir) {
Remove-Item $tessInstallDir -Recurse -Force -ErrorAction SilentlyContinue
}
New-Item -ItemType Directory -Path $tessInstallDir | Out-Null
& $sevenZipExe x $tessExePath "-o$tessInstallDir" -y | Out-Null
# Optional cleanup
Remove-Item $tessExePath -Force -ErrorAction SilentlyContinue
}
}
# Tesseract Language Data
Run-Step "Dependency: Tesseract-OCR - Pre-Trained Model Data" {
$langDataDir = Join-Path $scriptDir "Data\Server\Python_API_Endpoints\Tesseract-OCR\tessdata"
$engPath = Join-Path $langDataDir "eng.traineddata"
$osdPath = Join-Path $langDataDir "osd.traineddata"
if (-not (Test-Path $engPath)) {
Invoke-WebRequest -Uri "https://github.com/tesseract-ocr/tessdata/raw/main/eng.traineddata" -OutFile $engPath
}
if (-not (Test-Path $osdPath)) {
Invoke-WebRequest -Uri "https://github.com/tesseract-ocr/tessdata/raw/main/osd.traineddata" -OutFile $osdPath
}
}
# NodeJS (required for Vite / Web UI)
Run-Step "Dependency: NodeJS" {
if (-not (Test-Path $nodeExe)) {
# Download archive if not present
if (-not (Test-Path $node7zPath)) {
Invoke-WebRequest -Uri $node7zUrl -OutFile $node7zPath
}
# Extract using bundled 7z
if (-not (Test-Path $sevenZipExe)) {
throw "7-Zip CLI not found at: $sevenZipExe"
}
& $sevenZipExe x $node7zPath "-o$nodeInstallDir" -y | Out-Null
# The extracted contents might live under a subfolder; flatten if needed
$extracted = Get-ChildItem $nodeInstallDir | Where-Object { $_.PSIsContainer } | Select-Object -First 1
if ($extracted) {
Get-ChildItem $extracted.FullName | Move-Item -Destination $nodeInstallDir -Force
Remove-Item $extracted.FullName -Recurse -Force
}
# Clean Up 7z File After Extraction
Remove-Item -Recurse -Force -ErrorAction SilentlyContinue $node7zPath
}
}
}
function Install_Agent_Dependencies {
# AutoHotKey portable
Run-Step "Dependency: AutoHotKey" {
$ahkVersion = "2.0.19"
$ahkVersionTag = "v$ahkVersion"
$ahkZipName = "AutoHotkey_$ahkVersion.zip"
$ahkZipUrl = "https://github.com/AutoHotkey/AutoHotkey/releases/download/$ahkVersionTag/$ahkZipName"
$ahkZipPath = Join-Path $depsRoot $ahkZipName
$ahkInstallDir = Join-Path $depsRoot "AutoHotKey"
$ahkExePath = Join-Path $ahkInstallDir "AutoHotkey64.exe"
if (-not (Test-Path $ahkExePath)) {
if (-not (Test-Path $ahkZipPath)) {
Invoke-WebRequest -Uri $ahkZipUrl -OutFile $ahkZipPath
}
if (-not (Test-Path $sevenZipExe)) {
throw "7-Zip CLI not found at: $sevenZipExe"
}
if (Test-Path $ahkInstallDir) {
Remove-Item $ahkInstallDir -Recurse -Force -ErrorAction SilentlyContinue
}
New-Item -ItemType Directory -Path $ahkInstallDir | Out-Null
& $sevenZipExe x $ahkZipPath "-o$ahkInstallDir" -y | Out-Null
Remove-Item $ahkZipPath -Force -ErrorAction SilentlyContinue
if (-not (Test-Path $ahkExePath)) {
throw "AutoHotKey executable not found after extraction."
}
}
}
# Portable Git client for agent updates
Run-Step "Dependency: Git CLI" {
if (-not (Test-Path $gitExePath)) {
if (-not (Test-Path $gitZipPath)) {
Invoke-WebRequest -Uri $gitZipUrl -OutFile $gitZipPath
}
if (-not (Test-Path $sevenZipExe)) {
throw "7-Zip CLI not found at: $sevenZipExe"
}
if (Test-Path $gitInstallDir) {
Remove-Item $gitInstallDir -Recurse -Force -ErrorAction SilentlyContinue
}
New-Item -ItemType Directory -Path $gitInstallDir | Out-Null
& $sevenZipExe x $gitZipPath "-o$gitInstallDir" -y | Out-Null
Remove-Item $gitZipPath -Force -ErrorAction SilentlyContinue
if (-not (Test-Path $gitExePath)) {
throw "Git executable not found after extraction."
}
}
}
}
function Ensure-AnsibleExecutionEnvironment {
param(
[Parameter(Mandatory = $true)]
[string]$ProjectRoot,
[string]$PythonBootstrapExe,
[string]$RequirementsPath,
[string]$ExpectedVersion = '1.0.0',
[string]$LogName = 'Install.log'
)
$pythonBootstrap = $PythonBootstrapExe
$bundleCandidate = Join-Path $ProjectRoot 'Dependencies\Python\python.exe'
if ([string]::IsNullOrWhiteSpace($pythonBootstrap)) {
$pythonBootstrap = $bundleCandidate
}
if (-not (Test-Path $pythonBootstrap -PathType Leaf)) {
if ((-not [string]::IsNullOrWhiteSpace($PythonBootstrapExe)) -and ($PythonBootstrapExe -ne $pythonBootstrap)) {
Write-AgentLog -FileName $LogName -Message "[AnsibleEE] Provided Python bootstrap path '$PythonBootstrapExe' was not found."
}
if (Test-Path $bundleCandidate -PathType Leaf) {
$pythonBootstrap = $bundleCandidate
} else {
Write-AgentLog -FileName $LogName -Message "[AnsibleEE] Unable to locate bundled Python bootstrap executable at $bundleCandidate."
throw "Bundled Python executable not found for Ansible execution environment provisioning."
}
}
Write-AgentLog -FileName $LogName -Message "[AnsibleEE] Using Python bootstrap at $pythonBootstrap"
$eeRoot = Join-Path $ProjectRoot 'Agent\Ansible_EE'
$metadataPath = Join-Path $eeRoot 'metadata.json'
$versionTxtPath = Join-Path $eeRoot 'version.txt'
$requirementsHash = ''
if ($RequirementsPath -and (Test-Path $RequirementsPath -PathType Leaf)) {
try {
$requirementsHash = (Get-FileHash -Path $RequirementsPath -Algorithm SHA256).Hash
} catch {
$requirementsHash = ''
}
}
$currentVersion = ''
$currentHash = ''
if (Test-Path $metadataPath -PathType Leaf) {
try {
$metaRaw = Get-Content -Path $metadataPath -Raw -ErrorAction Stop
if ($metaRaw) {
$meta = $metaRaw | ConvertFrom-Json -ErrorAction Stop
if ($meta.version) {
$currentVersion = ($meta.version).ToString().Trim()
}
if ($meta.requirements_hash) {
$currentHash = ($meta.requirements_hash).ToString().Trim()
} elseif ($meta.requirements_sha256) {
$currentHash = ($meta.requirements_sha256).ToString().Trim()
}
}
} catch {
$currentVersion = ''
$currentHash = ''
}
}
$pythonCandidates = @(
Join-Path $eeRoot 'Scripts\python.exe',
Join-Path $eeRoot 'Scripts\python3.exe',
Join-Path $eeRoot 'bin\python3',
Join-Path $eeRoot 'bin\python'
)
$existingPython = $pythonCandidates | Where-Object { Test-Path $_ -PathType Leaf } | Select-Object -First 1
$expectedVersionNorm = $ExpectedVersion
if ([string]::IsNullOrWhiteSpace($expectedVersionNorm)) {
$expectedVersionNorm = '1.0.0'
}
$expectedVersionNorm = $expectedVersionNorm.Trim()
$isUpToDate = $false
if ($existingPython -and $currentVersion -and ($currentVersion -eq $expectedVersionNorm)) {
if (-not $requirementsHash -or ($currentHash -and $currentHash -eq $requirementsHash)) {
$isUpToDate = $true
}
}
if ($isUpToDate) {
Write-AgentLog -FileName $LogName -Message "[AnsibleEE] Existing execution environment is up-to-date (version $currentVersion)."
return
}
Write-AgentLog -FileName $LogName -Message "[AnsibleEE] Provisioning execution environment version $expectedVersionNorm."
if (Test-Path $eeRoot) {
try { Remove-Item -Path $eeRoot -Recurse -Force -ErrorAction Stop } catch {}
}
New-Item -ItemType Directory -Force -Path $eeRoot | Out-Null
& $pythonBootstrap -m venv $eeRoot | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-AgentLog -FileName $LogName -Message "[AnsibleEE] python -m venv failed with exit code $LASTEXITCODE"
throw "Failed to create Ansible execution environment virtual environment."
}
$pythonExe = $pythonCandidates | Where-Object { Test-Path $_ -PathType Leaf } | Select-Object -First 1
if (-not $pythonExe) {
Write-AgentLog -FileName $LogName -Message "[AnsibleEE] Unable to locate python executable inside execution environment."
throw "Ansible execution environment python executable missing after provisioning."
}
& $pythonExe -m pip install --upgrade pip setuptools wheel --disable-pip-version-check | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-AgentLog -FileName $LogName -Message "[AnsibleEE] pip bootstrap failed with exit code $LASTEXITCODE"
throw "Failed to bootstrap pip inside the Ansible execution environment."
}
if ($RequirementsPath -and (Test-Path $RequirementsPath -PathType Leaf)) {
& $pythonExe -m pip install --disable-pip-version-check -r $RequirementsPath | Out-Null
if ($LASTEXITCODE -ne 0) {
Write-AgentLog -FileName $LogName -Message "[AnsibleEE] pip install -r requirements failed with exit code $LASTEXITCODE"
throw "Failed to install Ansible execution environment requirements."
}
} else {
Write-AgentLog -FileName $LogName -Message "[AnsibleEE] Requirements file not found; skipping dependency installation."
}
$metadata = [ordered]@{
version = $expectedVersionNorm
created_utc = (Get-Date).ToUniversalTime().ToString('o')
python = $pythonExe
bootstrap_python = $pythonBootstrap
}
if ($requirementsHash) {
$metadata['requirements_hash'] = $requirementsHash
}
try {
$metadata | ConvertTo-Json -Depth 5 | Set-Content -Path $metadataPath -Encoding UTF8
} catch {
Write-AgentLog -FileName $LogName -Message "[AnsibleEE] Failed to persist metadata.json: $($_.Exception.Message)"
throw "Unable to persist Ansible execution environment metadata."
}
try {
Set-Content -Path $versionTxtPath -Value $expectedVersionNorm -Encoding UTF8
} catch {}
Write-AgentLog -FileName $LogName -Message "[AnsibleEE] Execution environment ready at $eeRoot"
}
function Ensure-AgentTasks {
param([string]$ScriptRoot)
$pyw = Join-Path $ScriptRoot 'Agent\Scripts\pythonw.exe'
$agentPy = Join-Path $ScriptRoot 'Agent\Borealis\agent.py'
$svcWrapper = Join-Path $ScriptRoot 'Agent\Borealis\launch_service.ps1'
if (-not (Test-Path $pyw)) { Write-Host "pythonw.exe not found under Agent\Scripts" -ForegroundColor Yellow; return }
if (-not (Test-Path $agentPy)) { Write-Host "Agent script not found under Agent\Borealis" -ForegroundColor Yellow; return }
if (-not (Test-Path $svcWrapper)) { Write-Host "launch_service.ps1 not found under Agent\Borealis" -ForegroundColor Yellow; return }
# Clean old tasks first
try { Unregister-ScheduledTask -TaskName 'Borealis Agent' -Confirm:$false -ErrorAction SilentlyContinue } catch {}
try { Unregister-ScheduledTask -TaskName 'Borealis Agent (UserHelper)' -Confirm:$false -ErrorAction SilentlyContinue } catch {}
# SYSTEM startup task
# Use a wrapper PowerShell to enforce WorkingDirectory and capture stdout/stderr
$sysArg = ('-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File "{0}"' -f $svcWrapper)
$sysAction = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument $sysArg -WorkingDirectory (Split-Path $svcWrapper -Parent)
$sysTrigger = New-ScheduledTaskTrigger -AtStartup
$sysSet = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -Hidden -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1) -ExecutionTimeLimit ([TimeSpan]::Zero)
$sysPrin = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest
Register-ScheduledTask -TaskName 'Borealis Agent' -Action $sysAction -Trigger $sysTrigger -Settings $sysSet -Principal $sysPrin -Force | Out-Null
try { Start-ScheduledTask -TaskName 'Borealis Agent' | Out-Null } catch {}
# Optional user-session helper for interactive roles (tray, overlays)
$helperName = 'Borealis Agent (UserHelper)'
$usrArg = ('"{0}" --config user' -f $agentPy)
$usrAction = New-ScheduledTaskAction -Execute $pyw -Argument $usrArg -WorkingDirectory (Split-Path $agentPy -Parent)
$usrTrig = New-ScheduledTaskTrigger -AtLogOn
$usrSet = New-ScheduledTaskSettingsSet -Hidden -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1) -ExecutionTimeLimit ([TimeSpan]::Zero)
$currentUser= [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
$usrPrin = New-ScheduledTaskPrincipal -UserId $currentUser -LogonType Interactive -RunLevel Limited
Register-ScheduledTask -TaskName $helperName -Action $usrAction -Trigger $usrTrig -Settings $usrSet -Principal $usrPrin -Force | Out-Null
try { Start-ScheduledTask -TaskName $helperName | Out-Null } catch {}
}
function InstallOrUpdate-BorealisAgent {
Write-Host "Ensuring Agent Dependencies Exist..." -ForegroundColor DarkCyan
Install_Shared_Dependencies
Install_Agent_Dependencies
if (-not (Test-Path $pythonExe)) {
Write-Host "`r$($symbols.Fail) Bundled Python not found at '$pythonExe'." -ForegroundColor Red
exit 1
}
$env:PATH = '{0};{1}' -f (Split-Path $pythonExe), $env:PATH
Write-Host "Cleaning previous agent tasks/processes..." -ForegroundColor Yellow
Remove-BorealisServicesAndTasks -LogName 'Install.log'
Ensure-SystemUtf8CodePage -LogName 'Install.log'
Write-Host "Deploying Borealis Agent..." -ForegroundColor Blue
# Resolve all paths relative to the script directory to avoid CWD issues
$venvFolderPath = Join-Path $scriptDir 'Agent'
$agentSourceRoot = Join-Path $scriptDir 'Data\Agent'
$agentSourcePath = Join-Path $agentSourceRoot 'agent.py'
$agentRequirements = Join-Path $agentSourceRoot 'agent-requirements.txt'
$agentDestinationFolder = Join-Path $venvFolderPath 'Borealis'
$agentDestinationFile = Join-Path $agentDestinationFolder 'agent.py'
$venvPython = Join-Path $venvFolderPath 'Scripts\python.exe'
Run-Step "Create Virtual Python Environment" {
if (-not (Test-Path (Join-Path $venvFolderPath 'Scripts\Activate'))) {
$pythonForVenv = $pythonExe
if (-not (Test-Path $pythonForVenv)) {
$pyCmd = Get-Command py -ErrorAction SilentlyContinue
$pythonCmd = Get-Command python -ErrorAction SilentlyContinue
if ($pyCmd) { $pythonForVenv = $pyCmd.Source }
elseif ($pythonCmd) { $pythonForVenv = $pythonCmd.Source }
else {
Write-Host "Python not found. Install Python or run Server setup (option 1)." -ForegroundColor Red
exit 1
}
}
& $pythonForVenv -m venv $venvFolderPath
}
if (Test-Path $agentSourcePath) {
# Cleanup Previous Agent Folder & Create New Folder
Remove-Item $agentDestinationFolder -Recurse -Force -ErrorAction SilentlyContinue
New-Item -Path $agentDestinationFolder -ItemType Directory -Force | Out-Null
# Copy Agent Files to Virtual Python Environment
$coreAgentFiles = @(
(Join-Path $agentSourceRoot 'agent.py'),
(Join-Path $agentSourceRoot 'Python_API_Endpoints'),
(Join-Path $agentSourceRoot 'agent_deployment.py'),
(Join-Path $agentSourceRoot 'Borealis.ico'),
(Join-Path $agentSourceRoot 'launch_service.ps1'),
(Join-Path $agentSourceRoot 'role_manager.py'),
(Join-Path $agentSourceRoot 'Roles')
)
Copy-Item $coreAgentFiles -Destination $agentDestinationFolder -Recurse -Force
}
. (Join-Path $venvFolderPath 'Scripts\Activate')
}
Run-Step "Install Python Dependencies" {
if (Test-Path $agentRequirements) {
& $venvPython -m pip install --disable-pip-version-check -q -r $agentRequirements | Out-Null
}
$stubSource = Join-Path $agentSourceRoot 'fcntl_stub.py'
if (Test-Path $stubSource) {
$stubDest = Join-Path $venvFolderPath 'Lib\site-packages\fcntl.py'
Write-AgentLog -FileName 'Install.log' -Message '[UTF8] Ensuring Windows fcntl shim is installed.'
Copy-Item $stubSource $stubDest -Force
}
$termiosSource = Join-Path $agentSourceRoot 'termios_stub.py'
if (Test-Path $termiosSource) {
$termiosDest = Join-Path $venvFolderPath 'Lib\site-packages\termios.py'
Write-AgentLog -FileName 'Install.log' -Message '[UTF8] Ensuring Windows termios shim is installed.'
Copy-Item $termiosSource $termiosDest -Force
}
$siteCustomSource = Join-Path $agentSourceRoot 'sitecustomize.py'
if (Test-Path $siteCustomSource) {
$siteCustomDest = Join-Path $venvFolderPath 'Lib\site-packages\sitecustomize.py'
Write-AgentLog -FileName 'Install.log' -Message '[UTF8] Ensuring sitecustomize shim is installed.'
Copy-Item $siteCustomSource $siteCustomDest -Force
}
}
Run-Step "Provision Ansible Execution Environment" {
Ensure-AnsibleExecutionEnvironment \
-ProjectRoot $scriptDir \
-PythonBootstrapExe $pythonExe \
-RequirementsPath $ansibleEeRequirementsPath \
-ExpectedVersion $script:AnsibleExecutionEnvironmentVersion \
-LogName 'Install.log'
}
Run-Step "Configure Agent Settings" {
$settingsDir = Join-Path $scriptDir 'Agent\Borealis\Settings'
$oldSettingsDir = Join-Path $scriptDir 'Agent\Settings'
if (-not (Test-Path $settingsDir)) { New-Item -Path $settingsDir -ItemType Directory -Force | Out-Null }
$serverUrlPath = Join-Path $settingsDir 'server_url.txt'
# Migrate any prior interim location file if present
$oldServerUrlPath = Join-Path $oldSettingsDir 'server_url.txt'
if (-not (Test-Path $serverUrlPath) -and (Test-Path $oldServerUrlPath)) {
try { Move-Item -Path $oldServerUrlPath -Destination $serverUrlPath -Force } catch { try { Copy-Item $oldServerUrlPath $serverUrlPath -Force } catch {} }
}
$defaultUrl = 'http://localhost:5000'
$currentUrl = $defaultUrl
if (Test-Path $serverUrlPath) {
try { $txt = (Get-Content -Path $serverUrlPath -ErrorAction SilentlyContinue | Select-Object -First 1) } catch { $txt = '' }
if ($txt -and $txt.Trim()) { $currentUrl = $txt.Trim() }
}
Write-Host ""; Write-Host "Set Borealis Server URL" -ForegroundColor DarkYellow
$prompt = "Server URL [$currentUrl]"
$inputUrl = Read-Host $prompt
if (-not $inputUrl) { $inputUrl = $currentUrl }
$inputUrl = $inputUrl.Trim()
if (-not $inputUrl) { $inputUrl = $defaultUrl }
# Write UTF-8 without BOM to avoid BOM being read into the URL
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
[System.IO.File]::WriteAllText($serverUrlPath, $inputUrl, $utf8NoBom)
}
Write-Host "`nConfiguring Borealis Agent (tasks)..." -ForegroundColor Blue
Write-Host "===================================================================================="
Ensure-AgentTasks -ScriptRoot $scriptDir
if ($script:Utf8CodePageChanged) {
$msg = 'System code pages set to UTF-8. A reboot is required before Ansible can run.'
Write-AgentLog -FileName 'Install.log' -Message ("[UTF8] {0}" -f $msg)
Write-Host "`n$msg" -ForegroundColor Yellow
}
}
# ---------------------- Main ----------------------
Clear-Host
# ASCII Art Banner
@'
███████████ ████ ███
░░███░░░░░███ ░░███ ░░░
░███ ░███ ██████ ████████ ██████ ██████ ░███ ████ █████
░██████████ ███░░███░░███░░███ ███░░███ ░░░░░███ ░███ ░░███ ███░░
░███░░░░░███░███ ░███ ░███ ░░░ ░███████ ███████ ░███ ░███ ░░█████
░███ ░███░███ ░███ ░███ ░███░░░ ███░░███ ░███ ░███ ░░░░███
███████████ ░░██████ █████ ░░██████ ░░████████ █████ █████ ██████
░░░░░░░░░░░ ░░░░░░ ░░░░░ ░░░░░░ ░░░░░░░░ ░░░░░ ░░░░░ ░░░░░░
'@ | Write-Host -ForegroundColor DarkCyan
Write-Host "Automation Platform" -ForegroundColor DarkGray
if (-not $choice) {
Write-Host " "
Write-Host "Please choose which function you want to launch:"
Write-Host " 1) Borealis Server" -ForegroundColor DarkGray
Write-Host " 2) Borealis Agent" -ForegroundColor DarkGray
Write-Host " 3) Build Electron App " -NoNewline -ForegroundColor DarkGray
Write-Host "[Experimental]" -ForegroundColor Red
Write-Host " 4) Package Self-Contained EXE of Server or Agent " -NoNewline -ForegroundColor DarkGray
Write-Host "[Experimental]" -ForegroundColor Red
# (Removed) AutoHotKey experimental testing
Write-Host "Type a number and press " -NoNewLine
Write-Host "<ENTER>" -ForegroundColor DarkCyan
$choice = Read-Host
}
switch ($choice) {
"1" {
$host.UI.RawUI.WindowTitle = "Borealis Server"
Write-Host "Ensuring Server Dependencies Exist..." -ForegroundColor DarkCyan
Install_Shared_Dependencies
Install_Server_Dependencies
foreach ($tool in @($pythonExe, $nodeExe, $npmCmd, $npxCmd)) {
if (-not (Test-Path $tool)) { Write-Host "`r$($symbols.Fail) Bundled executable not found at '$tool'." -ForegroundColor Red; exit 1 }
}
$env:PATH = '{0};{1};{2}' -f (Split-Path $pythonExe), (Split-Path $nodeExe), $env:PATH
if (-not $modeChoice) {
Write-Host " "
Write-Host "Configure Borealis Server Mode:" -ForegroundColor DarkYellow
Write-Host " 1) Build & Launch > Production Flask Server @ http://localhost:5000" -ForegroundColor DarkCyan
Write-Host " 2) [Skip Build] & Immediately Launch > Production Flask Server @ http://localhost:5000" -ForegroundColor DarkCyan
Write-Host " 3) Launch > [Hotload-Ready] Vite Dev Server @ http://localhost:5173" -ForegroundColor DarkCyan
$modeChoice = Read-Host "Enter choice [1/2/3]"
}
switch ($modeChoice) {
"1" { $borealis_operation_mode = "production" }
"2" {
Run-Step "Borealis: Launch Flask Server" {
Push-Location (Join-Path $scriptDir "Server")
& (Join-Path $scriptDir "Server\Scripts\python.exe") (Join-Path $scriptDir "Server\Borealis\server.py")
Pop-Location
}
break
}
"3" { $borealis_operation_mode = "developer" }
default { Write-Host "Invalid mode choice: $modeChoice" -ForegroundColor Red; break }
}
Write-Host "Deploying Borealis Server in '$borealis_operation_mode' mode" -ForegroundColor Blue
$venvFolder = "Server"
$dataSource = "Data"
$dataDestination = "$venvFolder\Borealis"
$customUIPath = "$dataSource\Server\WebUI"
$webUIDestination = "$venvFolder\web-interface"
$venvPython = Join-Path $venvFolder 'Scripts\python.exe'
Run-Step "Create Borealis Virtual Python Environment" {
if (-not (Test-Path "$venvFolder\Scripts\Activate")) { & $pythonExe -m venv $venvFolder | Out-Null }
if (Test-Path $dataSource) {
Remove-Item $dataDestination -Recurse -Force -ErrorAction SilentlyContinue
New-Item -Path $dataDestination -ItemType Directory -Force | Out-Null
Copy-Item "$dataSource\Server\Python_API_Endpoints" $dataDestination -Recurse
Copy-Item "$dataSource\Server\Sounds" $dataDestination -Recurse
Copy-Item "$dataSource\Server\server.py" $dataDestination
Copy-Item "$dataSource\Server\job_scheduler.py" $dataDestination
}
. "$venvFolder\Scripts\Activate"
}
Run-Step "Install Python Dependencies into Virtual Python Environment" {
if (Test-Path "$dataSource\Server\server-requirements.txt") { & $venvPython -m pip install --disable-pip-version-check -q -r "$dataSource\Server\server-requirements.txt" | Out-Null }
}
Run-Step "Copy Borealis WebUI Files into: $webUIDestination" {
if (Test-Path $webUIDestination) {
Remove-Item "$webUIDestination\public\*" -Recurse -Force -ErrorAction SilentlyContinue
Remove-Item "$webUIDestination\src\*" -Recurse -Force -ErrorAction SilentlyContinue
} else {
New-Item -Path $webUIDestination -ItemType Directory -Force | Out-Null
}
Copy-Item "$customUIPath\*" $webUIDestination -Recurse -Force
}
Run-Step "Vite Web Frontend: Install NPM Packages" {
Push-Location $webUIDestination
$env:npm_config_loglevel = "silent"
& $npmCmd install --silent --no-fund --audit=false | Out-Null
Pop-Location
}
Run-Step "Vite Web Frontend: Start ($borealis_operation_mode)" {
Push-Location $webUIDestination
if ($borealis_operation_mode -eq "developer") { $viteSubCommand = "dev" } else { $viteSubCommand = "build" }
Start-Process -NoNewWindow -FilePath $npmCmd -ArgumentList @("run",$viteSubCommand)
Pop-Location
}
Run-Step "Borealis: Launch Flask Server" {
Push-Location (Join-Path $scriptDir "Server")
$py = Join-Path $scriptDir "Server\Scripts\python.exe"
$server_py = Join-Path $scriptDir "Server\Borealis\server.py"
Write-Host "`nLaunching Borealis..." -ForegroundColor Green
Write-Host "===================================================================================="
Write-Host "$($symbols.Running) Python Flask API Server Started..."
& $py $server_py
Pop-Location
}
break
}
"2" {
$host.UI.RawUI.WindowTitle = "Borealis Agent"
Write-Host " "
# Ensure elevation before showing Agent menu
$scriptPath = $PSCommandPath
if (-not $scriptPath -or $scriptPath -eq '') { $scriptPath = $MyInvocation.MyCommand.Definition }
# If already elevated, skip prompt; otherwise prompt, then relaunch directly to the Agent menu via -Agent
$cont = Request-AgentElevation -ScriptPath $scriptPath -AgentActionParam $AgentAction
if (-not $cont -and -not (Test-IsAdmin)) { return }
if (Test-IsAdmin) {
Write-Host "Escalated Permissions Granted > Agent is Eligible for Deployment." -ForegroundColor Green
}
Write-Host "Agent Menu:" -ForegroundColor Cyan
Write-Host " 1) Install/Update Agent"
Write-Host " 2) Repair Borealis Agent"
Write-Host " 3) Remove Agent"
Write-Host " 4) Launch UserSession Helper (current session)"
Write-Host " 5) Back"
if (-not $agentSubChoice) { $agentSubChoice = Read-Host "Select an option" }
switch ($agentSubChoice) {
'1' { InstallOrUpdate-BorealisAgent; break }
'2' { Repair-BorealisAgent; break }
'3' { Remove-BorealisAgent; break }
'4' {
$venvPythonw = Join-Path $scriptDir 'Agent\Scripts\pythonw.exe'
$helper = Join-Path $scriptDir 'Agent\Borealis\agent.py'
if (-not (Test-Path $venvPythonw)) { Write-Host "pythonw.exe not found under Agent\Scripts" -ForegroundColor Yellow }
if (-not (Test-Path $helper)) { Write-Host "Helper not found under Agent\Borealis" -ForegroundColor Yellow }
if ((Test-Path $venvPythonw) -and (Test-Path $helper)) {
Start-Process -FilePath $venvPythonw -ArgumentList @('-W','ignore::SyntaxWarning', $helper) -WorkingDirectory (Split-Path $helper -Parent)
Write-Host "Launched user-session helper." -ForegroundColor Green
}
break
}
default { break }
}
}
"3" {
$host.UI.RawUI.WindowTitle = "Borealis Electron"
Clear-Host
Write-Host "Deploying Borealis Desktop App..." -ForegroundColor Cyan
Write-Host "===================================================================================="
$electronSource = "Data\Electron"
$electronDestination = "ElectronApp"
$scriptDir = Split-Path $MyInvocation.MyCommand.Path -Parent
Run-Step "Prepare ElectronApp folder" {
if (Test-Path $electronDestination) { Remove-Item $electronDestination -Recurse -Force }
New-Item -Path $electronDestination -ItemType Directory | Out-Null
$deployedServer = Join-Path $scriptDir 'Server\Borealis'
if (-not (Test-Path $deployedServer)) { throw "Server\Borealis not found - please run choice 1 first." }
Copy-Item $deployedServer "$electronDestination\Server" -Recurse
Copy-Item "$electronSource\package.json" "$electronDestination" -Force
Copy-Item "$electronSource\main.js" "$electronDestination" -Force
$staticBuild = Join-Path $scriptDir 'Server\web-interface\build'
if (-not (Test-Path $staticBuild)) { throw "WebUI build not found - run choice 1 to build WebUI first." }
Copy-Item "$staticBuild\*" "$electronDestination\renderer" -Recurse -Force
}
Run-Step "ElectronApp: Install Node dependencies" {
Push-Location $electronDestination
$env:NODE_ENV = ''
$env:npm_config_production = ''
& $npmCmd install --silent --no-fund --audit=false
Pop-Location
}
Run-Step "ElectronApp: Package with electron-builder" {
Push-Location $electronDestination
& $npmCmd run dist
Pop-Location
}
Run-Step "ElectronApp: Launch in dev mode" {
Push-Location $electronDestination
& $npmCmd run dev
Pop-Location
}
}
"4" {
$host.UI.RawUI.WindowTitle = "Borealis Packager"
Write-Host "Choose which module to package into a self-contained EXE file:" -ForegroundColor DarkYellow
Write-Host " 1) Server" -ForegroundColor DarkGray
Write-Host " 2) Agent" -ForegroundColor DarkGray
$exePackageChoice = Read-Host "Enter choice [1/2]"
switch ($exePackageChoice) {
"1" {
$serverScriptDir = Join-Path -Path $PSScriptRoot -ChildPath "Data\Server"
Set-Location -Path $serverScriptDir
& (Join-Path -Path $serverScriptDir -ChildPath "Package-Borealis-Server.ps1")
}
"2" {
$agentScriptDir = Join-Path -Path $PSScriptRoot -ChildPath "Data\Agent"
Set-Location -Path $agentScriptDir
& (Join-Path -Path $agentScriptDir -ChildPath "Package_Borealis-Agent.ps1")
}
default { Write-Host "Invalid Choice. Exiting..." -ForegroundColor Red; exit 1 }
}
}
# (Removed) case "6" experimental AHK test
default { Write-Host "Invalid selection. Exiting..." -ForegroundColor Red; exit 1 }
}