#!/bin/bash set -euo pipefail # InternalAI Platform Installer # ============================================================================ # Configuration # ============================================================================ PLATFORM_NAME="InternalAI Platform" DEFAULT_INSTALL_DIR="$HOME/internalai" REPO_URL="https://gitea.app.monadical.io/monadical/internalai.git" REPO_SSH="git@gitea.app.monadical.io:monadical/internalai.git" CLI_INSTALL_DIR="/usr/local/bin" CLI_NAME="internalai" VERSION="1.0.0" # Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' CYAN='\033[0;36m' BOLD='\033[1m' NC='\033[0m' # ============================================================================ # Logging Functions # ============================================================================ log_info() { echo -e "${BLUE}ℹ${NC} $1" } log_success() { echo -e "${GREEN}✓${NC} $1" } log_warning() { echo -e "${YELLOW}⚠${NC} $1" } log_error() { echo -e "${RED}✗${NC} $1" } log_header() { echo "" echo -e "${CYAN}═══════════════════════════════════════════════════════════${NC}" echo -e "${CYAN} $1${NC}" echo -e "${CYAN}═══════════════════════════════════════════════════════════${NC}" echo "" } print_banner() { clear echo -e "${BOLD}${CYAN}" cat << 'EOF' ╔═════════════════════════════════════════════════════════════════════════════════════════╗ ║ ║ ║ ██╗███╗ ██╗████████╗███████╗██████╗ ███╗ ██╗ █████╗ ██╗ █████╗ ██╗ ║ ║ ██║████╗ ██║╚══██╔══╝██╔════╝██╔══██╗████╗ ██║██╔══██╗██║ ██╔══██╗██║ ║ ║ ██║██╔██╗ ██║ ██║ █████╗ ██████╔╝██╔██╗ ██║███████║██║ ███████║██║ ║ ║ ██║██║╚██╗██║ ██║ ██╔══╝ ██╔══██╗██║╚██╗██║██╔══██║██║ ██╔══██║██║ ║ ║ ██║██║ ╚████║ ██║ ███████╗██║ ██║██║ ╚████║██║ ██║███████╗ ██║ ██║██║ ║ ║ ╚═╝╚═╝ ╚═══╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═╝╚═╝ ║ ║ ║ ║ Platform Installer v${VERSION} ║ ╚═════════════════════════════════════════════════════════════════════════════════════════╝ EOF echo -e "${NC}" } show_welcome() { print_banner log_info "This installer will:" echo " • Check system dependencies" echo " • Install missing dependencies (Git, Docker, just, uv)" echo " • Set up Gitea authentication" echo " • Clone the InternalAI platform repository" echo " • Install the 'internalai' command-line tool" echo "" } # ============================================================================ # Utility Functions # ============================================================================ add_to_path() { local dir_to_add="$1" local path_export="export PATH=\"$dir_to_add:\$PATH\"" # List of profile files to update (in order of preference) local profile_files=( "$HOME/.profile" "$HOME/.bash_profile" "$HOME/.bashrc" "$HOME/.zshrc" ) local added_to_file=false for profile_file in "${profile_files[@]}"; do # Only add to existing files or create .profile/.bashrc if they don't exist if [ -f "$profile_file" ] || [[ "$profile_file" == *".profile" ]] || [[ "$profile_file" == *".bashrc" ]]; then # Check if PATH is already in the file if [ -f "$profile_file" ] && grep -q "export PATH=\"$dir_to_add:\$PATH\"" "$profile_file" 2>/dev/null; then continue fi # Add PATH export to the file echo "" >> "$profile_file" echo "# Added by InternalAI installer" >> "$profile_file" echo "$path_export" >> "$profile_file" added_to_file=true log_info "Added $dir_to_add to PATH in $profile_file" fi done if [ "$added_to_file" = false ]; then log_warning "Could not find suitable profile file to persist PATH" log_info "Add this to your shell profile manually: $path_export" fi } prompt() { local varname=$1 local prompt_text=$2 local default_value=$3 local is_secret=${4:-false} if [ -n "$default_value" ]; then prompt_text="$prompt_text [${default_value}]" fi if [ "$is_secret" = true ]; then echo -ne "${YELLOW}${prompt_text}: ${NC}" >&2 value="" while IFS= read -r -s -n1 char < /dev/tty; do if [[ $char == $'\0' ]]; then break fi if [[ $char == $'\177' ]] || [[ $char == $'\b' ]]; then if [ ${#value} -gt 0 ]; then value="${value%?}" echo -ne "\b \b" >&2 fi else value+="$char" echo -n "*" >&2 fi done echo "" >&2 else echo -ne "${YELLOW}${prompt_text}: ${NC}" >&2 read value < /dev/tty fi if [ -z "$value" ] && [ -n "$default_value" ]; then value="$default_value" fi eval "$varname='$value'" } # ============================================================================ # Dependency Checking and Installation # ============================================================================ check_dependencies() { log_header "Checking Dependencies" # Check Git if command -v git &> /dev/null; then log_success "git installed ($(git --version | head -n1))" else log_warning "git is not installed (will attempt to install)" install_git fi # Check bash version if [ "${BASH_VERSINFO[0]}" -lt 4 ]; then log_warning "Bash version ${BASH_VERSION} detected. Version 4+ recommended." else log_success "bash ${BASH_VERSION} is installed" fi # Check Docker if command -v docker &> /dev/null; then log_success "docker installed ($(docker --version | head -n1))" else log_warning "docker is not installed (will attempt to install)" install_docker fi # Check Docker Compose (should be included with modern Docker installations) if docker compose version &> /dev/null 2>&1; then log_success "docker compose installed ($(docker compose version | head -n1))" elif command -v docker-compose &> /dev/null; then log_success "docker-compose installed ($(docker-compose --version | head -n1))" else log_warning "docker compose not found after Docker installation" log_info "Docker Compose should be included with Docker. Verifying installation..." # Try to verify docker is working first if docker --version &> /dev/null; then log_info "Docker is installed but compose plugin may not be available" log_info "This is usually not an issue with modern Docker installations" fi fi # Check Just if command -v just &> /dev/null; then log_success "just installed ($(just --version | head -n1))" else log_warning "just is not installed (will attempt to install)" install_just fi # Check uv if command -v uv &> /dev/null; then log_success "uv installed ($(uv --version | head -n1))" else log_warning "uv is not installed (will attempt to install)" install_uv fi log_success "All dependencies are installed!" } install_just() { # Try package managers first if [[ "$OSTYPE" == "darwin"* ]]; then if command -v brew &> /dev/null; then log_info "Installing just via Homebrew..." if brew install just; then log_success "just installed" return 0 else log_warning "Homebrew installation failed, trying binary download..." fi fi elif [[ "$OSTYPE" == "linux-gnu"* ]]; then if command -v cargo &> /dev/null; then log_info "Installing just via cargo..." if cargo install just; then log_success "just installed" return 0 else log_warning "Cargo installation failed, trying binary download..." fi fi fi # Fall back to downloading pre-built binary log_info "Downloading pre-built just binary..." # Detect architecture local arch=$(uname -m) local os=$(uname -s | tr '[:upper:]' '[:lower:]') # Map architecture names case "$arch" in x86_64) arch="x86_64" ;; aarch64|arm64) arch="aarch64" ;; *) log_error "Unsupported architecture: $arch" log_info "Please install just manually: https://github.com/casey/just#installation" exit 1 ;; esac # Map OS names case "$os" in darwin) os="apple-darwin" ;; linux) os="unknown-linux-musl" ;; *) log_error "Unsupported OS: $os" log_info "Please install just manually: https://github.com/casey/just#installation" exit 1 ;; esac # Get latest version from GitHub API log_info "Fetching latest version..." local latest_version=$(curl -fsSL https://api.github.com/repos/casey/just/releases/latest | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/') if [ -z "$latest_version" ]; then log_error "Failed to fetch latest version" log_info "Please install just manually: https://github.com/casey/just#installation" exit 1 fi local binary_name="just-${latest_version}-${arch}-${os}" local download_url="https://github.com/casey/just/releases/download/${latest_version}/${binary_name}.tar.gz" local install_dir="$HOME/.local/bin" # Create install directory mkdir -p "$install_dir" # Download and extract local tmp_dir=$(mktemp -d) trap "rm -rf $tmp_dir" EXIT log_info "Downloading from: $download_url" if ! curl -fsSL "$download_url" -o "$tmp_dir/just.tar.gz"; then log_error "Failed to download just binary" log_info "Please install just manually: https://github.com/casey/just#installation" exit 1 fi log_info "Extracting to $install_dir..." if ! tar -xzf "$tmp_dir/just.tar.gz" -C "$tmp_dir"; then log_error "Failed to extract just binary" exit 1 fi # Install binary if ! mv "$tmp_dir/just" "$install_dir/just"; then log_error "Failed to install just to $install_dir" exit 1 fi chmod +x "$install_dir/just" # Add to PATH if not already there if [[ ":$PATH:" != *":$install_dir:"* ]]; then log_info "Adding $install_dir to PATH..." export PATH="$install_dir:$PATH" fi # Persist PATH to shell profiles add_to_path "$install_dir" # Verify installation by checking the binary directly if [ -x "$install_dir/just" ]; then log_success "just installed successfully to $install_dir/just" else log_error "just binary not found or not executable at $install_dir/just" exit 1 fi } install_uv() { log_info "Installing uv (Python package manager)..." local install_dir="$HOME/.local/bin" if curl -LsSf https://astral.sh/uv/install.sh | sh; then # Add uv to PATH for current session if [[ ":$PATH:" != *":$install_dir:"* ]]; then export PATH="$install_dir:$PATH" fi # Persist PATH to shell profiles add_to_path "$install_dir" # Verify installation by checking the binary directly if [ -x "$install_dir/uv" ]; then log_success "uv installed successfully to $install_dir/uv" else log_error "uv binary not found or not executable at $install_dir/uv" exit 1 fi else log_error "Failed to install uv" log_info "Please install manually: curl -LsSf https://astral.sh/uv/install.sh | sh" exit 1 fi } detect_linux_distro() { if [ -f /etc/os-release ]; then . /etc/os-release echo "$ID" elif [ -f /etc/redhat-release ]; then echo "rhel" elif [ -f /etc/debian_version ]; then echo "debian" else echo "unknown" fi } install_git() { log_info "Installing Git..." if [[ "$OSTYPE" == "darwin"* ]]; then install_git_macos elif [[ "$OSTYPE" == "linux-gnu"* ]]; then install_git_linux else log_error "Unsupported OS: $OSTYPE" log_info "Please install Git manually: https://git-scm.com/downloads" exit 1 fi # Verify installation if command -v git &> /dev/null; then log_success "Git installed successfully ($(git --version | head -n1))" else log_error "Git installation failed" exit 1 fi } install_git_macos() { if command -v brew &> /dev/null; then log_info "Installing Git via Homebrew..." if brew install git; then log_success "Git installed via Homebrew" else log_error "Failed to install Git via Homebrew" exit 1 fi else # Try Xcode Command Line Tools log_info "Installing Xcode Command Line Tools (includes Git)..." log_info "A dialog will appear - please click 'Install'" if xcode-select --install 2>&1 | grep -q "already installed"; then log_info "Xcode Command Line Tools already installed" else log_info "Waiting for Xcode Command Line Tools installation..." log_info "This may take a few minutes. Please complete the installation dialog." # Wait for installation to complete until xcode-select -p &> /dev/null; do sleep 5 done log_success "Xcode Command Line Tools installed" fi fi } install_git_linux() { local distro=$(detect_linux_distro) log_info "Installing Git on $distro..." case "$distro" in ubuntu|debian|pop|linuxmint) sudo apt-get update sudo apt-get install -y git ;; fedora|rhel|centos|rocky|almalinux) if command -v dnf &> /dev/null; then sudo dnf install -y git else sudo yum install -y git fi ;; arch|manjaro) sudo pacman -Sy --noconfirm git ;; opensuse*|sles) sudo zypper install -y git ;; alpine) sudo apk add git ;; *) log_error "Unsupported Linux distribution: $distro" log_info "Please install Git manually for your distribution:" log_info " https://git-scm.com/download/linux" exit 1 ;; esac } install_docker() { log_info "Installing Docker..." if [[ "$OSTYPE" == "darwin"* ]]; then install_docker_macos elif [[ "$OSTYPE" == "linux-gnu"* ]]; then install_docker_linux else log_error "Unsupported OS: $OSTYPE" log_info "Please install Docker manually: https://docs.docker.com/get-docker/" exit 1 fi } install_docker_macos() { if command -v brew &> /dev/null; then log_info "Installing Docker via Homebrew..." if brew install --cask docker; then log_success "Docker installed via Homebrew" log_info "Starting Docker Desktop..." open -a Docker log_info "Waiting for Docker to start..." # Wait for docker to be ready (up to 60 seconds) local timeout=60 local elapsed=0 while ! docker info &> /dev/null; do if [ $elapsed -ge $timeout ]; then log_warning "Docker daemon did not start within ${timeout}s" log_info "Please start Docker Desktop manually and run the installer again" exit 1 fi sleep 2 elapsed=$((elapsed + 2)) done log_success "Docker is ready" else log_error "Failed to install Docker via Homebrew" log_info "Please install Docker Desktop manually: https://docs.docker.com/desktop/install/mac-install/" exit 1 fi else log_error "Homebrew is not installed" log_info "Install Homebrew first: https://brew.sh" log_info "Or install Docker Desktop manually: https://docs.docker.com/desktop/install/mac-install/" exit 1 fi } install_docker_linux() { local distro=$(detect_linux_distro) log_info "Detected Linux distribution: $distro" case "$distro" in ubuntu|debian|pop|linuxmint) install_docker_debian ;; fedora|rhel|centos|rocky|almalinux) install_docker_rhel ;; arch|manjaro) install_docker_arch ;; *) log_error "Unsupported Linux distribution: $distro" log_info "Please install Docker manually: https://docs.docker.com/engine/install/" exit 1 ;; esac # Start and enable Docker service start_docker_service # Add user to docker group and activate immediately setup_docker_permissions } install_docker_debian() { log_info "Installing Docker on Debian/Ubuntu..." # Update package index sudo apt-get update # Install prerequisites sudo apt-get install -y ca-certificates curl gnupg lsb-release # Add Docker's official GPG key sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/$(detect_linux_distro)/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg sudo chmod a+r /etc/apt/keyrings/docker.gpg # Set up the repository echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/$(detect_linux_distro) \ $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ sudo tee /etc/apt/sources.list.d/docker.list > /dev/null # Install Docker Engine sudo apt-get update if sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin; then log_success "Docker installed successfully" else log_error "Failed to install Docker" exit 1 fi } install_docker_rhel() { log_info "Installing Docker on RHEL/Fedora/CentOS..." # Remove old versions sudo dnf remove -y docker docker-client docker-client-latest docker-common docker-latest \ docker-latest-logrotate docker-logrotate docker-engine podman runc || true # Install prerequisites sudo dnf install -y dnf-plugins-core # Add Docker repository sudo dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo # Install Docker Engine if sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin; then log_success "Docker installed successfully" else log_error "Failed to install Docker" exit 1 fi } install_docker_arch() { log_info "Installing Docker on Arch Linux..." # Install Docker from official repositories if sudo pacman -Sy --noconfirm docker docker-compose; then log_success "Docker installed successfully" else log_error "Failed to install Docker" exit 1 fi } start_docker_service() { log_info "Starting Docker service..." if command -v systemctl &> /dev/null; then sudo systemctl start docker sudo systemctl enable docker log_success "Docker service started and enabled" else log_warning "systemctl not found, please start Docker manually" fi } setup_docker_permissions() { log_info "Setting up Docker permissions..." # Add current user to docker group if ! groups | grep -q docker; then sudo usermod -aG docker "$USER" log_success "User added to docker group" fi # Test docker access if docker ps &> /dev/null 2>&1; then log_success "Docker is accessible without sudo" return 0 fi # If docker requires sudo, set up for immediate use if sudo docker ps &> /dev/null 2>&1; then log_info "Docker group membership will be active in new terminal sessions" log_info "For this session, using docker with sudo privileges..." # Create a wrapper function for the current script session # This allows the rest of the script to use docker commands docker() { sudo /usr/bin/docker "$@" } export -f docker 2>/dev/null || true log_success "Docker is ready to use (with sudo for current session)" log_info "Note: New terminal sessions will have docker access without sudo" else log_error "Docker is not accessible" exit 1 fi } # ============================================================================ # Gitea Authentication # ============================================================================ AUTH_TYPE="" GITEA_TOKEN="" setup_gitea_auth() { log_header "Gitea Authentication" log_info "The InternalAI platform repository is private and requires authentication." echo "" log_info "Choose authentication method:" echo " 1) Personal Access Token (recommended)" echo " 2) SSH Key (if already configured)" echo "" prompt AUTH_CHOICE "Select authentication method" "1" case "$AUTH_CHOICE" in 1) AUTH_TYPE="token" echo "" log_info "To create a personal access token:" log_info " 1. Go to: https://gitea.app.monadical.io/user/settings/applications" log_info " 2. Click 'Generate New Token'" log_info " 3. Give it a name (e.g., 'InternalAI Setup')" log_info " 4. Give it Read repository scope" log_info " 5. Click 'Generate Token' and copy it" echo "" prompt GITEA_TOKEN "Enter your Gitea Personal Access Token" "" true if [ -z "$GITEA_TOKEN" ]; then log_error "Token is required" exit 1 fi ;; 2) AUTH_TYPE="ssh" log_info "Using SSH authentication" log_info "Ensure your SSH key is added to Gitea:" log_info " https://gitea.app.monadical.io/user/settings/keys" # Test SSH connection echo "" log_info "Testing SSH connection..." if ssh -T git@gitea.app.monadical.io 2>&1 | grep -q "Hi there"; then log_success "SSH connection successful" else log_warning "Could not verify SSH connection, but will try to continue..." fi ;; *) log_error "Invalid choice" exit 1 ;; esac } prepare_git_url() { if [ "$AUTH_TYPE" = "token" ] && [ -n "$GITEA_TOKEN" ]; then echo "$REPO_URL" | sed "s|https://gitea.app.monadical.io/|https://${GITEA_TOKEN}@gitea.app.monadical.io/|" elif [ "$AUTH_TYPE" = "ssh" ]; then echo "$REPO_SSH" else echo "$REPO_URL" fi } # ============================================================================ # Git Credential Caching # ============================================================================ configure_git_credentials() { log_header "Configuring Git Credentials" if [ "$AUTH_TYPE" = "ssh" ]; then log_info "Using SSH - no credential caching needed" return fi # Detect OS and configure appropriate credential helper if [[ "$OSTYPE" == "darwin"* ]]; then # macOS - use keychain log_info "Configuring git to use macOS Keychain..." git config --global credential.helper osxkeychain log_success "Git credentials will be stored in macOS Keychain" elif [[ "$OSTYPE" == "linux-gnu"* ]]; then # Linux - try to use libsecret, fall back to cache if command -v git-credential-libsecret &> /dev/null; then log_info "Configuring git to use libsecret..." git config --global credential.helper libsecret log_success "Git credentials will be stored in system keyring" else log_info "Configuring git credential cache (1 hour)..." git config --global credential.helper 'cache --timeout=3600' log_success "Git credentials will be cached in memory for 1 hour" log_info "For persistent storage, install: libsecret" fi else # Unknown OS - use cache log_info "Configuring git credential cache (1 hour)..." git config --global credential.helper 'cache --timeout=3600' log_success "Git credentials will be cached in memory for 1 hour" fi # Store credentials by triggering a git operation if [ "$AUTH_TYPE" = "token" ] && [ -n "$INSTALL_DIR" ] && [ -d "$INSTALL_DIR/.git" ]; then log_info "Caching credentials..." cd "$INSTALL_DIR" # Trigger credential storage by doing a fetch (which uses credentials) git fetch --dry-run 2>&1 | grep -v "password\|token" > /dev/null || true log_success "Credentials cached successfully" fi } # ============================================================================ # Repository Cloning # ============================================================================ clone_repository() { log_header "Cloning Repository" prompt INSTALL_DIR "Installation directory" "$DEFAULT_INSTALL_DIR" if [ -d "$INSTALL_DIR" ]; then if [ -d "$INSTALL_DIR/.git" ]; then log_warning "Directory already exists and appears to be a git repository" prompt UPDATE_CHOICE "Update existing repository? (y/n)" "y" if [[ "$UPDATE_CHOICE" =~ ^[Yy] ]]; then log_info "Pulling latest changes..." cd "$INSTALL_DIR" git pull log_success "Repository updated" return else log_info "Using existing repository" return fi else log_error "Directory exists but is not a git repository" log_info "Please remove or rename: $INSTALL_DIR" exit 1 fi fi local git_url=$(prepare_git_url) log_info "Cloning repository to: $INSTALL_DIR" log_info "This may take a few minutes..." if git clone "$git_url" "$INSTALL_DIR" 2>&1 | grep -v "password\|token"; then log_success "Repository cloned successfully" else log_error "Failed to clone repository" log_info "Please check your credentials and try again" exit 1 fi } # ============================================================================ # CLI Installation # ============================================================================ install_cli() { log_header "Installing CLI Tool" local cli_script="$INSTALL_DIR/scripts/$CLI_NAME" if [ ! -f "$cli_script" ]; then log_error "CLI script not found at: $cli_script" exit 1 fi log_info "Installing $CLI_NAME to $CLI_INSTALL_DIR..." if [ -w "$CLI_INSTALL_DIR" ]; then cp "$cli_script" "$CLI_INSTALL_DIR/$CLI_NAME" chmod +x "$CLI_INSTALL_DIR/$CLI_NAME" else log_warning "Need sudo permissions to write to $CLI_INSTALL_DIR" sudo cp "$cli_script" "$CLI_INSTALL_DIR/$CLI_NAME" sudo chmod +x "$CLI_INSTALL_DIR/$CLI_NAME" fi if command -v "$CLI_NAME" &> /dev/null; then log_success "$CLI_NAME installed successfully!" else log_warning "$CLI_NAME installed but not found in PATH" log_info "You may need to restart your terminal or run:" log_info " export PATH=\"$CLI_INSTALL_DIR:\$PATH\"" fi } # ============================================================================ # Post-Installation # ============================================================================ show_next_steps() { log_header "Installation Complete!" echo -e "${GREEN}${BOLD}The InternalAI platform has been set up successfully!${NC}" echo "" echo -e "${BOLD}Platform Location:${NC} ${CYAN}$INSTALL_DIR${NC}" echo "" echo -e "${BOLD}Next Steps:${NC}" echo "" echo " 1. Configure and start the platform:" echo -e " ${GREEN}$CLI_NAME install${NC}" echo "" echo " 2. View available commands:" echo -e " ${GREEN}$CLI_NAME help${NC}" echo "" echo " 3. Check platform status:" echo -e " ${GREEN}$CLI_NAME status${NC}" echo "" echo -e "${BOLD}Documentation:${NC}" echo " • Platform docs: $INSTALL_DIR/docs/" echo " • README: $INSTALL_DIR/README.md" echo "" } # ============================================================================ # Main Installation Flow # ============================================================================ main() { show_welcome check_dependencies setup_gitea_auth clone_repository configure_git_credentials install_cli show_next_steps echo -e "${GREEN}${BOLD}Setup completed successfully!${NC}" echo "" } main "$@"