fix: enhance tox -e dev with full dev environment launcher
Replaces the bare uvicorn command with a comprehensive dev launcher script that provides: auto port selection if 8000 is taken, docker compose isolation, clickable service links (dashboard, health, API docs, Ollama), a status line showing version/git commit/inference backend, smoke tests on startup, and a dev shell with the timmy CLI wired to the running instance. Fixes #385 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
191
scripts/dev_server.sh
Executable file
191
scripts/dev_server.sh
Executable file
@@ -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" <<EOF
|
||||
services:
|
||||
dashboard:
|
||||
ports:
|
||||
- "${PORT}:8000"
|
||||
EOF
|
||||
docker compose \
|
||||
-f docker-compose.yml \
|
||||
-f docker-compose.dev.yml \
|
||||
-f "$OVERRIDE_FILE" \
|
||||
-p "timmy-dev" \
|
||||
up -d --build \
|
||||
--remove-orphans 2>&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
|
||||
"
|
||||
5
tox.ini
5
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) ─────────────────────────────────────────────────────
|
||||
|
||||
|
||||
Reference in New Issue
Block a user