#!/usr/bin/env bash # # GoodMem Installer # # Downloads the GoodMem CLI binary and runs 'goodmem system install' with optional arguments. # All arguments are forwarded directly to the CLI installer. # # Usage: curl -s https://get.goodmem.ai | bash [-s -- [destination_dir] [installer_flags...]] # # Examples: # # Interactive installation (default) # curl -s https://get.goodmem.ai | bash # # # Non-interactive with required flags # curl -s https://get.goodmem.ai | bash -s -- --handsfree --db-password "my-secure-password" # # # Custom installation directory # curl -s https://get.goodmem.ai | bash -s -- /usr/local/bin # # # Remote database configuration # curl -s https://get.goodmem.ai | bash -s -- --handsfree --remote-db \ # --db-url "postgresql://user:pass@host:5432/goodmem" \ # --db-user myuser --db-password "my-password" # # # Install CLI only (skip GoodMem system installation) # curl -s https://get.goodmem.ai | bash -s -- --cli-only # # # See all available options # goodmem system install --help set -euo pipefail # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Base URL for downloads BASE_URL="https://get.goodmem.ai/tag/latest" # Parse arguments # First argument (if not a flag) is destination directory # All remaining arguments are passed to goodmem system install DEST_DIR="" INSTALL_ARGS=() CLI_ONLY=false # Check for --cli-only flag first for arg in "$@"; do if [ "$arg" = "--cli-only" ]; then CLI_ONLY=true break fi done if [ $# -gt 0 ] && [[ "$1" != --* ]]; then DEST_DIR="$1" shift fi # Remaining arguments go to the installer (excluding --cli-only) for arg in "$@"; do if [ "$arg" != "--cli-only" ]; then INSTALL_ARGS+=("$arg") fi done # Set default destination if not specified # We'll try /usr/local/bin first, then fall back to ~/.local/bin DEST_DIR_SPECIFIED=true if [ -z "$DEST_DIR" ]; then DEST_DIR_SPECIFIED=false DEST_DIR="/usr/local/bin" fi # Check if --no-sudo flag is present early (needed for binary installation) USE_SUDO=true for arg in "${INSTALL_ARGS[@]+"${INSTALL_ARGS[@]}"}"; do if [ "$arg" = "--no-sudo" ]; then USE_SUDO=false break fi done # Set sudo command # Preserve GHCR_* environment variables for Docker authentication SUDO_CMD="" if [ "$USE_SUDO" = true ] && command -v sudo >/dev/null 2>&1; then if [ -n "${GHCR_USER:-}" ] || [ -n "${GHCR_PASSWORD:-}" ]; then SUDO_CMD="sudo --preserve-env=GHCR_USER,GHCR_PASSWORD" else SUDO_CMD="sudo" fi fi # Function to print colored messages info() { echo -e "${GREEN}[INFO]${NC} $*" } warn() { echo -e "${YELLOW}[WARN]${NC} $*" } error() { echo -e "${RED}[ERROR]${NC} $*" >&2 } notice() { echo -e "${BLUE}[NOTICE]${NC} $*" } # Detect operating system detect_os() { local os os="$(uname -s)" case "$os" in Linux*) echo "linux" ;; Darwin*) echo "darwin" ;; MINGW*|MSYS*|CYGWIN*) echo "windows" ;; *) error "Unsupported operating system: $os" exit 1 ;; esac } # Detect architecture detect_arch() { local arch arch="$(uname -m)" case "$arch" in x86_64|amd64) echo "amd64" ;; aarch64|arm64) echo "arm64" ;; *) error "Unsupported architecture: $arch" exit 1 ;; esac } # Build the download filename build_filename() { local os="$1" local arch="$2" local filename="goodmem-${os}-${arch}" if [ "$os" = "windows" ]; then filename="${filename}.exe" fi echo "${filename}.tar.gz" } # Get binary name from tar.gz filename get_binary_name() { local filename="$1" # Remove .tar.gz extension to get binary name echo "${filename%.tar.gz}" } # Check if directory is in PATH check_path() { local dir="$1" if [[ ":$PATH:" == *":$dir:"* ]]; then return 0 else return 1 fi } # Main execution main() { info "Detecting platform..." local os local arch local filename local binary_name local url local download_path local temp_dir local licenses_file="THIRD_PARTY_LICENSES.html" os="$(detect_os)" arch="$(detect_arch)" info "Platform: ${os}-${arch}" filename="$(build_filename "$os" "$arch")" binary_name="$(get_binary_name "$filename")" url="${BASE_URL}/${filename}" download_path="/tmp/${filename}" temp_dir="/tmp/goodmem-extract-$$" # Final installed binary name (simple "goodmem" regardless of platform) local final_binary_name="goodmem" if [ "$os" = "windows" ]; then final_binary_name="goodmem.exe" fi # Check for E2E testing override: use local CLI binary instead of downloading local use_local_cli=false if [ -n "${GOODMEM_INSTALLER_CLI_PATH:-}" ]; then warn "E2E mode: Using local CLI binary from ${GOODMEM_INSTALLER_CLI_PATH}" # Validate the local binary exists if [ ! -f "$GOODMEM_INSTALLER_CLI_PATH" ]; then error "Local CLI binary not found at: ${GOODMEM_INSTALLER_CLI_PATH}" exit 1 fi use_local_cli=true fi # Skip download and extraction if using local CLI if [ "$use_local_cli" = false ]; then info "Downloading from: ${url}" # Create temporary extraction directory first mkdir -p "$temp_dir" # Download the file if command -v curl >/dev/null 2>&1; then if curl -fsSL -o "$download_path" "$url"; then info "Download completed successfully" else error "Download failed" rm -rf "$temp_dir" exit 1 fi elif command -v wget >/dev/null 2>&1; then if wget -q -O "$download_path" "$url"; then info "Download completed successfully" else error "Download failed" rm -rf "$temp_dir" exit 1 fi else error "Neither curl nor wget found. Please install one of them." rm -rf "$temp_dir" exit 1 fi # Extract the tar.gz file to temp directory info "Extracting ${filename}..." if ! tar -xzf "$download_path" -C "$temp_dir"; then error "Extraction failed" rm -rf "$temp_dir" exit 1 fi info "Extraction completed successfully" # Verify the binary exists in the archive if [ ! -f "${temp_dir}/${binary_name}" ]; then error "Binary not found in archive: ${binary_name}" rm -rf "$temp_dir" exit 1 fi # Try to install to the destination directory # If we're using the default and /usr/local/bin fails, fall back to ~/.local/bin install_success=false while [ "$install_success" = false ]; do # Try to create destination directory if [ ! -d "$DEST_DIR" ]; then info "Creating destination directory: ${DEST_DIR}" if $SUDO_CMD mkdir -p "$DEST_DIR" 2>/dev/null; then : # Success else # Failed to create directory if [ "$DEST_DIR_SPECIFIED" = false ] && [ "$DEST_DIR" = "/usr/local/bin" ]; then warn "Cannot create ${DEST_DIR}, falling back to ${HOME}/.local/bin" DEST_DIR="${HOME}/.local/bin" continue else error "Failed to create destination directory: ${DEST_DIR}" rm -rf "$temp_dir" exit 1 fi fi fi # Try to move the binary to destination if $SUDO_CMD mv "${temp_dir}/${binary_name}" "${DEST_DIR}/${final_binary_name}" 2>/dev/null; then info "Binary installed to ${DEST_DIR}/${final_binary_name}" install_success=true else # Failed to install binary if [ "$DEST_DIR_SPECIFIED" = false ] && [ "$DEST_DIR" = "/usr/local/bin" ]; then warn "Cannot write to ${DEST_DIR}, falling back to ${HOME}/.local/bin" DEST_DIR="${HOME}/.local/bin" continue else error "Failed to install binary to ${DEST_DIR}/${final_binary_name}" rm -rf "$temp_dir" exit 1 fi fi done # Check for third party licenses file if [ -f "${temp_dir}/${licenses_file}" ]; then notice "Third party licenses available at: ${temp_dir}/${licenses_file}" notice "The licenses file will remain at the above location for your review" fi # Make the binary executable (if not Windows) if [ "$os" != "windows" ] && [ -f "${DEST_DIR}/${final_binary_name}" ]; then $SUDO_CMD chmod +x "${DEST_DIR}/${final_binary_name}" info "Made binary executable" fi info "GoodMem CLI installed to: ${DEST_DIR}/${final_binary_name}" # Check if destination is in PATH if ! check_path "$DEST_DIR"; then warn "Directory ${DEST_DIR} is not in your PATH" warn "Add it to your PATH by adding this line to your shell profile:" warn " export PATH=\"\$PATH:${DEST_DIR}\"" fi # Show version if binary is accessible if [ -f "${DEST_DIR}/${final_binary_name}" ]; then info "Verifying installation..." "${DEST_DIR}/${final_binary_name}" version 2>/dev/null || warn "Could not verify version" fi else # Using local CLI binary - copy it to destination info "Installing local CLI binary..." # Create temp dir for consistency mkdir -p "$temp_dir" install_success=false while [ "$install_success" = false ]; do # Try to create destination directory if [ ! -d "$DEST_DIR" ]; then info "Creating destination directory: ${DEST_DIR}" if $SUDO_CMD mkdir -p "$DEST_DIR" 2>/dev/null; then : # Success else # Failed to create directory if [ "$DEST_DIR_SPECIFIED" = false ] && [ "$DEST_DIR" = "/usr/local/bin" ]; then warn "Cannot create ${DEST_DIR}, falling back to ${HOME}/.local/bin" DEST_DIR="${HOME}/.local/bin" continue else error "Failed to create destination directory: ${DEST_DIR}" rm -rf "$temp_dir" exit 1 fi fi fi # Copy the local binary to destination if $SUDO_CMD cp "$GOODMEM_INSTALLER_CLI_PATH" "${DEST_DIR}/${final_binary_name}" 2>/dev/null; then info "Binary installed to ${DEST_DIR}/${final_binary_name}" install_success=true else # Failed to install binary if [ "$DEST_DIR_SPECIFIED" = false ] && [ "$DEST_DIR" = "/usr/local/bin" ]; then warn "Cannot write to ${DEST_DIR}, falling back to ${HOME}/.local/bin" DEST_DIR="${HOME}/.local/bin" continue else error "Failed to install binary to ${DEST_DIR}/${final_binary_name}" rm -rf "$temp_dir" exit 1 fi fi done # Make the binary executable if [ "$os" != "windows" ] && [ -f "${DEST_DIR}/${final_binary_name}" ]; then $SUDO_CMD chmod +x "${DEST_DIR}/${final_binary_name}" info "Made binary executable" fi info "GoodMem CLI installed to: ${DEST_DIR}/${final_binary_name}" # Check if destination is in PATH if ! check_path "$DEST_DIR"; then warn "Directory ${DEST_DIR} is not in your PATH" warn "Add it to your PATH by adding this line to your shell profile:" warn " export PATH=\"\$PATH:${DEST_DIR}\"" fi # Show version if binary is accessible if [ -f "${DEST_DIR}/${final_binary_name}" ]; then info "Verifying installation..." "${DEST_DIR}/${final_binary_name}" version 2>/dev/null || warn "Could not verify version" fi # If --cli-only flag is set, skip system installation and initialization if [ "$CLI_ONLY" = true ]; then info "CLI-only mode: Skipping GoodMem system installation and initialization" info "Installation complete! Run 'goodmem --help' to get started" return 0 fi fi # Run goodmem system install with any provided arguments # Redirect stdin from /dev/tty to allow interactive prompts even when piped # (unless --handsfree is specified, which means non-interactive mode) info "Running GoodMem system installer..." # Check if --handsfree flag is present handsfree_mode=false for arg in "${INSTALL_ARGS[@]+"${INSTALL_ARGS[@]}"}"; do if [ "$arg" = "--handsfree" ]; then handsfree_mode=true break fi done # Check if --dir flag is already specified by the user dir_specified=false for arg in "${INSTALL_ARGS[@]+"${INSTALL_ARGS[@]}"}"; do if [ "$arg" = "--dir" ]; then dir_specified=true break fi done # Prepare final install arguments # If --dir not specified, default to ~/.goodmem FINAL_INSTALL_ARGS=("${INSTALL_ARGS[@]+"${INSTALL_ARGS[@]}"}") if [ "$dir_specified" = false ]; then FINAL_INSTALL_ARGS=("--dir" "${HOME}/.goodmem" "${FINAL_INSTALL_ARGS[@]}") fi # Log sudo usage (SUDO_CMD already set at top of script) if [ -n "$SUDO_CMD" ]; then info "Running installer with sudo (use --no-sudo to disable)" fi if [ "${#FINAL_INSTALL_ARGS[@]}" -gt 0 ]; then info "Passing arguments to installer: ${FINAL_INSTALL_ARGS[*]}" if [ -t 0 ] || [ "$handsfree_mode" = true ]; then # Already interactive or explicitly non-interactive (--handsfree) $SUDO_CMD "${DEST_DIR}/${final_binary_name}" system install "${FINAL_INSTALL_ARGS[@]}" else # Piped input, redirect to terminal for interactive prompts $SUDO_CMD "${DEST_DIR}/${final_binary_name}" system install "${FINAL_INSTALL_ARGS[@]}" /dev/null; then init_success=true info "GoodMem CLI initialized successfully" break fi if [ $attempt -lt $max_attempts ]; then warn "Initialization attempt $attempt/$max_attempts failed, retrying in 5 seconds..." sleep 5 fi attempt=$((attempt + 1)) done if [ "$init_success" = false ]; then error "Failed to initialize GoodMem CLI after $max_attempts attempts" error "The server may still be starting up. Try running 'goodmem init' manually in a few minutes." exit 1 fi } main