#!/bin/bash # ============================================================================ # Maia Installer # ============================================================================ # Installation script for Linux, macOS, WSL2, and Android/Termux. # Uses uv for desktop/server installs and Python's stdlib venv + pip on Termux. # # Usage: # curl -fsSL https://ampliia.com/maia/install.sh | bash # # Or with options: # curl -fsSL https://ampliia.com/maia/install.sh | bash -s -- --no-venv --skip-setup # # Corporate mirrors / private access: # curl -fsSL https://ampliia.com/maia/install.sh | MAIA_REPO_URL=git@github.com:and270/maia.git bash # # ============================================================================ set -e # Guard against environment leakage when the installer is launched from another # Python-driven tool session (e.g. an agent terminal tool). A pre-set PYTHONPATH # can force pip/entrypoints to import a different checkout than the one being # installed, which makes fresh installs appear broken or stale. if [ -n "${PYTHONPATH:-}" ]; then echo "⚠ Ignoring inherited PYTHONPATH during install to avoid module shadowing" unset PYTHONPATH fi if [ -n "${PYTHONHOME:-}" ]; then echo "⚠ Ignoring inherited PYTHONHOME during install" unset PYTHONHOME fi # Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[0;33m' BLUE='\033[0;34m' MAGENTA='\033[0;35m' CYAN='\033[0;36m' NC='\033[0m' # No Color BOLD='\033[1m' # Configuration REPO_URL_SSH="git@github.com:and270/maia.git" REPO_URL_HTTPS="https://github.com/and270/maia.git" # Data home: always ~/.maia unless MAIA_HOME (or --maia-home) says otherwise. # Maia NEVER uses ~/.hermes — that directory belongs to upstream Hermes Agent # on machines that have it, and sharing it mixes both products' config, # secrets, skills, and gateway services. Bring Hermes data into Maia only # through the guarded migration: maia import --from-hermes-export if [ -n "${MAIA_HOME:-}" ]; then MAIA_HOME="$MAIA_HOME" else if [ -n "${HERMES_HOME:-}" ]; then echo "⚠ HERMES_HOME is set in your environment (upstream Hermes?). The Maia" echo " installer ignores it and uses ~/.maia. Set MAIA_HOME to override." fi MAIA_HOME="$HOME/.maia" fi # INSTALL_DIR is resolved AFTER arg parsing and OS detection so we can pick an # FHS-style layout for root installs. Track whether the user gave us an # explicit directory — if so we never override it. if [ -n "${MAIA_INSTALL_DIR:-}" ]; then INSTALL_DIR="$MAIA_INSTALL_DIR" INSTALL_DIR_EXPLICIT=true elif [ -n "${HERMES_INSTALL_DIR:-}" ]; then INSTALL_DIR="$HERMES_INSTALL_DIR" INSTALL_DIR_EXPLICIT=true else INSTALL_DIR="" INSTALL_DIR_EXPLICIT=false fi PYTHON_VERSION="3.11" NODE_VERSION="22" # FHS-style root install layout (set by resolve_install_layout when applicable): # code at /usr/local/lib/maia, command at /usr/local/bin/maia, # data still at /root/.maia (MAIA_HOME). Matches Claude Code / Codex CLI # and keeps Docker bind-mounted /root/ volumes lean. ROOT_FHS_LAYOUT=false # Options USE_VENV=true RUN_SETUP=true BRANCH="main" MIGRATE_HERMES="" # ""=offer interactively, "yes"=copy, "no"=skip # Detect non-interactive mode (e.g. curl | bash) # When stdin is not a terminal, read -p will fail with EOF, # causing set -e to silently abort the entire script. if [ -t 0 ]; then IS_INTERACTIVE=true else IS_INTERACTIVE=false fi # Parse arguments while [[ $# -gt 0 ]]; do case $1 in --no-venv) USE_VENV=false shift ;; --skip-setup) RUN_SETUP=false shift ;; --branch) BRANCH="$2" shift 2 ;; --dir) INSTALL_DIR="$2" INSTALL_DIR_EXPLICIT=true shift 2 ;; --maia-home|--hermes-home) MAIA_HOME="$2" shift 2 ;; --migrate-hermes) MIGRATE_HERMES="yes" shift ;; --no-migrate-hermes) MIGRATE_HERMES="no" shift ;; -h|--help) echo "Maia Installer" echo "" echo "Usage: install.sh [OPTIONS]" echo "" echo "Options:" echo " --no-venv Don't create virtual environment" echo " --skip-setup Skip interactive setup (dashboard onboarding / wizard)" echo " --branch NAME Git branch to install (default: main)" echo " --dir PATH Installation directory" echo " default (non-root): ~/.maia/maia" echo " default (root, Linux): /usr/local/lib/maia" echo " --maia-home PATH Data directory (default: ~/.maia, or \$MAIA_HOME)" echo " --migrate-hermes Copy skills/crons/memories from ~/.hermes without asking" echo " --no-migrate-hermes Never offer the Hermes data copy" echo " -h, --help Show this help" echo "" echo "Environment:" echo " MAIA_HOME Data directory (config, sessions, logs)" echo " MAIA_INSTALL_DIR Code checkout directory" echo " MAIA_REPO_URL Clone URL override (corporate mirror or SSH access)" echo "" echo "Notes:" echo " When running as root on Linux, Maia installs the code under" echo " /usr/local/lib/maia and links the command into /usr/local/bin/maia" echo " (FHS layout, matches Claude Code / Codex CLI). Data, config," echo " sessions, and logs still live in \$MAIA_HOME (default /root/.maia)." echo " This keeps Docker bind-mounted volumes small and ensures the" echo " command is on PATH for all shells." echo " Existing installs at \$MAIA_HOME/maia are preserved in-place." exit 0 ;; *) echo "Unknown option: $1" exit 1 ;; esac done # Export so the setup wizard, skills sync, and any child process resolve the # same data home this installer used (the runtime bridges MAIA_* to HERMES_*). export MAIA_HOME case "$MAIA_HOME" in "$HOME"/*) MAIA_HOME_DISPLAY="~/${MAIA_HOME#"$HOME"/}" ;; *) MAIA_HOME_DISPLAY="$MAIA_HOME" ;; esac # ============================================================================ # Helper functions # ============================================================================ print_banner() { echo "" echo -e "${MAGENTA}${BOLD}" echo "┌─────────────────────────────────────────────────────────┐" echo "│ ◆ Maia Installer │" echo "├─────────────────────────────────────────────────────────┤" echo "│ An AI agent for the enterprise. Open source. Free. │" echo "│ By AmpliIA, based on Nous Research's Hermes Agent. │" echo "└─────────────────────────────────────────────────────────┘" echo -e "${NC}" } log_info() { echo -e "${CYAN}→${NC} $1" } log_success() { echo -e "${GREEN}✓${NC} $1" } log_warn() { echo -e "${YELLOW}⚠${NC} $1" } log_error() { echo -e "${RED}✗${NC} $1" } prompt_yes_no() { local question="$1" local default="${2:-yes}" local prompt_suffix local answer="" # Use case patterns (not ${var,,}) so this works on bash 3.2 (macOS /bin/bash). case "$default" in [yY]|[yY][eE][sS]|[tT][rR][uU][eE]|1) prompt_suffix="[Y/n]" ;; *) prompt_suffix="[y/N]" ;; esac if [ "$IS_INTERACTIVE" = true ]; then read -r -p "$question $prompt_suffix " answer || answer="" elif [ -r /dev/tty ] && [ -w /dev/tty ]; then printf "%s %s " "$question" "$prompt_suffix" > /dev/tty IFS= read -r answer < /dev/tty || answer="" else answer="" fi answer="${answer#"${answer%%[![:space:]]*}"}" answer="${answer%"${answer##*[![:space:]]}"}" if [ -z "$answer" ]; then case "$default" in [yY]|[yY][eE][sS]|[tT][rR][uU][eE]|1) return 0 ;; *) return 1 ;; esac fi case "$answer" in [yY]|[yY][eE][sS]) return 0 ;; *) return 1 ;; esac } is_termux() { [ -n "${TERMUX_VERSION:-}" ] || [[ "${PREFIX:-}" == *"com.termux/files/usr"* ]] } # Decide where the repo checkout + venv live, and where the `maia` command # link goes. Called after detect_os so $OS/$DISTRO are known. # # Defaults: # - Non-root, any OS: INSTALL_DIR = $MAIA_HOME/maia # command link in $HOME/.local/bin when writable, # otherwise $MAIA_HOME/bin # - Termux (any uid): INSTALL_DIR = $MAIA_HOME/maia # command link in $PREFIX/bin (already on PATH) # - Root on Linux (new): INSTALL_DIR = /usr/local/lib/maia # command link in /usr/local/bin # (unless an install already exists at # $MAIA_HOME/maia — then preserve it) # # Always no-op when the user set --dir, $MAIA_INSTALL_DIR, or the legacy # $HERMES_INSTALL_DIR. resolve_install_layout() { if [ "$INSTALL_DIR_EXPLICIT" = true ]; then log_info "Install directory: $INSTALL_DIR (explicit)" return 0 fi # Termux: package manager manages /data/data/..., keep code in MAIA_HOME. if is_termux; then INSTALL_DIR="$MAIA_HOME/maia" return 0 fi # Root on Linux: prefer FHS layout unless an install already exists. # macOS root installs keep the user-scoped layout because /usr/local/ on # macOS is Homebrew territory and we don't want to fight that. if [ "$OS" = "linux" ] && [ "$(id -u)" -eq 0 ]; then if [ -d "$MAIA_HOME/maia/.git" ]; then INSTALL_DIR="$MAIA_HOME/maia" log_info "Existing install detected at $INSTALL_DIR — keeping user-scoped layout" log_info " (new root installs use /usr/local/lib/maia)" return 0 fi INSTALL_DIR="/usr/local/lib/maia" ROOT_FHS_LAYOUT=true log_info "Root install on Linux — using FHS layout" log_info " Code: $INSTALL_DIR" log_info " Command: /usr/local/bin/maia" log_info " Data: $MAIA_HOME (unchanged)" return 0 fi # Default: non-root, non-Termux → user-scoped layout. INSTALL_DIR="$MAIA_HOME/maia" } ensure_writable_dir() { mkdir -p "$1" 2>/dev/null && [ -d "$1" ] && [ -w "$1" ] } get_user_bin_dir() { local preferred_dir="$HOME/.local/bin" local fallback_dir="$MAIA_HOME/bin" if ensure_writable_dir "$preferred_dir"; then echo "$preferred_dir" return 0 fi if ensure_writable_dir "$fallback_dir"; then echo "$fallback_dir" return 0 fi return 1 } get_uv_install_dir() { get_user_bin_dir } get_command_link_dir() { if is_termux && [ -n "${PREFIX:-}" ]; then echo "$PREFIX/bin" elif [ "$ROOT_FHS_LAYOUT" = true ]; then echo "/usr/local/bin" else get_user_bin_dir fi } get_command_link_display_dir() { local resolved_dir="${1:-}" if [ -z "$resolved_dir" ]; then resolved_dir="$(get_command_link_dir 2>/dev/null || true)" fi if is_termux && [ -n "${PREFIX:-}" ] && [ "$resolved_dir" = "$PREFIX/bin" ]; then echo '$PREFIX/bin' elif [ "$resolved_dir" = "/usr/local/bin" ]; then echo '/usr/local/bin' elif [ "$resolved_dir" = "$HOME/.local/bin" ]; then echo '~/.local/bin' elif [ "$resolved_dir" = "$MAIA_HOME/bin" ]; then if [ "$MAIA_HOME" = "$HOME/.maia" ]; then echo '~/.maia/bin' else echo "$MAIA_HOME/bin" fi elif [[ "$resolved_dir" == "$HOME/"* ]]; then echo "~/${resolved_dir#$HOME/}" else echo "$resolved_dir" fi } get_maia_command_path() { local link_dir if ! link_dir="$(get_command_link_dir)"; then echo "maia" return 0 fi if [ -x "$link_dir/maia" ]; then echo "$link_dir/maia" else echo "maia" fi } # ============================================================================ # System detection # ============================================================================ detect_os() { case "$(uname -s)" in Linux*) if is_termux; then OS="android" DISTRO="termux" else OS="linux" if [ -f /etc/os-release ]; then . /etc/os-release DISTRO="$ID" else DISTRO="unknown" fi fi ;; Darwin*) OS="macos" DISTRO="macos" ;; CYGWIN*|MINGW*|MSYS*) OS="windows" DISTRO="windows" log_error "Windows detected. Maia runs on Windows through WSL2." log_info "From PowerShell (as Administrator): wsl --install -d Ubuntu" log_info "Then, inside the Ubuntu/WSL terminal, re-run this installer:" log_info " curl -fsSL https://ampliia.com/maia/install.sh | bash" log_info "Guide: https://ampliia.com/en/maia/docs/#install" exit 1 ;; *) OS="unknown" DISTRO="unknown" log_warn "Unknown operating system" ;; esac log_success "Detected: $OS ($DISTRO)" } # ============================================================================ # Dependency checks # ============================================================================ install_uv() { if [ "$DISTRO" = "termux" ]; then log_info "Termux detected — using Python's stdlib venv + pip instead of uv" UV_CMD="" return 0 fi log_info "Checking for uv package manager..." # Check common locations for uv if command -v uv &> /dev/null; then UV_CMD="uv" UV_VERSION=$($UV_CMD --version 2>/dev/null) log_success "uv found ($UV_VERSION)" return 0 fi # Check ~/.local/bin (default uv install location) even if not on PATH yet if [ -x "$HOME/.local/bin/uv" ]; then UV_CMD="$HOME/.local/bin/uv" UV_VERSION=$($UV_CMD --version 2>/dev/null) log_success "uv found at ~/.local/bin ($UV_VERSION)" return 0 fi # Check Maia's fallback user bin (used when ~/.local/bin is not writable). if [ -x "$MAIA_HOME/bin/uv" ]; then UV_CMD="$MAIA_HOME/bin/uv" UV_VERSION=$($UV_CMD --version 2>/dev/null) log_success "uv found at $MAIA_HOME_DISPLAY/bin ($UV_VERSION)" return 0 fi # Check ~/.cargo/bin (alternative uv install location) if [ -x "$HOME/.cargo/bin/uv" ]; then UV_CMD="$HOME/.cargo/bin/uv" UV_VERSION=$($UV_CMD --version 2>/dev/null) log_success "uv found at ~/.cargo/bin ($UV_VERSION)" return 0 fi # Install uv log_info "Installing uv (fast Python package manager)..." local uv_install_dir if ! uv_install_dir="$(get_uv_install_dir)"; then log_error "Could not create a writable user command directory" log_info "Checked: $HOME/.local/bin and $MAIA_HOME/bin" exit 1 fi local uv_install_display_dir uv_install_display_dir="$(get_command_link_display_dir "$uv_install_dir")" if [ "$uv_install_dir" != "$HOME/.local/bin" ]; then log_warn "~/.local/bin is not writable; installing uv to $uv_install_display_dir" fi if curl -LsSf https://astral.sh/uv/install.sh | UV_INSTALL_DIR="$uv_install_dir" sh 2>/dev/null; then if [ -x "$uv_install_dir/uv" ]; then UV_CMD="$uv_install_dir/uv" elif [ -x "$HOME/.local/bin/uv" ]; then UV_CMD="$HOME/.local/bin/uv" elif [ -x "$MAIA_HOME/bin/uv" ]; then UV_CMD="$MAIA_HOME/bin/uv" elif [ -x "$HOME/.cargo/bin/uv" ]; then UV_CMD="$HOME/.cargo/bin/uv" elif command -v uv &> /dev/null; then UV_CMD="uv" else log_error "uv installed but not found on PATH" log_info "Expected it in $uv_install_display_dir" exit 1 fi UV_VERSION=$($UV_CMD --version 2>/dev/null) log_success "uv installed ($UV_VERSION)" else log_error "Failed to install uv" log_info "Install manually: https://docs.astral.sh/uv/getting-started/installation/" exit 1 fi } check_python() { if [ "$DISTRO" = "termux" ]; then log_info "Checking Termux Python..." if command -v python >/dev/null 2>&1; then PYTHON_PATH="$(command -v python)" if "$PYTHON_PATH" -c 'import sys; raise SystemExit(0 if sys.version_info >= (3, 11) else 1)' 2>/dev/null; then PYTHON_FOUND_VERSION="$("$PYTHON_PATH" --version 2>/dev/null)" log_success "Python found: $PYTHON_FOUND_VERSION" return 0 fi fi log_info "Installing Python via pkg..." pkg install -y python >/dev/null PYTHON_PATH="$(command -v python)" PYTHON_FOUND_VERSION="$("$PYTHON_PATH" --version 2>/dev/null)" log_success "Python installed: $PYTHON_FOUND_VERSION" return 0 fi log_info "Checking Python $PYTHON_VERSION..." # Let uv handle Python — it can download and manage Python versions # First check if a suitable Python is already available if PYTHON_PATH="$("$UV_CMD" python find "$PYTHON_VERSION" 2>/dev/null)"; then PYTHON_FOUND_VERSION="$("$PYTHON_PATH" --version 2>/dev/null)" log_success "Python found: $PYTHON_FOUND_VERSION" return 0 fi # Python not found — use uv to install it (no sudo needed!) log_info "Python $PYTHON_VERSION not found, installing via uv..." if "$UV_CMD" python install "$PYTHON_VERSION"; then PYTHON_PATH="$("$UV_CMD" python find "$PYTHON_VERSION")" PYTHON_FOUND_VERSION="$("$PYTHON_PATH" --version 2>/dev/null)" log_success "Python installed: $PYTHON_FOUND_VERSION" else log_error "Failed to install Python $PYTHON_VERSION" log_info "Install Python $PYTHON_VERSION manually, then re-run this script" exit 1 fi } check_git() { log_info "Checking Git..." if command -v git &> /dev/null; then GIT_VERSION=$(git --version | awk '{print $3}') log_success "Git $GIT_VERSION found" return 0 fi log_error "Git not found" if [ "$DISTRO" = "termux" ]; then log_info "Installing Git via pkg..." pkg install -y git >/dev/null if command -v git >/dev/null 2>&1; then GIT_VERSION=$(git --version | awk '{print $3}') log_success "Git $GIT_VERSION installed" return 0 fi fi log_info "Please install Git:" case "$OS" in linux) case "$DISTRO" in ubuntu|debian) log_info " sudo apt update && sudo apt install git" ;; fedora) log_info " sudo dnf install git" ;; arch) log_info " sudo pacman -S git" ;; *) log_info " Use your package manager to install git" ;; esac ;; android) log_info " pkg install git" ;; macos) log_info " xcode-select --install" log_info " Or: brew install git" ;; esac exit 1 } check_node() { log_info "Checking Node.js (for browser tools and the dashboard)..." if command -v node &> /dev/null; then local found_ver=$(node --version) log_success "Node.js $found_ver found" HAS_NODE=true return 0 fi # Check our own managed install from a previous run if [ -x "$MAIA_HOME/node/bin/node" ]; then export PATH="$MAIA_HOME/node/bin:$PATH" local found_ver=$("$MAIA_HOME/node/bin/node" --version) log_success "Node.js $found_ver found (Maia-managed)" HAS_NODE=true return 0 fi if [ "$DISTRO" = "termux" ]; then log_info "Node.js not found — installing Node.js via pkg..." else log_info "Node.js not found — installing Node.js $NODE_VERSION LTS..." fi install_node } install_node() { if [ "$DISTRO" = "termux" ]; then log_info "Installing Node.js via pkg..." if pkg install -y nodejs >/dev/null; then local installed_ver installed_ver=$(node --version 2>/dev/null) log_success "Node.js $installed_ver installed via pkg" HAS_NODE=true else log_warn "Failed to install Node.js via pkg" HAS_NODE=false fi return 0 fi local arch=$(uname -m) local node_arch case "$arch" in x86_64) node_arch="x64" ;; aarch64|arm64) node_arch="arm64" ;; armv7l) node_arch="armv7l" ;; *) log_warn "Unsupported architecture ($arch) for Node.js auto-install" log_info "Install manually: https://nodejs.org/en/download/" HAS_NODE=false return 0 ;; esac local node_os case "$OS" in linux) node_os="linux" ;; macos) node_os="darwin" ;; *) log_warn "Unsupported OS for Node.js auto-install" HAS_NODE=false return 0 ;; esac # Resolve the latest v22.x.x tarball name from the index page local index_url="https://nodejs.org/dist/latest-v${NODE_VERSION}.x/" local tarball_name tarball_name=$(curl -fsSL "$index_url" \ | grep -oE "node-v${NODE_VERSION}\.[0-9]+\.[0-9]+-${node_os}-${node_arch}\.tar\.xz" \ | head -1) # Fallback to .tar.gz if .tar.xz not available if [ -z "$tarball_name" ]; then tarball_name=$(curl -fsSL "$index_url" \ | grep -oE "node-v${NODE_VERSION}\.[0-9]+\.[0-9]+-${node_os}-${node_arch}\.tar\.gz" \ | head -1) fi if [ -z "$tarball_name" ]; then log_warn "Could not find Node.js $NODE_VERSION binary for $node_os-$node_arch" log_info "Install manually: https://nodejs.org/en/download/" HAS_NODE=false return 0 fi local download_url="${index_url}${tarball_name}" local tmp_dir tmp_dir=$(mktemp -d) log_info "Downloading $tarball_name..." if ! curl -fsSL "$download_url" -o "$tmp_dir/$tarball_name"; then log_warn "Download failed" rm -rf "$tmp_dir" HAS_NODE=false return 0 fi log_info "Extracting to $MAIA_HOME_DISPLAY/node/..." if [[ "$tarball_name" == *.tar.xz ]]; then tar xf "$tmp_dir/$tarball_name" -C "$tmp_dir" else tar xzf "$tmp_dir/$tarball_name" -C "$tmp_dir" fi local extracted_dir extracted_dir=$(ls -d "$tmp_dir"/node-v* 2>/dev/null | head -1) if [ ! -d "$extracted_dir" ]; then log_warn "Extraction failed" rm -rf "$tmp_dir" HAS_NODE=false return 0 fi # Place into $MAIA_HOME/node/ and expose binaries through the same writable # user bin policy as uv and the Maia launcher. rm -rf "$MAIA_HOME/node" mkdir -p "$MAIA_HOME" mv "$extracted_dir" "$MAIA_HOME/node" rm -rf "$tmp_dir" local user_bin_dir if user_bin_dir="$(get_user_bin_dir)"; then ln -sf "$MAIA_HOME/node/bin/node" "$user_bin_dir/node" ln -sf "$MAIA_HOME/node/bin/npm" "$user_bin_dir/npm" ln -sf "$MAIA_HOME/node/bin/npx" "$user_bin_dir/npx" else log_warn "Could not create a persistent user bin directory for Node.js" log_info "Node.js remains available to Maia from $MAIA_HOME_DISPLAY/node/bin" fi export PATH="$MAIA_HOME/node/bin:$PATH" local installed_ver installed_ver=$("$MAIA_HOME/node/bin/node" --version 2>/dev/null) log_success "Node.js $installed_ver installed to $MAIA_HOME_DISPLAY/node/" HAS_NODE=true } install_system_packages() { # Detect what's missing HAS_RIPGREP=false HAS_FFMPEG=false local need_ripgrep=false local need_ffmpeg=false log_info "Checking ripgrep (fast file search)..." if command -v rg &> /dev/null; then log_success "$(rg --version | head -1) found" HAS_RIPGREP=true else need_ripgrep=true fi log_info "Checking ffmpeg (TTS voice messages)..." if command -v ffmpeg &> /dev/null; then local ffmpeg_ver=$(ffmpeg -version 2>/dev/null | head -1 | awk '{print $3}') log_success "ffmpeg $ffmpeg_ver found" HAS_FFMPEG=true else need_ffmpeg=true fi # Termux always needs the Android build toolchain for the tested pip path, # even when ripgrep/ffmpeg are already present. if [ "$DISTRO" = "termux" ]; then local termux_pkgs=(clang rust make pkg-config libffi openssl) if [ "$need_ripgrep" = true ]; then termux_pkgs+=("ripgrep") fi if [ "$need_ffmpeg" = true ]; then termux_pkgs+=("ffmpeg") fi log_info "Installing Termux packages: ${termux_pkgs[*]}" if pkg install -y "${termux_pkgs[@]}" >/dev/null; then [ "$need_ripgrep" = true ] && HAS_RIPGREP=true && log_success "ripgrep installed" [ "$need_ffmpeg" = true ] && HAS_FFMPEG=true && log_success "ffmpeg installed" log_success "Termux build dependencies installed" return 0 fi log_warn "Could not auto-install all Termux packages" log_info "Install manually: pkg install ${termux_pkgs[*]}" return 0 fi # Nothing to install — done if [ "$need_ripgrep" = false ] && [ "$need_ffmpeg" = false ]; then return 0 fi # Build a human-readable description + package list local desc_parts=() local pkgs=() if [ "$need_ripgrep" = true ]; then desc_parts+=("ripgrep for faster file search") pkgs+=("ripgrep") fi if [ "$need_ffmpeg" = true ]; then desc_parts+=("ffmpeg for TTS voice messages") pkgs+=("ffmpeg") fi local description description=$(IFS=" and "; echo "${desc_parts[*]}") # ── macOS: brew ── if [ "$OS" = "macos" ]; then if command -v brew &> /dev/null; then log_info "Installing ${pkgs[*]} via Homebrew..." if brew install "${pkgs[@]}"; then [ "$need_ripgrep" = true ] && HAS_RIPGREP=true && log_success "ripgrep installed" [ "$need_ffmpeg" = true ] && HAS_FFMPEG=true && log_success "ffmpeg installed" return 0 fi fi log_warn "Could not auto-install (brew not found or install failed)" log_info "Install manually: brew install ${pkgs[*]}" return 0 fi # ── Linux: resolve package manager command ── local pkg_install="" case "$DISTRO" in ubuntu|debian) pkg_install="apt install -y" ;; fedora) pkg_install="dnf install -y" ;; arch) pkg_install="pacman -S --noconfirm" ;; esac if [ -n "$pkg_install" ]; then local install_cmd="$pkg_install ${pkgs[*]}" # Prevent needrestart/whiptail dialogs from blocking non-interactive installs case "$DISTRO" in ubuntu|debian) export DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a ;; esac # Already root — just install if [ "$(id -u)" -eq 0 ]; then log_info "Installing ${pkgs[*]}..." if $install_cmd; then [ "$need_ripgrep" = true ] && HAS_RIPGREP=true && log_success "ripgrep installed" [ "$need_ffmpeg" = true ] && HAS_FFMPEG=true && log_success "ffmpeg installed" return 0 fi # Passwordless sudo — just install elif command -v sudo &> /dev/null && sudo -n true 2>/dev/null; then log_info "Installing ${pkgs[*]}..." if sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a $install_cmd; then [ "$need_ripgrep" = true ] && HAS_RIPGREP=true && log_success "ripgrep installed" [ "$need_ffmpeg" = true ] && HAS_FFMPEG=true && log_success "ffmpeg installed" return 0 fi # sudo needs password — ask once for everything elif command -v sudo &> /dev/null; then if [ "$IS_INTERACTIVE" = true ]; then echo "" log_info "sudo is needed ONLY to install optional system packages (${pkgs[*]}) via your package manager." log_info "Maia itself does not require or retain root access." if prompt_yes_no "Install ${description}? (requires sudo)" "no"; then if sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a $install_cmd; then [ "$need_ripgrep" = true ] && HAS_RIPGREP=true && log_success "ripgrep installed" [ "$need_ffmpeg" = true ] && HAS_FFMPEG=true && log_success "ffmpeg installed" return 0 fi fi elif (: /dev/null; then # Non-interactive (e.g. curl | bash) but a terminal is available. # Read the prompt from /dev/tty (same approach the setup wizard uses). # Probe by actually opening /dev/tty: a bare existence test passes # in Docker builds where the device node is in the mount namespace # but opening fails with ENXIO. See #16746. echo "" log_info "sudo is needed ONLY to install optional system packages (${pkgs[*]}) via your package manager." log_info "Maia itself does not require or retain root access." if prompt_yes_no "Install ${description}?" "yes"; then if sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a $install_cmd < /dev/tty; then [ "$need_ripgrep" = true ] && HAS_RIPGREP=true && log_success "ripgrep installed" [ "$need_ffmpeg" = true ] && HAS_FFMPEG=true && log_success "ffmpeg installed" return 0 fi fi else log_warn "Non-interactive mode and no terminal available — cannot install system packages" log_info "Install manually after setup completes: sudo $install_cmd" fi fi fi # ── Fallback for ripgrep: cargo ── if [ "$need_ripgrep" = true ] && [ "$HAS_RIPGREP" = false ]; then if command -v cargo &> /dev/null; then log_info "Trying cargo install ripgrep (no sudo needed)..." if cargo install ripgrep; then log_success "ripgrep installed via cargo" HAS_RIPGREP=true fi fi fi # ── Show manual instructions for anything still missing ── if [ "$HAS_RIPGREP" = false ] && [ "$need_ripgrep" = true ]; then log_warn "ripgrep not installed (file search will use grep fallback)" show_manual_install_hint "ripgrep" fi if [ "$HAS_FFMPEG" = false ] && [ "$need_ffmpeg" = true ]; then log_warn "ffmpeg not installed (TTS voice messages will be limited)" show_manual_install_hint "ffmpeg" fi } show_manual_install_hint() { local pkg="$1" log_info "To install $pkg manually:" case "$OS" in linux) case "$DISTRO" in ubuntu|debian) log_info " sudo apt install $pkg" ;; fedora) log_info " sudo dnf install $pkg" ;; arch) log_info " sudo pacman -S $pkg" ;; *) log_info " Use your package manager or visit the project homepage" ;; esac ;; android) log_info " pkg install $pkg" ;; macos) log_info " brew install $pkg" ;; esac } # ============================================================================ # Installation # ============================================================================ clone_repo() { log_info "Installing to $INSTALL_DIR..." if [ -d "$INSTALL_DIR" ]; then if [ -d "$INSTALL_DIR/.git" ]; then log_info "Existing installation found, updating..." cd "$INSTALL_DIR" local autostash_ref="" if [ -n "$(git status --porcelain)" ]; then local stash_name stash_name="maia-install-autostash-$(date -u +%Y%m%d-%H%M%S)" log_info "Local changes detected, stashing before update..." git stash push --include-untracked -m "$stash_name" autostash_ref="$(git rev-parse --verify refs/stash)" fi git fetch origin git checkout "$BRANCH" git pull --ff-only origin "$BRANCH" if [ -n "$autostash_ref" ]; then local restore_now="yes" if [ -t 0 ] && [ -t 1 ]; then echo log_warn "Local changes were stashed before updating." log_warn "Restoring them may reapply local customizations onto the updated codebase." printf "Restore local changes now? [Y/n] " read -r restore_answer case "$restore_answer" in ""|y|Y|yes|YES|Yes) restore_now="yes" ;; *) restore_now="no" ;; esac fi if [ "$restore_now" = "yes" ]; then log_info "Restoring local changes..." if git stash apply "$autostash_ref"; then git stash drop "$autostash_ref" >/dev/null log_warn "Local changes were restored on top of the updated codebase." log_warn "Review git diff / git status if Maia behaves unexpectedly." else log_error "Update succeeded, but restoring local changes failed. Your changes are still preserved in git stash." log_info "Resolve manually with: git stash apply $autostash_ref" exit 1 fi else log_info "Skipped restoring local changes." log_info "Your changes are still preserved in git stash." log_info "Restore manually with: git stash apply $autostash_ref" fi fi else log_error "Directory exists but is not a git repository: $INSTALL_DIR" log_info "Remove it or choose a different directory with --dir" exit 1 fi elif [ -n "${MAIA_REPO_URL:-}" ]; then # Explicit clone source: corporate mirror, private SSH URL, or local path. log_info "Cloning from MAIA_REPO_URL..." if git clone --branch "$BRANCH" "$MAIA_REPO_URL" "$INSTALL_DIR"; then log_success "Cloned from $MAIA_REPO_URL" else log_error "Failed to clone from MAIA_REPO_URL ($MAIA_REPO_URL)" exit 1 fi else # Try SSH first (for private repo access), fall back to HTTPS # GIT_SSH_COMMAND disables interactive prompts and sets a short timeout # so SSH fails fast instead of hanging when no key is configured. log_info "Trying SSH clone..." if GIT_SSH_COMMAND="ssh -o BatchMode=yes -o ConnectTimeout=5" \ git clone --branch "$BRANCH" "$REPO_URL_SSH" "$INSTALL_DIR" 2>/dev/null; then log_success "Cloned via SSH" else rm -rf "$INSTALL_DIR" 2>/dev/null # Clean up partial SSH clone log_info "SSH failed, trying HTTPS..." if GIT_TERMINAL_PROMPT=0 git clone --branch "$BRANCH" "$REPO_URL_HTTPS" "$INSTALL_DIR"; then log_success "Cloned via HTTPS" else log_error "Failed to clone repository" log_info "If the Maia repository requires access (private repo or corporate mirror)," log_info "set MAIA_REPO_URL to a clone URL you are authorized for and re-run:" log_info " curl -fsSL https://ampliia.com/maia/install.sh | MAIA_REPO_URL=git@github.com:and270/maia.git bash" exit 1 fi fi fi cd "$INSTALL_DIR" log_success "Repository ready" } setup_venv() { if [ "$USE_VENV" = false ]; then log_info "Skipping virtual environment (--no-venv)" return 0 fi if [ "$DISTRO" = "termux" ]; then log_info "Creating virtual environment with Termux Python..." if [ -d "venv" ]; then log_info "Virtual environment already exists, recreating..." rm -rf venv fi "$PYTHON_PATH" -m venv venv log_success "Virtual environment ready ($(./venv/bin/python --version 2>/dev/null))" return 0 fi log_info "Creating virtual environment with Python $PYTHON_VERSION..." if [ -d "venv" ]; then log_info "Virtual environment already exists, recreating..." rm -rf venv fi # uv creates the venv and pins the Python version in one step $UV_CMD venv venv --python "$PYTHON_VERSION" log_success "Virtual environment ready (Python $PYTHON_VERSION)" } install_deps() { log_info "Installing dependencies..." if [ "$DISTRO" = "termux" ]; then if [ "$USE_VENV" = true ]; then export VIRTUAL_ENV="$INSTALL_DIR/venv" PIP_PYTHON="$INSTALL_DIR/venv/bin/python" else PIP_PYTHON="$PYTHON_PATH" fi if [ -z "${ANDROID_API_LEVEL:-}" ]; then ANDROID_API_LEVEL="$(getprop ro.build.version.sdk 2>/dev/null || true)" if [ -z "$ANDROID_API_LEVEL" ]; then ANDROID_API_LEVEL=24 fi export ANDROID_API_LEVEL log_info "Using ANDROID_API_LEVEL=$ANDROID_API_LEVEL for Android wheel builds" fi "$PIP_PYTHON" -m pip install --upgrade pip setuptools wheel >/dev/null if ! "$PIP_PYTHON" -m pip install -e '.[termux]' -c constraints-termux.txt; then log_warn "Termux feature install (.[termux]) failed, trying base install..." if ! "$PIP_PYTHON" -m pip install -e '.' -c constraints-termux.txt; then log_error "Package installation failed on Termux." log_info "Ensure these packages are installed: pkg install clang rust make pkg-config libffi openssl" log_info "Then re-run: cd $INSTALL_DIR && python -m pip install -e '.[termux]' -c constraints-termux.txt" exit 1 fi fi log_success "Main package installed" log_info "Termux note: browser/WhatsApp tooling is not installed by default; see the Termux guide for optional follow-up steps." if [ -d "tinker-atropos" ] && [ -f "tinker-atropos/pyproject.toml" ]; then log_info "tinker-atropos submodule found — skipping install (optional, for RL training)" log_info " To install later: $PIP_PYTHON -m pip install -e \"./tinker-atropos\"" fi log_success "All dependencies installed" return 0 fi if [ "$USE_VENV" = true ]; then # Tell uv to install into our venv (no need to activate) export VIRTUAL_ENV="$INSTALL_DIR/venv" fi # On Debian/Ubuntu (including WSL), some Python packages need build tools. # Check and offer to install them if missing. if [ "$DISTRO" = "ubuntu" ] || [ "$DISTRO" = "debian" ]; then local need_build_tools=false for pkg in gcc python3-dev libffi-dev; do if ! dpkg -s "$pkg" &>/dev/null; then need_build_tools=true break fi done if [ "$need_build_tools" = true ]; then log_info "Some build tools may be needed for Python packages..." if command -v sudo &> /dev/null; then if sudo -n true 2>/dev/null; then sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get update -qq && sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y -qq build-essential python3-dev libffi-dev >/dev/null 2>&1 || true log_success "Build tools installed" else log_info "sudo is needed ONLY to install build tools (build-essential, python3-dev, libffi-dev) via apt." log_info "Maia itself does not require or retain root access." if prompt_yes_no "Install build tools?" "yes"; then sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get update -qq && sudo DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt-get install -y -qq build-essential python3-dev libffi-dev >/dev/null 2>&1 || true log_success "Build tools installed" fi fi fi fi fi # Prefer uv sync with the lockfile (hash-verified installs) like # setup-maia.sh does, fall back to editable pip installs when the # lockfile is stale or sync is unavailable. if [ "$USE_VENV" = true ] && [ -f "uv.lock" ]; then log_info "Using uv.lock for hash-verified installation..." UV_SYNC_LOG="$(mktemp 2>/dev/null || echo /tmp/maia-uv-sync.log)" if UV_PROJECT_ENVIRONMENT="$INSTALL_DIR/venv" $UV_CMD sync --all-extras --locked 2>"$UV_SYNC_LOG"; then log_success "Dependencies installed (lockfile verified)" rm -f "$UV_SYNC_LOG" 2>/dev/null || true else log_warn "Lockfile install failed, falling back to pip install..." log_info "Reason (last lines from uv):" tail -n 6 "$UV_SYNC_LOG" 2>/dev/null | sed 's/^/ /' rm -f "$UV_SYNC_LOG" 2>/dev/null || true install_deps_pip_fallback fi else install_deps_pip_fallback fi # Sanity check: a core import must work before we advertise success or # offer the wizard. Catches silent partial installs (seen on WSL /mnt/*) # that otherwise crash later with ModuleNotFoundError. if [ "$USE_VENV" = true ]; then if ! "$INSTALL_DIR/venv/bin/python" -c "import dotenv, httpx" 2>/dev/null; then log_error "Core dependencies did not import after install." log_info "Re-run this installer, or install manually:" log_info " cd $INSTALL_DIR && uv pip install -e '.[all]'" exit 1 fi fi # tinker-atropos (RL training) is optional — skip by default. # To enable RL tools: git submodule update --init tinker-atropos && uv pip install -e "./tinker-atropos" if [ -d "tinker-atropos" ] && [ -f "tinker-atropos/pyproject.toml" ]; then log_info "tinker-atropos submodule found — skipping install (optional, for RL training)" log_info " To install: $UV_CMD pip install -e \"./tinker-atropos\"" fi log_success "All dependencies installed" } install_deps_pip_fallback() { # Install the main package in editable mode with all extras. # Try [all] first, fall back to base install if extras have issues. ALL_INSTALL_LOG=$(mktemp) if ! $UV_CMD pip install -e ".[all]" 2>"$ALL_INSTALL_LOG"; then log_warn "Full install (.[all]) failed, trying base install..." log_info "Reason: $(tail -5 "$ALL_INSTALL_LOG" | head -3)" rm -f "$ALL_INSTALL_LOG" if ! $UV_CMD pip install -e "."; then log_error "Package installation failed." log_info "Check that build tools are installed: sudo apt install build-essential python3-dev" log_info "Then re-run: cd $INSTALL_DIR && uv pip install -e '.[all]'" exit 1 fi else rm -f "$ALL_INSTALL_LOG" fi log_success "Main package installed" } setup_path() { log_info "Setting up maia command..." if [ "$USE_VENV" = true ]; then MAIA_BIN="$INSTALL_DIR/venv/bin/maia" else MAIA_BIN="$(which maia 2>/dev/null || echo "")" if [ -z "$MAIA_BIN" ]; then log_warn "maia not found on PATH after install" return 0 fi fi # Verify the entry point script was actually generated if [ ! -x "$MAIA_BIN" ]; then log_warn "maia entry point not found at $MAIA_BIN" log_info "This usually means the pip install didn't complete successfully." if [ "$DISTRO" = "termux" ]; then log_info "Try: cd $INSTALL_DIR && python -m pip install -e '.[termux]' -c constraints-termux.txt" else log_info "Try: cd $INSTALL_DIR && uv pip install -e '.[all]'" fi return 0 fi local command_link_dir local command_link_display_dir if ! command_link_dir="$(get_command_link_dir)"; then log_error "Could not create a writable directory for the maia command" log_info "Checked: $HOME/.local/bin and $MAIA_HOME/bin" exit 1 fi command_link_display_dir="$(get_command_link_display_dir "$command_link_dir")" if [ "$ROOT_FHS_LAYOUT" != true ] && [ "$DISTRO" != "termux" ] \ && [ "$command_link_dir" != "$HOME/.local/bin" ]; then log_warn "~/.local/bin is not writable; using $command_link_display_dir automatically" fi # Create a user-facing shim for the maia command. # We intentionally clear PYTHONPATH/PYTHONHOME here so inherited env vars # can't make this launcher import modules from another checkout. mkdir -p "$command_link_dir" cat > "$command_link_dir/maia" </dev/null 2>&1; then log_info "/usr/local/bin is already on PATH for all shells" log_success "maia command ready" return 0 fi log_info "maia not on PATH in non-login shells (common on RHEL-family)" PATH_LINE='export PATH="/usr/local/bin:$PATH"' PATH_COMMENT='# Maia - ensure /usr/local/bin is on PATH (RHEL non-login shells)' for SHELL_CONFIG in "$HOME/.bashrc" "$HOME/.bash_profile"; do [ -f "$SHELL_CONFIG" ] || continue if ! grep -v '^[[:space:]]*#' "$SHELL_CONFIG" 2>/dev/null \ | grep -qE 'PATH=.*(/usr/local/bin|\$command_link_dir)'; then echo "" >> "$SHELL_CONFIG" echo "$PATH_COMMENT" >> "$SHELL_CONFIG" echo "$PATH_LINE" >> "$SHELL_CONFIG" log_success "Added /usr/local/bin to PATH in $SHELL_CONFIG" fi done log_success "maia command ready" return 0 fi # Check if the resolved user command directory is on PATH; if not, add it # to the relevant shell config. This is normally ~/.local/bin, with # $MAIA_HOME/bin as an automatic fallback when ~/.local/bin is unwritable. # Detect the user's actual login shell (not the shell running this script, # which is always bash when piped from curl). if ! echo "$PATH" | tr ':' '\n' | grep -Fxq "$command_link_dir"; then SHELL_CONFIGS=() IS_FISH=false LOGIN_SHELL="$(basename "${SHELL:-/bin/bash}")" case "$LOGIN_SHELL" in zsh) [ -f "$HOME/.zshrc" ] && SHELL_CONFIGS+=("$HOME/.zshrc") [ -f "$HOME/.zprofile" ] && SHELL_CONFIGS+=("$HOME/.zprofile") # If neither exists, create ~/.zshrc (common on fresh macOS installs) if [ ${#SHELL_CONFIGS[@]} -eq 0 ]; then touch "$HOME/.zshrc" SHELL_CONFIGS+=("$HOME/.zshrc") fi ;; bash) [ -f "$HOME/.bashrc" ] && SHELL_CONFIGS+=("$HOME/.bashrc") [ -f "$HOME/.bash_profile" ] && SHELL_CONFIGS+=("$HOME/.bash_profile") ;; fish) # fish uses ~/.config/fish/config.fish and fish_add_path — not export PATH= IS_FISH=true FISH_CONFIG="$HOME/.config/fish/config.fish" mkdir -p "$(dirname "$FISH_CONFIG")" touch "$FISH_CONFIG" ;; *) [ -f "$HOME/.bashrc" ] && SHELL_CONFIGS+=("$HOME/.bashrc") [ -f "$HOME/.zshrc" ] && SHELL_CONFIGS+=("$HOME/.zshrc") ;; esac # Also ensure ~/.profile has it (sourced by login shells on # Ubuntu/Debian/WSL even when ~/.bashrc is skipped) [ "$IS_FISH" = "false" ] && [ -f "$HOME/.profile" ] && SHELL_CONFIGS+=("$HOME/.profile") PATH_EXPORT_DIR="$command_link_dir" if [[ "$command_link_dir" == "$HOME/"* ]]; then PATH_EXPORT_DIR="\$HOME/${command_link_dir#$HOME/}" fi PATH_LINE="export PATH=\"$PATH_EXPORT_DIR:\$PATH\"" for SHELL_CONFIG in "${SHELL_CONFIGS[@]}"; do if ! grep -v '^[[:space:]]*#' "$SHELL_CONFIG" 2>/dev/null | grep -Fq "$command_link_dir" \ && ! grep -v '^[[:space:]]*#' "$SHELL_CONFIG" 2>/dev/null | grep -Fq "$PATH_EXPORT_DIR"; then echo "" >> "$SHELL_CONFIG" echo "# Maia - ensure $command_link_display_dir is on PATH" >> "$SHELL_CONFIG" echo "$PATH_LINE" >> "$SHELL_CONFIG" log_success "Added $command_link_display_dir to PATH in $SHELL_CONFIG" fi done # fish uses fish_add_path instead of export PATH=... if [ "$IS_FISH" = "true" ]; then FISH_PATH_LINE="fish_add_path \"$PATH_EXPORT_DIR\"" if ! grep -Fq "$FISH_PATH_LINE" "$FISH_CONFIG" 2>/dev/null; then echo "" >> "$FISH_CONFIG" echo "# Maia - ensure $command_link_display_dir is on PATH" >> "$FISH_CONFIG" echo "$FISH_PATH_LINE" >> "$FISH_CONFIG" log_success "Added $command_link_display_dir to PATH in $FISH_CONFIG" fi fi if [ "$IS_FISH" = "false" ] && [ ${#SHELL_CONFIGS[@]} -eq 0 ]; then log_warn "Could not detect shell config file to add $command_link_display_dir to PATH" log_info "Add manually: $PATH_LINE" fi else log_info "$command_link_display_dir already on PATH" fi # Export for current session so maia works immediately export PATH="$command_link_dir:$PATH" log_success "maia command ready" } configure_secure_runtime() { local maia_cmd maia_cmd="$(get_maia_command_path)" echo "" log_info "Checking the secure runtime and sandbox image for governed automation..." if "$maia_cmd" secure-runtime status --quiet >/dev/null 2>&1; then log_success "Full governed automation is ready" return 0 fi "$maia_cmd" secure-runtime status || true if [ "$OS" = "android" ] || [ "$OS" = "unknown" ]; then log_info "Maia will continue in Restricted mode on this platform." return 0 fi if [ "$IS_INTERACTIVE" = true ] || (: /dev/null; then if prompt_yes_no "Set up full governed automation now?" "yes"; then if "$maia_cmd" secure-runtime setup --yes; then log_success "Secure runtime configured" else log_warn "Secure runtime setup still needs a manual operating-system step" log_info "Maia is installed and safe in Restricted mode." log_info "Finish later with: maia secure-runtime setup" fi else log_info "Continuing in Restricted mode." log_info "Finish later with: maia secure-runtime setup" fi else log_info "No interactive terminal is available, so system software was not changed." log_info "Maia is installed and safe in Restricted mode." log_info "Finish later with: maia secure-runtime setup" fi } copy_config_templates() { log_info "Setting up configuration files..." # Create the Maia data directory structure (config at top level, code in subdir) mkdir -p "$MAIA_HOME"/{cron,sessions,logs,pairing,hooks,image_cache,audio_cache,memories,skills} # Create .env at $MAIA_HOME/.env (top level, easy to find) if [ ! -f "$MAIA_HOME/.env" ]; then if [ -f "$INSTALL_DIR/.env.example" ]; then cp "$INSTALL_DIR/.env.example" "$MAIA_HOME/.env" log_success "Created $MAIA_HOME_DISPLAY/.env from template" else touch "$MAIA_HOME/.env" log_success "Created $MAIA_HOME_DISPLAY/.env" fi else log_info "$MAIA_HOME_DISPLAY/.env already exists, keeping it" fi # Create config.yaml at $MAIA_HOME/config.yaml (top level, easy to find) if [ ! -f "$MAIA_HOME/config.yaml" ]; then if [ -f "$INSTALL_DIR/cli-config.yaml.example" ]; then cp "$INSTALL_DIR/cli-config.yaml.example" "$MAIA_HOME/config.yaml" log_success "Created $MAIA_HOME_DISPLAY/config.yaml from template" fi else log_info "$MAIA_HOME_DISPLAY/config.yaml already exists, keeping it" fi # Create SOUL.md if it doesn't exist (global persona file) if [ ! -f "$MAIA_HOME/SOUL.md" ]; then cat > "$MAIA_HOME/SOUL.md" << 'SOUL_EOF' # Maia Persona SOUL_EOF log_success "Created $MAIA_HOME_DISPLAY/SOUL.md (edit to customize personality)" fi log_success "Configuration directory ready: $MAIA_HOME_DISPLAY/" # Seed bundled skills into $MAIA_HOME/skills/ (manifest-based, one-time per skill) log_info "Syncing bundled skills to $MAIA_HOME_DISPLAY/skills/ ..." if "$INSTALL_DIR/venv/bin/python" "$INSTALL_DIR/tools/skills_sync.py" 2>/dev/null; then log_success "Skills synced to $MAIA_HOME_DISPLAY/skills/" else # Fallback: simple directory copy if Python sync fails if [ -d "$INSTALL_DIR/skills" ] && [ ! "$(ls -A "$MAIA_HOME/skills/" 2>/dev/null | grep -v '.bundled_manifest')" ]; then cp -r "$INSTALL_DIR/skills/"* "$MAIA_HOME/skills/" 2>/dev/null || true log_success "Skills copied to $MAIA_HOME_DISPLAY/skills/" fi fi } install_node_deps() { if [ "$HAS_NODE" = false ]; then log_info "Skipping Node.js dependencies (Node not installed)" return 0 fi if [ "$DISTRO" = "termux" ]; then log_info "Skipping automatic Node/browser dependency setup on Termux" log_info "Browser automation is not part of the tested Termux install path yet." log_info "If you want to experiment manually later, run: cd $INSTALL_DIR && npm install" return 0 fi if [ -f "$INSTALL_DIR/package.json" ]; then log_info "Installing Node.js dependencies (browser tools)..." cd "$INSTALL_DIR" npm install --silent 2>/dev/null || { log_warn "npm install failed (browser tools may not work)" } log_success "Node.js dependencies installed" # Install Playwright browser + system dependencies. # Playwright's --with-deps only supports apt-based systems natively. # For Arch/Manjaro we install the system libs via pacman first. # Other systems must install Chromium dependencies manually. log_info "Installing browser engine (Playwright Chromium)..." case "$DISTRO" in ubuntu|debian|raspbian|pop|linuxmint|elementary|zorin|kali|parrot) log_info "Playwright may request sudo to install browser system dependencies (shared libraries)." log_info "This is standard Playwright setup — Maia itself does not require root access." cd "$INSTALL_DIR" && npx playwright install --with-deps chromium 2>/dev/null || { log_warn "Playwright browser installation failed — browser tools will not work." log_warn "Try running manually: cd $INSTALL_DIR && npx playwright install --with-deps chromium" } ;; arch|manjaro) if command -v pacman &> /dev/null; then log_info "Arch/Manjaro detected — installing Chromium system dependencies via pacman..." if command -v sudo &> /dev/null && sudo -n true 2>/dev/null; then sudo NEEDRESTART_MODE=a pacman -S --noconfirm --needed \ nss atk at-spi2-core cups libdrm libxkbcommon mesa pango cairo alsa-lib >/dev/null 2>&1 || true elif [ "$(id -u)" -eq 0 ]; then pacman -S --noconfirm --needed \ nss atk at-spi2-core cups libdrm libxkbcommon mesa pango cairo alsa-lib >/dev/null 2>&1 || true else log_warn "Cannot install browser deps without sudo. Run manually:" log_warn " sudo pacman -S nss atk at-spi2-core cups libdrm libxkbcommon mesa pango cairo alsa-lib" fi fi cd "$INSTALL_DIR" && npx playwright install chromium 2>/dev/null || { log_warn "Playwright browser installation failed — browser tools will not work." } ;; fedora|rhel|centos|rocky|alma) log_warn "Playwright does not support automatic dependency installation on RPM-based systems." log_info "Install Chromium system dependencies manually before using browser tools:" log_info " sudo dnf install nss atk at-spi2-core cups-libs libdrm libxkbcommon mesa-libgbm pango cairo alsa-lib" cd "$INSTALL_DIR" && npx playwright install chromium 2>/dev/null || { log_warn "Playwright browser installation failed — install dependencies above and retry." } ;; opensuse*|sles) log_warn "Playwright does not support automatic dependency installation on zypper-based systems." log_info "Install Chromium system dependencies manually before using browser tools:" log_info " sudo zypper install mozilla-nss libatk-1_0-0 at-spi2-core cups-libs libdrm2 libxkbcommon0 Mesa-libgbm1 pango cairo libasound2" cd "$INSTALL_DIR" && npx playwright install chromium 2>/dev/null || { log_warn "Playwright browser installation failed — install dependencies above and retry." } ;; *) log_warn "Playwright does not support automatic dependency installation on $DISTRO." log_info "Install Chromium/browser system dependencies for your distribution, then run:" log_info " cd $INSTALL_DIR && npx playwright install chromium" log_info "Browser tools will not work until dependencies are installed." cd "$INSTALL_DIR" && npx playwright install chromium 2>/dev/null || true ;; esac log_success "Browser engine setup complete" fi # Install and build the TUI (terminal chat + the dashboard's chat tab) if [ -f "$INSTALL_DIR/ui-tui/package.json" ]; then log_info "Installing TUI dependencies..." cd "$INSTALL_DIR/ui-tui" npm install --silent 2>/dev/null || { log_warn "TUI npm install failed (maia --tui may not work)" } # npm can exit 0 without linking the @maia/ink workspace package # (seen on WSL). Verify the link and retry loudly once before # building, otherwise the build fails with a confusing error. if [ ! -e node_modules/@maia/ink/package.json ]; then log_warn "TUI workspace link missing after npm install — retrying..." npm install --no-fund --no-audit --progress=false || { log_warn "TUI npm install failed (maia --tui may not work)" } fi log_info "Building TUI (chat interface)..." if npm run build >/dev/null 2>&1; then log_success "TUI built" else log_warn "TUI build failed — terminal chat and the dashboard chat tab may not work." log_info "Retry manually: cd $INSTALL_DIR/ui-tui && npm install && npm run build" fi log_success "TUI dependencies installed" fi # Build the corporate dashboard frontend (maia dashboard) if [ -f "$INSTALL_DIR/web/package.json" ]; then log_info "Building dashboard frontend..." ( cd "$INSTALL_DIR/web" if [ -f package-lock.json ]; then npm ci --silent 2>/dev/null || npm install --silent else npm install --silent fi npm run build ) >/dev/null 2>&1 && \ log_success "Dashboard frontend built" || { log_warn "Dashboard frontend build failed (maia dashboard may not work)" log_info "Run manually: cd $INSTALL_DIR/web && npm install && npm run build" } fi } offer_hermes_migration() { local hermes_dir="$HOME/.hermes" # Nothing to migrate, or Maia was explicitly pointed at that directory. if [ ! -d "$hermes_dir" ] || [ "$MAIA_HOME" = "$hermes_dir" ]; then return 0 fi if [ "$MIGRATE_HERMES" = "no" ]; then log_info "Skipping Hermes data copy (--no-migrate-hermes). ~/.hermes stays untouched." return 0 fi local do_copy="no" if [ "$MIGRATE_HERMES" = "yes" ]; then do_copy="yes" else # Interactive offer only. Probe by actually opening /dev/tty (see # #16746): in Docker/CI builds the device node can exist but not # open, and unattended installs must never copy data silently. if [ "$IS_INTERACTIVE" = true ] || (: /dev/null; then echo "" log_info "Hermes Agent data detected at ~/.hermes." log_info "Maia keeps its own data in $MAIA_HOME_DISPLAY and never touches ~/.hermes." log_info "It can copy your personal Hermes data into Maia now:" log_info " skills, cron jobs, memories, and SOUL.md (persona)." log_info "Not copied: API keys, config.yaml, sessions (the dashboard onboarding sets up the provider)." if prompt_yes_no "Copy Hermes skills, crons, and memories into Maia?" "yes"; then do_copy="yes" fi else log_info "Hermes data detected at ~/.hermes — not copied in non-interactive installs." log_info "Copy it later by re-running the installer with: --migrate-hermes" fi fi if [ "$do_copy" != "yes" ]; then log_info "Skipped. ~/.hermes stays untouched either way." return 0 fi log_info "Copying Hermes data into $MAIA_HOME_DISPLAY (originals are never modified)..." local copied=0 # Directories: copy without overwriting anything Maia already has, so # bundled Maia skills that share a name keep the fork's version and the # user's custom Hermes skills come across intact. local dir for dir in skills cron memories; do if [ -d "$hermes_dir/$dir" ]; then mkdir -p "$MAIA_HOME/$dir" if cp -Rn "$hermes_dir/$dir/." "$MAIA_HOME/$dir/" 2>/dev/null; then log_success "Copied $dir/ from ~/.hermes" copied=1 else log_warn "Some entries in $dir/ could not be copied" fi fi done # SOUL.md: the user's real persona beats the template we just wrote. if [ -f "$hermes_dir/SOUL.md" ]; then if cp "$hermes_dir/SOUL.md" "$MAIA_HOME/SOUL.md" 2>/dev/null; then log_success "Copied SOUL.md (persona)" copied=1 fi fi if [ "$copied" = 1 ]; then log_success "Hermes data copied. The copies are independent from now on." log_warn "Copied cron jobs will also run in Maia once its gateway is active — disable duplicates on one side if you run both products." else log_info "Nothing to copy (no skills/, cron/, memories/, or SOUL.md found in ~/.hermes)." fi } run_setup_wizard() { if [ "$RUN_SETUP" = false ]; then log_info "Skipping setup (--skip-setup). Finish later with: maia dashboard (or: maia setup)" return 0 fi # The dashboard launch and the terminal wizard both read from /dev/tty, # so this works even when the install script itself is piped # (curl | bash). Only skip if no terminal is available at all # (e.g. Docker build, CI). # # Probe by actually opening /dev/tty: a bare existence test passes # in Docker builds where the device node is in the mount namespace # but opening fails with ENXIO, so setup would proceed and then # crash on `< /dev/tty` below. if ! (: /dev/null; then log_info "Setup skipped (no terminal available)." log_info "Next: run 'maia dashboard' to finish setup in the browser, or 'maia setup' for the terminal wizard." return 0 fi echo "" log_info "Maia is installed. The dashboard walks you through the rest, in order:" log_info " 1. Model provider and API key" log_info " 2. Messaging gateway (Slack, Discord, WhatsApp, ...)" log_info " 3. Governance and dashboard access" log_info "It also includes a chat tab so you can talk to Maia right away." echo "" if prompt_yes_no "Open the Maia dashboard in your browser now?" "yes"; then MAIA_CMD="$(get_maia_command_path)" echo "" log_info "Dashboard URL: http://127.0.0.1:9119 (copy it if the browser does not open)" log_info "Ctrl+C stops the dashboard; restart it any time with: maia dashboard" echo "" cd "$INSTALL_DIR" # The chat tab is embedded by default; --open-path lands the browser # on the guided onboarding steps. # Redirect stdin from /dev/tty so it stays usable when piped from curl. $MAIA_CMD dashboard --open-path /onboarding < /dev/tty || { log_warn "Dashboard exited with an error. Retry with: maia dashboard" log_info "Or configure from the terminal instead: maia setup" } return 0 fi if prompt_yes_no "Run the terminal setup wizard instead?" "no"; then echo "" cd "$INSTALL_DIR" # Run maia setup using the venv Python directly (no activation needed). # Redirect stdin from /dev/tty so prompts work when piped from curl. if [ "$USE_VENV" = true ]; then "$INSTALL_DIR/venv/bin/python" -m hermes_cli.main setup < /dev/tty else python -m hermes_cli.main setup < /dev/tty fi return 0 fi log_info "Skipped. Finish setup any time with: maia dashboard (or: maia setup)" } maybe_start_gateway() { # Check if any messaging platform tokens were configured ENV_FILE="$MAIA_HOME/.env" if [ ! -f "$ENV_FILE" ]; then return 0 fi HAS_MESSAGING=false for VAR in TELEGRAM_BOT_TOKEN DISCORD_BOT_TOKEN SLACK_BOT_TOKEN SLACK_APP_TOKEN WHATSAPP_ENABLED; do VAL=$(grep "^${VAR}=" "$ENV_FILE" 2>/dev/null | cut -d'=' -f2-) if [ -n "$VAL" ] && [ "$VAL" != "your-token-here" ]; then HAS_MESSAGING=true break fi done if [ "$HAS_MESSAGING" = false ]; then return 0 fi echo "" log_info "Messaging platform token detected!" log_info "The gateway needs to be running for Maia to send/receive messages." # If WhatsApp is enabled and no session exists yet, run foreground first for QR scan WHATSAPP_VAL=$(grep "^WHATSAPP_ENABLED=" "$ENV_FILE" 2>/dev/null | cut -d'=' -f2-) WHATSAPP_SESSION="$MAIA_HOME/whatsapp/session/creds.json" if [ "$WHATSAPP_VAL" = "true" ] && [ ! -f "$WHATSAPP_SESSION" ]; then if [ "$IS_INTERACTIVE" = true ]; then echo "" log_info "WhatsApp is enabled but not yet paired." log_info "Running 'maia whatsapp' to pair via QR code..." echo "" if prompt_yes_no "Pair WhatsApp now?" "yes"; then MAIA_CMD="$(get_maia_command_path)" $MAIA_CMD whatsapp || true fi else log_info "WhatsApp pairing skipped (non-interactive). Run 'maia whatsapp' to pair." fi fi # Probe by actually opening /dev/tty: a bare existence test passes # in Docker builds where the device node is in the mount namespace # but opening fails with ENXIO. See #16746. if ! (: /dev/null; then log_info "Gateway setup skipped (no terminal available). Run 'maia gateway install' later." return 0 fi echo "" local should_install_gateway=false if [ "$DISTRO" = "termux" ]; then if prompt_yes_no "Would you like to start the gateway in the background?" "yes"; then should_install_gateway=true fi else if prompt_yes_no "Would you like to install the gateway as a background service?" "yes"; then should_install_gateway=true fi fi if [ "$should_install_gateway" = true ]; then MAIA_CMD="$(get_maia_command_path)" if [ "$DISTRO" != "termux" ] && command -v systemctl &> /dev/null; then log_info "Installing systemd service..." if $MAIA_CMD gateway install 2>/dev/null; then log_success "Gateway service installed" if $MAIA_CMD gateway start 2>/dev/null; then log_success "Gateway started! Your bot is now online." else log_warn "Service installed but failed to start. Try: maia gateway start" fi else log_warn "Systemd install failed. You can start manually: maia gateway" fi else if [ "$DISTRO" = "termux" ]; then log_info "Termux detected — starting gateway in best-effort background mode..." else log_info "systemd not available — starting gateway in background..." fi nohup $MAIA_CMD gateway > "$MAIA_HOME/logs/gateway.log" 2>&1 & GATEWAY_PID=$! log_success "Gateway started (PID $GATEWAY_PID). Logs: $MAIA_HOME_DISPLAY/logs/gateway.log" log_info "To stop: kill $GATEWAY_PID" log_info "To restart later: maia gateway" if [ "$DISTRO" = "termux" ]; then log_warn "Android may stop background processes when Termux is suspended or the system reclaims resources." fi fi else log_info "Skipped. Start the gateway later with: maia gateway" fi } print_success() { echo "" echo -e "${GREEN}${BOLD}" echo "┌─────────────────────────────────────────────────────────┐" echo "│ ✓ Installation Complete! │" echo "└─────────────────────────────────────────────────────────┘" echo -e "${NC}" echo "" # Show file locations echo -e "${CYAN}${BOLD}📁 Your files:${NC}" echo "" echo -e " ${YELLOW}Config:${NC} $MAIA_HOME/config.yaml" echo -e " ${YELLOW}API Keys:${NC} $MAIA_HOME/.env" echo -e " ${YELLOW}Data:${NC} $MAIA_HOME/cron/, sessions/, logs/" echo -e " ${YELLOW}Code:${NC} $INSTALL_DIR" echo "" echo -e "${CYAN}─────────────────────────────────────────────────────────${NC}" echo "" echo -e "${CYAN}${BOLD}🚀 Commands:${NC}" echo "" echo -e " ${GREEN}maia${NC} Start chatting" echo -e " ${GREEN}maia setup${NC} Configure API keys & settings" echo -e " ${GREEN}maia dashboard${NC} Corporate dashboard (governance, approvals)" echo -e " ${GREEN}maia gateway install${NC} Install gateway service (messaging + cron)" echo -e " ${GREEN}maia doctor${NC} Diagnose issues" echo -e " ${GREEN}maia update${NC} Update to latest version" echo -e " ${GREEN}maia uninstall${NC} Remove Maia (can keep configs/data)" echo "" echo -e "${CYAN}─────────────────────────────────────────────────────────${NC}" echo "" if [ "$DISTRO" = "termux" ]; then echo -e "${YELLOW}⚡ 'maia' was linked into $(get_command_link_display_dir), which is already on PATH in Termux.${NC}" echo "" elif [ "$ROOT_FHS_LAYOUT" = true ]; then echo -e "${YELLOW}⚡ 'maia' was linked into /usr/local/bin and is ready to use — no shell reload needed.${NC}" echo "" else echo -e "${YELLOW}⚡ Reload your shell to use the 'maia' command:${NC}" echo "" LOGIN_SHELL="$(basename "${SHELL:-/bin/bash}")" if [ "$LOGIN_SHELL" = "zsh" ]; then echo " source ~/.zshrc" elif [ "$LOGIN_SHELL" = "bash" ]; then echo " source ~/.bashrc" elif [ "$LOGIN_SHELL" = "fish" ]; then echo " source ~/.config/fish/config.fish" else echo " source ~/.bashrc # or ~/.zshrc" fi echo "" fi # Show Node.js warning if auto-install failed if [ "$HAS_NODE" = false ]; then echo -e "${YELLOW}" echo "Note: Node.js could not be installed automatically." echo "Browser tools and the dashboard need Node.js. Install manually:" if [ "$DISTRO" = "termux" ]; then echo " pkg install nodejs" else echo " https://nodejs.org/en/download/" fi echo -e "${NC}" fi # Show ripgrep note if not installed if [ "$HAS_RIPGREP" = false ]; then echo -e "${YELLOW}" echo "Note: ripgrep (rg) was not found. File search will use" echo "grep as a fallback. For faster search in large codebases," if [ "$DISTRO" = "termux" ]; then echo "install ripgrep: pkg install ripgrep" else echo "install ripgrep: sudo apt install ripgrep (or brew install ripgrep)" fi echo -e "${NC}" fi } # ============================================================================ # Main # ============================================================================ main() { print_banner detect_os resolve_install_layout # Upstream Hermes coexistence: never touch ~/.hermes; just say so. # (After install, offer_hermes_migration offers an optional COPY of # skills/crons/memories into Maia's own home.) if [ -d "$HOME/.hermes" ] && [ "$MAIA_HOME" != "$HOME/.hermes" ]; then log_info "Upstream Hermes data detected at ~/.hermes — Maia will NOT touch it." log_info " Maia keeps its own data in $MAIA_HOME_DISPLAY; you'll be offered a copy of" log_info " your Hermes skills, crons, and memories at the end of the install." fi install_uv check_python check_git check_node install_system_packages clone_repo setup_venv install_deps install_node_deps setup_path configure_secure_runtime copy_config_templates offer_hermes_migration maybe_start_gateway # Success banner first, then hand over to the dashboard onboarding — # the last thing the user sees is the browser opening on setup. print_success run_setup_wizard } main