302 lines
8.7 KiB
PowerShell
302 lines
8.7 KiB
PowerShell
param(
|
|
[Parameter(Position = 0)]
|
|
[ValidateSet("test", "dev", "typecheck", "sync-config", "cmd")]
|
|
[string]$Task = "test",
|
|
|
|
[Parameter(ValueFromRemainingArguments = $true)]
|
|
[string[]]$Rest
|
|
)
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
|
|
$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
|
|
$SandboxRoot = Join-Path $RepoRoot ".sandbox"
|
|
$HermesHome = Join-Path $SandboxRoot "hermes-home"
|
|
$ElectronUserData = Join-Path $SandboxRoot "electron-user-data"
|
|
$PortsFile = Join-Path $SandboxRoot "ports.json"
|
|
|
|
function Looks-LikeHermesHome([string]$Path) {
|
|
if (-not (Test-Path -LiteralPath $Path)) { return $false }
|
|
return (
|
|
(Test-Path -LiteralPath (Join-Path $Path "config.yaml")) -or
|
|
(Test-Path -LiteralPath (Join-Path $Path ".env")) -or
|
|
(Test-Path -LiteralPath (Join-Path $Path "auth.json")) -or
|
|
(Test-Path -LiteralPath (Join-Path $Path "hermes-agent")) -or
|
|
(Test-Path -LiteralPath (Join-Path $Path "desktop.json"))
|
|
)
|
|
}
|
|
|
|
function Resolve-SourceHermesHome {
|
|
$candidates = @()
|
|
if ($env:HERMES_HOME) { $candidates += $env:HERMES_HOME }
|
|
if ($env:LOCALAPPDATA) { $candidates += (Join-Path $env:LOCALAPPDATA "hermes") }
|
|
$candidates += (Join-Path $HOME ".hermes")
|
|
|
|
foreach ($candidate in $candidates) {
|
|
if (Looks-LikeHermesHome $candidate) {
|
|
return (Resolve-Path -LiteralPath $candidate).Path
|
|
}
|
|
}
|
|
|
|
throw "Could not find a source Hermes home. Set HERMES_HOME and rerun."
|
|
}
|
|
|
|
function Ensure-SandboxHome {
|
|
New-Item -ItemType Directory -Force -Path $SandboxRoot, $HermesHome, $ElectronUserData | Out-Null
|
|
|
|
$desktopConfig = Join-Path $HermesHome "desktop.json"
|
|
if (-not (Test-Path -LiteralPath $desktopConfig)) {
|
|
@{
|
|
connectionMode = "local"
|
|
remoteUrl = ""
|
|
remoteApiKey = ""
|
|
} | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath $desktopConfig -Encoding UTF8
|
|
}
|
|
}
|
|
|
|
function Clear-CopiedRuntimeState {
|
|
$runtimeTargets = @(
|
|
(Join-Path $HermesHome "desktop\sessions.json"),
|
|
(Join-Path $HermesHome "sessions"),
|
|
(Join-Path $HermesHome "audio_cache"),
|
|
(Join-Path $HermesHome "image_cache")
|
|
)
|
|
|
|
foreach ($target in $runtimeTargets) {
|
|
if (Test-Path -LiteralPath $target) {
|
|
Remove-Item -LiteralPath $target -Recurse -Force -ErrorAction SilentlyContinue
|
|
}
|
|
}
|
|
}
|
|
|
|
function Test-PortFree([int]$Port) {
|
|
$listener = $null
|
|
try {
|
|
$listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, $Port)
|
|
$listener.Start()
|
|
return $true
|
|
} catch {
|
|
return $false
|
|
} finally {
|
|
if ($listener) { $listener.Stop() }
|
|
}
|
|
}
|
|
|
|
function Find-FreePort([int]$Start) {
|
|
for ($port = $Start; $port -lt 65000; $port++) {
|
|
if (Test-PortFree $port) { return $port }
|
|
}
|
|
throw "Could not find a free port starting at $Start"
|
|
}
|
|
|
|
function ConvertTo-HashtableDeep($Value) {
|
|
if ($null -eq $Value) { return $null }
|
|
if ($Value -is [System.Collections.IDictionary]) {
|
|
$result = @{}
|
|
foreach ($key in $Value.Keys) {
|
|
$result[$key] = ConvertTo-HashtableDeep $Value[$key]
|
|
}
|
|
return $result
|
|
}
|
|
if ($Value -is [System.Management.Automation.PSCustomObject]) {
|
|
$result = @{}
|
|
foreach ($property in $Value.PSObject.Properties) {
|
|
$result[$property.Name] = ConvertTo-HashtableDeep $property.Value
|
|
}
|
|
return $result
|
|
}
|
|
if (($Value -is [System.Collections.IEnumerable]) -and -not ($Value -is [string])) {
|
|
$items = @()
|
|
foreach ($item in $Value) {
|
|
$items += ,(ConvertTo-HashtableDeep $item)
|
|
}
|
|
return $items
|
|
}
|
|
return $Value
|
|
}
|
|
|
|
function Set-SandboxPorts {
|
|
$apiPort = Find-FreePort 19642
|
|
$sshLocalPort = Find-FreePort ($apiPort + 120)
|
|
$rendererPort = Find-FreePort ($apiPort + 240)
|
|
$claw3dPort = Find-FreePort ($apiPort + 360)
|
|
|
|
$env:HERMES_DESKTOP_DEFAULT_API_PORT = [string]$apiPort
|
|
$env:HERMES_DESKTOP_PORT_RANGE_START = [string]($apiPort + 1)
|
|
$env:HERMES_DESKTOP_PORT_RANGE_END = [string]($apiPort + 99)
|
|
$env:HERMES_DESKTOP_RENDERER_PORT = [string]$rendererPort
|
|
|
|
@{
|
|
apiPort = $apiPort
|
|
profilePortStart = $apiPort + 1
|
|
profilePortEnd = $apiPort + 99
|
|
sshLocalPort = $sshLocalPort
|
|
rendererPort = $rendererPort
|
|
claw3dPort = $claw3dPort
|
|
} | ConvertTo-Json -Depth 4 | Set-Content -LiteralPath $PortsFile -Encoding UTF8
|
|
|
|
$desktopConfig = Join-Path $HermesHome "desktop.json"
|
|
$desktop = @{}
|
|
if (Test-Path -LiteralPath $desktopConfig) {
|
|
try {
|
|
$desktop = ConvertTo-HashtableDeep (Get-Content -LiteralPath $desktopConfig -Raw | ConvertFrom-Json)
|
|
} catch {
|
|
$desktop = @{}
|
|
}
|
|
}
|
|
if (-not $desktop.ContainsKey("connectionMode")) { $desktop.connectionMode = "local" }
|
|
if (-not $desktop.ContainsKey("remoteUrl")) { $desktop.remoteUrl = "" }
|
|
if (-not $desktop.ContainsKey("remoteApiKey")) { $desktop.remoteApiKey = "" }
|
|
if (-not $desktop.ContainsKey("ssh") -or -not ($desktop.ssh -is [hashtable])) {
|
|
$desktop.ssh = @{}
|
|
}
|
|
if (-not $desktop.ssh.ContainsKey("port")) { $desktop.ssh.port = 22 }
|
|
if (-not $desktop.ssh.ContainsKey("remotePort")) { $desktop.ssh.remotePort = 8642 }
|
|
$desktop.ssh.localPort = $sshLocalPort
|
|
$desktop | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath $desktopConfig -Encoding UTF8
|
|
|
|
Set-Content -LiteralPath (Join-Path $HermesHome "claw3d-port") -Value ([string]$claw3dPort) -Encoding UTF8
|
|
|
|
Write-Host "[hermes-sandbox] ports: api=$apiPort profiles=$($apiPort + 1)-$($apiPort + 99) renderer=$rendererPort ssh-local=$sshLocalPort claw3d=$claw3dPort"
|
|
}
|
|
|
|
function Sync-Config {
|
|
Ensure-SandboxHome
|
|
$source = Resolve-SourceHermesHome
|
|
|
|
$excludedDirs = @(
|
|
"audio_cache",
|
|
"backups",
|
|
"cache",
|
|
"desktop",
|
|
"desktop-staging",
|
|
"gateway-service",
|
|
"hermes-agent",
|
|
"hermes-agent-backup-*",
|
|
"hermes-office",
|
|
"image_cache",
|
|
"logs",
|
|
"lsp",
|
|
"sandboxes",
|
|
"sessions",
|
|
"state-snapshots",
|
|
"tmp-*"
|
|
)
|
|
|
|
$excludedFiles = @(
|
|
"*.db",
|
|
"*.db-shm",
|
|
"*.db-wal",
|
|
"*.lock",
|
|
"*.log",
|
|
"*.pid",
|
|
".hermes_history",
|
|
".restart_last_processed.json",
|
|
".update_check",
|
|
"HermesGateway.task-backup.xml",
|
|
"sessions.json",
|
|
"tmp_*.json"
|
|
)
|
|
|
|
$robocopyArgs = @(
|
|
$source,
|
|
$HermesHome,
|
|
"/E",
|
|
"/NFL",
|
|
"/NDL",
|
|
"/NJH",
|
|
"/NJS",
|
|
"/NP",
|
|
"/XD"
|
|
) + $excludedDirs + @("/XF") + $excludedFiles
|
|
|
|
& robocopy @robocopyArgs | Out-Host
|
|
$code = $LASTEXITCODE
|
|
if ($code -ge 8) {
|
|
throw "robocopy failed with exit code $code"
|
|
}
|
|
|
|
Clear-CopiedRuntimeState
|
|
$env:HERMES_HOME = $HermesHome
|
|
Set-SandboxPorts
|
|
Write-Host "[hermes-sandbox] copied config from $source"
|
|
Write-Host "[hermes-sandbox] sandbox HERMES_HOME=$HermesHome"
|
|
}
|
|
|
|
function Use-SandboxEnv {
|
|
Ensure-SandboxHome
|
|
$env:HERMES_HOME = $HermesHome
|
|
$env:HERMES_DESKTOP_SANDBOX = "1"
|
|
$env:HERMES_DESKTOP_APP_NAME = "Hermes One"
|
|
$env:HERMES_DESKTOP_USER_DATA_DIR = $ElectronUserData
|
|
$env:VITE_HERMES_DESKTOP_APP_NAME = "Hermes One"
|
|
Set-SandboxPorts
|
|
Write-Host "[hermes-sandbox] HERMES_HOME=$HermesHome"
|
|
Write-Host "[hermes-sandbox] userData=$ElectronUserData"
|
|
Write-Host "[hermes-sandbox] app name=Hermes One"
|
|
Write-Host "[hermes-sandbox] dashboard chat=default-on"
|
|
}
|
|
|
|
function Repair-SandboxUserEnvironment {
|
|
$sandboxVenv = Join-Path $HermesHome "hermes-agent\venv\Scripts"
|
|
$userHermesHome = [Environment]::GetEnvironmentVariable("HERMES_HOME", "User")
|
|
if ($userHermesHome -and ($userHermesHome.TrimEnd("\") -ieq $HermesHome.TrimEnd("\"))) {
|
|
[Environment]::SetEnvironmentVariable("HERMES_HOME", $null, "User")
|
|
Write-Host "[hermes-sandbox] removed sandbox HERMES_HOME from user environment"
|
|
}
|
|
|
|
$userPath = [Environment]::GetEnvironmentVariable("Path", "User")
|
|
if ($userPath) {
|
|
$hadSandboxPath = $false
|
|
$parts = $userPath -split ";" | Where-Object {
|
|
if (-not $_) { return $false }
|
|
if ($_.TrimEnd("\") -ieq $sandboxVenv.TrimEnd("\")) {
|
|
$hadSandboxPath = $true
|
|
return $false
|
|
}
|
|
return $true
|
|
}
|
|
if ($hadSandboxPath) {
|
|
[Environment]::SetEnvironmentVariable("Path", ($parts -join ";"), "User")
|
|
Write-Host "[hermes-sandbox] removed sandbox venv from user PATH"
|
|
}
|
|
}
|
|
}
|
|
|
|
switch ($Task) {
|
|
"sync-config" {
|
|
Sync-Config
|
|
Repair-SandboxUserEnvironment
|
|
exit 0
|
|
}
|
|
"test" {
|
|
Use-SandboxEnv
|
|
& npm run test -- @Rest
|
|
Repair-SandboxUserEnvironment
|
|
exit $LASTEXITCODE
|
|
}
|
|
"dev" {
|
|
Use-SandboxEnv
|
|
& npm run dev -- @Rest
|
|
Repair-SandboxUserEnvironment
|
|
exit $LASTEXITCODE
|
|
}
|
|
"typecheck" {
|
|
Use-SandboxEnv
|
|
& npm run typecheck -- @Rest
|
|
Repair-SandboxUserEnvironment
|
|
exit $LASTEXITCODE
|
|
}
|
|
"cmd" {
|
|
Use-SandboxEnv
|
|
if (-not $Rest -or $Rest.Length -eq 0) {
|
|
throw "cmd task requires a command after --"
|
|
}
|
|
$program = $Rest[0]
|
|
$argsForProgram = if ($Rest.Length -gt 1) { $Rest[1..($Rest.Length - 1)] } else { @() }
|
|
& $program @argsForProgram
|
|
Repair-SandboxUserEnvironment
|
|
exit $LASTEXITCODE
|
|
}
|
|
}
|