fix: enhance tox -e dev with full dev environment launcher
All checks were successful
Tests / lint (pull_request) Successful in 4s
Tests / test (pull_request) Successful in 1m0s

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:
kimi
2026-03-19 01:08:18 -04:00
parent da43421d4e
commit fcccfed1d4
2 changed files with 193 additions and 3 deletions

191
scripts/dev_server.sh Executable file
View 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
"

View File

@@ -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) ─────────────────────────────────────────────────────