#!/usr/bin/env bash # Loki Agent — One-Shot Installer # Usage: curl -sfL https://raw.githubusercontent.com/inceptionstack/loki-agent/main/install.sh -o /tmp/loki-install.sh && bash /tmp/loki-install.sh # Flags: --non-interactive / -y Accept all defaults, minimal prompts # --pack Pre-select agent pack (e.g. --pack claude-code, --pack openclaw) # --method Pre-select deploy method: cfn, terraform (or tf) # --debug-in-repo Copy local repo to /tmp instead of cloning (for local testing) # Require bash — printf -v and other bashisms won't work in dash/sh if [ -z "${BASH_VERSION:-}" ]; then echo "This script requires bash. Run with: bash $0" >&2; exit 1 fi set -euo pipefail # Save original dir before changing (needed for --debug-in-repo) _ORIG_DIR="$(pwd)" # Ensure we run from a safe CWD — avoid interference from local .env, direnv, etc. # (--debug-in-repo will cd back after arg parsing) cd "$HOME" 2>/dev/null || cd /tmp export AWS_PAGER="" export PAGER="" aws() { command aws --no-cli-pager "$@"; } # Persistent log file for debugging (survives script exit) INSTALL_LOG="/tmp/loki-install.log" : > "$INSTALL_LOG" show_debug_locations() { echo -e "\033[1;33m Debug info:\033[0m" >&2 if [[ -s "${INSTALL_LOG:-}" ]]; then echo -e "\033[1;33m Installer log: ${INSTALL_LOG}\033[0m" >&2 fi if [[ -s "${_TF_LOG:-}" ]]; then echo -e "\033[1;33m Terraform log: ${_TF_LOG}\033[0m" >&2 fi if [[ -n "${CLONE_DIR:-}" && "${CLONE_DIR}" == /tmp/* && -d "$CLONE_DIR" ]]; then echo -e "\033[1;33m Clone dir: ${CLONE_DIR}\033[0m" >&2 fi if [[ -n "${TF_WORKDIR:-}" && -d "$TF_WORKDIR" ]]; then echo -e "\033[1;33m Terraform dir: ${TF_WORKDIR}\033[0m" >&2 fi } # Ctrl-C: kill background jobs and exit immediately cleanup_on_interrupt() { echo -e "\n\033[0;31m✗ Interrupted\033[0m" >&2 # Kill all child processes (terraform, gum, tee, etc.) kill -- -$$ 2>/dev/null || kill 0 2>/dev/null exit 130 } trap cleanup_on_interrupt INT TERM # Always show debug info on non-zero exit (EXIT trap is more reliable than ERR) trap ' exit_code=$? if [[ $exit_code -ne 0 ]]; then echo -e "\n\033[0;31m✗ Installer failed (exit code $exit_code)\033[0m" >&2 show_debug_locations fi ' EXIT REPO_URL="https://github.com/inceptionstack/loki-agent.git" DOCS_URL="https://github.com/inceptionstack/loki-agent/wiki" TEMPLATE_RAW_URL="https://raw.githubusercontent.com/inceptionstack/loki-agent/main/deploy/cloudformation/template.yaml" SSM_DOC_NAME="" INSTALLER_VERSION="0.5.77" # --non-interactive / --yes / -y: accept all defaults, minimal prompts # --pack : pre-select agent pack # --method : pre-select deploy method (cfn, terraform/tf) # --profile

: pre-select permission profile (builder, account_assistant, personal_assistant) # --simple / --advanced: pre-select install mode AUTO_YES=false PRESELECT_PACK="" PRESELECT_METHOD="" PRESELECT_PROFILE="" INSTALL_MODE="" # "simple" or "advanced", empty = ask DEBUG_IN_REPO=false while [[ $# -gt 0 ]]; do case "$1" in --non-interactive|--yes|-y) AUTO_YES=true; shift ;; --simple) INSTALL_MODE="simple"; shift ;; --advanced) INSTALL_MODE="advanced"; shift ;; --pack) if [[ $# -lt 2 || "$2" == --* ]]; then echo -e "\033[0;31m✗\033[0m --pack requires a pack name (e.g. --pack openclaw, --pack claude-code)" >&2 exit 1 fi PRESELECT_PACK="$2"; shift 2 ;; --method) if [[ $# -lt 2 || "$2" == --* ]]; then echo -e "\033[0;31m✗\033[0m --method requires a value (cfn, terraform, tf)" >&2 exit 1 fi PRESELECT_METHOD="$2"; shift 2 ;; --profile) if [[ $# -lt 2 || "$2" == --* ]]; then echo -e "\033[0;31m✗\033[0m --profile requires a value (builder, account_assistant, personal_assistant)" >&2 exit 1 fi PRESELECT_PROFILE="$2"; shift 2 ;; --debug-in-repo) DEBUG_IN_REPO=true; shift ;; *) shift ;; esac done # If --debug-in-repo, go back to the original directory (before cd $HOME) if [[ "$DEBUG_IN_REPO" == "true" ]]; then cd "$_ORIG_DIR" fi SCRIPT_DIR="$_ORIG_DIR" # Debug logging — writes to install log only, never to terminal dbg() { [[ "$DEBUG_IN_REPO" == "true" ]] && echo "[DBG] $*" >> "$INSTALL_LOG" return 0 } # Deploy method constants DEPLOY_CFN_CONSOLE=1 DEPLOY_CFN_CLI=2 DEPLOY_TERRAFORM=3 # Stamped at release; fall back to git info at runtime INSTALLER_COMMIT="${INSTALLER_COMMIT:-$(git -C "$SCRIPT_DIR" rev-parse --short HEAD 2>/dev/null || echo dev)}" INSTALLER_DATE="${INSTALLER_DATE:-$(d=$(git -C "$SCRIPT_DIR" log -1 --format='%ci' 2>/dev/null | cut -d' ' -f1,2); echo "${d:-unknown}")}" REPO_BRANCH="${REPO_BRANCH:-$(git -C "$SCRIPT_DIR" rev-parse --abbrev-ref HEAD 2>/dev/null || echo main)}" [[ "$REPO_BRANCH" == "HEAD" ]] && REPO_BRANCH="main" # Detect AWS CloudShell (limited ~1GB home dir, use /tmp for large files) IS_CLOUDSHELL=false if [[ -n "${AWS_EXECUTION_ENV:-}" && "${AWS_EXECUTION_ENV}" == *"CloudShell"* ]] || [[ -d /home/cloudshell-user && "$(whoami)" == "cloudshell-user" ]]; then IS_CLOUDSHELL=true fi # ============================================================================ # gum — UI toolkit (installed to /tmp, no root required) # ============================================================================ GUM="" # set by install_gum — required, script fails without it GUM_VERSION="0.14.5" # fallback version # ── Shared platform detection ──────────────────────────────────────────────── # Sets DETECTED_OS and DETECTED_ARCH. Accepts optional arch style: # "go" → amd64/arm64 (Terraform, Go binaries) # default → x86_64/arm64 (gum, generic) DETECTED_OS="" DETECTED_ARCH="" # Get real hardware arch (uname -m and sysctl hw.machine lie under Rosetta) hw_arch() { if [[ "$(sysctl -n hw.optional.arm64 2>/dev/null)" == "1" ]]; then echo "arm64" else uname -m fi } detect_platform() { local arch_style="${1:-default}" case "$(uname -s)" in Darwin) DETECTED_OS="Darwin" ;; Linux) DETECTED_OS="Linux" ;; *) DETECTED_OS=""; return 1 ;; esac case "$(hw_arch)" in x86_64|amd64) if [[ "$arch_style" == "go" ]]; then DETECTED_ARCH="amd64"; else DETECTED_ARCH="x86_64"; fi ;; aarch64|arm64) DETECTED_ARCH="arm64" ;; *) DETECTED_ARCH=""; return 1 ;; esac } install_gum() { # Already installed? if command -v gum &>/dev/null; then GUM="gum"; return 0 fi local gum_bin="/tmp/gum-bin/gum" if [[ -x "$gum_bin" ]]; then GUM="$gum_bin"; return 0 fi detect_platform || fail "Unsupported OS/architecture for gum: $(uname -s)/$(uname -m)" local os="$DETECTED_OS" arch="$DETECTED_ARCH" # Try to get latest version from GitHub API, fall back to known good local version version=$(curl -sf https://api.github.com/repos/charmbracelet/gum/releases/latest 2>/dev/null \ | grep '"tag_name"' | head -1 | sed 's/.*"v\([^"]*\)".*/\1/' || echo "") [[ -z "$version" ]] && version="$GUM_VERSION" local url="https://github.com/charmbracelet/gum/releases/download/v${version}/gum_${version}_${os}_${arch}.tar.gz" mkdir -p /tmp/gum-bin if curl -sfL "$url" | tar xz --strip-components=1 -C /tmp/gum-bin 2>/dev/null; then chmod +x "$gum_bin" GUM="$gum_bin" else fail "Could not install gum. Check network connectivity and try again." fi } # ============================================================================ # UI helpers # ============================================================================ RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; CYAN='\033[0;36m'; BOLD='\033[1m'; DIM='\033[2m'; NC='\033[0m' MAGENTA='\033[0;35m'; WHITE='\033[1;37m' info() { echo -e " ${BLUE}▸${NC} $1"; } ok() { echo -e " ${GREEN}✓${NC} $1"; } warn() { echo -e " ${YELLOW}⚠${NC} $1"; } fail() { echo -e " ${RED}✗${NC} $1"; show_debug_locations; exit 1; } # ── Elapsed time formatting ────────────────────────────────────────────────── elapsed_fmt() { local secs=$1 if [[ $secs -lt 60 ]]; then printf '%ds' "$secs" else printf '%dm %ds' "$((secs / 60))" "$((secs % 60))" fi } # ── Step progress tracker ──────────────────────────────────────────────────── STEP_NUM=0 TOTAL_STEPS=7 STEP_NAMES=() step() { STEP_NUM=$((STEP_NUM + 1)) STEP_NAMES+=("$1") echo "" $GUM style --foreground 117 --bold --border double --border-foreground 240 \ --padding "0 2" --margin "0 2" "[${STEP_NUM}/${TOTAL_STEPS}] $1" echo "" } prompt() { local text="$1" var="$2" default="${3:-}" if [[ "$AUTO_YES" == true && -n "$default" ]]; then printf -v "$var" '%s' "$default" return fi local value value=$($GUM input --header "$text" --value "$default" --placeholder "$text" < /dev/tty) || value="$default" printf -v "$var" '%s' "${value:-$default}" } confirm() { local text="$1" default="${2:-default_no}" if [[ "$AUTO_YES" == true ]]; then return 0; fi local rc=0 if [[ "$default" == "default_yes" ]]; then $GUM confirm --default=yes "$text" < /dev/tty || rc=$? else $GUM confirm "$text" < /dev/tty || rc=$? fi return $rc } toggle() { local text="$1" var="$2" default="${3:-true}" if [[ "$AUTO_YES" == true ]]; then printf -v "$var" '%s' "$default" return fi local rc=0 if [[ "$default" == "true" ]]; then $GUM confirm --default=yes " $text" < /dev/tty || rc=$? else $GUM confirm " $text" < /dev/tty || rc=$? fi [[ $rc -eq 0 ]] && printf -v "$var" '%s' "true" || printf -v "$var" '%s' "false" } require_cmd() { command -v "$1" &>/dev/null || fail "$2"; } # Confirm or exit cleanly confirm_or_abort() { confirm "$@" || { echo "Aborted."; exit 0; }; } # Extract a key from JSON on stdin json_field() { jq -r ".$1" 2>/dev/null; } # URL-encode a string url_encode() { jq -rn --arg s "$1" '$s | @uri'; } # ── Reusable helpers (DRY) ────────────────────────────────────────────────── # Animate a gum spinner with label for N seconds. # Usage: animate_spinner