#!/usr/bin/env bash # Zoro CLI installer # Binary installer for public GitHub Release assets. # Override the release repo with ZORO_RELEASE_REPO if needed. set -euo pipefail REPO="${ZORO_RELEASE_REPO:-cerdowizard/zero-cli}" INSTALL_DIR="${ZORO_INSTALL_DIR:-/usr/local/bin}" BIN_NAME="zoro" VERSION="${ZORO_VERSION:-}" # ── Colours ─────────────────────────────────────────────────────────────────── if [ -t 1 ]; then BOLD="\033[1m" DIM="\033[2m" GREEN="\033[32m" YELLOW="\033[33m" RED="\033[31m" RESET="\033[0m" else BOLD="" DIM="" GREEN="" YELLOW="" RED="" RESET="" fi info() { echo -e " ${DIM}info${RESET} $*"; } success() { echo -e " ${GREEN}✓${RESET} $*"; } warn() { echo -e " ${YELLOW}warn${RESET} $*"; } error() { echo -e " ${RED}error${RESET} $*" >&2; exit 1; } normalize_version() { printf '%s' "$1" | sed -E 's#^cli/v##; s#^v##' } url_exists() { local url="$1" if command -v curl &>/dev/null; then curl -fsSL --head "$url" &>/dev/null elif command -v wget &>/dev/null; then wget -q --spider "$url" else error "curl or wget is required." fi } resolve_download_url() { local tag asset candidate local -a tag_candidates=("v${VERSION}" "cli/v${VERSION}" "${VERSION}") local -a asset_candidates=("zoro-${VERSION}-${PLATFORM}-${ARCH_TAG}") if [[ "${VERSION}" != v* ]]; then asset_candidates+=("zoro-v${VERSION}-${PLATFORM}-${ARCH_TAG}") fi for tag in "${tag_candidates[@]}"; do for asset in "${asset_candidates[@]}"; do candidate="https://github.com/${REPO}/releases/download/${tag}/${asset}" if url_exists "$candidate"; then ASSET="$asset" DOWNLOAD_URL="$candidate" return 0 fi done done return 1 } # ── Arg parsing ─────────────────────────────────────────────────────────────── while [[ $# -gt 0 ]]; do case "$1" in --version|-v) VERSION="$2"; shift 2 ;; --dir) INSTALL_DIR="$2"; shift 2 ;; --help|-h) echo "Usage: install.sh [--version ] [--dir ]" exit 0 ;; *) error "Unknown argument: $1" ;; esac done if [ -n "$VERSION" ]; then VERSION="$(normalize_version "$VERSION")" fi # ── Detect OS / arch ────────────────────────────────────────────────────────── OS="$(uname -s)" ARCH="$(uname -m)" case "$OS" in Linux) PLATFORM="linux" ;; Darwin) PLATFORM="darwin" ;; *) error "Unsupported OS: $OS. Install manually from https://github.com/${REPO}/releases" ;; esac case "$ARCH" in x86_64|amd64) ARCH_TAG="x86_64" ;; arm64|aarch64) ARCH_TAG="arm64" ;; *) error "Unsupported architecture: $ARCH" ;; esac # ── Fetch latest version if not pinned ─────────────────────────────────────── if [ -z "$VERSION" ]; then info "Fetching latest release…" if command -v curl &>/dev/null; then VERSION="$(curl -fsSL "https://api.github.com/repos/${REPO}/releases/latest" \ | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/')" elif command -v wget &>/dev/null; then VERSION="$(wget -qO- "https://api.github.com/repos/${REPO}/releases/latest" \ | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/')" else error "curl or wget is required." fi VERSION="$(normalize_version "$VERSION")" [ -z "$VERSION" ] && error "Could not determine latest version." fi info "Version : ${BOLD}${VERSION}${RESET}" info "Platform : ${BOLD}${PLATFORM}-${ARCH_TAG}${RESET}" info "Install : ${BOLD}${INSTALL_DIR}/${BIN_NAME}${RESET}" info "Repo : ${BOLD}${REPO}${RESET}" # ── Build download URL ──────────────────────────────────────────────────────── ASSET="" DOWNLOAD_URL="" resolve_download_url || \ error "No matching binary release found for ${VERSION} (${PLATFORM}-${ARCH_TAG}). Check https://github.com/${REPO}/releases" info "Asset : ${BOLD}${ASSET}${RESET}" # ── Download ────────────────────────────────────────────────────────────────── TMP_DIR="$(mktemp -d)" TMP_BIN="${TMP_DIR}/${BIN_NAME}" trap 'rm -rf "$TMP_DIR"' EXIT info "Downloading…" if command -v curl &>/dev/null; then curl -fsSL --progress-bar "$DOWNLOAD_URL" -o "$TMP_BIN" || \ error "Download failed. Check https://github.com/${REPO}/releases for available assets." elif command -v wget &>/dev/null; then wget -q --show-progress "$DOWNLOAD_URL" -O "$TMP_BIN" || \ error "Download failed. Check https://github.com/${REPO}/releases for available assets." fi chmod +x "$TMP_BIN" # ── Verify checksum (if .sha256 is published) ───────────────────────────────── CHECKSUM_URL="${DOWNLOAD_URL}.sha256" if url_exists "$CHECKSUM_URL"; then if command -v curl &>/dev/null; then EXPECTED="$(curl -fsSL "$CHECKSUM_URL" | awk '{print $1}')" else EXPECTED="$(wget -qO- "$CHECKSUM_URL" | awk '{print $1}')" fi if command -v sha256sum &>/dev/null; then ACTUAL="$(sha256sum "$TMP_BIN" | awk '{print $1}')" elif command -v shasum &>/dev/null; then ACTUAL="$(shasum -a 256 "$TMP_BIN" | awk '{print $1}')" fi if [ "${EXPECTED}" != "${ACTUAL}" ]; then error "Checksum mismatch — download may be corrupted." fi info "Checksum verified." fi # ── Install ─────────────────────────────────────────────────────────────────── if [ ! -d "$INSTALL_DIR" ]; then mkdir -p "$INSTALL_DIR" 2>/dev/null || \ error "Cannot create ${INSTALL_DIR}. Try: sudo ZORO_INSTALL_DIR=/usr/local/bin bash install.sh" fi if [ -w "$INSTALL_DIR" ]; then mv "$TMP_BIN" "${INSTALL_DIR}/${BIN_NAME}" else warn "${INSTALL_DIR} requires elevated permissions — trying sudo…" sudo mv "$TMP_BIN" "${INSTALL_DIR}/${BIN_NAME}" fi # ── Verify ──────────────────────────────────────────────────────────────────── if command -v zoro &>/dev/null; then INSTALLED_VERSION="$(zoro --version 2>/dev/null | head -1 || true)" success "Installed ${BOLD}zoro ${VERSION}${RESET}${INSTALLED_VERSION:+ ($(zoro --version 2>/dev/null | head -1))}" else success "Installed to ${INSTALL_DIR}/${BIN_NAME}" warn "${INSTALL_DIR} is not in your PATH." echo "" echo " Add it with:" echo " export PATH=\"\$PATH:${INSTALL_DIR}\"" fi echo "" echo -e " ${BOLD}Get started:${RESET}" echo " zoro login --token # copy from Settings → API Keys" echo " zoro --help" echo ""