#!/bin/bash # VFAT AI Agent Installation Script # Interactive installer with multiple modes # # Usage: # curl -fsSL https://ai.vf.at/install.sh | TOKEN=xxx bash # # Environment variables: # TOKEN - Required: Agent authentication token # INSTALL_MODE - Optional: user or isolated (skips interactive prompt) # PROVIDERS - Optional: comma-separated LLM CLIs to install. # Valid: claude, codex, gemini, antigravity, none, all (skips prompt). # Non-interactive default: none. # CONTROL_PLANE_URL - Optional: WebSocket URL (auto-set by server) # START_SERVICE - Optional: 1 (default) or 0. Isolated mode only. # # Note: URLs below are replaced dynamically when served via /install.sh endpoint # set -e # Pinned nvm version (used when codex or gemini is selected). NVM_VERSION="v0.40.4" VFAT_AI_NODE_VERSION="26.13.0" # If we're being piped (stdin is not a tty and $0 is bash), save to temp file # This allows re-execution with sudo for isolated mode if [ ! -t 0 ] && echo "$0" | grep -q "bash"; then SCRIPT_FILE=$(mktemp /tmp/vfat-ai-install.XXXXXX) cat > "$SCRIPT_FILE" chmod +x "$SCRIPT_FILE" export VFAT_AI_SCRIPT_FILE="$SCRIPT_FILE" exec bash "$SCRIPT_FILE" "$@" fi # Configuration from environment TOKEN="${TOKEN:-}" CONTROL_PLANE_URL="${CONTROL_PLANE_URL:-wss://ai.vf.at/agent/connect}" INSTALL_MODE="${INSTALL_MODE:-}" PROVIDERS="${PROVIDERS:-}" START_SERVICE="${START_SERVICE:-1}" # Clean up temp file on exit if we created one if [ -n "$VFAT_AI_SCRIPT_FILE" ]; then trap "rm -f '$VFAT_AI_SCRIPT_FILE'" EXIT fi if [ -z "$TOKEN" ]; then echo "TOKEN is required. Get your token from https://ai.vf.at" echo " curl -fsSL https://ai.vf.at/install.sh | TOKEN=your-token bash" exit 1 fi # Derive package URL from control plane URL HTTPS_URL="${CONTROL_PLANE_URL/wss:/https:}" HTTPS_URL="${HTTPS_URL/ws:/http:}" PACKAGE_URL="${HTTPS_URL%/agent/connect}/package/vfat-ai.tar.gz" echo "=== VFAT AI Agent Installation ===" echo # --- Helpers for prompts --- read_tty() { # Read a line; prefer stdin if it's a tty, fall back to /dev/tty when piped. local prompt="$1" local _ans if [ -t 0 ]; then read -p "$prompt" _ans elif [ -e /dev/tty ]; then read -p "$prompt" _ans < /dev/tty else _ans="" fi echo "$_ans" } has_tty() { [ -t 0 ] || [ -e /dev/tty ] } # Interactive mode selection (unless pre-selected) if [ -z "$INSTALL_MODE" ]; then echo "Choose installation mode:" echo echo " 1) User mode (recommended for personal dev machines)" echo " - Runs as your current user ($USER)" echo " - Uses ~/.local/vfat-ai/ for installation" echo " - Start manually or add to shell profile" echo echo " 2) Isolated mode (recommended for shared/production servers)" echo " - Creates dedicated 'vfat-ai' system user" echo " - Runs as a lingering systemd user service (auto-start on boot)" echo " - More secure isolation" echo if has_tty; then choice="$(read_tty "Enter choice [1/2]: ")" else echo "No interactive terminal detected. Defaulting to user mode." echo "Use INSTALL_MODE=isolated for isolated mode." choice="1" fi case "$choice" in 1) INSTALL_MODE="user" ;; 2) INSTALL_MODE="isolated" ;; *) echo "Invalid choice. Defaulting to user mode." INSTALL_MODE="user" ;; esac fi echo echo "Selected mode: $INSTALL_MODE" echo # --- Provider selection --- INSTALL_CLAUDE=0 INSTALL_CODEX=0 INSTALL_GEMINI=0 INSTALL_ANTIGRAVITY=0 INSTALL_CURSOR=0 parse_providers() { local raw raw="$(echo "$1" | tr '[:upper:]' '[:lower:]')" if [ -z "$raw" ] || [ "$raw" = "none" ]; then return fi if [ "$raw" = "all" ]; then INSTALL_CLAUDE=1; INSTALL_CODEX=1; INSTALL_GEMINI=1; INSTALL_ANTIGRAVITY=1; INSTALL_CURSOR=1 return fi local unknown="" local IFS=',' for p in $raw; do p="$(echo "$p" | tr -d '[:space:]')" case "$p" in claude) INSTALL_CLAUDE=1 ;; codex) INSTALL_CODEX=1 ;; gemini) INSTALL_GEMINI=1 ;; antigravity) INSTALL_ANTIGRAVITY=1 ;; cursor|composer) INSTALL_CURSOR=1 ;; ""|none) ;; *) unknown="${unknown}${p} " ;; esac done if [ -n "$unknown" ]; then echo "ERROR: unknown provider(s): ${unknown% }" >&2 echo "Valid values: claude, codex, gemini, antigravity, cursor, composer, all, none (comma-separated)" >&2 exit 1 fi } serialize_providers() { # Reverse of parse_providers — emits a canonical comma list (or "none") # so prompt selections survive the sudo reexec into the root install. local s="" [ "$INSTALL_CLAUDE" = "1" ] && s="${s}claude," [ "$INSTALL_CODEX" = "1" ] && s="${s}codex," [ "$INSTALL_GEMINI" = "1" ] && s="${s}gemini," [ "$INSTALL_ANTIGRAVITY" = "1" ] && s="${s}antigravity," [ "$INSTALL_CURSOR" = "1" ] && s="${s}cursor," s="${s%,}" [ -z "$s" ] && s="none" echo "$s" } prompt_providers() { if ! has_tty; then echo "No interactive terminal; skipping provider installation (default: none)." echo "Set PROVIDERS=claude,codex,gemini,antigravity,cursor to install non-interactively." return fi echo "Select LLM provider CLIs to install (default no for each):" case "$(read_tty " Install Claude Code? [y/N]: ")" in y|Y|yes|YES) INSTALL_CLAUDE=1 ;; esac case "$(read_tty " Install OpenAI Codex CLI (requires Node.js via nvm)? [y/N]: ")" in y|Y|yes|YES) INSTALL_CODEX=1 ;; esac case "$(read_tty " Install Google Gemini CLI (requires Node.js via nvm)? [y/N]: ")" in y|Y|yes|YES) INSTALL_GEMINI=1 ;; esac case "$(read_tty " Install Google Antigravity CLI? [y/N]: ")" in y|Y|yes|YES) INSTALL_ANTIGRAVITY=1 ;; esac case "$(read_tty " Install Cursor Agent CLI (Composer)? [y/N]: ")" in y|Y|yes|YES) INSTALL_CURSOR=1 ;; esac } if [ -n "$PROVIDERS" ]; then parse_providers "$PROVIDERS" else prompt_providers fi PROVIDERS_SUMMARY="" [ "$INSTALL_CLAUDE" = "1" ] && PROVIDERS_SUMMARY="${PROVIDERS_SUMMARY}claude " [ "$INSTALL_CODEX" = "1" ] && PROVIDERS_SUMMARY="${PROVIDERS_SUMMARY}codex " [ "$INSTALL_GEMINI" = "1" ] && PROVIDERS_SUMMARY="${PROVIDERS_SUMMARY}gemini " [ "$INSTALL_ANTIGRAVITY" = "1" ] && PROVIDERS_SUMMARY="${PROVIDERS_SUMMARY}antigravity " [ "$INSTALL_CURSOR" = "1" ] && PROVIDERS_SUMMARY="${PROVIDERS_SUMMARY}cursor " [ -z "$PROVIDERS_SUMMARY" ] && PROVIDERS_SUMMARY="(none)" echo echo "Providers to install: $PROVIDERS_SUMMARY" echo # --- Run-as-user helper --- run_as() { # run_as USER COMMAND... # USER may be empty — runs in current shell. local target="$1"; shift if [ -z "$target" ] || [ "$(id -un)" = "$target" ]; then bash -c "$*" else sudo -u "$target" -H bash -c "$*" fi } # --- Dependency: tmux --- ensure_tmux() { if command -v tmux >/dev/null 2>&1; then return fi echo "Installing tmux..." if [ "$EUID" -eq 0 ]; then if command -v apt-get >/dev/null 2>&1; then apt-get install -y -qq tmux elif command -v dnf >/dev/null 2>&1; then dnf install -y tmux elif command -v pacman >/dev/null 2>&1; then pacman -S --noconfirm tmux else echo "ERROR: cannot install tmux automatically (unknown package manager)." exit 1 fi else echo "ERROR: tmux is required but not installed." if command -v apt-get >/dev/null 2>&1; then echo " sudo apt install tmux" elif command -v dnf >/dev/null 2>&1; then echo " sudo dnf install tmux" elif command -v pacman >/dev/null 2>&1; then echo " sudo pacman -S tmux" else echo " Use your package manager to install tmux" fi echo "Then re-run this script." exit 1 fi } # --- Provider installers --- install_claude_for_user() { local target="$1" echo "Installing Claude Code (native installer)..." run_as "$target" 'curl -fsSL https://claude.ai/install.sh | bash' } install_antigravity_for_user() { local target="$1" echo "Installing Google Antigravity CLI (native installer)..." run_as "$target" 'curl -fsSL https://antigravity.google/cli/install.sh | bash' } install_cursor_for_user() { local target="$1" echo "Installing Cursor Agent CLI (native installer)..." run_as "$target" 'curl https://cursor.com/install -fsS | bash' } ensure_nvm_for_user() { local target="$1" local check check="$(run_as "$target" '[ -s "$HOME/.nvm/nvm.sh" ] && echo yes || echo no')" if [ "$check" != "yes" ]; then echo "Installing nvm $NVM_VERSION..." run_as "$target" "curl -fsSL https://raw.githubusercontent.com/nvm-sh/nvm/$NVM_VERSION/install.sh | bash" else echo "nvm already present, ensuring Node.js $VFAT_AI_NODE_VERSION..." fi # Keep provider installs on the tested LTS release, including existing nvm # homes without a default. A later LTS major needs its own compatibility check. run_as "$target" "export NVM_DIR=\"\$HOME/.nvm\"; . \"\$NVM_DIR/nvm.sh\" && nvm install $VFAT_AI_NODE_VERSION && nvm alias default $VFAT_AI_NODE_VERSION" } npm_install_global_for_user() { local target="$1" local pkg="$2" echo "Installing $pkg via npm (nvm)..." run_as "$target" "export NVM_DIR=\"\$HOME/.nvm\"; . \"\$NVM_DIR/nvm.sh\" && npm install -g $pkg" } install_providers_for_user() { local target="$1" if [ "$INSTALL_CLAUDE" = "1" ]; then install_claude_for_user "$target" fi if [ "$INSTALL_CODEX" = "1" ] || [ "$INSTALL_GEMINI" = "1" ]; then ensure_nvm_for_user "$target" fi if [ "$INSTALL_CODEX" = "1" ]; then npm_install_global_for_user "$target" "@openai/codex" fi if [ "$INSTALL_GEMINI" = "1" ]; then npm_install_global_for_user "$target" "@google/gemini-cli" fi if [ "$INSTALL_ANTIGRAVITY" = "1" ]; then install_antigravity_for_user "$target" fi if [ "$INSTALL_CURSOR" = "1" ]; then install_cursor_for_user "$target" fi } # Function to install the package into a venv install_package() { local VENV_DIR="$1" local RUN_AS_USER="${2:-}" # Optional: run commands as this user echo "Creating virtual environment at $VENV_DIR..." if [ -n "$RUN_AS_USER" ]; then if ! sudo -u "$RUN_AS_USER" python3 -m venv "$VENV_DIR"; then echo "ERROR: Failed to create virtual environment" exit 1 fi else if ! python3 -m venv "$VENV_DIR"; then echo "ERROR: Failed to create virtual environment" exit 1 fi fi # Verify venv was created if [ ! -f "$VENV_DIR/bin/pip" ]; then echo "ERROR: Virtual environment creation failed - pip not found" echo "Make sure python3-venv is installed" exit 1 fi echo "Downloading package from $PACKAGE_URL..." if [ -n "$RUN_AS_USER" ]; then if ! sudo -u "$RUN_AS_USER" "$VENV_DIR/bin/pip" install --quiet "$PACKAGE_URL"; then echo "ERROR: Failed to install package" exit 1 fi else if ! "$VENV_DIR/bin/pip" install --quiet "$PACKAGE_URL"; then echo "ERROR: Failed to install package" exit 1 fi fi # Verify installation if [ ! -f "$VENV_DIR/bin/vfat-ai-agent" ]; then echo "ERROR: vfat-ai-agent not found after installation" echo "Check: $PACKAGE_URL" exit 1 fi echo "Installed: $("$VENV_DIR/bin/vfat-ai-agent" --version 2>/dev/null || echo 'vfat-ai-agent')" } # Function to install in user mode install_user_mode() { echo "--- Installing User Mode ---" VENV_DIR="$HOME/.local/vfat-ai" PROJECTS_DIR="$HOME/projects" ENV_FILE="$HOME/.config/vfat-ai/agent.env" TOKEN_FILE_PATH="$HOME/.config/vfat-ai/agent.token" AGENT_BIN="$VENV_DIR/bin/vfat-ai-agent" # Check for python3-venv if ! python3 -c "import ensurepip" 2>/dev/null; then echo "ERROR: python3-venv is required but not installed." echo echo "Install it with:" if command -v apt-get &>/dev/null; then echo " sudo apt install python3-venv" elif command -v dnf &>/dev/null; then echo " sudo dnf install python3-venv" elif command -v pacman &>/dev/null; then echo " sudo pacman -S python" else echo " Install python3-venv using your package manager" fi echo echo "Then re-run this script." exit 1 fi ensure_tmux # Create directories echo "Setting up directories..." mkdir -p "$PROJECTS_DIR" mkdir -p "$(dirname "$ENV_FILE")" mkdir -p "$(dirname "$VENV_DIR")" # Install package into venv install_package "$VENV_DIR" # Install selected providers (current user) install_providers_for_user "" # Store token separately so it is not exposed through process environments. echo "Configuring agent token..." printf '%s\n' "$TOKEN" > "$TOKEN_FILE_PATH" chmod 600 "$TOKEN_FILE_PATH" cat > "$ENV_FILE" << EOF # VFAT AI Agent Configuration (auto-generated) VFAT_AI_AGENT_TOKEN_FILE=$TOKEN_FILE_PATH VFAT_AI_CONTROL_PLANE_URL=$CONTROL_PLANE_URL VFAT_AI_LLM_MODEL=qwen3.5:9b EOF chmod 600 "$ENV_FILE" echo echo "User mode installed!" echo echo "Start the agent with:" echo " export \$(cat $ENV_FILE | xargs) && $AGENT_BIN" echo echo "Or add to your shell profile for persistence." } # Function to install in isolated mode install_isolated_mode() { echo "--- Installing Isolated Mode ---" # Check for root if [ "$EUID" -ne 0 ]; then echo "Isolated mode requires root privileges." echo "Re-running with sudo..." # Pass the resolved provider selection (from prompt or env) so the # root process doesn't re-prompt or silently default to none. exec sudo TOKEN="$TOKEN" CONTROL_PLANE_URL="$CONTROL_PLANE_URL" \ INSTALL_MODE="isolated" PROVIDERS="$(serialize_providers)" \ START_SERVICE="$START_SERVICE" \ bash "$0" fi AGENT_USER="vfat-ai" AGENT_HOME="/var/lib/vfat-ai" # Isolated agents advertise Composer and need the CLI for session creation. INSTALL_CURSOR=1 VENV_DIR="$AGENT_HOME/venv" PROJECTS_DIR="$AGENT_HOME/projects" CONFIG_DIR="$AGENT_HOME/.config/vfat-ai" CONFIG_FILE="$CONFIG_DIR/config.toml" ENV_FILE="$CONFIG_DIR/agent.env" TOKEN_FILE_PATH="$CONFIG_DIR/agent.token" AGENT_BIN="$VENV_DIR/bin/vfat-ai-agent" WRAPPER="$AGENT_HOME/bin/agent-wrapper" USER_UNIT_DIR="$AGENT_HOME/.config/systemd/user" USER_UNIT_FILE="$USER_UNIT_DIR/vfat-ai-agent.service" # Create system user if id "vfat-ai" &>/dev/null; then echo "User 'vfat-ai' already exists" else echo "Creating user 'vfat-ai'..." useradd --system \ --home-dir "$AGENT_HOME" \ --shell /bin/bash \ --create-home \ vfat-ai fi # Create directories echo "Setting up directories..." sudo -u vfat-ai mkdir -p "$PROJECTS_DIR" sudo -u vfat-ai mkdir -p "$AGENT_HOME/.ssh" sudo -u vfat-ai mkdir -p "$AGENT_HOME/bin" sudo -u vfat-ai mkdir -p "$CONFIG_DIR" sudo -u vfat-ai mkdir -p "$USER_UNIT_DIR" chmod 700 "$AGENT_HOME" chmod 700 "$PROJECTS_DIR" chmod 700 "$AGENT_HOME/bin" chmod 700 "$AGENT_HOME/.ssh" chmod 700 "$CONFIG_DIR" chmod 700 "$AGENT_HOME/.config/systemd" chmod 700 "$USER_UNIT_DIR" # Generate SSH key if [ ! -f "$AGENT_HOME/.ssh/id_ed25519" ]; then echo "Generating SSH key..." sudo -u vfat-ai ssh-keygen -t ed25519 \ -f "$AGENT_HOME/.ssh/id_ed25519" \ -N "" \ -C "vfat-ai-agent" fi # Configure git echo "Configuring git..." cd "$AGENT_HOME" sudo -u vfat-ai git config --global user.name "VFAT AI Agent" sudo -u vfat-ai git config --global user.email "agent@vfat-ai.local" # Install system dependencies echo "Installing system dependencies..." apt-get update -qq || true # Ignore update errors (some repos may fail) if ! apt-get install -y -qq python3-venv python3-pip curl ca-certificates; then echo "ERROR: Failed to install python3-venv" echo "Try running: sudo apt install python3-venv python3-pip curl ca-certificates" exit 1 fi ensure_tmux # Install package into venv install_package "$VENV_DIR" "vfat-ai" # Install selected providers (vfat-ai user) install_providers_for_user "vfat-ai" # Store token separately so it is not exposed through process environments. echo "Configuring agent token..." printf '%s\n' "$TOKEN" > "$TOKEN_FILE_PATH" chown vfat-ai:vfat-ai "$TOKEN_FILE_PATH" chmod 600 "$TOKEN_FILE_PATH" cat > "$ENV_FILE" << EOF # VFAT AI Agent Configuration (auto-generated) VFAT_AI_AGENT_TOKEN_FILE=$TOKEN_FILE_PATH VFAT_AI_CONTROL_PLANE_URL=$CONTROL_PLANE_URL VFAT_AI_LLM_MODEL=qwen3.5:9b VFAT_AI_AUTO_UPDATE=true EOF chown vfat-ai:vfat-ai "$ENV_FILE" chmod 600 "$ENV_FILE" if [ ! -f "$CONFIG_FILE" ]; then echo "Writing default isolated agent provider config..." cat > "$CONFIG_FILE" << EOF [sessions] advertised_providers = ["claude", "codex", "cursor", "claude-atom-gpt-oss-120b"] [sessions.providers.cursor] command = "cursor-agent" display_name = "Cursor Composer 2.5" args = ["--model", "composer-2.5"] [sessions.providers.claude-atom-gpt-oss-120b] command = "claude" display_name = "GPT-OSS 120B (atom-ai)" env = { ANTHROPIC_BASE_URL = "http://atom-ai:11434", ANTHROPIC_AUTH_TOKEN = "ollama", ANTHROPIC_MODEL = "gpt-oss:120b" } EOF chown vfat-ai:vfat-ai "$CONFIG_FILE" chmod 600 "$CONFIG_FILE" else echo "Keeping existing $CONFIG_FILE" fi # Install wrapper script that sources nvm so codex/gemini are on PATH for tmux children. # Antigravity, Claude, and Cursor install native binaries into ~/.local/bin. echo "Installing systemd wrapper..." cat > "$WRAPPER" </dev/null 2>&1 nvm use default >/dev/null 2>&1 \ || nvm use --lts >/dev/null 2>&1 \ || { latest_node_dir="\$(find "\$NVM_DIR/versions/node" -mindepth 1 -maxdepth 1 -type d -printf '%f\n' 2>/dev/null | sort -V | tail -n 1)" [ -n "\$latest_node_dir" ] && nvm use "\$latest_node_dir" >/dev/null 2>&1 } \ || true fi export PATH="\$HOME/.local/bin:\$PATH" exec "$AGENT_BIN" "\$@" EOF chown vfat-ai:vfat-ai "$WRAPPER" chmod 700 "$WRAPPER" # Install the isolated agent into vfat-ai's own systemd user manager. The # same manager owns the transient per-session scopes, so systemd-run # --user can enforce memory/pid limits without a topology mismatch. echo "Installing systemd user service..." cat > "$USER_UNIT_FILE" <<'EOF' [Unit] Description=VFAT AI Agent (Isolated User Service) After=network-online.target Wants=network-online.target [Service] Type=simple # Keep tmux/provider sessions alive across agent restarts and during the # one-time system-service to user-service migration. KillMode=process # Allow the bounded 120s installer, its cleanup and the 15s create settle. TimeoutStopSec=150s # The agent watchdog is driven from an OS thread only while the asyncio loop # is healthy. Keep this above LoopWatchdog.STALL_THRESHOLD (30s). WatchdogSec=90s NotifyAccess=main WorkingDirectory=%h EnvironmentFile=%h/.config/vfat-ai/agent.env Environment=HOME=%h # Make the user bus explicit for both the agent and any tmux server it starts. # A preserved legacy tmux server is reconciled by the migration script. Environment=XDG_RUNTIME_DIR=%t Environment=DBUS_SESSION_BUS_ADDRESS=unix:path=%t/bus ExecStart=%h/bin/agent-wrapper Restart=always RestartSec=10 UMask=0077 NoNewPrivileges=yes RestrictSUIDSGID=yes RestrictRealtime=yes LockPersonality=yes RemoveIPC=yes SystemCallArchitectures=native # Filesystem and control-group protection directives are intentionally absent. # In an unprivileged systemd user manager they create a one-UID user namespace; # Ubuntu's unprivileged_userns AppArmor profile then blocks access to preserved # tmux sockets, and user-scope discovery can fail. The dedicated service account # remains the filesystem boundary. # A user manager has no CAP_SETPCAP, so capability-bounding directives (and # sandbox directives implemented by dropping bounding capabilities) make exec # fail with EXIT_CAPABILITIES. The service account already starts without # effective or bounding capabilities; keep only user-manager-safe hardening. AmbientCapabilities= [Install] WantedBy=default.target EOF chown vfat-ai:vfat-ai "$USER_UNIT_FILE" chmod 0644 "$USER_UNIT_FILE" # Start the agent if [ "$START_SERVICE" = "0" ]; then # In particular, do not enable linger or start user@UID.service here: # an existing enabled user unit could otherwise start alongside a # legacy system agent before the migration guard runs. echo "START_SERVICE=0, leaving the user manager and agent stopped/unchanged" else LEGACY_UNIT="" for candidate in vfat-ai-isolated-agent.service vfat-ai-agent.service; do if systemctl is-active --quiet "$candidate" 2>/dev/null || \ systemctl is-enabled --quiet "$candidate" 2>/dev/null; then LEGACY_UNIT="$candidate" break fi done if [ -n "$LEGACY_UNIT" ]; then echo "ERROR: Legacy system unit $LEGACY_UNIT is still active or enabled." >&2 echo "Refusing to start a duplicate user agent." >&2 echo "Run the tracked deploy/migrate-isolated-agent-to-user-service.sh --migrate workflow." >&2 exit 1 fi # Linger is the boot contract for a service account with no interactive # login. The duplicate guard above must run before either command: # starting user@UID.service reaches default.target and starts an # already-enabled user agent. loginctl enable-linger vfat-ai AGENT_UID="$(id -u vfat-ai)" systemctl start "user@$AGENT_UID.service" systemctl --user --machine="vfat-ai@.host" daemon-reload echo "Starting agent user service..." systemctl --user --machine="vfat-ai@.host" enable vfat-ai-agent.service systemctl --user --machine="vfat-ai@.host" start vfat-ai-agent.service systemctl --user --machine="vfat-ai@.host" is-active --quiet vfat-ai-agent.service fi echo echo "Isolated mode installed!" echo if [ "$START_SERVICE" = "0" ]; then echo "Agent user service files are installed but not started." else echo "Agent is running as a lingering systemd user service." fi # Print login hints with absolute paths / explicit nvm sourcing so they # work regardless of sudo secure_path or shell init quirks. if [ "$INSTALL_CLAUDE" = "1" ]; then echo echo "Next step - log in to Claude Code as the agent user:" echo " sudo -u vfat-ai $AGENT_HOME/.local/bin/claude" fi if [ "$INSTALL_CODEX" = "1" ] || [ "$INSTALL_GEMINI" = "1" ]; then echo echo "For codex/gemini, source nvm to put Node on PATH:" fi if [ "$INSTALL_CODEX" = "1" ]; then echo " sudo -u vfat-ai bash -c '. $AGENT_HOME/.nvm/nvm.sh && codex login'" fi if [ "$INSTALL_GEMINI" = "1" ]; then echo " sudo -u vfat-ai bash -c '. $AGENT_HOME/.nvm/nvm.sh && gemini'" fi if [ "$INSTALL_ANTIGRAVITY" = "1" ]; then echo echo "Next step - log in to Antigravity CLI as the agent user:" echo " sudo -u vfat-ai $AGENT_HOME/.local/bin/agy" fi if [ "$INSTALL_CURSOR" = "1" ]; then echo echo "Next step - log in to Cursor Agent CLI as the agent user:" echo " sudo -u vfat-ai -H $AGENT_HOME/.local/bin/cursor-agent login" echo "Repo-managed skills sync into $AGENT_HOME/.cursor/skills on agent startup and hourly." fi } # Execute based on mode case "$INSTALL_MODE" in user) install_user_mode ;; isolated) install_isolated_mode ;; *) echo "Unknown mode: $INSTALL_MODE" exit 1 ;; esac echo echo "=== Installation Complete ===" echo echo "Return to the web UI to set up GitHub access."