#!/bin/sh # # Drover CLI Installation Script # Usage: curl -fsSL https://get.drover.run | sh # set -e # Configuration INSTALL_DIR="${INSTALL_DIR:-/usr/local/bin}" BINARY_NAME="drover" # The command the CLI shipped under before the rename. Installed as a symlink # beside the real binary so existing shell history, scripts and the cron lines # in our own docs keep working. LEGACY_BINARY_NAME="iot" # The release workflow publishes each artifact under its build basename, # `iot-cli--`, NOT `drover--`. These must stay separate: the # binary is installed as `drover`, but it is downloaded as `iot-cli-*`. The # artifact name is a distribution contract and the CLI's own self-update # resolves it by that name. ARTIFACT_NAME="iot-cli" API_BASE_URL="${API_BASE_URL:-https://api.iot.bader.solutions}" RELEASE_URL="${API_BASE_URL}/api/releases/cli" # Colors for output # Colours hold real escape characters, not the two-character sequence \033. # `echo -e` used to expand those; POSIX printf '%s' correctly does not, so they # are expanded once here instead. $(printf) is portable where `echo -e` is not. RED=$(printf '\033[0;31m') GREEN=$(printf '\033[0;32m') YELLOW=$(printf '\033[1;33m') NC=$(printf '\033[0m') # Print functions print_error() { printf '%s\n' "${RED}ERROR: $1${NC}" >&2 } print_success() { printf '%s\n' "${GREEN}$1${NC}" } print_warning() { printf '%s\n' "${YELLOW}$1${NC}" } print_info() { echo "$1" } # Parse command line arguments parse_args() { while [ $# -gt 0 ]; do case $1 in --install-dir=*) INSTALL_DIR="${1#*=}" shift ;; -h|--help) print_usage exit 0 ;; *) print_error "Unknown option: $1" print_usage exit 1 ;; esac done } print_usage() { echo "Usage: curl -fsSL https://get.drover.run | sh [-s -- [options]]" echo "" echo "Optional arguments:" echo " --install-dir= Installation directory (default: /usr/local/bin)" echo " -h, --help Show this help message" echo "" echo "Examples:" echo " curl -fsSL ${RELEASE_URL}/install.sh | bash" echo " curl -fsSL ${RELEASE_URL}/install.sh | bash -s -- --install-dir=\$HOME/.local/bin" } # Detect OS and architecture detect_platform() { OS=$(uname -s | tr '[:upper:]' '[:lower:]') ARCH=$(uname -m) case "$ARCH" in x86_64) ARCH="amd64" ;; aarch64|arm64) ARCH="arm64" ;; armv7l) ARCH="arm" ;; *) print_error "Unsupported architecture: $ARCH" exit 1 ;; esac case "$OS" in linux|darwin) # Supported ;; mingw*|msys*|cygwin*) OS="windows" ;; *) print_error "Unsupported operating system: $OS" exit 1 ;; esac PLATFORM="${OS}-${ARCH}" print_info "Detected platform: $PLATFORM" } # Check write permissions and handle sudo if needed check_permissions() { if [ ! -d "$INSTALL_DIR" ]; then print_info "Creating installation directory: $INSTALL_DIR" if ! mkdir -p "$INSTALL_DIR" 2>/dev/null; then if command -v sudo >/dev/null 2>&1; then sudo mkdir -p "$INSTALL_DIR" else print_error "Cannot create $INSTALL_DIR. Please run with sudo or specify a different directory." exit 1 fi fi fi if [ ! -w "$INSTALL_DIR" ]; then if command -v sudo >/dev/null 2>&1; then USE_SUDO="sudo" print_info "Using sudo for installation to $INSTALL_DIR" else print_error "No write permission to $INSTALL_DIR. Please run with sudo or specify a different directory." exit 1 fi else USE_SUDO="" fi } # Verify a downloaded artifact against its published .sha256. verify_checksum() { local file="$1" local sum_url="${DOWNLOAD_URL}.sha256" local expected actual sum_file sum_file=$(mktemp) if command -v curl >/dev/null 2>&1; then curl -fsSL "$sum_url" -o "$sum_file" 2>/dev/null || { rm -f "$sum_file"; print_warning "Checksum not available - skipping verification"; return 0; } elif command -v wget >/dev/null 2>&1; then wget -q "$sum_url" -O "$sum_file" 2>/dev/null || { rm -f "$sum_file"; print_warning "Checksum not available - skipping verification"; return 0; } else rm -f "$sum_file" return 0 fi # Published format: " " expected=$(cut -d' ' -f1 < "$sum_file") rm -f "$sum_file" [ -n "$expected" ] || { print_warning "Empty checksum - skipping verification"; return 0; } if command -v sha256sum >/dev/null 2>&1; then actual=$(sha256sum "$file" | cut -d' ' -f1) elif command -v shasum >/dev/null 2>&1; then actual=$(shasum -a 256 "$file" | cut -d' ' -f1) else print_warning "No sha256 tool found - skipping verification" return 0 fi if [ "$expected" != "$actual" ]; then print_error "Checksum mismatch! expected $expected, got $actual" rm -f "$file" return 1 fi print_info "Checksum verified." return 0 } # Download the CLI binary download_cli() { SUFFIX="" if [ "$OS" = "windows" ]; then SUFFIX=".exe" fi DOWNLOAD_URL="${RELEASE_URL}/latest/${ARTIFACT_NAME}-${PLATFORM}${SUFFIX}" DOWNLOAD_PATH="${INSTALL_DIR}/${BINARY_NAME}${SUFFIX}" TMP_PATH=$(mktemp) print_info "Downloading CLI from $DOWNLOAD_URL..." # Download binary to temp location if command -v curl >/dev/null 2>&1; then HTTP_CODE=$(curl -fsSL -w "%{http_code}" "$DOWNLOAD_URL" -o "$TMP_PATH") if [ "$HTTP_CODE" != "200" ]; then print_error "Failed to download CLI (HTTP $HTTP_CODE)" rm -f "$TMP_PATH" exit 1 fi elif command -v wget >/dev/null 2>&1; then wget -q "$DOWNLOAD_URL" -O "$TMP_PATH" || { print_error "Failed to download CLI" rm -f "$TMP_PATH" exit 1 } else print_error "Neither curl nor wget found. Please install one of them." exit 1 fi # Verify the published checksum before trusting the binary. The release # workflow uploads a .sha256 next to every artifact (`iot update` already # verifies it); this script did not, which for a `curl | bash` installer is # the one integrity check worth having. Skipped with a warning rather than # failed if the checksum can't be fetched, so a transient S3 hiccup doesn't # block an install. verify_checksum "$TMP_PATH" || exit 1 # Make executable chmod +x "$TMP_PATH" # Move to install directory $USE_SUDO mv "$TMP_PATH" "$DOWNLOAD_PATH" print_success "Downloaded CLI to $DOWNLOAD_PATH" install_legacy_symlink } # Keep `iot` working alongside `drover`. # # -f so re-running the installer replaces the symlink a previous run made, and # so an upgrade over a pre-rename install replaces the old real binary that sits # at this path. Failure here is a warning, not an error: the install itself # succeeded, and an unwritable symlink target must not fail the whole run. install_legacy_symlink() { local legacy_path="${INSTALL_DIR}/${LEGACY_BINARY_NAME}${SUFFIX}" if [ "$legacy_path" = "$DOWNLOAD_PATH" ]; then return 0 fi if $USE_SUDO ln -sf "$DOWNLOAD_PATH" "$legacy_path" 2>/dev/null; then print_info "Also linked ${LEGACY_BINARY_NAME} -> ${BINARY_NAME} (the old command still works)" else print_warning "Could not link ${legacy_path}; use ${BINARY_NAME} instead of ${LEGACY_BINARY_NAME}" fi } # Verify installation verify_installation() { if [ -x "${INSTALL_DIR}/${BINARY_NAME}" ]; then VERSION=$("${INSTALL_DIR}/${BINARY_NAME}" version 2>/dev/null | head -1 || echo "unknown") print_success "Installed: $VERSION" else print_error "Installation verification failed" exit 1 fi } # Check if CLI is in PATH check_path() { if ! command -v "$BINARY_NAME" >/dev/null 2>&1; then print_warning "Note: $INSTALL_DIR is not in your PATH" echo "" print_info "Add it to your PATH by running:" case "$(basename "$SHELL")" in zsh) print_info " echo 'export PATH=\"$INSTALL_DIR:\$PATH\"' >> ~/.zshrc && source ~/.zshrc" ;; bash) print_info " echo 'export PATH=\"$INSTALL_DIR:\$PATH\"' >> ~/.bashrc && source ~/.bashrc" ;; *) print_info " export PATH=\"$INSTALL_DIR:\$PATH\"" ;; esac echo "" fi } # Main installation flow main() { echo "" echo "========================================" echo " Drover CLI Installer" echo "========================================" echo "" parse_args "$@" detect_platform check_permissions download_cli verify_installation check_path echo "" print_success "Installation complete!" echo "" print_info "Get started:" print_info " ${BINARY_NAME} auth login Authenticate with your account" print_info " ${BINARY_NAME} device list List your devices" print_info " ${BINARY_NAME} --help Show all commands" echo "" } # Run main with all arguments main "$@"