#!/usr/bin/env bash # Geocentric Code installer for macOS and Linux. # # curl -fsSL https://geocentricai.com/install.sh | bash # # It installs what Geocentric Code needs (git, Python 3.9+, Ollama, ripgrep), # fetches the source from github.com/GeocentricAI/geocentric-code into a # private virtualenv, puts `geo` on your PATH, and downloads a local model. # Run it again at any time to update. # # Options, passed after `bash -s --`: # --no-model skip the language model download # --gateway also install the web gateway (geo-gateway) # --yes never ask for confirmation # --dry-run show what would happen and change nothing # --uninstall remove Geocentric Code (keeps Ollama and your models) # # curl -fsSL https://geocentricai.com/install.sh | bash -s -- --no-model # # Environment: GEO_HOME (install dir), GEO_BIN (where `geo` is linked), # GEO_MODEL (model to pull), GEO_REPO, GEO_BRANCH. set -euo pipefail REPO_URL="${GEO_REPO:-https://github.com/GeocentricAI/geocentric-code.git}" BRANCH="${GEO_BRANCH:-main}" GEO_HOME="${GEO_HOME:-${XDG_DATA_HOME:-$HOME/.local/share}/geocentric-code}" BIN_DIR="${GEO_BIN:-$HOME/.local/bin}" OLLAMA_URL="http://127.0.0.1:11434" MARK_BEGIN="# >>> geocentric >>>" MARK_END="# <<< geocentric <<<" NO_MODEL=0 GATEWAY=0 ASSUME_YES=0 DRY=0 UNINSTALL=0 USE_UV=0 if [ -t 1 ]; then C_BLUE=$'\033[1;34m'; C_GREEN=$'\033[1;32m'; C_YELLOW=$'\033[1;33m'; C_RED=$'\033[1;31m'; C_DIM=$'\033[2m'; C_OFF=$'\033[0m' else C_BLUE=""; C_GREEN=""; C_YELLOW=""; C_RED=""; C_DIM=""; C_OFF="" fi say() { printf '%s==>%s %s\n' "$C_BLUE" "$C_OFF" "$1"; } ok() { printf '%s ok%s %s\n' "$C_GREEN" "$C_OFF" "$1"; } warn() { printf '%s !!%s %s\n' "$C_YELLOW" "$C_OFF" "$1" >&2; } die() { printf '%sERROR:%s %s\n' "$C_RED" "$C_OFF" "$1" >&2; exit 1; } run() { if [ "$DRY" = 1 ]; then printf '%s [dry-run] %s%s\n' "$C_DIM" "$*" "$C_OFF" return 0 fi "$@" } have() { command -v "$1" >/dev/null 2>&1; } has_tty() { (exec /dev/null; } confirm() { [ "$ASSUME_YES" = 1 ] && return 0 [ "$DRY" = 1 ] && return 0 if has_tty; then local ans="" printf '%s [Y/n] ' "$1" >/dev/tty read -r ans /dev/null | sed 's/^# \{0,1\}//' ; exit 0 ;; *) die "Unknown option: $1 (try --help)" ;; esac shift done } # ---------------------------------------------------------------- platform detect_platform() { case "$(uname -s)" in Darwin) PLATFORM=macos ;; Linux) PLATFORM=linux ;; *) die "This installer supports macOS and Linux. On Windows, use: irm https://geocentricai.com/install.ps1 | iex" ;; esac ARCH="$(uname -m)" SUDO="" if [ "$(id -u)" -ne 0 ]; then if have sudo; then SUDO="sudo"; fi fi PKG="" if [ "$PLATFORM" = macos ]; then PKG=brew elif have apt-get; then PKG=apt elif have dnf; then PKG=dnf elif have yum; then PKG=yum elif have pacman; then PKG=pacman elif have zypper; then PKG=zypper elif have apk; then PKG=apk fi } ram_gb() { if [ "$PLATFORM" = macos ]; then echo $(( $(sysctl -n hw.memsize) / 1073741824 )) elif [ -r /proc/meminfo ]; then awk '/MemTotal/ { printf "%d", $2 / 1048576 }' /proc/meminfo else echo 8 fi } # ------------------------------------------------------------------ python py_ok() { have "$1" && "$1" -c 'import sys; sys.exit(0 if sys.version_info >= (3, 9) else 1)' 2>/dev/null \ && "$1" -c 'import venv, ensurepip' 2>/dev/null } find_python() { local c for c in python3.13 python3.12 python3.11 python3.10 python3.9 python3; do if py_ok "$c"; then echo "$c"; return 0; fi done return 1 } # ---------------------------------------------------------- system packages brew_env() { if have brew; then return 0; fi local b for b in /opt/homebrew/bin/brew /usr/local/bin/brew /home/linuxbrew/.linuxbrew/bin/brew; do if [ -x "$b" ]; then eval "$("$b" shellenv)"; return 0; fi done return 1 } ensure_homebrew() { brew_env && return 0 say "Homebrew is not installed. It is the standard way to install developer tools on macOS." confirm "Install Homebrew now?" || die "Homebrew is required to install dependencies on macOS. Install it from https://brew.sh and run this again." if [ "$DRY" = 1 ]; then run /bin/bash -c "curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh | bash"; return 0; fi /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" brew_env || die "Homebrew installed but is not on PATH. Open a new terminal and run this installer again." } pkg_install() { # pkg_install : best effort per package manager. case "$PKG" in brew) run brew install "$@" ;; apt) run $SUDO apt-get update -qq; run $SUDO apt-get install -y "$@" ;; dnf) run $SUDO dnf install -y "$@" ;; yum) run $SUDO yum install -y "$@" ;; pacman) run $SUDO pacman -Sy --noconfirm --needed "$@" ;; zypper) run $SUDO zypper --non-interactive install "$@" ;; apk) run $SUDO apk add "$@" ;; *) return 1 ;; esac } pkg_names() { # pkg_names -> distro package names case "$1:$PKG" in git:*) echo git ;; curl:*) echo curl ;; python:brew) echo python ;; python:apt) echo "python3 python3-venv python3-pip" ;; python:dnf|python:yum) echo "python3 python3-pip" ;; python:pacman) echo "python python-pip" ;; python:zypper) echo "python3 python3-pip" ;; python:apk) echo "python3 py3-pip" ;; ripgrep:*) echo ripgrep ;; esac } install_deps() { local need_git=0 need_curl=0 need_python=0 need_rg=0 need_ollama=0 have git || need_git=1 have curl || need_curl=1 PY="$(find_python || true)" [ -n "$PY" ] || need_python=1 have rg || need_rg=1 have ollama || need_ollama=1 say "Checking what is already installed" [ "$need_git" = 0 ] && ok "git" || warn "git is missing" [ "$need_curl" = 0 ] && ok "curl" || warn "curl is missing" [ "$need_python" = 0 ] && ok "python ($PY)" || warn "Python 3.9+ with venv is missing" [ "$need_rg" = 0 ] && ok "ripgrep" || warn "ripgrep is missing (optional, makes search faster)" [ "$need_ollama" = 0 ] && ok "ollama" || warn "Ollama is missing (runs the models)" if [ "$need_git$need_curl$need_python$need_rg$need_ollama" = "00000" ]; then return 0; fi if [ -z "$PKG" ]; then warn "No supported package manager found (apt, dnf, yum, pacman, zypper, apk, brew)." [ "$need_git" = 1 ] && die "Install git, then run this again." else confirm "Install the missing pieces with $PKG${SUDO:+ (uses sudo)}?" || die "Cancelled." [ "$PLATFORM" = macos ] && ensure_homebrew local pk="" [ "$need_git" = 1 ] && pk="$pk $(pkg_names git)" [ "$need_curl" = 1 ] && pk="$pk $(pkg_names curl)" [ "$need_python" = 1 ] && pk="$pk $(pkg_names python)" # shellcheck disable=SC2086 [ -n "${pk# }" ] && { pkg_install $pk || warn "Package install reported a problem; continuing."; } if [ "$need_rg" = 1 ]; then # shellcheck disable=SC2046 pkg_install $(pkg_names ripgrep) || warn "Could not install ripgrep. Geocentric falls back to a built-in search." fi fi have git || [ "$DRY" = 1 ] || die "git is still missing after the install step." PY="$(find_python || true)" if [ -z "$PY" ] && [ "$DRY" != 1 ]; then warn "No usable system Python. Fetching a managed Python 3.12 with uv." USE_UV=1 if ! have uv; then curl -LsSf https://astral.sh/uv/install.sh | sh export PATH="$HOME/.local/bin:$HOME/.cargo/bin:$PATH" fi have uv || die "Could not install uv. Install Python 3.9+ yourself and run this again." fi [ -n "$PY" ] || PY=python3 } # ------------------------------------------------------------------ ollama ollama_up() { curl -fsS --max-time 2 "$OLLAMA_URL/api/version" >/dev/null 2>&1; } install_ollama() { if have ollama; then return 0; fi say "Installing Ollama" if [ "$PLATFORM" = macos ]; then ensure_homebrew run brew install ollama else if [ "$PKG" = apk ]; then warn "Ollama does not publish an Alpine build. Install it from https://ollama.com/download and run this again." return 0 fi if [ "$DRY" = 1 ]; then run sh -c "curl -fsSL https://ollama.com/install.sh | sh"; else curl -fsSL https://ollama.com/install.sh | sh; fi fi } start_ollama() { have ollama || return 0 ollama_up && return 0 say "Starting Ollama" if [ "$DRY" = 1 ]; then run ollama serve; return 0; fi if [ "$PLATFORM" = macos ] && have brew; then brew services start ollama >/dev/null 2>&1 || true elif have systemctl && systemctl list-unit-files 2>/dev/null | grep -q '^ollama'; then $SUDO systemctl start ollama >/dev/null 2>&1 || true fi ollama_up || { nohup ollama serve >/dev/null 2>&1 & } local i=0 while [ $i -lt 40 ]; do ollama_up && { ok "Ollama is running"; return 0; } sleep 1; i=$((i + 1)) done warn "Ollama did not respond yet. Start it with: ollama serve" } pick_model() { if [ -n "${GEO_MODEL:-}" ]; then MODEL="$GEO_MODEL"; MODEL_SIZE="?"; return; fi local gb; gb="$(ram_gb)" if [ "$gb" -ge 16 ]; then MODEL="qwen2.5-coder:14b"; MODEL_SIZE="9 GB" elif [ "$gb" -ge 8 ]; then MODEL="qwen2.5-coder:7b"; MODEL_SIZE="4.7 GB" else MODEL="qwen2.5-coder:3b"; MODEL_SIZE="1.9 GB" fi } pull_model() { [ "$NO_MODEL" = 1 ] && { say "Skipping the model download (--no-model)"; return 0; } have ollama || { warn "Ollama is not available, so no model was downloaded."; return 0; } if [ "$DRY" != 1 ] && ollama_up; then local count; count="$(ollama list 2>/dev/null | tail -n +2 | grep -c . || true)" if [ "${count:-0}" -gt 0 ]; then ok "Ollama already has $count model(s). Skipping the download."; return 0; fi fi pick_model say "Local model: $MODEL (about $MODEL_SIZE for $(ram_gb) GB of RAM)" confirm "Download $MODEL now?" || { say "Skipped. Pull one later with: ollama pull $MODEL"; return 0; } if [ "$DRY" != 1 ] && ! ollama_up; then warn "Ollama is not running. Later, run: ollama pull $MODEL"; return 0; fi run ollama pull "$MODEL" } # ------------------------------------------------------------------ source fetch_source() { say "Fetching Geocentric Code from GitHub" run mkdir -p "$GEO_HOME" if [ -d "$GEO_HOME/src/.git" ]; then run git -C "$GEO_HOME/src" fetch --quiet --depth 1 origin "$BRANCH" run git -C "$GEO_HOME/src" reset --quiet --hard FETCH_HEAD return 0 fi if ! run git clone --quiet --depth 1 --branch "$BRANCH" --filter=blob:none --sparse "$REPO_URL" "$GEO_HOME/src" 2>/dev/null \ || ! run git -C "$GEO_HOME/src" sparse-checkout set geocentric geocentric-gateway 2>/dev/null; then warn "Partial clone is not available here. Falling back to a full shallow clone." run rm -rf "$GEO_HOME/src" run git clone --quiet --depth 1 --branch "$BRANCH" "$REPO_URL" "$GEO_HOME/src" fi } build_env() { say "Installing Python dependencies" if [ "$USE_UV" = 1 ]; then run uv venv --python 3.12 "$GEO_HOME/venv" run uv pip install --quiet --python "$GEO_HOME/venv/bin/python" -e "$GEO_HOME/src" [ "$GATEWAY" = 1 ] && run uv pip install --quiet --python "$GEO_HOME/venv/bin/python" -e "$GEO_HOME/src/geocentric-gateway" return 0 fi run "$PY" -m venv "$GEO_HOME/venv" run "$GEO_HOME/venv/bin/python" -m pip install --quiet --upgrade pip run "$GEO_HOME/venv/bin/python" -m pip install --quiet -e "$GEO_HOME/src" if [ "$GATEWAY" = 1 ]; then run "$GEO_HOME/venv/bin/python" -m pip install --quiet -e "$GEO_HOME/src/geocentric-gateway" fi } link_commands() { say "Adding geo to $BIN_DIR" run mkdir -p "$BIN_DIR" run ln -sf "$GEO_HOME/venv/bin/geo" "$BIN_DIR/geo" if [ "$GATEWAY" = 1 ]; then run ln -sf "$GEO_HOME/venv/bin/geo-gateway" "$BIN_DIR/geo-gateway"; fi } rc_files() { local sh; sh="$(basename "${SHELL:-/bin/sh}")" case "$sh" in zsh) echo "$HOME/.zshrc" ;; bash) if [ "$PLATFORM" = macos ]; then echo "$HOME/.bash_profile"; else echo "$HOME/.bashrc"; fi ;; fish) echo "$HOME/.config/fish/conf.d/geocentric.fish" ;; *) echo "$HOME/.profile" ;; esac } ensure_path() { case ":$PATH:" in *":$BIN_DIR:"*) PATH_NEEDS_RESTART=0; return 0 ;; esac PATH_NEEDS_RESTART=1 local rc; rc="$(rc_files)" if [ -f "$rc" ] && grep -qF "$MARK_BEGIN" "$rc" 2>/dev/null; then return 0; fi say "Adding $BIN_DIR to your PATH in $rc" run mkdir -p "$(dirname "$rc")" if [ "$DRY" = 1 ]; then return 0; fi { printf '\n%s\n' "$MARK_BEGIN" if [ "$(basename "$rc")" = "geocentric.fish" ]; then printf 'fish_add_path %s\n' "$BIN_DIR" else printf 'export PATH="%s:$PATH"\n' "$BIN_DIR" fi printf '%s\n' "$MARK_END" } >> "$rc" } verify() { [ "$DRY" = 1 ] && return 0 "$GEO_HOME/venv/bin/python" -c 'import geocentric' 2>/dev/null || die "The install finished but Geocentric will not import. Re-run with a clean directory: rm -rf \"$GEO_HOME\"" local v; v="$("$GEO_HOME/venv/bin/geo" --version 2>/dev/null || true)" ok "${v:-geocentric installed}" } do_uninstall() { say "Removing Geocentric Code" run rm -rf "$GEO_HOME" local l for l in geo geo-gateway; do if [ -L "$BIN_DIR/$l" ]; then run rm -f "$BIN_DIR/$l"; fi done local rc; rc="$(rc_files)" if [ -f "$rc" ] && grep -qF "$MARK_BEGIN" "$rc" && [ "$DRY" != 1 ]; then local tmp; tmp="$(mktemp)" awk -v b="$MARK_BEGIN" -v e="$MARK_END" '$0==b{skip=1;next} $0==e{skip=0;next} !skip' "$rc" > "$tmp" && cat "$tmp" > "$rc" rm -f "$tmp" fi ok "Removed. Ollama and your downloaded models were left in place." } summary() { if [ "$DRY" = 1 ]; then printf '\nDry run complete. Nothing was changed.\n\n'; return 0; fi printf '\n%sGeocentric Code is ready.%s\n\n' "$C_GREEN" "$C_OFF" if [ "${PATH_NEEDS_RESTART:-0}" = 1 ]; then printf ' Open a new terminal so the PATH change takes effect, then:\n\n' else printf ' Start it in any project folder:\n\n' fi printf ' cd your-project\n geo\n\n' printf ' One-shot: geo "add a healthcheck endpoint"\n' printf ' Help: geo --help\n' printf ' Update: curl -fsSL https://geocentricai.com/install.sh | bash\n' printf ' Remove: curl -fsSL https://geocentricai.com/install.sh | bash -s -- --uninstall\n\n' } main() { parse_args "$@" detect_platform if [ "$UNINSTALL" = 1 ]; then do_uninstall; exit 0; fi [ "$DRY" = 1 ] && say "Dry run: nothing will be changed" say "Geocentric Code installer ($PLATFORM, $ARCH)" install_deps fetch_source build_env link_commands ensure_path install_ollama start_ollama pull_model verify summary } if [ -z "${GEO_INSTALL_NO_MAIN:-}" ]; then main "$@"; fi