slancha-wire 0.14.2

Magic-wormhole for AI agents — bilateral signed-message bus over a mailbox relay
Documentation
<#
install.ps1 — single-binary installer for `wire` (native PowerShell).

Usage:
  iex (irm https://wireup.net/install.ps1)
  iex (irm https://wireup.net/install.ps1) -Prefix C:\Tools

What it does:
  1. Detects platform (Windows x86_64 / ARM64).
  2. Downloads the matching pre-built `wire.exe` binary from $WIRE_DIST_URL
     (default: GitHub Releases — $WIRE_REPO_URL/releases/latest/download/wire-<triple>.exe).
  3. Verifies SHA-256 against the sibling .sha256 at the dist URL.
  4. Installs to $Prefix\wire.exe (default: $env:LOCALAPPDATA\Programs\wire\wire.exe,
     XDG-style user-local; no admin elevation required).
  5. Adds the install dir to the User PATH if not already present.
  6. If pre-built binary unavailable AND `cargo` is on $PATH, falls back to
     `cargo install slancha-wire` from crates.io. (Source-build path; ~2 min.)
  7. Runs `wire upgrade --check` for stale-daemon cleanup (best-effort).

What it does NOT do:
  - install Windows services (use `wire service install` opt-in)
  - install Scoop, winget, or any other package manager
  - require admin elevation unless writing outside $env:LOCALAPPDATA

Override defaults via PowerShell parameters:
  iex (& { (irm https://wireup.net/install.ps1) } -Prefix C:\bin)
  $env:WIRE_REPO_URL = "https://github.com/your-fork/wire"; iex (irm ...)
  $env:WIRE_DIST_URL = "https://your-host/dist";            iex (irm ...)
#>

[CmdletBinding()]
param(
    [string]$Prefix
)

$ErrorActionPreference = 'Stop'

$RepoUrl = if ($env:WIRE_REPO_URL) { $env:WIRE_REPO_URL } else { 'https://github.com/SlanchaAi/wire' }
$DistUrl = if ($env:WIRE_DIST_URL) { $env:WIRE_DIST_URL } else { "$RepoUrl/releases/latest/download" }

# 1. Detect arch.
$arch = $env:PROCESSOR_ARCHITECTURE
$triple = switch ($arch) {
    'AMD64' { 'x86_64-pc-windows-msvc' }
    'ARM64' { 'aarch64-pc-windows-msvc' }
    default { throw "unsupported Windows arch: $arch (need AMD64 or ARM64)" }
}

# 2. Choose install dir.
#
# Default precedence:
#   1. Explicit `-Prefix <dir>` (or PREFIX env) — always wins.
#   2. $env:LOCALAPPDATA\Programs\wire — user-local, no admin needed, XDG-equivalent.
#
# Why not $env:ProgramFiles by default? Hitting UAC on `irm | iex` interactively is
# friction (and breaks in non-interactive scripted contexts); leaving a binary at a
# path that isn't on PATH is a worse silent failure than either alternative.
# Matches what rustup-init.exe, ollama, gh, and uv all do on Windows.
if (-not $Prefix) {
    if ($env:PREFIX) {
        $Prefix = $env:PREFIX
    } else {
        $Prefix = Join-Path $env:LOCALAPPDATA 'Programs\wire'
    }
}
if (-not (Test-Path $Prefix)) {
    New-Item -ItemType Directory -Path $Prefix -Force | Out-Null
}
$target = Join-Path $Prefix 'wire.exe'

$assetName = "wire-$triple.exe"
$binaryUrl = "$DistUrl/$assetName"
Write-Host "fetching $binaryUrl ..."

$tmp = [System.IO.Path]::GetTempFileName()
$tmpExe = "$tmp.exe"
$tmpSha = "$tmp.sha256"

function Normalize-PathForCompare {
    param([string]$Path)
    try {
        return ([System.IO.Path]::GetFullPath($Path)).TrimEnd('\')
    } catch {
        return $Path.TrimEnd('\')
    }
}

function Move-WireBinaryIntoPlace {
    param(
        [string]$Source,
        [string]$Destination
    )
    try {
        Move-Item -Path $Source -Destination $Destination -Force
    } catch {
        if (-not (Test-Path $Destination)) {
            throw
        }
        $aside = "$Destination.old-$(Get-Random)"
        Write-Warning "$Destination is in use; renaming to $aside and moving the new binary into place."
        Move-Item -Path $Destination -Destination $aside -Force
        Move-Item -Path $Source -Destination $Destination
    }
}

function Find-WireOnPathValue {
    param([string]$PathValue)
    if (-not $PathValue) {
        return $null
    }
    foreach ($seg in $PathValue.Split(';')) {
        if (-not $seg) {
            continue
        }
        $candidate = Join-Path $seg 'wire.exe'
        if (Test-Path $candidate) {
            return (Normalize-PathForCompare $candidate)
        }
    }
    return $null
}

function Resolve-ExpectedHash {
    param(
        [string]$ShaFile,
        [string]$ExpectedAssetName
    )

    $lines = @(Get-Content $ShaFile | ForEach-Object { $_.Trim() } | Where-Object { $_ })
    if ($lines.Count -eq 0) {
        throw "SHA-256 file is empty: $ShaFile"
    }

    $matches = @()
    foreach ($line in $lines) {
        $parts = @($line -split '\s+', 2)
        if ($parts.Count -eq 1) {
            if ($lines.Count -eq 1) {
                $matches += $parts[0]
            }
            continue
        }
        $name = (Split-Path -Leaf ($parts[1].TrimStart('*'))).Trim()
        if ($name -eq $ExpectedAssetName) {
            $matches += $parts[0]
        }
    }

    if ($matches.Count -ne 1) {
        throw "SHA-256 file must contain exactly one hash for $ExpectedAssetName (found $($matches.Count))"
    }
    $hash = $matches[0].ToLower()
    if ($hash -notmatch '^[0-9a-f]{64}$') {
        throw "SHA-256 entry for $ExpectedAssetName is not a 64-hex digest: $hash"
    }
    return $hash
}

$downloaded = $false
try {
    Invoke-WebRequest -UseBasicParsing -Uri $binaryUrl -OutFile $tmpExe
    $downloaded = $true
} catch {
    Write-Warning "pre-built binary unavailable at $binaryUrl"
}

if ($downloaded) {
    # 3. Required SHA-256 sibling. `irm | iex` installs MUST NOT silently
    # accept an unverified binary; operators using custom mirrors should serve
    # the same per-asset `.sha256` shape as GitHub Releases.
    try {
        Invoke-WebRequest -UseBasicParsing -Uri "$binaryUrl.sha256" -OutFile $tmpSha
    } catch {
        throw "missing SHA-256 sibling at $binaryUrl.sha256; refusing to install unverified binary"
    }
    $expected = Resolve-ExpectedHash -ShaFile $tmpSha -ExpectedAssetName $assetName
    $actual = (Get-FileHash -Path $tmpExe -Algorithm SHA256).Hash.ToLower()
    if ($expected -ne $actual) {
        throw "SHA-256 mismatch for $assetName — expected $expected, got $actual"
    }

    # 4. Move into place. If target is currently running (the running-exe
    # rename-aside trick `wire update` uses), we fall back to renaming the
    # existing target so the move can land.
    Move-WireBinaryIntoPlace -Source $tmpExe -Destination $target
} else {
    # 5. Cargo fallback.
    $cargo = Get-Command cargo -ErrorAction SilentlyContinue
    if ($cargo) {
        Write-Host 'pre-built binary unavailable — building from source via cargo install slancha-wire (~2 min)'
        $cargoRoot = Join-Path ([System.IO.Path]::GetTempPath()) "wire-cargo-install-$([System.Guid]::NewGuid())"
        New-Item -ItemType Directory -Path $cargoRoot -Force | Out-Null
        try {
            & cargo install slancha-wire --root $cargoRoot
            if ($LASTEXITCODE -ne 0) {
                throw "cargo install slancha-wire exited with $LASTEXITCODE"
            }
        } catch {
            Write-Warning 'crates.io install failed — falling back to git source build'
            & cargo install --git $RepoUrl --root $cargoRoot --bin wire
            if ($LASTEXITCODE -ne 0) {
                throw "cargo install --git $RepoUrl exited with $LASTEXITCODE"
            }
        }
        $built = Join-Path $cargoRoot 'bin\wire.exe'
        if (-not (Test-Path $built)) {
            throw "cargo fallback succeeded but did not produce expected binary at $built"
        }
        try {
            Move-WireBinaryIntoPlace -Source $built -Destination $target
        } finally {
            Remove-Item -Path $cargoRoot -Recurse -Force -ErrorAction SilentlyContinue
        }
    } else {
        Write-Error @"
pre-built binary unavailable and cargo not found.
Install Rust from https://rustup.rs/ and re-run this script, or:
  cargo install slancha-wire    (after rustup)
  git clone $RepoUrl; cd wire; cargo build --release
"@
        exit 1
    }
}

# Cleanup temp.
Remove-Item -Path $tmp, $tmpSha -ErrorAction SilentlyContinue

if (Test-Path $target) {
    Write-Host "wire installed at $target"
    Write-Host ''
    & $target --version
    Write-Host ''

    # 6. PATH check: add $Prefix to user PATH if not already present.
    # Matches install.sh's $HOME/.local/bin nudge — without the install
    # dir on PATH the user runs the install and then `wire` returns
    # "command not found".
    $userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
    $pathSegs = if ($userPath) { $userPath.Split(';') } else { @() }
    $onPath = $pathSegs -contains $Prefix
    if (-not $onPath) {
        Write-Host "adding $Prefix to user PATH ..."
        $newPath = if ($userPath) { "$Prefix;$userPath" } else { $Prefix }
        [Environment]::SetEnvironmentVariable('Path', $newPath, 'User')
        # Make the change visible in THIS session too (the User PATH edit
        # only takes effect in NEW processes).
        $env:Path = "$Prefix;$env:Path"
        Write-Host "PATH updated. Open a new terminal to inherit the change."
        Write-Host ''
    }
    $targetCmp = Normalize-PathForCompare $target
    $sessionWire = Find-WireOnPathValue $env:Path
    if ($sessionWire -and -not [string]::Equals($sessionWire, $targetCmp, [System.StringComparison]::OrdinalIgnoreCase)) {
        Write-Warning "wire.exe on current PATH resolves to $sessionWire, not newly installed $target. Move $Prefix earlier in PATH or remove the shadowing binary."
    }
    $machinePath = [Environment]::GetEnvironmentVariable('Path', 'Machine')
    $newUserPath = [Environment]::GetEnvironmentVariable('Path', 'User')
    $newShellPath = if ($machinePath) { "$machinePath;$newUserPath" } else { $newUserPath }
    $newShellWire = Find-WireOnPathValue $newShellPath
    if ($newShellWire -and -not [string]::Equals($newShellWire, $targetCmp, [System.StringComparison]::OrdinalIgnoreCase)) {
        Write-Warning "wire.exe in a new terminal may resolve to $newShellWire before $target (machine PATH precedes user PATH). Move $Prefix earlier in PATH or remove the shadowing binary."
    }

    # 7. Stale-cleanup pass (best-effort; silently skipped on older binaries
    # that lack `upgrade --check`).
    try {
        & $target upgrade --check 2>$null | Out-Null
        if ($LASTEXITCODE -eq 0) {
            Write-Host 'running stale-cleanup pass (wire upgrade)...'
            & $target upgrade
            if ($LASTEXITCODE -ne 0) {
                Write-Warning 'wire upgrade returned non-zero; running daemons may need a manual restart'
            }
            Write-Host ''
        }
    } catch {
        # Older binaries without `upgrade --check` — silent skip.
    }

    Write-Host 'next steps:'
    Write-Host "  wire up                              # one-shot: identity + relay + claim your persona + daemon"
    Write-Host "  wire here                            # see your persona (handle == DID-derived name) + who's around"
    Write-Host "  wire dial <peer>@wireup.net          # pair a peer, then: wire send <peer> ""hi"""
    Write-Host "  wire session new --local-only        # per-project isolated identity (multi-agent box)"
    Write-Host "  wire session pair-all-local          # mesh-pair every sister"
    Write-Host ''
    Write-Host "see 'wire --help' or https://github.com/SlanchaAi/wire for more."
}