#!/bin/sh # single-agent installer for Linux and macOS. # # curl -fsSL https://cbac-admin-dev.agentdna.io/install.sh | sh # curl -fsSL https://cbac-admin-dev.agentdna.io/install.sh | sh -s -- --yes --dir ~/agent -- --provider gemini # # Options: # --dir install location (default: ./single-agent) # --force-download fetch a release even if a local project is detected # --version release to install (default: latest) # -y, --yes non-interactive: accept every prompt # --dry-run show what would happen, change nothing # -h, --help show this help # Anything after a second "--" is passed through to the setup wizard. # # Local mode: if agent.py and requirements.txt sit next to this script (or in the # current directory), that project is used as-is and nothing is downloaded. # # Env overrides: SINGLE_AGENT_BASE_URL, SINGLE_AGENT_VERSION, # SINGLE_AGENT_DIR, SINGLE_AGENT_PYTHON set -eu # Everything lives inside main(), which is only called on the very last line, # so a truncated download can never execute a half-finished script. main() { BASE_URL="${SINGLE_AGENT_BASE_URL:-https://try-dev.agentdna.io}" VERSION="${SINGLE_AGENT_VERSION:-latest}" INSTALL_DIR="${SINGLE_AGENT_DIR:-${PWD%/}/single-agent}" PYTHON_VERSION="${SINGLE_AGENT_PYTHON:-3.12}" YES=0 DRY_RUN=0 FORCE_DOWNLOAD=0 DIR_SET=0 if [ -n "${SINGLE_AGENT_DIR:-}" ]; then DIR_SET=1; fi # ---- arguments ---------------------------------------------------------- while [ $# -gt 0 ]; do case "$1" in --dir) INSTALL_DIR="${2:?--dir needs a value}"; DIR_SET=1; shift 2 ;; --force-download) FORCE_DOWNLOAD=1; shift ;; --version) VERSION="${2:?--version needs a value}"; shift 2 ;; -y|--yes) YES=1; shift ;; --dry-run) DRY_RUN=1; shift ;; -h|--help) cat <<'EOF' Usage: install.sh [--dir ] [--force-download] [--version ] [--yes] [--dry-run] [-- ] --dir install location (default: ./single-agent) --force-download fetch a release even if a local project is detected --version release to install (default: latest) -y, --yes non-interactive: accept every prompt --dry-run show what would happen, change nothing -- pass everything after it to the setup wizard EOF exit 0 ;; --) shift; break ;; *) printf 'error: unknown option: %s (put wizard options after --)\n' "$1" >&2; exit 2 ;; esac done ARCHIVE="single-agent-$VERSION.tar.gz" # ---- local project? (script's own folder first, then the current directory) ---- LOCAL=0 if [ "$DIR_SET" -eq 0 ] && [ "$FORCE_DOWNLOAD" -eq 0 ]; then SCRIPT_DIR="" case "$0" in *install.sh) [ -f "$0" ] && SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd) ;; esac for d in "$SCRIPT_DIR" "$PWD"; do if [ -n "$d" ] && [ -f "$d/agent.py" ] && [ -f "$d/requirements.txt" ]; then INSTALL_DIR="$d" LOCAL=1 break fi done fi # ---- platform ----------------------------------------------------------- case "$(uname -s)" in Linux|Darwin) ;; *) printf 'error: unsupported OS. On Windows use install.ps1 (PowerShell) or WSL.\n' >&2; exit 1 ;; esac if [ "$DRY_RUN" -eq 1 ]; then printf 'Dry run - nothing will be changed.\n' if [ "$LOCAL" -eq 1 ]; then printf ' download : skipped (local project detected)\n' else printf ' download : %s/%s (+ .sha256)\n' "$BASE_URL" "$ARCHIVE" fi printf ' install : %s\n' "$INSTALL_DIR" printf ' python : %s via uv (uv is installed to ~/.local/bin if missing)\n' "$PYTHON_VERSION" printf ' then : run the setup wizard\n' exit 0 fi # ---- prerequisites ------------------------------------------------------ if command -v curl >/dev/null 2>&1; then DL="curl -fsSL -o" elif command -v wget >/dev/null 2>&1; then DL="wget -qO" else printf 'error: curl or wget is required\n' >&2; exit 1 fi command -v tar >/dev/null 2>&1 || { printf 'error: tar is required\n' >&2; exit 1; } if command -v sha256sum >/dev/null 2>&1; then SHA="sha256sum" elif command -v shasum >/dev/null 2>&1; then SHA="shasum -a 256" else printf 'error: sha256sum or shasum is required\n' >&2; exit 1 fi # Under `curl | sh` stdin is the script itself, so prompts must read /dev/tty. TTY=0 if [ -t 0 ] || (exec /dev/null; then TTY=1; fi if [ "$TTY" -eq 0 ] && [ "$YES" -eq 0 ]; then printf 'error: no terminal available for prompts. Re-run with --yes (and wizard options after --).\n' >&2 exit 1 fi TMP=$(mktemp -d) trap 'rm -rf "$TMP"' EXIT trap 'exit 130' INT TERM # ---- existing install? -------------------------------------------------- DO_DOWNLOAD=1 if [ "$LOCAL" -eq 1 ]; then DO_DOWNLOAD=0 printf '==> Using local project at %s (skipping download)\n' "$INSTALL_DIR" elif [ -f "$INSTALL_DIR/agent.py" ] && [ "$YES" -eq 0 ]; then printf 'Existing install found in %s.\nOverwrite code files with the latest release? Your .env is kept. [y/N] ' "$INSTALL_DIR" >&2 read -r ans Downloading %s\n' "$ARCHIVE" $DL "$TMP/$ARCHIVE" "$BASE_URL/$ARCHIVE" || { printf 'error: download failed: %s/%s\n' "$BASE_URL" "$ARCHIVE" >&2; exit 1; } $DL "$TMP/$ARCHIVE.sha256" "$BASE_URL/$ARCHIVE.sha256" || { printf 'error: checksum download failed\n' >&2; exit 1; } expected=$(cut -d' ' -f1 <"$TMP/$ARCHIVE.sha256") actual=$($SHA "$TMP/$ARCHIVE" | cut -d' ' -f1) if [ "$expected" != "$actual" ]; then printf 'error: checksum mismatch (expected %s, got %s). Aborting.\n' "$expected" "$actual" >&2 exit 1 fi printf '==> Extracting to %s\n' "$INSTALL_DIR" mkdir -p "$INSTALL_DIR" tar -xzf "$TMP/$ARCHIVE" -C "$INSTALL_DIR" fi INSTALL_DIR=$(cd "$INSTALL_DIR" && pwd) # ---- uv (manages Python + venv; no sudo, no shell-profile edits) -------- if ! command -v uv >/dev/null 2>&1; then if [ -x "$HOME/.local/bin/uv" ]; then PATH="$HOME/.local/bin:$PATH" else if [ "$YES" -eq 0 ]; then printf 'uv (Python package manager) is required and will be installed to ~/.local/bin. Continue? [Y/n] ' >&2 read -r ans &2; exit 1 ;; esac fi printf '==> Installing uv\n' UV_NO_MODIFY_PATH=1 export UV_NO_MODIFY_PATH $DL - https://astral.sh/uv/install.sh | sh PATH="$HOME/.local/bin:$PATH" command -v uv >/dev/null 2>&1 || { printf 'error: uv installation failed\n' >&2; exit 1; } fi fi # ---- virtualenv + dependencies ----------------------------------------- cd "$INSTALL_DIR" if [ ! -f wizard/__main__.py ]; then printf 'error: wizard/__main__.py not found in %s\n' "$INSTALL_DIR" >&2 printf 'The wizard/ folder is missing from this project. Copy it in, or run from an empty directory to download a full release.\n' >&2 exit 1 fi if [ ! -x .venv/bin/python ]; then printf '==> Creating Python %s environment\n' "$PYTHON_VERSION" uv venv --python "$PYTHON_VERSION" --quiet .venv fi printf '==> Installing dependencies\n' uv pip install --quiet --python .venv/bin/python -r requirements.txt # ---- hand over to the setup wizard -------------------------------------- if [ "$YES" -eq 1 ]; then set -- --yes "$@"; fi printf '==> Starting setup wizard\n\n' rm -rf "$TMP" trap - EXIT if [ ! -t 0 ] && [ "$TTY" -eq 1 ]; then exec .venv/bin/python -m wizard "$@"