#!/usr/bin/env bash # # GlucoPilot — one-command local installer. # # Gets GlucoPilot running on your own machine at http://localhost:. # Works on macOS, Linux, and Windows (via WSL2 / Git Bash). All it needs is # Docker. Run it from the folder you cloned this repo into: # # ./install.sh # # It's safe to re-run: it won't overwrite an existing .env unless you say so. set -euo pipefail # ── pretty output ─────────────────────────────────────────────────────────── if [ -t 1 ]; then B=$'\033[1m'; DIM=$'\033[2m'; G=$'\033[32m'; Y=$'\033[33m'; R=$'\033[31m'; C=$'\033[36m'; N=$'\033[0m' else B=""; DIM=""; G=""; Y=""; R=""; C=""; N="" fi say() { printf '%s\n' "$*"; } ok() { printf '%s✓%s %s\n' "$G" "$N" "$*"; } warn() { printf '%s!%s %s\n' "$Y" "$N" "$*"; } die() { printf '%s✗ %s%s\n' "$R" "$*" "$N" >&2; exit 1; } ask() { # ask "Prompt" "default" -> echoes answer (default if non-interactive/empty) local prompt="$1" default="${2:-}" reply="" if [ -t 0 ]; then printf '%s%s%s ' "$C" "$prompt" "$N" >&2 [ -n "$default" ] && printf '%s[%s]%s ' "$DIM" "$default" "$N" >&2 read -r reply || true fi printf '%s' "${reply:-$default}" } say "" say "${B}GlucoPilot installer${N}" say "${DIM}A private, self-hosted health companion. This sets it up on this machine.${N}" say "" # ── 1. preflight: docker ──────────────────────────────────────────────────── command -v docker >/dev/null 2>&1 || die "Docker isn't installed. Get it at https://docs.docker.com/get-docker/ then re-run this." if docker compose version >/dev/null 2>&1; then DC="docker compose" elif command -v docker-compose >/dev/null 2>&1; then DC="docker-compose" else die "Docker Compose isn't available. Install Docker Desktop (macOS/Windows) or the compose plugin (Linux)." fi docker info >/dev/null 2>&1 || die "Docker is installed but not running. Start Docker Desktop (or the docker service) and re-run." ok "Docker is ready" # ── 2. in the repo? ───────────────────────────────────────────────────────── cd "$(dirname "$0")" [ -f docker-compose.local.yml ] || die "Run this from the folder you cloned GlucoPilot into (docker-compose.local.yml not found)." # ── 3. wizard ─────────────────────────────────────────────────────────────── detect_tz() { if command -v timedatectl >/dev/null 2>&1; then timedatectl show -p Timezone --value 2>/dev/null && return; fi [ -f /etc/timezone ] && { cat /etc/timezone; return; } if [ -L /etc/localtime ]; then readlink /etc/localtime | sed 's#.*/zoneinfo/##'; return; fi printf 'America/New_York' } if [ -f .env ]; then warn "An .env already exists." if [ "$(ask 'Keep it and just (re)start? y/n' 'y')" = "y" ]; then KEEP_ENV=1 else KEEP_ENV=0; cp .env ".env.bak.$(date +%s 2>/dev/null || echo old)" 2>/dev/null || true ok "Backed up your old .env" fi else KEEP_ENV=0 fi PORT="8000" if [ "${KEEP_ENV:-0}" = "0" ]; then say "" PORT="$(ask 'Which port should GlucoPilot use?' '8000')" [[ "$PORT" =~ ^[0-9]+$ ]] || die "Port must be a number." TZ_DEFAULT="$(detect_tz)"; [ -n "$TZ_DEFAULT" ] || TZ_DEFAULT="America/New_York" APP_TZ="$(ask 'Your timezone (IANA name)?' "$TZ_DEFAULT")" say "" say "${DIM}Optional: the AI features (Companion, insights, Visit Report) can use the" say "Anthropic API, a local model, or be set up later in Settings. Leave blank to skip.${N}" ANTHROPIC_KEY="$(ask 'Anthropic API key (optional, starts with sk-ant-):' '')" # random secret for signing session cookies if command -v openssl >/dev/null 2>&1; then SECRET="$(openssl rand -hex 32)" else SECRET="$(head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n')"; fi { echo "# GlucoPilot — generated by install.sh. Advanced/server options: see .env.example" echo "APP_PUBLIC_URL=http://localhost:${PORT}" echo "APP_SECRET_KEY=${SECRET}" echo "APP_TIMEZONE=${APP_TZ}" echo "SYNC_ENABLED=true" echo "PORT=${PORT}" [ -n "$ANTHROPIC_KEY" ] && echo "ANTHROPIC_API_KEY=${ANTHROPIC_KEY}" } > .env ok "Wrote .env (secret key generated for you)" else # reuse the port already in .env if present PORT="$(grep -E '^PORT=' .env 2>/dev/null | head -1 | cut -d= -f2)"; [ -n "$PORT" ] || PORT="8000" fi # ── 4. launch ─────────────────────────────────────────────────────────────── say "" say "${B}Starting GlucoPilot…${N} ${DIM}(first run downloads the image — a few minutes)${N}" if ! PORT="$PORT" $DC -f docker-compose.local.yml up -d 2>/dev/null; then warn "Prebuilt image unavailable — building from source instead (slower, one-time)." PORT="$PORT" $DC -f docker-compose.local.yml up -d --build fi # ── 5. wait for it to answer ──────────────────────────────────────────────── URL="http://localhost:${PORT}" printf '%sWaiting for GlucoPilot to come up' "$DIM" up=0 for _ in $(seq 1 60); do code="$(curl -s -o /dev/null -w '%{http_code}' "$URL" 2>/dev/null || echo 000)" if [ "$code" != "000" ]; then up=1; break; fi printf '.'; sleep 2 done printf '%s\n' "$N" say "" if [ "$up" = "1" ]; then ok "GlucoPilot is running!" else warn "It didn't answer yet — it may still be starting. Check logs with: $DC -f docker-compose.local.yml logs -f" fi say "" say "${B}Open:${N} ${C}${URL}${N}" say " ${DIM}The first visit lets you create your login. Then open Settings to connect" say " data sources (Dexcom, Oura, …) and choose your AI. That's it.${N}" say "" say "${B}Handy commands${N}" say " ${DIM}Stop:${N} $DC -f docker-compose.local.yml down" say " ${DIM}Update:${N} git pull && $DC -f docker-compose.local.yml pull && $DC -f docker-compose.local.yml up -d" say " ${DIM}Logs:${N} $DC -f docker-compose.local.yml logs -f" say " ${DIM}Backup:${N} see docs/DEPLOY.md" say ""