diff --git a/scripts/dev_server.sh b/scripts/dev_server.sh new file mode 100755 index 00000000..f22326d8 --- /dev/null +++ b/scripts/dev_server.sh @@ -0,0 +1,191 @@ +#!/usr/bin/env bash +# ── Timmy Time — Development Environment Launcher ──────────────────────────── +# +# Starts the full dev stack via docker compose with: +# • Auto port selection (finds a free port starting at 8000) +# • Clickable dashboard + service links +# • Status line (version, git commit, backend inference) +# • Smoke test on startup +# • Drops into a dev shell with `timmy` CLI wired to this instance +# +# Usage: +# tox -e dev +# # or directly: bash scripts/dev_server.sh +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$REPO_ROOT" + +# ── Colours ────────────────────────────────────────────────────────────────── +GREEN='\033[0;32m' AMBER='\033[0;33m' RED='\033[0;31m' +CYAN='\033[0;36m' BOLD='\033[1m' DIM='\033[2m' +RESET='\033[0m' + +# ── Find a free port ───────────────────────────────────────────────────────── +find_free_port() { + local port="${1:-8000}" + local max_port=$((port + 20)) + while [ "$port" -le "$max_port" ]; do + if ! lsof -iTCP:"$port" -sTCP:LISTEN >/dev/null 2>&1; then + echo "$port" + return 0 + fi + echo -e "${DIM} port $port in use, trying next...${RESET}" >&2 + port=$((port + 1)) + done + echo -e "${RED}ERROR: No free port found in range 8000-$max_port${RESET}" >&2 + return 1 +} + +PORT=$(find_free_port 8000) + +# ── Gather version info ───────────────────────────────────────────────────── +VERSION=$(python3 -c " +try: + import tomllib +except ImportError: + import tomli as tomllib +with open('pyproject.toml', 'rb') as f: + print(tomllib.load(f)['tool']['poetry']['version']) +" 2>/dev/null || echo "unknown") + +GIT_COMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown") +GIT_TIMESTAMP=$(git log -1 --format='%ci' 2>/dev/null || echo "unknown") +GIT_BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown") + +# ── Detect backend inference source ────────────────────────────────────────── +OLLAMA_URL="${OLLAMA_URL:-http://localhost:11434}" +INFERENCE_SOURCE="Ollama @ ${OLLAMA_URL}" +if curl -sf "${OLLAMA_URL}/api/tags" >/dev/null 2>&1; then + OLLAMA_STATUS="${GREEN}reachable${RESET}" +else + OLLAMA_STATUS="${AMBER}unreachable${RESET}" +fi + +# ── Banner ─────────────────────────────────────────────────────────────────── +echo "" +echo -e "${BOLD}${CYAN}╔══════════════════════════════════════════════════════════════╗${RESET}" +echo -e "${BOLD}${CYAN}║ TIMMY TIME — Development Environment ║${RESET}" +echo -e "${BOLD}${CYAN}╚══════════════════════════════════════════════════════════════╝${RESET}" +echo "" + +# ── Status line ────────────────────────────────────────────────────────────── +echo -e "${BOLD}Status${RESET}" +echo -e " Version: ${GREEN}v${VERSION}${RESET} ${DIM}(${GIT_BRANCH} @ ${GIT_COMMIT})${RESET}" +echo -e " Commit: ${DIM}${GIT_TIMESTAMP}${RESET}" +echo -e " Inference: ${INFERENCE_SOURCE} [${OLLAMA_STATUS}]" +echo "" + +# ── Start docker compose ──────────────────────────────────────────────────── +echo -e "${BOLD}Starting services...${RESET}" +export TIMMY_DEV_PORT="$PORT" + +# Ensure data directory exists for volume mount +mkdir -p "$REPO_ROOT/data" + +docker compose \ + -f docker-compose.yml \ + -f docker-compose.dev.yml \ + -p "timmy-dev" \ + up -d --build \ + --remove-orphans 2>&1 | tail -5 + +# Override the published port via env +docker compose \ + -f docker-compose.yml \ + -f docker-compose.dev.yml \ + -p "timmy-dev" \ + port dashboard 8000 >/dev/null 2>&1 || true + +# If the port override didn't work via compose, stop and re-create with the right port +# We use a dev-specific override to set the port dynamically +if [ "$PORT" != "8000" ]; then + echo -e "${DIM} Rebinding to port $PORT...${RESET}" + docker compose -p "timmy-dev" down --remove-orphans >/dev/null 2>&1 || true + + # Create a temporary override for the port + OVERRIDE_FILE=$(mktemp /tmp/timmy-dev-port.XXXXXX.yml) + cat > "$OVERRIDE_FILE" <&1 | tail -5 + rm -f "$OVERRIDE_FILE" +fi + +# ── Wait for healthy ──────────────────────────────────────────────────────── +echo -ne "${DIM} Waiting for dashboard to become healthy...${RESET}" +HEALTHY=false +for i in $(seq 1 30); do + if curl -sf "http://localhost:${PORT}/health" >/dev/null 2>&1; then + HEALTHY=true + break + fi + echo -n "." + sleep 1 +done +echo "" + +# ── Smoke test ─────────────────────────────────────────────────────────────── +echo "" +echo -e "${BOLD}Smoke Tests${RESET}" +if [ "$HEALTHY" = true ]; then + echo -e " Dashboard health: ${GREEN}PASS${RESET}" +else + echo -e " Dashboard health: ${RED}FAIL${RESET} ${DIM}(may still be starting)${RESET}" +fi + +# Check Ollama connectivity from inside the container +if curl -sf "${OLLAMA_URL}/api/tags" >/dev/null 2>&1; then + echo -e " Ollama reachable: ${GREEN}PASS${RESET}" +else + echo -e " Ollama reachable: ${AMBER}SKIP${RESET} ${DIM}(optional service)${RESET}" +fi + +# ── Clickable links ───────────────────────────────────────────────────────── +echo "" +echo -e "${BOLD}Services${RESET}" +echo -e " Dashboard: ${CYAN}http://localhost:${PORT}${RESET}" +echo -e " Health: ${CYAN}http://localhost:${PORT}/health${RESET}" +echo -e " API docs: ${CYAN}http://localhost:${PORT}/docs${RESET}" +echo -e " Ollama: ${CYAN}${OLLAMA_URL}${RESET}" +echo "" + +# ── Dev shell ──────────────────────────────────────────────────────────────── +echo -e "${BOLD}Dev Shell${RESET}" +echo -e " ${DIM}The \`timmy\` CLI is available and points to this dev instance.${RESET}" +echo -e " ${DIM}Type 'exit' or Ctrl-D to stop the dev environment.${RESET}" +echo "" + +cleanup() { + echo "" + echo -e "${AMBER}Shutting down dev environment...${RESET}" + docker compose -p "timmy-dev" down --remove-orphans 2>/dev/null || true + echo -e "${GREEN}Dev environment stopped.${RESET}" +} +trap cleanup EXIT + +# Drop into a subshell with the dev environment on PATH +export PYTHONPATH="$REPO_ROOT/src:${PYTHONPATH:-}" +export PATH="$REPO_ROOT/.venv/bin:$PATH" +export TIMMY_DEV=1 +export OLLAMA_URL +export PS1="(timmy-dev) \w \$ " + +exec bash --norc --noprofile -c " + export PS1='${GREEN}(timmy-dev)${RESET} \w \$ ' + export PYTHONPATH='$PYTHONPATH' + export PATH='$PATH' + export TIMMY_DEV=1 + export OLLAMA_URL='$OLLAMA_URL' + echo 'Ready. Try: timmy status' + exec bash --norc -i +" diff --git a/tox.ini b/tox.ini index 6626fd87..e7be18f9 100644 --- a/tox.ini +++ b/tox.ini @@ -162,10 +162,9 @@ commands = # ── Dev Server ─────────────────────────────────────────────────────────────── [testenv:dev] -description = Start dashboard with auto-reload (local development) +description = Full dev environment — docker compose + auto-reload + dev shell commands = - uvicorn dashboard.app:app --reload --host 0.0.0.0 --port 8000 \ - --reload-exclude ".claude" + bash scripts/dev_server.sh # ── All Tests (parallel) ─────────────────────────────────────────────────────