download-latest.sh
#!/bin/sh

# This script downloads the specified version (or latest by default) of context-generator from GitHub
# and installs it to a user bin directory.
# To use a GitHub token, pass it through the GITHUB_PAT environment variable.

# GLOBALS

# Colors
ESC=$(printf '\033')
RED="${ESC}[31m"
MUTED="${ESC}[2m"
GREEN="${ESC}[32m"
YELLOW="${ESC}[33m"
BLUE="${ESC}[34m"
BOLD="${ESC}[1m"
DEFAULT="${ESC}[0m"

# Project name
PNAME='ctx'
REPO_OWNER='context-hub'
REPO_NAME='generator'

# GitHub API address
GITHUB_API="https://api.github.com/repos/$REPO_OWNER/$REPO_NAME/releases"
# GitHub Release address
GITHUB_REL="https://github.com/$REPO_OWNER/$REPO_NAME/releases/download"

# Default install directories will be set based on OS
DEFAULT_BIN_DIR=""

# Default to empty (meaning get latest version)
VERSION=""

# Is this Windows?
IS_WINDOWS=0
# Is this macOS?
IS_MACOS=0

# FUNCTIONS

# Print a section header
print_header() {
  printf "\n${BOLD}$1${DEFAULT}\n"
  printf "%-${#1}s\n" | tr " " "-"
  printf "\n"
}

# Print a status message
print_status() {
  printf " ${MUTED} >>  ${MUTED}$1${DEFAULT}\n"
}

# Print a success message
print_success() {
  printf " ${GREEN}[OK]${DEFAULT} $1\n"
}

# Print an error message
print_error() {
  printf " ${RED}[ERROR]${DEFAULT} $1\n"
}

# Print a warning message
print_warning() {
  printf " ${YELLOW}[WARNING]${DEFAULT} $1\n"
}

# Check if ctx is already installed and get its location
check_existing_installation() {
  print_header "Checking for existing installation"

  # Get the binary name based on OS
  if [ "$IS_WINDOWS" -eq 1 ]; then
    binary_to_find="${PNAME}.exe"
  else
    binary_to_find="$PNAME"
  fi

  # Try to find existing installation
  if command -v "$PNAME" >/dev/null 2>&1; then
    existing_path=$(command -v "$PNAME")
    existing_dir=$(dirname "$existing_path")
    print_success "Found existing installation at: $existing_path"

    # Set default bin directory to the location of the existing installation
    DEFAULT_BIN_DIR="$existing_dir"

    # Get current version if possible
    if "$existing_path" --version >/dev/null 2>&1; then
      current_version=$("$existing_path" --version | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+' | head -1)
      if [ -n "$current_version" ]; then
        print_status "Current version: $current_version"
      fi
    fi

    return 0
  else
    # Check common directories for the binary
    for check_dir in /usr/local/bin /usr/bin "$HOME/.local/bin" "$HOME/bin" "$HOME/AppData/Local/bin"; do
      if [ -f "$check_dir/$binary_to_find" ]; then
        existing_path="$check_dir/$binary_to_find"
        existing_dir="$check_dir"
        print_success "Found existing installation at: $existing_path"

        # Set default bin directory to the location of the existing installation
        DEFAULT_BIN_DIR="$existing_dir"

        # Get current version if possible
        if "$existing_path" --version >/dev/null 2>&1; then
          current_version=$("$existing_path" --version | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+' | head -1)
          if [ -n "$current_version" ]; then
            print_status "Current version: $current_version"
          fi
        fi

        return 0
      fi
    done

    print_status "No existing installation found"
    return 1
  fi
}

# Gets the version either from user input or latest from GitHub
# Sets the $latest and $latestV variables.
# Returns 0 in case of success, 1 otherwise.
get_latest() {
  # If version was specified, use it directly
  if [ -n "$VERSION" ]; then
    # Remove 'v' prefix if present
    latest=$(echo "$VERSION" | sed 's/^v//')
    latestV="$latest"
    print_success "Using specified version: $latestV"
    return 0
  fi

  # Otherwise, get latest from GitHub
  # temp_file is needed because the grep would start before the download is over
  temp_file=$(mktemp -q /tmp/$PNAME.XXXXXXXXX)
  latest_release="$GITHUB_API/latest"

  if ! temp_file=$(mktemp -q /tmp/$PNAME.XXXXXXXXX); then
    print_error "Can't create temp file."
    fetch_release_failure_usage
    exit 1
  fi

  print_status "Checking for latest version..."

  if [ -z "$GITHUB_PAT" ]; then
    curl -s "$latest_release" >"$temp_file" || return 1
  else
    curl -H "Authorization: token $GITHUB_PAT" -s "$latest_release" >"$temp_file" || return 1
  fi

  latest="$(grep <"$temp_file" '"tag_name":' | sed 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/' | sed 's/^v//')"
  latestV="$(grep <"$temp_file" '"tag_name":' | sed 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')"

  rm -f "$temp_file"

  if [ -n "$latest" ]; then
    print_success "Latest version found: $latestV"

    # Compare versions if we have a current version
    if [ -n "$current_version" ] && [ "$latest" = "$current_version" ]; then
      print_warning "You already have the latest version installed"
      if ! prompt_for_confirmation "Do you want to reinstall the same version?"; then
        print_status "Installation cancelled by user."
        exit 0
      fi
    elif [ -n "$current_version" ]; then
      print_status "Upgrading from version $current_version to $latestV"
    fi
  fi

  return 0
}

# Gets the OS by setting the $os variable.
# Returns 0 in case of success, 1 otherwise.
get_os() {
  os_name=$(uname -s)
  case "$os_name" in
    # ---
  'Linux')
    os='linux'
    ;;

    # ---
  'MINGW'* | 'MSYS'* | 'CYGWIN'*)
    os='windows'
    IS_WINDOWS=1
    ;;

    # ---
  'Darwin')
    os='darwin'
    IS_MACOS=1
    ;;

    # ---
  *)
    return 1
    ;;
  esac

  # Set default bin directory based on OS
  set_default_bin_dir

  return 0
}

# Sets the default bin directory based on detected OS
set_default_bin_dir() {
  if [ "$IS_WINDOWS" -eq 1 ]; then
    # For Windows, we'll use AppData\Local\bin or HOME\bin
    if [ -d "$HOME/AppData/Local/bin" ]; then
      DEFAULT_BIN_DIR="$HOME/AppData/Local/bin"
    else
      DEFAULT_BIN_DIR="$HOME/bin"
    fi
  elif [ "$IS_MACOS" -eq 1 ]; then
    # For macOS, check if user has write access to /usr/local/bin
    if [ -w "/usr/local/bin" ]; then
      DEFAULT_BIN_DIR="/usr/local/bin"
    else
      # Otherwise use ~/.local/bin for non-sudo installation
      DEFAULT_BIN_DIR="$HOME/.local/bin"
    fi
  else
    # For Linux and other systems
    if [ -w "/usr/local/bin" ]; then
      DEFAULT_BIN_DIR="/usr/local/bin"
    else
      DEFAULT_BIN_DIR="$HOME/.local/bin"
    fi
  fi
}

# Gets the architecture by setting the $arch variable.
# Returns 0 in case of success, 1 otherwise.
get_arch() {
  architecture=$(uname -m)

  # case 1
  case "$architecture" in
  'x86_64' | 'amd64')
    arch='amd64'
    ;;

    # case 2
  'arm64' | 'aarch64')
    arch='arm64'
    ;;

  # all other
  *)
    return 1
    ;;
  esac

  return 0
}

not_available_failure_usage() {
  print_error 'ctx binary is not available for your OS distribution or your architecture yet.'
  echo ''
  echo 'However, you can easily compile the binary from the source files.'
  echo 'Follow the steps at the page ("Source" tab): TODO'
}

fetch_release_failure_usage() {
  print_error "Impossible to get the latest stable version of $PNAME."
  printf "Please let us know about this issue: https://github.com/$REPO_OWNER/$REPO_NAME/issues/new\n"
  printf "\nIn the meantime, you can manually download the appropriate binary from the GitHub release assets here: https://github.com/$REPO_OWNER/$REPO_NAME/releases/latest\n"
}

# Detect appropriate Windows installation directory
detect_windows_dir() {
  # Try to use more standard Windows paths
  if [ -d "$HOME/AppData/Local/bin" ]; then
    bin_dir="$HOME/AppData/Local/bin"
  elif [ -d "$HOME/bin" ]; then
    bin_dir="$HOME/bin"
  else
    # Create in user's home directory if nothing else suitable
    bin_dir="$HOME/bin"
  fi
}

ensure_bin_dir() {
  # Create bin directory if it doesn't exist
  if [ ! -d "$bin_dir" ]; then
    print_status "Creating directory $bin_dir..."
    mkdir -p "$bin_dir" || {
      print_error "Could not create directory $bin_dir"
      exit 1
    }
    print_success "Directory created successfully"
  fi

  # Check if bin_dir is in PATH
  if ! echo "$PATH" | tr ':' '\n' | grep -q "^$bin_dir$"; then
    print_warning "$bin_dir is not in your PATH."

    if [ "$IS_WINDOWS" -eq 1 ]; then
      printf "You might want to add it to your Windows PATH:\n"
      printf "    ${GREEN}1. Right-click on 'This PC' or 'My Computer' and select 'Properties'${DEFAULT}\n"
      printf "    ${GREEN}2. Click on 'Advanced system settings'${DEFAULT}\n"
      printf "    ${GREEN}3. Click on 'Environment Variables'${DEFAULT}\n"
      printf "    ${GREEN}4. Under 'User variables', select 'Path' and click 'Edit'${DEFAULT}\n"
      printf "    ${GREEN}5. Click 'New' and add: $bin_dir${DEFAULT}\n"
      printf "    ${GREEN}6. Click 'OK' on all dialogs${DEFAULT}\n\n"
      printf "    Or in PowerShell, run:${DEFAULT}\n"
      printf "    ${GREEN}\$env:Path += \";$bin_dir\"${DEFAULT}\n\n"
    elif [ "$IS_MACOS" -eq 1 ]; then
      printf "You might want to add the following line to your shell profile (~/.zshrc or ~/.bash_profile):\n"
      printf "    ${GREEN}export PATH=\"\$PATH:$bin_dir\"${DEFAULT}\n\n"
    else
      printf "You might want to add the following line to your shell profile (.bashrc, .zshrc, etc.):\n"
      printf "    ${GREEN}export PATH=\"\$PATH:$bin_dir\"${DEFAULT}\n\n"
    fi
  fi
}

download_and_install() {
  # Download the binary file
  print_header "Downloading the latest version"
  print_status "Preparing download from: $GITHUB_REL/$latestV/$release_file"

  # Create appropriate temp file
  if [ "$IS_WINDOWS" -eq 1 ] && [ -d "$TEMP" ]; then
    temp_dir="$TEMP"
  else
    temp_dir="/tmp"
  fi

  temp_file="$temp_dir/$release_file-XXXXXXXXX"
  if ! temp_file=$(mktemp -q "$temp_file"); then
    print_error "Can't create temp file for download."
    exit 1
  fi

  echo "\n"

  # Use curl with progress bar but suppress most headers
  if ! curl --fail -L "$GITHUB_REL/$latestV/$release_file" -o "$temp_file" \
       --progress-bar --write-out "%{http_code}" | grep -q "^2"; then
    echo "] ${RED}Failed!${DEFAULT}"
    print_error "Failed to download $GITHUB_REL/$latestV/$release_file"
    rm -f "$temp_file"
    exit 1
  fi

  echo "\n"

  print_success "Successfully downloaded version $latestV"
  print_status "Saved to temporary file: $temp_file"

  # Install the binary
  print_header "Installing the update"
  print_status "Replacing current binary at: $bin_dir/$binary_name"

  if ! mv "$temp_file" "$bin_dir/$binary_name"; then
    print_error "Failed to move binary to $bin_dir/$binary_name"
    rm -f "$temp_file"
    exit 1
  fi

  # Make executable (not necessary on Windows but doesn't hurt)
  if ! chmod 755 "$bin_dir/$binary_name"; then
    print_error "Failed to make $bin_dir/$binary_name executable"
    exit 1
  fi

  print_success "Successfully replaced the binary file"
  print_success "Successfully installed $latestV to $bin_dir/$binary_name\n"

  echo "     You can now run it using:"
  if [ "$IS_WINDOWS" -eq 1 ]; then
    echo "         ${BOLD}$binary_name${DEFAULT}"
  else
    echo "         ${BOLD}$PNAME${DEFAULT}"
  fi
  echo "     📚 Documentation: https://docs.ctxgithub.com"
  echo "     🚀 Happy AI coding!"
}

# Prompt user for confirmation
prompt_for_confirmation() {
  echo "${YELLOW}${DEFAULT} $1 [Y/n] "
  read -r response
  case "$response" in
    [nN])
      return 1
      ;;
    *)
      return 0
      ;;
  esac
}

# Ask user for installation directory
prompt_for_directory() {
  # If we found an existing installation, suggest that directory first
  if [ -n "$existing_dir" ]; then
    echo "${YELLOW}${DEFAULT} Install to the same location? [${GREEN}$existing_dir${DEFAULT}]: "
    read -r user_bin_dir
    if [ -z "$user_bin_dir" ]; then
      bin_dir="$existing_dir"
    else
      bin_dir="$user_bin_dir"
    fi
  else
    echo "${YELLOW}${DEFAULT} Enter installation directory [${GREEN}$DEFAULT_BIN_DIR${DEFAULT}]: "
    read -r user_bin_dir
    if [ -z "$user_bin_dir" ]; then
      bin_dir="$DEFAULT_BIN_DIR"
    else
      bin_dir="$user_bin_dir"
    fi
  fi
}

# Main script execution
printf "${BOLD}Context Generator Installer${DEFAULT}\n"
printf "===========================\n\n"

# First detect OS to set appropriate default bin directory
get_os

# Parse arguments
bin_dir=""
while [ $# -gt 0 ]; do
  case "$1" in
    -v=*|--version=*)
      VERSION="${1#*=}"
      shift
      ;;
    -v|--version)
      if [ $# -gt 1 ]; then
        VERSION="$2"
        shift 2
      else
        print_error "Version argument is missing"
        exit 1
      fi
      ;;
    *)
      bin_dir="$1"
      shift
      ;;
  esac
done

# Check if running on PowerShell and provide guidance if needed
if [ "$IS_WINDOWS" -eq 1 ] && echo "$SHELL" | grep -q "powershell"; then
  print_warning "Detected PowerShell environment."
  print_status "For a better PowerShell experience, consider using the PowerShell installation script instead."
  print_status "Continuing with this script, but some features might not work as expected."
fi

# Check for existing installation before asking for directory
check_existing_installation

# If no bin_dir was specified as argument, ask user
if [ -z "$bin_dir" ]; then
  prompt_for_directory
fi

print_status "Installation directory: $bin_dir"
if [ -n "$VERSION" ]; then
  print_status "Installing version: $VERSION"
else
  print_status "No version specified. Will install the latest version."
fi

# Get the latest version information before showing summary
if ! get_latest; then
  fetch_release_failure_usage
  exit 1
fi

if [ "$latest" = '' ]; then
  fetch_release_failure_usage
  exit 1
fi

# Fill $os and $arch variables
if ! get_os; then
  not_available_failure_usage
  exit 1
fi

if ! get_arch; then
  not_available_failure_usage
  exit 1
fi

# Determine release file name
if [ "$IS_WINDOWS" -eq 1 ]; then
  binary_name="${PNAME}.exe"
  release_file="$PNAME-$latest-$os-$arch.exe"
else
  binary_name="$PNAME"
  release_file="$PNAME-$latest-$os-$arch"
fi

# Ask for confirmation before proceeding
printf "\n"
echo "${BLUE}Summary:${DEFAULT}"
printf "\n"
echo " - Install location: ${bin_dir}/${binary_name}"
echo " - Version: ${latestV}"
if [ -n "$current_version" ]; then
  echo " - Current version: ${current_version}"
fi
echo " - Download file: ${release_file}"
echo ""

if ! prompt_for_confirmation "Do you want to proceed with the installation?"; then
  print_status "Installation cancelled by user."
  exit 0
fi

printf "\n"

# Ensure bin directory exists and is in PATH
ensure_bin_dir

# Download and install the latest version
download_and_install
