From 03b45013a69fc3167958938297df5f81494b18c2 Mon Sep 17 00:00:00 2001 From: Nikhil Date: Thu, 26 Mar 2026 17:05:21 -0700 Subject: [PATCH] feat(cli): add install scripts for macOS, Linux, and Windows (#587) * feat(cli): add install scripts for macOS, Linux, and Windows Bash script (install.sh) for macOS/Linux and PowerShell script (install.ps1) for Windows. Both download the correct platform binary from GitHub Releases with checksum verification, version resolution, and PATH setup. * fix(cli): address PR review comments for install scripts - Add checksum verification to install.ps1 using Get-FileHash - Add warnings on all checksum skip paths in install.sh - Use grep -F (fixed-string) instead of regex for filename matching - Add ?per_page=100 to GitHub API call in install.ps1 - Use random temp directory name in install.ps1 to avoid collisions * fix(cli): address installer review feedback --- .../apps/cli/scripts/install.ps1 | 147 +++++++++++++++++ .../apps/cli/scripts/install.sh | 153 ++++++++++++++++++ 2 files changed, 300 insertions(+) create mode 100644 packages/browseros-agent/apps/cli/scripts/install.ps1 create mode 100755 packages/browseros-agent/apps/cli/scripts/install.sh diff --git a/packages/browseros-agent/apps/cli/scripts/install.ps1 b/packages/browseros-agent/apps/cli/scripts/install.ps1 new file mode 100644 index 000000000..601e5d1b2 --- /dev/null +++ b/packages/browseros-agent/apps/cli/scripts/install.ps1 @@ -0,0 +1,147 @@ +# +# Install browseros-cli for Windows — downloads the latest release binary. +# +# Usage (PowerShell — save and run): +# Invoke-WebRequest -Uri "https://raw.githubusercontent.com/browseros-ai/BrowserOS/main/packages/browseros-agent/apps/cli/scripts/install.ps1" -OutFile install.ps1 +# .\install.ps1 +# .\install.ps1 -Version "0.1.0" -Dir "C:\tools\browseros" +# +# Usage (one-liner, uses env vars for options): +# & { $env:BROWSEROS_VERSION="0.1.0"; irm https://raw.githubusercontent.com/browseros-ai/BrowserOS/main/packages/browseros-agent/apps/cli/scripts/install.ps1 | iex } +# + +param( + [string]$Version = "", + [string]$Dir = "" +) + +$ErrorActionPreference = "Stop" + +# TLS 1.2 — required for GitHub, older PS 5.1 defaults to TLS 1.0 +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + +$Repo = "browseros-ai/BrowserOS" +$Binary = "browseros-cli" + +# When piped via irm | iex, param() is ignored — fall back to env vars +if (-not $Version) { $Version = $env:BROWSEROS_VERSION } +if (-not $Dir) { $Dir = if ($env:BROWSEROS_DIR) { $env:BROWSEROS_DIR } else { "$env:LOCALAPPDATA\browseros-cli\bin" } } + +# ── Resolve latest version ─────────────────────────────────────────────────── + +if (-not $Version) { + Write-Host "Fetching latest version..." + $releases = Invoke-RestMethod "https://api.github.com/repos/$Repo/releases?per_page=100" + $tag = ($releases ` + | Where-Object { $_.tag_name -match "^browseros-cli-v" -and $_.tag_name -notmatch "-rc" } ` + | Select-Object -First 1).tag_name + if (-not $tag) { + Write-Error "Could not determine latest version. Try: -Version 0.1.0" + exit 1 + } + $Version = $tag -replace "^browseros-cli-v", "" +} + +Write-Host "Installing browseros-cli v$Version..." + +# ── Detect architecture ────────────────────────────────────────────────────── + +# $env:PROCESSOR_ARCHITECTURE lies under x64 emulation on ARM64 Windows. +# Use .NET RuntimeInformation when available, fall back to PROCESSOR_ARCHITEW6432. +$Arch = "amd64" +try { + $osArch = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture + if ($osArch -eq [System.Runtime.InteropServices.Architecture]::Arm64) { $Arch = "arm64" } +} catch { + if ($env:PROCESSOR_ARCHITEW6432 -eq "ARM64" -or $env:PROCESSOR_ARCHITECTURE -eq "ARM64") { + $Arch = "arm64" + } +} + +if (-not [Environment]::Is64BitOperatingSystem) { + Write-Error "32-bit Windows is not supported." + exit 1 +} + +# ── Download and extract ───────────────────────────────────────────────────── + +$Tag = "browseros-cli-v$Version" +$Filename = "${Binary}_${Version}_windows_${Arch}.zip" +$Url = "https://github.com/$Repo/releases/download/$Tag/$Filename" +$ChecksumUrl = "https://github.com/$Repo/releases/download/$Tag/checksums.txt" +$TmpDir = Join-Path ([System.IO.Path]::GetTempPath()) ("browseros-cli-install-" + [System.IO.Path]::GetRandomFileName()) + +try { + New-Item -ItemType Directory -Path $TmpDir | Out-Null + + $ZipPath = Join-Path $TmpDir $Filename + + Write-Host "Downloading $Url..." + Invoke-WebRequest -Uri $Url -OutFile $ZipPath -UseBasicParsing + + $ChecksumPath = Join-Path $TmpDir "checksums.txt" + $ChecksumAvailable = $true + try { + Invoke-WebRequest -Uri $ChecksumUrl -OutFile $ChecksumPath -UseBasicParsing + } catch { + $ChecksumAvailable = $false + Write-Warning "Could not fetch checksums.txt; skipping checksum verification. $($_.Exception.Message)" + } + + if ($ChecksumAvailable) { + $ExpectedChecksum = $null + foreach ($line in Get-Content $ChecksumPath) { + $parts = $line -split '\s+', 2 + if ($parts.Length -eq 2 -and $parts[1] -eq $Filename) { + $ExpectedChecksum = $parts[0].ToLowerInvariant() + break + } + } + + if ($ExpectedChecksum) { + $ActualChecksum = (Get-FileHash -Path $ZipPath -Algorithm SHA256).Hash.ToLowerInvariant() + if ($ActualChecksum -ne $ExpectedChecksum) { + Write-Error "Checksum mismatch (expected $ExpectedChecksum, got $ActualChecksum)" + exit 1 + } + Write-Host "Checksum verified." + } else { + Write-Warning "Checksum not found in checksums.txt; skipping checksum verification." + } + } + + Expand-Archive -Path $ZipPath -DestinationPath $TmpDir -Force + + $Exe = Get-ChildItem -Path $TmpDir -Filter "$Binary.exe" -File -Recurse | Select-Object -First 1 + if (-not $Exe) { + Write-Error "Binary not found in archive." + exit 1 + } + + # ── Install ────────────────────────────────────────────────────────────── + + if (-not (Test-Path $Dir)) { + New-Item -ItemType Directory -Path $Dir -Force | Out-Null + } + + Move-Item -Force $Exe.FullName (Join-Path $Dir "$Binary.exe") + + Write-Host "Installed $Binary.exe to $Dir" +} finally { + if (Test-Path $TmpDir) { Remove-Item -Recurse -Force $TmpDir -ErrorAction SilentlyContinue } +} + +# ── PATH ───────────────────────────────────────────────────────────────────── + +$UserPath = [Environment]::GetEnvironmentVariable("Path", "User") +$PathEntries = $UserPath -split ";" | Where-Object { $_ -ne "" } +if ($Dir -notin $PathEntries) { + Write-Host "" + Write-Host "Adding $Dir to your user PATH..." + [Environment]::SetEnvironmentVariable("Path", "$Dir;$UserPath", "User") + $env:Path = "$Dir;$env:Path" + Write-Host "Done. Restart your terminal for PATH changes to take effect." +} + +Write-Host "" +Write-Host "Run 'browseros-cli --help' to get started." diff --git a/packages/browseros-agent/apps/cli/scripts/install.sh b/packages/browseros-agent/apps/cli/scripts/install.sh new file mode 100755 index 000000000..d0e37fccb --- /dev/null +++ b/packages/browseros-agent/apps/cli/scripts/install.sh @@ -0,0 +1,153 @@ +#!/usr/bin/env bash +# +# Install browseros-cli — downloads the latest release binary for your platform. +# +# Usage: +# curl -fsSL https://raw.githubusercontent.com/browseros-ai/BrowserOS/main/packages/browseros-agent/apps/cli/scripts/install.sh | bash +# +# # Or with options: +# curl -fsSL ... | bash -s -- --version 0.1.0 --dir /usr/local/bin + +set -euo pipefail + +REPO="browseros-ai/BrowserOS" +BINARY="browseros-cli" +INSTALL_DIR="${HOME}/.browseros/bin" + +# ── Parse arguments ────────────────────────────────────────────────────────── + +VERSION="" +CUSTOM_DIR="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --version) + [[ $# -lt 2 ]] && { echo "Error: --version requires a value" >&2; exit 1; } + VERSION="$2"; shift 2 ;; + --dir) + [[ $# -lt 2 ]] && { echo "Error: --dir requires a value" >&2; exit 1; } + CUSTOM_DIR="$2"; shift 2 ;; + --help) + echo "Usage: install.sh [--version VERSION] [--dir INSTALL_DIR]" + echo "" + echo " --version Install a specific version (default: latest)" + echo " --dir Install directory (default: ~/.browseros/bin)" + exit 0 + ;; + *) echo "Unknown option: $1" >&2; exit 1 ;; + esac +done + +[[ -n "$CUSTOM_DIR" ]] && INSTALL_DIR="$CUSTOM_DIR" + +# ── Resolve latest version ─────────────────────────────────────────────────── + +if [[ -z "$VERSION" ]]; then + # Use per_page=1 with a tag name filter via the releases endpoint. + # The tags all start with "browseros-cli-v" so we grab page 1 of those. + VERSION=$(curl -fsSL "https://api.github.com/repos/${REPO}/releases?per_page=100" \ + | grep -o '"tag_name": *"browseros-cli-v[^"]*"' \ + | grep -v -- "-rc" \ + | head -1 \ + | sed 's/.*browseros-cli-v//; s/"//') + + if [[ -z "$VERSION" ]]; then + echo "Error: could not determine latest version." >&2 + echo " Try: install.sh --version 0.1.0" >&2 + exit 1 + fi +fi + +echo "Installing browseros-cli v${VERSION}..." + +# ── Detect platform ────────────────────────────────────────────────────────── + +OS=$(uname -s | tr '[:upper:]' '[:lower:]') +ARCH=$(uname -m) + +case "$OS" in + darwin) OS="darwin" ;; + linux) OS="linux" ;; + *) echo "Error: unsupported OS: $OS" >&2; exit 1 ;; +esac + +case "$ARCH" in + x86_64|amd64) ARCH="amd64" ;; + arm64|aarch64) ARCH="arm64" ;; + *) echo "Error: unsupported architecture: $ARCH" >&2; exit 1 ;; +esac + +# ── Download and extract ───────────────────────────────────────────────────── + +FILENAME="${BINARY}_${VERSION}_${OS}_${ARCH}.tar.gz" +TAG="browseros-cli-v${VERSION}" +URL="https://github.com/${REPO}/releases/download/${TAG}/${FILENAME}" +CHECKSUM_URL="https://github.com/${REPO}/releases/download/${TAG}/checksums.txt" + +TMPDIR_DL=$(mktemp -d) +trap 'rm -rf "$TMPDIR_DL"' EXIT + +echo "Downloading ${URL}..." +curl -fSL --progress-bar -o "${TMPDIR_DL}/${FILENAME}" "$URL" + +# Verify checksum if sha256sum/shasum is available +if curl -fsSL -o "${TMPDIR_DL}/checksums.txt" "$CHECKSUM_URL" 2>/dev/null; then + expected=$(awk -v filename="$FILENAME" '$2 == filename { print $1; exit }' "${TMPDIR_DL}/checksums.txt") + if [[ -n "$expected" ]]; then + if command -v sha256sum >/dev/null 2>&1; then + actual=$(sha256sum "${TMPDIR_DL}/${FILENAME}" | awk '{print $1}') + elif command -v shasum >/dev/null 2>&1; then + actual=$(shasum -a 256 "${TMPDIR_DL}/${FILENAME}" | awk '{print $1}') + else + actual="" + echo "Warning: no sha256sum/shasum found; skipping checksum verification." >&2 + fi + if [[ -n "$actual" && "$actual" != "$expected" ]]; then + echo "Error: checksum mismatch (expected ${expected}, got ${actual})" >&2 + exit 1 + fi + [[ -n "$actual" ]] && echo "Checksum verified." + else + echo "Warning: checksum not found in checksums.txt; skipping verification." >&2 + fi +else + echo "Warning: could not fetch checksums.txt; skipping checksum verification." >&2 +fi + +tar -xzf "${TMPDIR_DL}/${FILENAME}" -C "$TMPDIR_DL" + +BINARY_PATH="${TMPDIR_DL}/${BINARY}" +if [[ ! -f "$BINARY_PATH" ]]; then + BINARY_PATH=$(find "$TMPDIR_DL" -type f -name "$BINARY" -print -quit) +fi + +if [[ -z "$BINARY_PATH" || ! -f "$BINARY_PATH" ]]; then + echo "Error: binary not found in archive." >&2 + exit 1 +fi + +# ── Install ────────────────────────────────────────────────────────────────── + +mkdir -p "$INSTALL_DIR" +mv "$BINARY_PATH" "${INSTALL_DIR}/${BINARY}" +chmod +x "${INSTALL_DIR}/${BINARY}" + +echo "Installed ${BINARY} to ${INSTALL_DIR}/${BINARY}" + +# ── PATH hint ──────────────────────────────────────────────────────────────── + +if ! echo "$PATH" | tr ':' '\n' | grep -qx "$INSTALL_DIR"; then + echo "" + echo "Add browseros-cli to your PATH:" + echo "" + + SHELL_NAME=$(basename "${SHELL:-/bin/bash}") + case "$SHELL_NAME" in + zsh) echo " echo 'export PATH=\"${INSTALL_DIR}:\$PATH\"' >> ~/.zshrc && source ~/.zshrc" ;; + fish) echo " fish_add_path ${INSTALL_DIR}" ;; + *) echo " echo 'export PATH=\"${INSTALL_DIR}:\$PATH\"' >> ~/.bashrc && source ~/.bashrc" ;; + esac +fi + +echo "" +echo "Run 'browseros-cli --help' to get started."