#!/bin/sh set -eu # ── colors ──────────────────────────────────────────────────────────────────── BOLD="\033[1m" DIM="\033[2m" GREEN="\033[32m" RED="\033[31m" RESET="\033[0m" BASE_URL="https://static.abacus.ai/cli/releases" INSTALL_DIR="${ABACUSAI_INSTALL_DIR:-${HOME:-}/.abacusai/bin}" VERSION="${1:-${ABACUSAI_VERSION:-}}" CHANNEL="${ABACUSAI_CHANNEL:-latest}" ABACUS_API_BASE="${ABACUS_BASE_URL:-https://apps.abacus.ai}" # Detect real terminal (enables spinner + cursor control) if [ -t 1 ]; then IS_TTY=true; else IS_TTY=false; fi _spin_pid="" tmpdir="" # ── cleanup ─────────────────────────────────────────────────────────────────── _cleanup() { [ -n "$_spin_pid" ] && kill "$_spin_pid" 2>/dev/null || true [ -n "$_spin_pid" ] && wait "$_spin_pid" 2>/dev/null || true _spin_pid="" tput cnorm 2>/dev/null || true [ -n "$tmpdir" ] && rm -rf "$tmpdir" 2>/dev/null || true } trap '_cleanup' EXIT INT TERM # ── output helpers ──────────────────────────────────────────────────────────── fail() { # Usage: fail "primary message" ["hint line 1" "hint line 2" ...] msg="$1"; shift printf "\n ${RED}error${RESET}: %s\n" "$msg" >&2 for hint in "$@"; do printf " %s\n" "$hint" >&2 done printf "\n" >&2 exit 1 } # $HOME is required to derive the default install dir unless an explicit path is given. if [ -z "${HOME:-}" ] && [ -z "${ABACUSAI_INSTALL_DIR:-}" ]; then fail "\$HOME is not set." \ "Set ABACUSAI_INSTALL_DIR to an explicit path:" \ " ABACUSAI_INSTALL_DIR=/opt/abacusai/bin sh install.sh" fi # run_step ACTIVE_LABEL DONE_LABEL COMMAND [ARGS...] # # While running: ⠙ Downloading # On success: ✓ Downloaded # On failure: ✗ Downloading (caller should then call fail) run_step() { label="$1" # in-progress text label_ok="$2" # success text shift 2 if $IS_TTY; then tput civis 2>/dev/null || true ( # Spinner subprocess — killed when the step finishes i=0 while true; do case $(( i % 10 )) in 0) f='⠋' ;; 1) f='⠙' ;; 2) f='⠹' ;; 3) f='⠸' ;; 4) f='⠼' ;; 5) f='⠴' ;; 6) f='⠦' ;; 7) f='⠧' ;; 8) f='⠇' ;; 9) f='⠏' ;; esac printf "\r %s %s" "$f" "$label" i=$(( i + 1 )) sleep 0.1 done ) & _spin_pid=$! fi set +e "$@" >/dev/null 2>&1 rc=$? set -e if $IS_TTY; then kill "$_spin_pid" 2>/dev/null || true wait "$_spin_pid" 2>/dev/null || true _spin_pid="" tput cnorm 2>/dev/null || true fi if [ "$rc" -eq 0 ]; then printf "\r ${GREEN}✓${RESET} %s\033[K\n" "$label_ok" else printf "\r ${RED}✗${RESET} %s\033[K\n" "$label" fi return "$rc" } # ── downloader ──────────────────────────────────────────────────────────────── _curl_retry="" DOWNLOADER="" if command -v curl >/dev/null 2>&1; then DOWNLOADER="curl" # curl installed via snap is confined and cannot read /tmp — prefer wget # if available (the classic Ubuntu "curl: (23) Failed writing body"). case "$(command -v curl)" in */snap/*) command -v wget >/dev/null 2>&1 && DOWNLOADER="wget" ;; esac # Old curls lack --retry / --continue-at; feature-detect each separately. if curl --help all 2>/dev/null | grep -q -- --retry; then _curl_retry="--retry 3" curl --help all 2>/dev/null | grep -q -- --continue-at && _curl_retry="--retry 3 -C -" fi elif command -v wget >/dev/null 2>&1; then DOWNLOADER="wget" fi [ -n "$DOWNLOADER" ] || fail "curl or wget is required but neither is installed." # Retries on flaky networks; TLS 1.2 floor. (No --proto/--https-only so a custom # ABACUS_BASE_URL over http still works.) _download() { url="$1"; out="$2" if [ "$DOWNLOADER" = "curl" ]; then # shellcheck disable=SC2086 if [ -n "$out" ]; then curl $_curl_retry --tlsv1.2 -fsSL -o "$out" "$url" else curl $_curl_retry --tlsv1.2 -fsSL "$url"; fi else if [ -n "$out" ]; then wget --secure-protocol=TLSv1_2 -q -O "$out" "$url" else wget --secure-protocol=TLSv1_2 -q -O - "$url"; fi fi } # ── platform detection ──────────────────────────────────────────────────────── case "$(uname -s)" in Darwin) os="darwin" ;; Linux) os="linux" ;; MINGW*|MSYS*|CYGWIN*) fail "Windows is not supported by this script." \ "Use install.ps1 (recommended) or install.cmd instead." ;; *) fail "Unsupported operating system: $(uname -s)" ;; esac case "$(uname -m)" in x86_64|amd64) arch="x64" ;; arm64|aarch64) arch="arm64" ;; i386|i486|i586|i686) fail "32-bit x86 is not supported." "abacusai requires a 64-bit OS." ;; armv6l|armv7l|armv8l|armhf|arm) fail "32-bit ARM is not supported." "Only arm64/aarch64 builds are available." ;; riscv64|ppc64|ppc64le|s390x|loongarch64|mips*) fail "Architecture $(uname -m) is not supported yet." \ "Track support at https://github.com/abacusai/codellm/issues" ;; *) fail "Unsupported architecture: $(uname -m)" ;; esac # On Apple Silicon, detect arm64 even when uname reports x86_64: either the shell # is Rosetta-translated (proc_translated) or it's a native x86_64 process on an # arm64 machine (hw.optional.arm64). Either way the arm64 build is correct. if [ "$os" = "darwin" ] && [ "$arch" = "x64" ]; then if [ "$(sysctl -n sysctl.proc_translated 2>/dev/null)" = "1" ] \ || [ "$(sysctl -n hw.optional.arm64 2>/dev/null)" = "1" ]; then arch="arm64" fi fi # Detect WSL (handled as normal Linux; only affects the interop warning below). IS_WSL=false if [ "$os" = "linux" ] && { grep -qi microsoft /proc/version 2>/dev/null || grep -qi microsoft /proc/sys/kernel/osrelease 2>/dev/null }; then IS_WSL=true fi # A 64-bit kernel can run a 32-bit userland (uname -m still says x86_64/aarch64); # our 64-bit binary would fail cryptically. The ELF class byte (offset 4) of the # running shell is 1 for 32-bit, 2 for 64-bit. if [ "$os" = "linux" ] && [ -r /proc/self/exe ]; then _eiclass=$(od -An -tu1 -j4 -N1 /proc/self/exe 2>/dev/null | tr -d ' ') if [ "$_eiclass" = "1" ]; then fail "This system has a 32-bit userland — the 64-bit abacusai binary cannot run here." fi fi # Pick the build variant. -musl is a hard requirement (glibc binaries can't load # on musl) so it wins over -baseline, which is only for x64 CPUs lacking AVX2. variant="" if [ "$os" = "linux" ] && { ldd --version 2>&1 | grep -qi musl || [ -f /etc/alpine-release ]; }; then variant="-musl" elif [ "$arch" = "x64" ]; then case "$os" in linux) grep -q avx2 /proc/cpuinfo 2>/dev/null || variant="-baseline" ;; darwin) sysctl -a 2>/dev/null | grep machdep.cpu | grep -qi AVX2 || variant="-baseline" ;; esac fi # glibc floor: the binary is bun-compiled and needs glibc 2.17+ (RHEL 7 / # manylinux2014 baseline — the same floor Bun targets). On older glibc, fail # with a pointer to the musl build instead of a cryptic "GLIBC_2.x not found". if [ "$os" = "linux" ] && [ "$variant" != "-musl" ]; then _glibc=$(ldd --version 2>/dev/null | head -n1 | grep -oE '[0-9]+\.[0-9]+' | head -n1) if [ -n "$_glibc" ]; then _maj=${_glibc%.*}; _min=${_glibc#*.} if [ "$_maj" -lt 2 ] || { [ "$_maj" -eq 2 ] && [ "$_min" -lt 17 ]; }; then fail "glibc ${_glibc} is too old — abacusai needs glibc 2.17 or newer." \ "Upgrade your distribution, or run on a musl system (Alpine) instead." fi fi fi platform="${os}-${arch}${variant}" # ── resolve version ─────────────────────────────────────────────────────────── if [ -z "$VERSION" ]; then VERSION=$( _download "${ABACUS_API_BASE}/api/v0/_getCodellmCliVersion?channel=${CHANNEL}" "" 2>/dev/null | grep -o '"version"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/.*"version"[[:space:]]*:[[:space:]]*"//' | sed 's/".*//' ) if [ -z "$VERSION" ]; then fail "Could not fetch the latest version." \ "Check your internet connection, or specify a version explicitly:" \ " ABACUSAI_VERSION= sh install.sh" fi fi # ── header ──────────────────────────────────────────────────────────────────── printf "\n" printf " ${BOLD}Abacus AI CLI${RESET} ${DIM}v${VERSION} · ${platform}${RESET}\n" printf "\n" # ── download ────────────────────────────────────────────────────────────────── archive="abacusai-${platform}.tar.gz" url="${BASE_URL}/${VERSION}/${archive}" tmpdir=$(mktemp -d) run_step "Downloading" "Downloaded" \ _download "$url" "${tmpdir}/${archive}" || fail "Could not download abacusai v${VERSION} for ${platform}." \ "Check your internet connection, or try a different version:" \ " ABACUSAI_VERSION= sh install.sh" # ── extract ─────────────────────────────────────────────────────────────────── run_step "Extracting" "Extracted" \ tar -xzf "${tmpdir}/${archive}" -C "$tmpdir" || fail "Failed to extract the downloaded archive." \ "The file may be corrupted. Try running the installer again." [ -f "${tmpdir}/abacusai" ] || fail "The archive is missing the abacusai binary." \ "This is unexpected — please report it at https://github.com/abacusai/codellm/issues" # ── install ─────────────────────────────────────────────────────────────────── mkdir -p "$INSTALL_DIR" 2>/dev/null || fail "Cannot create install directory: ${INSTALL_DIR}" \ "Set ABACUSAI_INSTALL_DIR to a writable path and retry:" \ " ABACUSAI_INSTALL_DIR=~/bin sh install.sh" # Stage to a temp name then atomically rename. # This keeps any currently-running binary usable until it exits naturally. _do_install() { cp "${tmpdir}/abacusai" "${INSTALL_DIR}/abacusai.new" && chmod +x "${INSTALL_DIR}/abacusai.new" && mv "${INSTALL_DIR}/abacusai.new" "${INSTALL_DIR}/abacusai" } run_step "Installing" "Installed to ${INSTALL_DIR}/abacusai" \ _do_install || fail "Cannot write to ${INSTALL_DIR}." \ "Ensure you have write permission to that directory, or set a different path:" \ " ABACUSAI_INSTALL_DIR=~/bin sh install.sh" # Sigstore signature (best-effort, silent) if [ -f "${tmpdir}/abacusai-${platform}.sigstore" ]; then mv "${tmpdir}/abacusai-${platform}.sigstore" \ "${INSTALL_DIR}/abacusai-${platform}.sigstore" 2>/dev/null || true fi # ── remove legacy deepagent-app / AbacusAI.app installations ───────────────── # The old desktop app (a VS Code fork) created a symlink at /usr/local/bin/abacusai # pointing into the app bundle. Remove it so it does not shadow the new binary. _remove_deepagent_symlinks() { for candidate in \ "/usr/local/bin/abacusai" \ "/usr/bin/abacusai" do if [ -L "$candidate" ]; then link_target=$(readlink "$candidate" 2>/dev/null || true) case "$link_target" in */AbacusAI.app/*|*/abacusai-app/*|*/deepagent-app/*|*/resources/app/bin/abacusai*) rm -f "$candidate" 2>/dev/null || true printf " ${DIM}removed legacy symlink: %s${RESET}\n" "$candidate" ;; esac fi done # The old Linux app placed a real binary at /usr/share/abacusai/bin/abacusai; # warn about that (removing system-wide files needs root). We do NOT flag # /usr/local/bin/abacusai — that's a normal install path, not proof of the # legacy app, and flagging it printed a spurious warning on clean machines. if [ "$os" = "linux" ] && [ -f "/usr/share/abacusai/bin/abacusai" ] && ! [ -L "/usr/share/abacusai/bin/abacusai" ]; then printf " ${DIM}warning: found a legacy AbacusAI binary at /usr/share/abacusai/bin/abacusai — consider removing it manually.${RESET}\n" fi } _remove_deepagent_symlinks # ── warn about package-manager global installs of @abacus-ai/cli ───────────── _check_pkg_manager_installs() { found_managers="" if command -v npm >/dev/null 2>&1; then if npm list -g --depth=0 "@abacus-ai/cli" 2>/dev/null | grep -q "@abacus-ai/cli"; then found_managers="${found_managers} npm" fi fi if command -v bun >/dev/null 2>&1; then if bun pm ls -g 2>/dev/null | grep -q "@abacus-ai/cli"; then found_managers="${found_managers} bun" fi fi if command -v yarn >/dev/null 2>&1; then if yarn global list --depth=0 2>/dev/null | grep -q "@abacus-ai/cli"; then found_managers="${found_managers} yarn" fi fi if command -v pnpm >/dev/null 2>&1; then if pnpm list -g --depth=0 2>/dev/null | grep -q "@abacus-ai/cli"; then found_managers="${found_managers} pnpm" fi fi if [ -n "$found_managers" ]; then printf "\n" printf " ${BOLD}Warning:${RESET} @abacus-ai/cli is also installed via:%s\n" "$found_managers" printf " This creates a duplicate %s command that may shadow the new binary.\n" "abacusai" printf " To remove the duplicate, run one of:\n" for mgr in $found_managers; do case "$mgr" in npm) printf " npm uninstall -g @abacus-ai/cli\n" ;; bun) printf " bun remove -g @abacus-ai/cli\n" ;; yarn) printf " yarn global remove @abacus-ai/cli\n" ;; pnpm) printf " pnpm remove -g @abacus-ai/cli\n" ;; esac done printf "\n" fi } _check_pkg_manager_installs # WSL: Windows-interop paths on $PATH can place a stale Windows abacusai.exe # ahead of the Linux binary. Point it out so the user can fix ordering. if $IS_WSL && command -v abacusai.exe >/dev/null 2>&1; then printf " ${DIM}note: a Windows abacusai.exe is on your PATH (WSL interop); ensure %s precedes Windows paths.${RESET}\n" "$INSTALL_DIR" fi # ── update shell profile ────────────────────────────────────────────────────── path_added=false _shell_rc="" # profile file we wrote to (used in the summary hint) _add_to_profile() { profile="$1" if [ -f "$profile" ] && grep -q "$INSTALL_DIR" "$profile" 2>/dev/null; then return 0; fi printf '\n# Abacus AI CLI\nexport PATH="%s:$PATH"\n' "$INSTALL_DIR" >> "$profile" path_added=true _shell_rc="$profile" } case "${SHELL:-}" in */zsh) # Respect $ZDOTDIR (where zsh actually reads .zshrc from) when set. _add_to_profile "${ZDOTDIR:-$HOME}/.zshrc" ;; */bash) if [ "$os" = "darwin" ]; then _add_to_profile "$HOME/.bash_profile" else _add_to_profile "$HOME/.bashrc"; fi ;; */fish) _fish_conf="${XDG_CONFIG_HOME:-$HOME/.config}/fish/conf.d" mkdir -p "$_fish_conf" if [ ! -f "${_fish_conf}/abacusai.fish" ] || ! grep -q "$INSTALL_DIR" "${_fish_conf}/abacusai.fish" 2>/dev/null then printf 'fish_add_path %s\n' "$INSTALL_DIR" > "${_fish_conf}/abacusai.fish" path_added=true # fish_add_path takes effect in new sessions; no single-file source equivalent _shell_rc="" fi ;; */ksh) _add_to_profile "$HOME/.kshrc" ;; *) # POSIX sh / dash / ash / unknown: .profile is the portable login-shell rc. _add_to_profile "$HOME/.profile" ;; esac # Also update PATH for the lifetime of this installer process case ":$PATH:" in *":${INSTALL_DIR}:"*) ;; *) export PATH="${INSTALL_DIR}:$PATH" ;; esac # ── summary ─────────────────────────────────────────────────────────────────── printf "\n" if $path_added; then if [ -n "$_shell_rc" ]; then printf " To use ${BOLD}abacusai${RESET} in this terminal, run:\n" printf " ${DIM}source %s${RESET}\n" "$_shell_rc" printf "\n" printf " ${DIM}Or open a new terminal.${RESET}\n" else # fish or unknown shell — no single source command available printf " Open a new terminal to use ${BOLD}abacusai${RESET}.\n" fi else printf " Run ${BOLD}abacusai${RESET} to get started.\n" fi printf "\n"