#!/usr/bin/env bash # ============================================================================= # Tesslate Studio — One-Command Installer # ============================================================================= # Usage: curl -fsSL https://tesslate.com/install.sh | bash # # Installs Tesslate Studio locally using Docker Compose. # Idempotent — safe to re-run. Does NOT auto-install Docker. # ============================================================================= set -euo pipefail REPO_URL="https://github.com/TesslateAI/Studio.git" INSTALL_DIR="${TESSLATE_DIR:-$HOME/tesslate-studio}" HEALTH_URL="http://studio.localhost" HEALTH_TIMEOUT=60 # ── Colors (respects NO_COLOR) ── if [[ -z "${NO_COLOR:-}" ]] && [[ -t 1 ]]; then B='\033[1m' G='\033[0;32m' R='\033[0;31m' Y='\033[1;33m' C='\033[0;36m' D='\033[0;90m' NC='\033[0m' else B='' G='' R='' Y='' C='' D='' NC='' fi ok() { echo -e " ${G}✓${NC} $*"; } err() { echo -e " ${R}✗${NC} $*"; } warn() { echo -e " ${Y}!${NC} $*"; } info() { echo -e " ${C}▸${NC} $*"; } hdr() { echo -e "\n${B}$*${NC}\n"; } die() { err "$@"; exit 1; } # ── Banner ── echo "" echo -e "${B} ╔══════════════════════════════════════════╗${NC}" echo -e "${B} ║ Tesslate Studio — Local Installer ║${NC}" echo -e "${B} ║ Open source · Self-hosted · Your data ║${NC}" echo -e "${B} ╚══════════════════════════════════════════╝${NC}" echo "" # ── Step 1: Check prerequisites ── hdr "Checking prerequisites..." # Docker if command -v docker &>/dev/null; then ok "Docker found: $(docker --version | head -1)" else die "Docker not found. Please install Docker first: macOS / Windows: https://docs.docker.com/desktop/ Linux: https://docs.docker.com/engine/install/ Then re-run this script." fi # Docker daemon running if ! docker info &>/dev/null; then die "Docker daemon is not running. Start Docker Desktop (macOS/Windows) or run: sudo systemctl start docker (Linux) Then re-run this script." fi # Docker Compose if docker compose version &>/dev/null; then ok "Docker Compose found: $(docker compose version --short 2>/dev/null || echo 'available')" elif command -v docker-compose &>/dev/null; then ok "docker-compose found (legacy)" # Alias for the rest of the script docker() { if [[ "${1:-}" == "compose" ]]; then shift command docker-compose "$@" else command docker "$@" fi } else die "Docker Compose not found. Install Docker Compose: https://docs.docker.com/compose/install/ Then re-run this script." fi # Git if command -v git &>/dev/null; then ok "Git found: $(git --version)" else die "Git not found. Install Git: https://git-scm.com/downloads Then re-run this script." fi # ── Step 2: Clone or update repo ── hdr "Setting up Tesslate Studio..." if [[ -d "$INSTALL_DIR/.git" ]]; then info "Existing installation found at $INSTALL_DIR" info "Pulling latest changes..." cd "$INSTALL_DIR" git pull --ff-only || { warn "Could not fast-forward. You may have local changes." warn "Continuing with existing code." } ok "Repository updated" else info "Cloning Tesslate Studio to $INSTALL_DIR..." git clone "$REPO_URL" "$INSTALL_DIR" ok "Repository cloned" fi cd "$INSTALL_DIR" # ── Step 3: Configure environment ── hdr "Configuring environment..." if [[ -f .env ]]; then info "Existing .env found — preserving your configuration." ok ".env already configured" else if [[ -f .env.example ]]; then cp .env.example .env ok "Created .env from .env.example" else warn ".env.example not found — creating minimal .env" touch .env fi # Generate SECRET_KEY if command -v openssl &>/dev/null; then SECRET_KEY=$(openssl rand -hex 32) else SECRET_KEY=$(head -c 32 /dev/urandom | xxd -p -c 64 2>/dev/null || echo "change-me-$(date +%s)") fi # Set SECRET_KEY in .env if grep -q "^SECRET_KEY=" .env 2>/dev/null; then sed -i.bak "s|^SECRET_KEY=.*|SECRET_KEY=$SECRET_KEY|" .env && rm -f .env.bak else echo "SECRET_KEY=$SECRET_KEY" >> .env fi ok "Generated SECRET_KEY" # Generate LITELLM_MASTER_KEY if command -v openssl &>/dev/null; then LITELLM_KEY="sk-$(openssl rand -hex 24)" else LITELLM_KEY="sk-$(head -c 24 /dev/urandom | xxd -p -c 48 2>/dev/null || echo "change-me")" fi if grep -q "^LITELLM_MASTER_KEY=" .env 2>/dev/null; then sed -i.bak "s|^LITELLM_MASTER_KEY=.*|LITELLM_MASTER_KEY=$LITELLM_KEY|" .env && rm -f .env.bak else echo "LITELLM_MASTER_KEY=$LITELLM_KEY" >> .env fi ok "Generated LITELLM_MASTER_KEY" # Ask about API key echo "" info "Tesslate needs an LLM API key to power its agents." info "You can configure this now or add it later in .env." echo "" read -rp " Enter your API key (or press Enter to skip): " API_KEY if [[ -n "$API_KEY" ]]; then if grep -q "^OPENAI_API_KEY=" .env 2>/dev/null; then sed -i.bak "s|^OPENAI_API_KEY=.*|OPENAI_API_KEY=$API_KEY|" .env && rm -f .env.bak else echo "OPENAI_API_KEY=$API_KEY" >> .env fi ok "API key configured" else warn "Skipped — add your API key to $INSTALL_DIR/.env later" fi fi # ── Step 4: Start services ── hdr "Starting Tesslate Studio..." docker compose up -d ok "Docker Compose services started" # ── Step 5: Build devserver image ── hdr "Building devserver image..." if [[ -f orchestrator/Dockerfile.devserver ]]; then docker build -t tesslate-devserver:latest -f orchestrator/Dockerfile.devserver orchestrator/ ok "Devserver image built" else warn "orchestrator/Dockerfile.devserver not found — skipping devserver build" fi # ── Step 6: Wait for health check ── hdr "Waiting for Tesslate to be ready..." SECONDS_WAITED=0 while [[ $SECONDS_WAITED -lt $HEALTH_TIMEOUT ]]; do if curl -sf "$HEALTH_URL" -o /dev/null 2>/dev/null; then ok "Tesslate Studio is ready!" break fi sleep 2 SECONDS_WAITED=$((SECONDS_WAITED + 2)) echo -ne " ${D}Waiting... ${SECONDS_WAITED}s${NC}\r" done if [[ $SECONDS_WAITED -ge $HEALTH_TIMEOUT ]]; then warn "Health check timed out after ${HEALTH_TIMEOUT}s" warn "Tesslate may still be starting. Check: docker compose logs -f" fi # ── Done ── echo "" echo -e "${B} ╔══════════════════════════════════════════╗${NC}" echo -e "${B} ║ Tesslate Studio is running! ║${NC}" echo -e "${B} ╚══════════════════════════════════════════╝${NC}" echo "" echo -e " ${G}➜${NC} Open ${B}http://studio.localhost${NC}" echo "" echo -e " ${D}Install dir: $INSTALL_DIR${NC}" echo -e " ${D}Stop: cd $INSTALL_DIR && docker compose down${NC}" echo -e " ${D}Logs: cd $INSTALL_DIR && docker compose logs -f${NC}" echo -e " ${D}Update: curl -fsSL https://tesslate.com/install.sh | bash${NC}" echo "" echo -e " ${D}Docs: https://docs.tesslate.com${NC}" echo -e " ${D}Discord: https://discord.gg/WgXabcN2r2${NC}" echo -e " ${D}GitHub: https://github.com/TesslateAI/Studio${NC}" echo ""