#!/usr/bin/env bash set -euo pipefail umask 022 shopt -s lastpipe 2>/dev/null || true VERSION="${VERSION:-}" OWNER="${OWNER:-Dicklesworthstone}" REPO="${REPO:-coding_agent_session_search}" FALLBACK_VERSION="${FALLBACK_VERSION:-}" DEST_DEFAULT="$HOME/.local/bin" DEST="${DEST:-$DEST_DEFAULT}" EASY=0 QUIET=0 VERIFY=0 QUICKSTART=0 FROM_SOURCE=0 CHECKSUM="${CHECKSUM:-}" CHECKSUM_URL="${CHECKSUM_URL:-}" ARTIFACT_URL="${ARTIFACT_URL:-}" LOCK_FILE="/tmp/coding-agent-search-install.lock" SYSTEM=0 log() { [ "$QUIET" -eq 1 ] && return 0; echo -e "$@"; } info() { log "\033[0;34m→\033[0m $*"; } ok() { log "\033[0;32m✓\033[0m $*"; } warn() { log "\033[1;33m⚠\033[0m $*"; } err() { log "\033[0;31m✗\033[0m $*"; } resolve_version() { if [ -n "$VERSION" ]; then return 0; fi local latest="" if command -v curl >/dev/null 2>&1; then # Try 1: Fetch latest release tag from GitHub API latest=$(curl -fsSL "https://api.github.com/repos/$OWNER/$REPO/releases/latest" 2>/dev/null \ | grep '"tag_name"' | head -1 | sed 's/.*"tag_name": *"\([^"]*\)".*/\1/') # Try 2: If no releases exist, fall back to latest git tag (sorted by version) if [ -z "$latest" ]; then warn "No GitHub releases found; falling back to latest git tag" latest=$(curl -fsSL "https://api.github.com/repos/$OWNER/$REPO/tags?per_page=1" 2>/dev/null \ | grep '"name"' | head -1 | sed 's/.*"name": *"\([^"]*\)".*/\1/') fi fi if [ -n "$latest" ]; then VERSION="$latest" info "Using latest version: $VERSION" elif [ -n "$FALLBACK_VERSION" ]; then VERSION="$FALLBACK_VERSION" info "Using fallback version: $VERSION" else err "Could not determine latest version. Pass --version explicitly." exit 1 fi } maybe_add_path() { case ":$PATH:" in *:"$DEST":*) return 0;; *) if [ "$EASY" -eq 1 ]; then UPDATED=0 for rc in "$HOME/.zshrc" "$HOME/.bashrc"; do if [ -e "$rc" ] && [ -w "$rc" ]; then if ! grep -F "$DEST" "$rc" >/dev/null 2>&1; then echo "export PATH=\"$DEST:\$PATH\"" >> "$rc" fi UPDATED=1 fi done if [ "$UPDATED" -eq 1 ]; then warn "PATH updated in ~/.zshrc/.bashrc; restart shell to use coding-agent-search" else warn "Add $DEST to PATH to use coding-agent-search" fi else warn "Add $DEST to PATH to use coding-agent-search" fi ;; esac } ensure_rust() { if [ "${RUSTUP_INIT_SKIP:-0}" != "0" ]; then info "Skipping rustup install (RUSTUP_INIT_SKIP set)" return 0 fi # Require Rust 1.85+ (edition 2024 support) or any future major version (2.x+) if command -v cargo >/dev/null 2>&1 && rustc --version 2>/dev/null | grep -qE 'rustc ([2-9]+|1\.(8[5-9]|9[0-9]|[1-9][0-9]{2,}))\.'; then return 0; fi if [ "$EASY" -ne 1 ]; then if [ -t 0 ]; then echo -n "Install Rust stable via rustup? (y/N): " read -r ans case "$ans" in y|Y) :;; *) warn "Skipping rustup install"; return 0;; esac fi fi info "Installing rustup (stable)" curl -fsSL https://sh.rustup.rs | sh -s -- -y --default-toolchain stable --profile minimal export PATH="$HOME/.cargo/bin:$PATH" rustup component add rustfmt clippy || true } usage() { cat </dev/null; then LOCKED=1 # Store PID for stale lock detection echo $$ > "$LOCK_DIR/pid" else # Check if existing lock is stale (process no longer running) if [ -f "$LOCK_DIR/pid" ]; then OLD_PID=$(cat "$LOCK_DIR/pid" 2>/dev/null || echo "") if [ -n "$OLD_PID" ] && ! kill -0 "$OLD_PID" 2>/dev/null; then # Stale lock, remove and retry rm -rf "$LOCK_DIR" if mkdir "$LOCK_DIR" 2>/dev/null; then LOCKED=1 echo $$ > "$LOCK_DIR/pid" fi fi fi if [ "$LOCKED" -eq 0 ]; then err "Another installer is running (lock $LOCK_DIR)" exit 1 fi fi cleanup() { rm -rf "$TMP" if [ "$LOCKED" -eq 1 ]; then rm -rf "$LOCK_DIR"; fi } TMP=$(mktemp -d) trap cleanup EXIT if [ "$FROM_SOURCE" -eq 0 ]; then info "Downloading $URL" if ! curl -fsSL "$URL" -o "$TMP/$TAR"; then warn "Artifact download failed; falling back to build-from-source" FROM_SOURCE=1 fi fi if [ "$FROM_SOURCE" -eq 1 ]; then info "Building from source (requires git, rust nightly)" ensure_rust git clone --depth 1 --branch "$VERSION" "https://github.com/${OWNER}/${REPO}.git" "$TMP/src" (cd "$TMP/src" && cargo build --release) BIN="$TMP/src/target/release/cass" [ -x "$BIN" ] || { err "Build failed"; exit 1; } install -m 0755 "$BIN" "$DEST" ok "Installed to $DEST/cass (source build)" maybe_add_path if [ "$VERIFY" -eq 1 ]; then "$DEST/cass" --version || true; ok "Self-test complete"; fi if [ "$QUICKSTART" -eq 1 ]; then info "Running index --full (quickstart)"; "$DEST/cass" index --full || warn "index --full failed"; fi ok "Done. Run: cass" exit 0 fi if [ -z "$CHECKSUM" ]; then [ -z "$CHECKSUM_URL" ] && CHECKSUM_URL="${URL}.sha256" info "Fetching checksum from ${CHECKSUM_URL}" CHECKSUM_FILE="$TMP/checksum.sha256" if ! curl -fsSL "$CHECKSUM_URL" -o "$CHECKSUM_FILE"; then err "Checksum required and could not be fetched"; exit 1; fi # Extract just the hash (first field) from the file CHECKSUM=$(awk '{print $1}' "$CHECKSUM_FILE") if [ -z "$CHECKSUM" ]; then err "Empty checksum file"; exit 1; fi fi echo "$CHECKSUM $TMP/$TAR" | sha256sum -c - || { err "Checksum mismatch"; exit 1; } ok "Checksum verified" info "Extracting" case "$TAR" in *.zip) unzip -q "$TMP/$TAR" -d "$TMP" ;; *.tar.gz) tar -xzf "$TMP/$TAR" -C "$TMP" ;; *.tar.xz) tar -xJf "$TMP/$TAR" -C "$TMP" ;; *) tar -xf "$TMP/$TAR" -C "$TMP" ;; esac BIN="$TMP/cass" if [ ! -x "$BIN" ] && [ -n "$TARGET" ]; then BIN="$TMP/cass-${TARGET}/cass" fi if [ ! -x "$BIN" ]; then BIN=$(find "$TMP" -maxdepth 3 -type f -name "cass" -perm -111 | head -n 1) fi # Check for Windows .exe if [ ! -x "$BIN" ] && [ -f "$TMP/cass.exe" ]; then BIN="$TMP/cass.exe" fi if [ ! -x "$BIN" ] && [ -n "$TARGET" ] && [ -f "$TMP/cass-${TARGET}/cass.exe" ]; then BIN="$TMP/cass-${TARGET}/cass.exe" fi # Fallback for older versions or if name mismatch? if [ ! -x "$BIN" ]; then BIN=$(find "$TMP" -maxdepth 3 -type f -name "coding-agent-search" -perm -111 | head -n 1) if [ -x "$BIN" ]; then warn "Found 'coding-agent-search' binary instead of 'cass'; installing as 'cass'" fi fi [ -x "$BIN" ] || { err "Binary not found in tar"; exit 1; } install -m 0755 "$BIN" "$DEST/cass" ok "Installed to $DEST/cass" maybe_add_path if [ "$VERIFY" -eq 1 ]; then "$DEST/cass" --version || true ok "Self-test complete" fi if [ "$QUICKSTART" -eq 1 ]; then info "Running index --full (quickstart)" "$DEST/cass" index --full || warn "index --full failed" fi ok "Done. Run: cass" info "Tip: If installed via Homebrew, update with: brew upgrade cass"