#!/usr/bin/env bash # Kelify Advanced Installer – AI Chat UI with Custom Domain & SSL # Usage: curl -sSL https://kelify.xkelvin.space/install.sh | bash set -euo pipefail # ----- Configuration ----- KELIFY_URL="https://kelify.xkelvin.space/kelify" VERSION_URL="https://kelify.xkelvin.space/version.txt" INSTALL_DIR="$HOME/.local/bin" KELIFY_PATH="$INSTALL_DIR/kelify" CONFIG_DIR="$HOME/.kelify" AI_DIR="$HOME/.kelify-ai" AI_PORT="6770" BACKUP_SUFFIX=".backup-$(date +%Y%m%d-%H%M%S)" LOG_FILE="$HOME/.kelify_install.log" # ----- Colors ----- RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m' error() { echo -e "${RED}❌ $1${NC}" | tee -a "$LOG_FILE"; exit 1; } info() { echo -e "${GREEN}✅ $1${NC}" | tee -a "$LOG_FILE"; } warn() { echo -e "${YELLOW}⚠️ $1${NC}" | tee -a "$LOG_FILE"; } log() { echo -e "${BLUE}🔹 $1${NC}" | tee -a "$LOG_FILE"; } # ----- OS Detection (safe for VPS) ----- detect_os() { if [ -n "${PREFIX:-}" ] && [ -d "/data/data/com.termux" ]; then echo "termux" elif [ -f /etc/os-release ]; then . /etc/os-release case "$ID" in debian|ubuntu|linuxmint|pop) echo "debian" ;; centos|rhel|fedora) echo "rhel" ;; arch|manjaro) echo "arch" ;; alpine) echo "alpine" ;; *) echo "unknown" ;; esac else echo "unknown" fi } OS=$(detect_os) log "Detected OS: $OS" # ----- Package Management ----- install_pkg() { case "$OS" in termux) pkg install -y "$@" 2>&1 | tee -a "$LOG_FILE" ;; debian) sudo apt update && sudo apt install -y "$@" 2>&1 | tee -a "$LOG_FILE" ;; rhel) sudo yum install -y "$@" 2>&1 | tee -a "$LOG_FILE" ;; arch) sudo pacman -S --noconfirm "$@" 2>&1 | tee -a "$LOG_FILE" ;; alpine) sudo apk add "$@" 2>&1 | tee -a "$LOG_FILE" ;; *) error "Unsupported OS. Please install dependencies manually." ;; esac } update_pkg() { case "$OS" in termux) pkg upgrade -y 2>&1 | tee -a "$LOG_FILE" ;; debian) sudo apt update && sudo apt upgrade -y 2>&1 | tee -a "$LOG_FILE" ;; rhel) sudo yum update -y 2>&1 | tee -a "$LOG_FILE" ;; arch) sudo pacman -Syu --noconfirm 2>&1 | tee -a "$LOG_FILE" ;; alpine) sudo apk update && sudo apk upgrade 2>&1 | tee -a "$LOG_FILE" ;; *) warn "Cannot update packages automatically." ;; esac } # ----- Helper functions ----- is_installed() { [ -f "$KELIFY_PATH" ] && return 0 || return 1; } is_ai_installed() { [ -d "$AI_DIR" ] && [ -f "$AI_DIR/app.py" ] && return 0 || return 1; } get_installed_version() { if is_installed; then "$KELIFY_PATH" version 2>/dev/null | grep -oE 'v?[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' || echo "unknown" else echo "not installed" fi } get_remote_version() { local ver ver=$(curl -sSL "$VERSION_URL" 2>/dev/null | head -1 | tr -d '\r') if [ -z "$ver" ]; then local tmp tmp=$(mktemp) curl -sSL -o "$tmp" "$KELIFY_URL" ver=$(grep -m1 '^VERSION=' "$tmp" | sed -E 's/^VERSION="?([^"]+)"?/\1/' | tr -d '\r') rm -f "$tmp" fi echo "$ver" } install_dependencies() { log "Updating package lists..." case "$OS" in termux) pkg update -y 2>&1 | tee -a "$LOG_FILE" ;; debian) sudo apt update 2>&1 | tee -a "$LOG_FILE" ;; rhel) sudo yum check-update 2>&1 | tee -a "$LOG_FILE" || true ;; arch) sudo pacman -Sy 2>&1 | tee -a "$LOG_FILE" ;; alpine) sudo apk update 2>&1 | tee -a "$LOG_FILE" ;; esac local core_pkgs=() case "$OS" in termux) core_pkgs=(git curl wget jq python nodejs termux-api ffmpeg gnupg) ;; debian) core_pkgs=(git curl wget jq python3 python3-pip nodejs ffmpeg gnupg) ;; rhel) core_pkgs=(git curl wget jq python3 python3-pip nodejs ffmpeg gnupg) ;; arch) core_pkgs=(git curl wget jq python python-pip nodejs ffmpeg gnupg) ;; alpine) core_pkgs=(git curl wget jq python3 py3-pip nodejs ffmpeg gnupg) ;; *) error "Unsupported OS." ;; esac log "Installing core dependencies: ${core_pkgs[*]}" local retry=3 until install_pkg "${core_pkgs[@]}"; do if [ $retry -le 0 ]; then error "Core dependency installation failed."; fi warn "Retrying... ($retry attempts left)" sleep 2 ((retry--)) done local optional_pkgs=() case "$OS" in termux) optional_pkgs=(cloudflared yt-dlp speedtest-cli qrencode) ;; debian) optional_pkgs=(cloudflared yt-dlp speedtest-cli qrencode) ;; rhel) optional_pkgs=(cloudflared yt-dlp speedtest-cli qrencode) ;; arch) optional_pkgs=(cloudflared yt-dlp speedtest-cli qrencode) ;; alpine) optional_pkgs=(cloudflared yt-dlp speedtest-cli qrencode) ;; esac log "Installing optional dependencies (if available): ${optional_pkgs[*]}" for pkg in "${optional_pkgs[@]}"; do if install_pkg "$pkg" 2>/dev/null; then info "Installed $pkg" else warn "Could not install $pkg – some commands may not work." fi done } install_kelify() { log "Downloading Kelify from $KELIFY_URL ..." mkdir -p "$INSTALL_DIR" [ -f "$KELIFY_PATH" ] && mv "$KELIFY_PATH" "$KELIFY_PATH$BACKUP_SUFFIX" && warn "Backed up existing kelify" if ! curl -# -o "$KELIFY_PATH" "$KELIFY_URL" 2>&1 | tee -a "$LOG_FILE"; then error "Download failed." fi head -1 "$KELIFY_PATH" | grep -q "^#!/.*bash" || error "Corrupted download." chmod +x "$KELIFY_PATH" info "Kelify installed to $KELIFY_PATH" local PATH_EXPORT='export PATH="$HOME/.local/bin:$PATH"' for rc in "$HOME/.bashrc" "$HOME/.zshrc" "$HOME/.config/fish/config.fish"; do if [ -f "$rc" ] && ! grep -q '\.local/bin' "$rc"; then echo "$PATH_EXPORT" >> "$rc" info "Added ~/.local/bin to PATH in $rc" fi done export PATH="$HOME/.local/bin:$PATH" } uninstall_kelify() { if is_installed; then rm -v "$KELIFY_PATH" | tee -a "$LOG_FILE" info "Removed $KELIFY_PATH" else warn "Kelify not found." fi [ -d "$CONFIG_DIR" ] && rm -rf "$CONFIG_DIR" && info "Removed config directory" warn "You may want to remove PATH additions from shell configs manually." } update_kelify() { log "Checking for updates..." local current_ver remote_ver current_ver=$(get_installed_version) remote_ver=$(get_remote_version) log "Current version: $current_ver" log "Remote version: $remote_ver" if [ "$current_ver" = "$remote_ver" ]; then info "No updates available (you have version $current_ver)." return 0 fi if [ "$remote_ver" = "unknown" ] || [ -z "$remote_ver" ]; then warn "Could not determine remote version. Skipping." return 1 fi echo -e "${BLUE}🔹 Updating from $current_ver to $remote_ver...${NC}" cp "$KELIFY_PATH" "$KELIFY_PATH$BACKUP_SUFFIX" warn "Backed up current kelify" if ! curl -# -o "$KELIFY_PATH.new" "$KELIFY_URL" 2>&1 | tee -a "$LOG_FILE"; then error "Download failed. Restoring backup." mv "$KELIFY_PATH$BACKUP_SUFFIX" "$KELIFY_PATH" exit 1 fi head -1 "$KELIFY_PATH.new" | grep -q "^#!/.*bash" || { error "Corrupted download. Restoring backup." mv "$KELIFY_PATH$BACKUP_SUFFIX" "$KELIFY_PATH" exit 1 } chmod +x "$KELIFY_PATH.new" mv "$KELIFY_PATH.new" "$KELIFY_PATH" info "Updated successfully to version $remote_ver." log "Checking for optional dependency updates..." update_pkg } # ----- AI Chat UI Installation (with domain & SSL) ----- install_ai_ui() { if is_ai_installed; then warn "AI Chat UI is already installed in $AI_DIR." return 1 fi # Only proceed on Linux (not Termux) because Nginx/Let's Encrypt require root if [ "$OS" = "termux" ]; then warn "Domain & SSL features require a Linux VPS. On Termux, the AI will run on localhost only." fi log "Installing AI Chat UI on port $AI_PORT ..." # Install Flask and requests if command -v pip3 &>/dev/null; then pip3 install flask requests 2>&1 | tee -a "$LOG_FILE" || error "Failed to install Flask/requests." elif command -v pip &>/dev/null; then pip install flask requests 2>&1 | tee -a "$LOG_FILE" || error "Failed to install Flask/requests." else error "pip not found. Please install Python pip." fi # Prompt for OpenRouter API key and model echo -e "${BLUE}🔹 OpenRouter API key (get one from https://openrouter.ai/):${NC}" read -r OPENROUTER_KEY "$AI_DIR/config" << EOF OPENROUTER_KEY=$OPENROUTER_KEY MODEL_ID=$MODEL_ID PORT=$AI_PORT DOMAIN=$DOMAIN EMAIL=$EMAIL EOF # Create the Flask application (same as before, but now reads config) cat > "$AI_DIR/app.py" << 'EOF' #!/usr/bin/env python3 """ Kelify AI Chat UI – Powered by OpenRouter """ import os import requests from flask import Flask, request, jsonify, render_template_string app = Flask(__name__) CONFIG_PATH = os.path.expanduser("~/.kelify-ai/config") def load_config(): config = {} with open(CONFIG_PATH) as f: for line in f: if '=' in line and not line.startswith('#'): key, val = line.strip().split('=', 1) config[key] = val return config HTML = '''