# Token Harbor connect — universal installer (Windows PowerShell). # irm https://tokenharbor.ai/connect.ps1 | iex # # Mirrors connect.sh: pinned Node LTS verified against nodejs.org's published # SHA-256, CLI fetched by manifest and hash-bound, staged and swapped # atomically so a failed install never destroys a working one. $ErrorActionPreference = 'Stop' $Base = if ($env:TOKENHARBOR_BASE) { $env:TOKENHARBOR_BASE } else { 'https://tokenharbor.ai' } $ThDir = Join-Path $env:USERPROFILE '.tokenharbor' $BinDir = Join-Path $ThDir 'bin' $CliDir = Join-Path $ThDir 'cli' $NodeDir = Join-Path $ThDir 'node' # Pinned — see the note in connect.sh. A moving "latest" URL is an unreviewed # dependency that updates itself on every user's machine. $NodeVersion = 'v24.19.0' $NodeDist = "https://nodejs.org/dist/$NodeVersion" # Pinned by scripts/installers/build.mjs at release time — see connect.sh for # why this is embedded rather than fetched. A hash that arrives with the # download is not a check. # TH_CLI_PINS_START $CliVersion = '0.6.1' $CliFile = 'connect-0.6.1.tgz' $CliSha256 = 'c1595895338d00019f86344136da6a54f19b29b221bb8707a2102aad7d6aa7fa' # TH_CLI_PINS_END function Fail($msg) { Write-Host $msg -ForegroundColor Red; exit 1 } function Assert-Sha256($path, $want, $what) { $got = (Get-FileHash -Path $path -Algorithm SHA256).Hash.ToLower() if ($got -ne $want.ToLower()) { Fail "Checksum mismatch for $what.`n expected $want`n got $got`nNothing was installed. If this repeats, contact support@tokenharbor.ai." } } Write-Host '' Write-Host 'Token Harbor connect - one command to wire your AI agents.' -ForegroundColor White Write-Host '' New-Item -ItemType Directory -Force -Path $BinDir | Out-Null function Test-NodeOk($exe) { try { $v = & $exe -e "console.log(process.versions.node.split('.')[0])" 2>$null return ([int]$v -ge 18) } catch { return $false } } # 1. Find (or provision) Node >= 18 $NodeBin = $null $sys = Get-Command node -ErrorAction SilentlyContinue if ($sys -and (Test-NodeOk $sys.Source)) { $NodeBin = $sys.Source } elseif ((Test-Path (Join-Path $NodeDir 'node.exe')) -and (Test-NodeOk (Join-Path $NodeDir 'node.exe'))) { $NodeBin = Join-Path $NodeDir 'node.exe' } else { Write-Host "Node.js 18+ not found - installing a private copy ($NodeVersion, ~30 MB, one time)..." -ForegroundColor DarkGray $arch = if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64') { 'arm64' } else { 'x64' } $file = "node-$NodeVersion-win-$arch.zip" $tmp = Join-Path $env:TEMP ("th-" + [Guid]::NewGuid()) New-Item -ItemType Directory -Force -Path $tmp | Out-Null # The checksum file is fetched to be USED. The previous version read it only # to discover a filename and verified nothing. $sums = (Invoke-WebRequest -Uri "$NodeDist/SHASUMS256.txt" -UseBasicParsing).Content $line = ($sums -split "`n") | Where-Object { $_ -match [regex]::Escape($file) + '\s*$' } | Select-Object -First 1 if (-not $line) { Fail "No checksum published for $file. Nothing was installed." } $want = ($line -split '\s+')[0] Invoke-WebRequest -Uri "$NodeDist/$file" -OutFile (Join-Path $tmp 'node.zip') -UseBasicParsing Assert-Sha256 (Join-Path $tmp 'node.zip') $want 'the Node runtime' Expand-Archive -Path (Join-Path $tmp 'node.zip') -DestinationPath $tmp -Force $inner = Get-ChildItem -Path $tmp -Directory | Where-Object { $_.Name -like 'node-v*' } | Select-Object -First 1 if (-not $inner) { Remove-Item -Recurse -Force $tmp; Fail 'Node archive looks wrong. Nothing was changed.' } # Stage then swap — a failure must not take the previous runtime with it. $stage = "$NodeDir.new" if (Test-Path $stage) { Remove-Item -Recurse -Force $stage } Move-Item $inner.FullName $stage if (-not (Test-NodeOk (Join-Path $stage 'node.exe'))) { Remove-Item -Recurse -Force $stage, $tmp; Fail 'Node runtime install failed. Nothing was changed.' } # old -> .old, stage -> live, restore .old if the second move fails. # Remove-Item on the live directory followed by Move-Item is NOT atomic: an # interrupt between them leaves no runtime at all. $old = "$NodeDir.old" if (Test-Path $old) { Remove-Item -Recurse -Force $old } $movedAside = $false try { if (Test-Path $NodeDir) { Move-Item $NodeDir $old; $movedAside = $true } Move-Item $stage $NodeDir } catch { if ($movedAside -and -not (Test-Path $NodeDir)) { Move-Item $old $NodeDir } Remove-Item -Recurse -Force $stage -ErrorAction SilentlyContinue Fail 'Could not install the Node runtime. Your previous one is untouched.' } finally { Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue } if (Test-Path $old) { Remove-Item -Recurse -Force $old -ErrorAction SilentlyContinue } $NodeBin = Join-Path $NodeDir 'node.exe' } # 2. Install the CLI (versioned + hash-bound). tar.exe ships with Windows 10+. Write-Host 'Downloading the Token Harbor CLI...' -ForegroundColor DarkGray $tmp = Join-Path $env:TEMP ("th-" + [Guid]::NewGuid()) New-Item -ItemType Directory -Force -Path $tmp | Out-Null Invoke-WebRequest -Uri "$Base/cli/$CliFile" -OutFile (Join-Path $tmp 'cli.tgz') -UseBasicParsing Assert-Sha256 (Join-Path $tmp 'cli.tgz') $CliSha256 'the Token Harbor CLI' $stage = "$CliDir.new" if (Test-Path $stage) { Remove-Item -Recurse -Force $stage } New-Item -ItemType Directory -Force -Path $stage | Out-Null tar -xzf (Join-Path $tmp 'cli.tgz') -C $stage --strip-components=1 if (-not (Test-Path (Join-Path $stage 'src\cli.mjs'))) { Remove-Item -Recurse -Force $stage, $tmp; Fail 'CLI archive looks wrong. Nothing was changed.' } $oldCli = "$CliDir.old" if (Test-Path $oldCli) { Remove-Item -Recurse -Force $oldCli } $movedAside = $false try { if (Test-Path $CliDir) { Move-Item $CliDir $oldCli; $movedAside = $true } Move-Item $stage $CliDir } catch { if ($movedAside -and -not (Test-Path $CliDir)) { Move-Item $oldCli $CliDir } Remove-Item -Recurse -Force $stage -ErrorAction SilentlyContinue Fail 'Could not install the CLI. Your previous one is untouched.' } finally { Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue } if (Test-Path $oldCli) { Remove-Item -Recurse -Force $oldCli -ErrorAction SilentlyContinue } Write-Host "Installed CLI $CliVersion." -ForegroundColor DarkGray # 3. `tokenharbor` shim on the user PATH $shim = Join-Path $BinDir 'tokenharbor.cmd' "@echo off`r`n`"$NodeBin`" `"$CliDir\src\cli.mjs`" %*" | Set-Content -Path $shim -Encoding ASCII $userPath = [Environment]::GetEnvironmentVariable('Path', 'User') if (-not ($userPath -split ';' | Where-Object { $_ -eq $BinDir })) { $newPath = if ([string]::IsNullOrEmpty($userPath)) { $BinDir } else { "$userPath;$BinDir" } [Environment]::SetEnvironmentVariable('Path', $newPath, 'User') $env:Path = "$env:Path;$BinDir" } Write-Host '' Write-Host "Installed. ($shim)" -ForegroundColor Green Write-Host '' # 4. Run connect now (interactive prompt works in a normal PS session) & $NodeBin (Join-Path $CliDir 'src\cli.mjs') menu Write-Host '' Write-Host 'Anytime: tokenharbor status / models / disconnect / uninstall (open a NEW terminal first)'