sparrow-cli 0.10.1

A local-first Rust agent cockpit — route, run, replay, rewind
# Sparrow one-click installer for Windows.
#
# By default this installs the binary and STOPS — it does not launch the agent.
# Run `sparrow launch` yourself when ready, or pass -Launch to start at the end.
#
# Usage:
#   irm https://raw.githubusercontent.com/ucav/Sparrow/master/install.ps1 | iex
#   iex "& { $(irm .../install.ps1) } -Launch"            # opt in to auto-launch
#   iex "& { $(irm .../install.ps1) } -FromSource"        # build from git
#   iex "& { $(irm .../install.ps1) } -AllowUnverified"   # skip checksum (NOT recommended)

[CmdletBinding()]
param(
    [string]$InstallDir = "$env:LOCALAPPDATA\Sparrow\bin",
    [switch]$Launch,            # default: do NOT auto-launch
    [switch]$NoLaunch,          # back-compat no-op (no-launch is already the default)
    [switch]$NoShortcut,
    [switch]$FromSource,
    [switch]$AllowUnverified    # install even without a SHA256 checksum
)

$ErrorActionPreference = "Stop"
$Repo = "ucav/Sparrow"
$Artifact = "sparrow-windows-x86_64.exe"
$BinaryPath = Join-Path $InstallDir "sparrow.exe"

function Add-UserPath {
    param([string]$PathToAdd)

    $userPath = [Environment]::GetEnvironmentVariable("Path", "User")
    if ([string]::IsNullOrWhiteSpace($userPath)) {
        $userPath = ""
    }

    $parts = $userPath -split ';' | Where-Object { $_ -ne "" }
    if ($parts -notcontains $PathToAdd) {
        $next = if ($userPath.Trim().Length -eq 0) { $PathToAdd } else { "$userPath;$PathToAdd" }
        [Environment]::SetEnvironmentVariable("Path", $next, "User")
        $env:Path = "$env:Path;$PathToAdd"
        Write-Host "Added Sparrow to your user PATH."
    }
}

function Install-FromSource {
    if (-not (Get-Command cargo -ErrorAction SilentlyContinue)) {
        throw "Source install requires Rust/Cargo. Install Rust from https://rustup.rs or run without -FromSource."
    }
    if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
        throw "Source install requires Git."
    }

    $tmp = Join-Path ([System.IO.Path]::GetTempPath()) ("sparrow-install-" + [guid]::NewGuid())
    git clone --depth 1 "https://github.com/$Repo.git" $tmp
    Push-Location $tmp
    try {
        cargo build --release
        New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null
        Copy-Item -Force (Join-Path $tmp "target\release\sparrow.exe") $BinaryPath
    } finally {
        Pop-Location
        Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
    }
}

# Returns: 'ok' (verified), 'mismatch' (tampering), or 'absent' (no checksum).
function Test-ReleaseChecksum {
    param([string]$BinFile)

    $sumsUrl = "https://github.com/$Repo/releases/latest/download/$Artifact.sha256"
    $sumsFile = "$BinFile.sha256"
    try {
        Invoke-WebRequest -Uri $sumsUrl -OutFile $sumsFile -UseBasicParsing -ErrorAction Stop
    } catch {
        return 'absent'
    }

    try {
        $have = (Get-FileHash -Algorithm SHA256 -Path $BinFile).Hash.ToLower()
        $text = (Get-Content -Raw $sumsFile).ToLower()
        Remove-Item -Force $sumsFile -ErrorAction SilentlyContinue
        if ($text -match [regex]::Escape($have)) { return 'ok' } else { return 'mismatch' }
    } catch {
        Remove-Item -Force $sumsFile -ErrorAction SilentlyContinue
        return 'absent'
    }
}

function Install-FromRelease {
    $url = "https://github.com/$Repo/releases/latest/download/$Artifact"
    $tmp = "$BinaryPath.tmp"
    New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null

    Write-Host "Downloading Sparrow release artifact: $Artifact"
    try {
        Invoke-WebRequest -Uri $url -OutFile $tmp -UseBasicParsing
    } catch {
        Remove-Item -Force $tmp -ErrorAction SilentlyContinue
        Write-Warning "Release artifact unavailable. Falling back to source build."
        Install-FromSource
        return
    }

    if (-not $AllowUnverified) {
        $verdict = Test-ReleaseChecksum -BinFile $tmp
        if ($verdict -eq 'mismatch') {
            Remove-Item -Force $tmp -ErrorAction SilentlyContinue
            throw "SHA256 MISMATCH for $Artifact - refusing to install a possibly tampered binary. Re-run with -FromSource."
        } elseif ($verdict -eq 'absent') {
            Remove-Item -Force $tmp -ErrorAction SilentlyContinue
            Write-Warning "No SHA256 checksum available for $Artifact - cannot verify integrity."
            Write-Warning "Falling back to a source build (safer than an unverified binary). Pass -AllowUnverified to override."
            Install-FromSource
            return
        }
        Write-Host "SHA256 verified."
    }

    Move-Item -Force $tmp $BinaryPath
}

function New-SparrowShortcut {
    if ($NoShortcut) {
        return
    }
    try {
        $desktop = [Environment]::GetFolderPath("Desktop")
        if ([string]::IsNullOrWhiteSpace($desktop)) {
            return
        }
        $shortcutPath = Join-Path $desktop "Sparrow.lnk"
        $shell = New-Object -ComObject WScript.Shell
        $shortcut = $shell.CreateShortcut($shortcutPath)
        $shortcut.TargetPath = $BinaryPath
        $shortcut.WorkingDirectory = [Environment]::GetFolderPath("UserProfile")
        $shortcut.Description = "Open Sparrow"
        $shortcut.Save()
        Write-Host "Desktop shortcut created: $shortcutPath"
    } catch {
        Write-Warning "Could not create desktop shortcut: $($_.Exception.Message)"
    }
}

Write-Host "Installing Sparrow into $InstallDir"
if ($FromSource) {
    Install-FromSource
} else {
    Install-FromRelease
}

Add-UserPath -PathToAdd $InstallDir
New-SparrowShortcut

Write-Host ""
Write-Host "Sparrow installed: $BinaryPath"
Write-Host "Next command: sparrow launch"

if ($Launch) {
    Write-Host ""
    Write-Host "Launching Sparrow WebView cockpit..."
    & $BinaryPath launch
}