$ErrorActionPreference = 'Stop' $hcServer = $env:HERDR_CONNECT_BACKEND_URL if (-not $hcServer) { throw 'HERDR_CONNECT_BACKEND_URL is required.' } $hcUri = [Uri]$hcServer if ($hcUri.Scheme -ne 'https' -and -not ($hcUri.Scheme -eq 'http' -and $hcUri.IsLoopback)) { throw 'Use HTTPS, or localhost for development.' } if (-not $env:HERDR_CONNECT_ENROLLMENT_GRANT) { throw 'Generate a fresh enrollment command in the dashboard.' } $hcNode = (Get-Command node -ErrorAction Stop).Source $hcHerdr = (Get-Command herdr -ErrorAction Stop).Source $hcVersion = (& $hcNode --version).TrimStart('v').Split('.') if ([int]$hcVersion[0] -lt 22 -or ([int]$hcVersion[0] -eq 22 -and [int]$hcVersion[1] -lt 13)) { throw 'Install Node.js 22.13+ or 24 LTS first.' } $hcDirectory = if ($env:HERDR_CONNECT_PLUGIN_DIRECTORY) { [IO.Path]::GetFullPath($env:HERDR_CONNECT_PLUGIN_DIRECTORY) } else { Join-Path $env:APPDATA 'herdr-connect/plugin' } New-Item -ItemType Directory -Path $hcDirectory -Force | Out-Null foreach ($hcFile in @('remote.mjs', 'remote-lifecycle.mjs', 'remote.sha256')) { Invoke-WebRequest -Uri "$($hcServer.TrimEnd('/'))/downloads/$hcFile" -OutFile (Join-Path $hcDirectory $hcFile) } $hcExpected = (Get-Content -LiteralPath (Join-Path $hcDirectory 'remote.sha256') -Raw).Trim() $hcActual = (Get-FileHash -LiteralPath (Join-Path $hcDirectory 'remote.mjs') -Algorithm SHA256).Hash.ToLowerInvariant() if ($hcExpected -ne $hcActual) { throw 'Plugin download checksum did not match.' } $hcLock = Join-Path $env:APPDATA 'herdr-connect/remote-helper.lock' if (Test-Path -LiteralPath $hcLock) { & $hcNode (Join-Path $hcDirectory 'remote-lifecycle.mjs') stop if ($LASTEXITCODE -ne 0) { throw 'Existing helper could not be stopped safely. Inspect it before enrolling again.' } for ($hcAttempt = 0; $hcAttempt -lt 70 -and (Test-Path -LiteralPath $hcLock); $hcAttempt++) { Start-Sleep -Milliseconds 100 } if (Test-Path -LiteralPath $hcLock) { throw 'Existing helper has not released its lock yet.' } } try { if ($env:HERDR_CONNECT_CONFIG_HOME) { $env:XDG_CONFIG_HOME = [IO.Path]::GetFullPath($env:HERDR_CONNECT_CONFIG_HOME) } $hcName = if ($env:HERDR_CONNECT_MACHINE_NAME) { $env:HERDR_CONNECT_MACHINE_NAME } else { $env:COMPUTERNAME } & $hcNode (Join-Path $hcDirectory 'remote.mjs') enroll --server $hcServer --name $hcName if ($LASTEXITCODE -ne 0) { throw 'Enrollment failed.' } } finally { Remove-Item Env:HERDR_CONNECT_ENROLLMENT_GRANT -ErrorAction SilentlyContinue } $hcManifest = @' id = "herdr-connect.remote" name = "Herdr Connect Remote" version = "0.1.0" min_herdr_version = "0.9.3" description = "Connect this Herdr session to your hosted account" platforms = ["linux", "macos", "windows"] [[actions]] id = "start" title = "Connect to Herdr Connect" command = ["node", "remote-lifecycle.mjs", "start"] [[actions]] id = "status" title = "Remote helper status" command = ["node", "remote-lifecycle.mjs", "status"] [[actions]] id = "stop" title = "Disconnect remote helper" command = ["node", "remote-lifecycle.mjs", "stop"] '@ $hcManifest = $hcManifest.Replace('["node",', '[' + (ConvertTo-Json $hcNode -Compress) + ',') $hcSettings = @{ herdrBinary = $hcHerdr } if ($env:HERDR_CONNECT_ENABLE_WRITES -eq '1') { $hcSettings.enableWrites = $true } if ($env:XDG_CONFIG_HOME) { $hcSettings.configHome = $env:XDG_CONFIG_HOME } [IO.File]::WriteAllText((Join-Path $hcDirectory 'runtime-settings.json'), (ConvertTo-Json $hcSettings -Compress), [Text.UTF8Encoding]::new($false)) [IO.File]::WriteAllText((Join-Path $hcDirectory 'herdr-plugin.toml'), $hcManifest, [Text.UTF8Encoding]::new($false)) & $hcHerdr plugin link (Join-Path $hcDirectory 'herdr-plugin.toml') if ($LASTEXITCODE -ne 0) { throw 'Herdr plugin registration failed; enrollment was saved.' } & $hcNode (Join-Path $hcDirectory 'remote-lifecycle.mjs') start if ($LASTEXITCODE -ne 0) { throw 'Helper start failed; use the plugin start action after starting Herdr.' } Write-Host 'Installed and registered. Check the dashboard for connection availability. Use the Herdr plugin start action after a reboot.'