#!/usr/bin/env bash set -euo pipefail umask 022 VERSION="${VERSION:-}" OWNER="${OWNER:-Dicklesworthstone}" REPO="${REPO:-cass_memory_system}" DEST_DEFAULT="$HOME/.local/bin" DEST="${DEST:-$DEST_DEFAULT}" EASY=0 QUIET=0 VERIFY=0 FROM_SOURCE=0 CHECKSUM="${CHECKSUM:-}" CHECKSUM_URL="${CHECKSUM_URL:-}" ARTIFACT_URL="${ARTIFACT_URL:-}" LOCK_FILE="/tmp/cass-memory-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 info "Resolving latest version..." # ========================================================================= # Redirect-Based Version Resolution (GitHub Issue #11) # ========================================================================= # We use GitHub's redirect behavior instead of the API: # - NO RATE LIMITING: API limits to 60/hr; redirects have no limit # - NO JSON PARSING: API requires grep+sed which varies GNU/BSD # - SIMPLER FAILURES: Only fails if GitHub is completely down # # How it works: GitHub redirects /releases/latest → /releases/tag/{version} # We capture the final URL and extract the tag with shell parameter expansion # ========================================================================= local releases_url="https://github.com/${OWNER}/${REPO}/releases/latest" local final_url # Timeouts: 10s connect, 30s total (handles slow/flaky networks) # User-Agent: Identifies installer (GitHub may block empty UA) # -o /dev/null: Discard body, we only want the URL # -w '%{url_effective}': Capture final URL after redirects final_url="$(curl -fsSL \ --connect-timeout 10 \ --max-time 30 \ -A "cass-memory-installer/1.0" \ -o /dev/null \ -w '%{url_effective}' \ "$releases_url" 2>/dev/null || true)" # Extract tag: .../releases/tag/v0.2.1 → v0.2.1 local tag="${final_url##*/}" # Validate: URL must contain /releases/tag/ (proves redirect worked) # This catches: network errors, no releases, unexpected redirects if [ -n "$tag" ] && [[ "$final_url" == *"/releases/tag/"* ]]; then VERSION="$tag" info "Resolved latest version: $VERSION" else # Fallback to known-good version (update when releasing major versions) VERSION="v0.2.2" warn "Could not resolve latest version; defaulting to $VERSION" 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 cm" else warn "Add $DEST to PATH to use cm" fi else warn "Add $DEST to PATH to use cm" fi ;; esac } ensure_bun() { if command -v bun >/dev/null 2>&1; then return 0; fi if [ "$EASY" -ne 1 ]; then if [ -t 0 ]; then echo -n "Install Bun? (y/N): " read -r ans case "$ans" in y|Y) :;; *) warn "Skipping bun install"; return 1;; esac else # Non-interactive mode without --easy-mode: auto-install with warning warn "Non-interactive mode: auto-installing Bun (use --easy-mode to suppress this warning)" fi fi info "Installing Bun" curl -fsSL https://bun.sh/install | bash export PATH="$HOME/.bun/bin:$PATH" } usage() { cat </dev/null 2>&1; then exec 9>"$LOCK_FILE" || true if flock -n 9 2>/dev/null; then LOCKED=1 else err "Another installer is running (lock $LOCK_FILE)" exit 1 fi fi cleanup() { rm -rf "$TMP" if [ "$LOCKED" -eq 1 ]; then rm -f "$LOCK_FILE"; fi } TMP=$(mktemp -d) trap cleanup EXIT if [ "$FROM_SOURCE" -eq 0 ]; then info "Downloading $URL" if ! curl -fsSL "$URL" -o "$TMP/$ARTIFACT"; 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, bun)" ensure_bun || { err "Bun required for source build"; exit 1; } git clone --depth 1 "https://github.com/${OWNER}/${REPO}.git" "$TMP/src" (cd "$TMP/src" && bun install && bun run build) BIN="$TMP/src/dist/cass-memory" [ -x "$BIN" ] || { err "Build failed"; exit 1; } if [ "$SYSTEM" -eq 1 ]; then sudo install -m 0755 "$BIN" "$DEST/cm" else install -m 0755 "$BIN" "$DEST/cm" fi ok "Installed to $DEST/cm (source build)" maybe_add_path if [ "$VERIFY" -eq 1 ]; then if "$DEST/cm" --version; then ok "Self-test passed" else warn "Self-test failed (cm --version returned non-zero)" fi fi ok "Done. Run: cm --help" exit 0 fi # Checksum verification 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" 2>/dev/null; then CHECKSUM=$(awk '{print $1}' "$CHECKSUM_FILE") fi fi if [ -n "$CHECKSUM" ]; then # Use shasum on macOS, sha256sum on Linux if command -v sha256sum >/dev/null 2>&1; then echo "$CHECKSUM $TMP/$ARTIFACT" | sha256sum -c - || { err "Checksum mismatch"; exit 1; } ok "Checksum verified" elif command -v shasum >/dev/null 2>&1; then echo "$CHECKSUM $TMP/$ARTIFACT" | shasum -a 256 -c - || { err "Checksum mismatch"; exit 1; } ok "Checksum verified" else warn "No sha256 tool found; skipping checksum verification" fi else warn "No checksum available; skipping verification" fi BIN="$TMP/$ARTIFACT" [ -f "$BIN" ] || { err "Binary not found"; exit 1; } chmod +x "$BIN" if [ "$SYSTEM" -eq 1 ]; then sudo install -m 0755 "$BIN" "$DEST/cm" else install -m 0755 "$BIN" "$DEST/cm" fi ok "Installed to $DEST/cm" maybe_add_path if [ "$VERIFY" -eq 1 ]; then if "$DEST/cm" --version; then ok "Self-test passed" else warn "Self-test failed (cm --version returned non-zero)" fi fi ok "Done. Run: cm --help" info "Tip: For automatic updates, install via Homebrew: brew install dicklesworthstone/tap/cm"