#!/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 = ''' Kelify AI Chat

⚡ Kelify AI Chat

''' @app.route('/') def index(): return render_template_string(HTML) @app.route('/chat', methods=['POST']) def chat(): data = request.get_json() user_message = data.get('message', '') if not user_message: return jsonify({'error': 'Empty message'}), 400 config = load_config() api_key = config.get('OPENROUTER_KEY') model = config.get('MODEL_ID', 'openai/gpt-3.5-turbo') headers = { 'Authorization': f'Bearer {api_key}', 'Content-Type': 'application/json' } payload = { 'model': model, 'messages': [{'role': 'user', 'content': user_message}] } try: response = requests.post('https://openrouter.ai/api/v1/chat/completions', headers=headers, json=payload) response.raise_for_status() reply = response.json()['choices'][0]['message']['content'] return jsonify({'reply': reply}) except Exception as e: return jsonify({'error': str(e)}), 500 if __name__ == '__main__': config = load_config() port = int(config.get('PORT', 6770)) app.run(host='127.0.0.1', port=port, debug=False) # bind to localhost only; Nginx will proxy EOF # Create control script cat > "$INSTALL_DIR/kelify-ai" << EOF #!/usr/bin/env bash # Kelify AI Chat UI controller – with domain/SSL support AI_DIR="$AI_DIR" CONFIG_FILE="\$AI_DIR/config" PID_FILE="\$AI_DIR/ai.pid" LOG_FILE="\$AI_DIR/ai.log" case "\$1" in start) cd "\$AI_DIR" if [ -f "\$PID_FILE" ] && kill -0 \$(cat "\$PID_FILE") 2>/dev/null; then echo "AI Chat UI already running (PID \$(cat \$PID_FILE))" else nohup python3 app.py > "\$LOG_FILE" 2>&1 & echo \$! > "\$PID_FILE" echo "AI Chat UI started (backend only)." # If domain is configured, show URL if [ -f "\$CONFIG_FILE" ]; then source "\$CONFIG_FILE" if [ -n "\$DOMAIN" ]; then echo "Access it at https://\$DOMAIN" else echo "Access it at http://localhost:\$(grep PORT "\$CONFIG_FILE" | cut -d= -f2)" fi fi fi ;; stop) if [ -f "\$PID_FILE" ]; then kill \$(cat "\$PID_FILE") 2>/dev/null && echo "Stopped." rm -f "\$PID_FILE" else echo "AI Chat UI not running." fi ;; status) if [ -f "\$PID_FILE" ] && kill -0 \$(cat "\$PID_FILE") 2>/dev/null; then echo "AI Chat UI is running (PID \$(cat \$PID_FILE))" else echo "AI Chat UI is not running." fi ;; logs) tail -f "\$LOG_FILE" ;; config) if command -v nano &>/dev/null; then nano "\$CONFIG_FILE" elif command -v vim &>/dev/null; then vim "\$CONFIG_FILE" else vi "\$CONFIG_FILE" fi echo "Config updated. Restart the UI with 'kelify-ai restart' if running." ;; domain) # Re-run domain/SSL setup if [ ! -f "\$CONFIG_FILE" ]; then echo "AI Chat UI not installed. Run install option 4 first." exit 1 fi source "\$CONFIG_FILE" echo "Current domain: \${DOMAIN:-none}" echo "Enter new domain (or leave empty to disable):" read -r NEW_DOMAIN /dev/null << NGINX server { listen 80; server_name $DOMAIN; location / { proxy_pass http://127.0.0.1:$AI_PORT; proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; } } NGINX sudo ln -sf /etc/nginx/sites-available/kelify-ai /etc/nginx/sites-enabled/ sudo nginx -t || error "Nginx config test failed." # Obtain SSL certificate sudo certbot --nginx -d "$DOMAIN" --non-interactive --agree-tos --email "$EMAIL" || warn "Let's Encrypt failed. Check domain DNS." # Reload nginx sudo systemctl reload nginx || sudo service nginx reload info "SSL configured for https://$DOMAIN" fi # Create a helper script for SSL renewal (if domain set) if [ -n "$DOMAIN" ] && [ "$OS" != "termux" ]; then cat > "$AI_DIR/setup_ssl.sh" << 'SSLF' #!/bin/bash # Helper to set up SSL for a new domain DOMAIN="$1" EMAIL="$2" if [ -z "$DOMAIN" ] || [ -z "$EMAIL" ]; then echo "Usage: $0 " exit 1 fi # Update Nginx config sudo tee /etc/nginx/sites-available/kelify-ai > /dev/null << NGINX server { listen 80; server_name $DOMAIN; location / { proxy_pass http://127.0.0.1:6770; proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; } } NGINX sudo ln -sf /etc/nginx/sites-available/kelify-ai /etc/nginx/sites-enabled/ sudo nginx -t && sudo systemctl reload nginx sudo certbot --nginx -d "$DOMAIN" --non-interactive --agree-tos --email "$EMAIL" echo "SSL configured for $DOMAIN" SSLF chmod +x "$AI_DIR/setup_ssl.sh" fi info "AI Chat UI installed." info "Control with: kelify-ai {start|stop|restart|status|logs|config|domain}" if [ -n "$DOMAIN" ]; then info "After starting, your AI will be available at https://$DOMAIN" else info "After starting, open http://localhost:$AI_PORT in your browser." fi } uninstall_ai_ui() { if ! is_ai_installed; then warn "AI Chat UI is not installed." return 1 fi # Stop the UI if [ -f "$INSTALL_DIR/kelify-ai" ]; then "$INSTALL_DIR/kelify-ai" stop 2>/dev/null || true fi # Remove Nginx config if present if [ -f /etc/nginx/sites-enabled/kelify-ai ]; then sudo rm -f /etc/nginx/sites-enabled/kelify-ai sudo rm -f /etc/nginx/sites-available/kelify-ai sudo systemctl reload nginx 2>/dev/null || sudo service nginx reload 2>/dev/null || true fi # Remove certbot certificates (optional) if [ -f "$AI_DIR/config" ]; then source "$AI_DIR/config" if [ -n "$DOMAIN" ]; then sudo certbot delete --cert-name "$DOMAIN" 2>/dev/null || true fi fi rm -rf "$AI_DIR" rm -f "$INSTALL_DIR/kelify-ai" info "AI Chat UI removed." } show_menu() { echo "=================================" echo " Kelify Installer Menu" echo "=================================" echo "1) Install Kelify (CLI only)" echo "2) Uninstall Kelify (CLI only)" echo "3) Update Kelify" echo "4) Install AI Chat UI (OpenRouter + custom domain & SSL)" echo "5) Uninstall AI Chat UI" echo "6) Exit" echo -n "Choose an option [1-6]: " } # ----- Parse command-line arguments ----- MODE="" if [ $# -gt 0 ]; then case "$1" in --install) MODE="install" ;; --uninstall) MODE="uninstall" ;; --update) MODE="update" ;; --ai-install) MODE="ai-install" ;; --ai-uninstall) MODE="ai-uninstall" ;; --help) echo "Usage: install.sh [--install|--uninstall|--update|--ai-install|--ai-uninstall|--help]"; exit 0 ;; *) error "Unknown option: $1" ;; esac fi # ----- Main ----- if [ -z "$MODE" ]; then while true; do show_menu read -r choice