#!/usr/bin/env python3 """Timmy Time — Development server launcher. Satisfies tox -e dev criteria: - Graceful port selection (finds next free port if default is taken) - Clickable links to dashboard and other web GUIs - Status line: backend inference source, version, git commit, smoke tests - Auto-reload on code changes (delegates to uvicorn --reload) Usage: python scripts/dev_server.py [--port PORT] """ import argparse import datetime import os import socket import subprocess import sys DEFAULT_PORT = 8000 MAX_PORT_ATTEMPTS = 10 OLLAMA_DEFAULT = "http://localhost:11434" def _port_free(port: int) -> bool: """Return True if the TCP port is available on localhost.""" with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: try: s.bind(("0.0.0.0", port)) return True except OSError: return False def _find_port(start: int) -> int: """Return *start* if free, otherwise probe up to MAX_PORT_ATTEMPTS higher.""" for offset in range(MAX_PORT_ATTEMPTS): candidate = start + offset if _port_free(candidate): return candidate raise RuntimeError( f"No free port found in range {start}–{start + MAX_PORT_ATTEMPTS - 1}" ) def _git_info() -> str: """Return short commit hash + timestamp, or 'unknown'.""" try: sha = subprocess.check_output( ["git", "rev-parse", "--short", "HEAD"], stderr=subprocess.DEVNULL, text=True, ).strip() ts = subprocess.check_output( ["git", "log", "-1", "--format=%ci"], stderr=subprocess.DEVNULL, text=True, ).strip() return f"{sha} ({ts})" except Exception: return "unknown" def _project_version() -> str: """Read version from pyproject.toml without importing toml libs.""" pyproject = os.path.join(os.path.dirname(__file__), "..", "pyproject.toml") try: with open(pyproject) as f: for line in f: if line.strip().startswith("version"): # version = "1.0.0" return line.split("=", 1)[1].strip().strip('"').strip("'") except Exception: pass return "unknown" def _ollama_url() -> str: return os.environ.get("OLLAMA_URL", OLLAMA_DEFAULT) def _smoke_ollama(url: str) -> str: """Quick connectivity check against Ollama.""" import urllib.request import urllib.error try: req = urllib.request.Request(url, method="GET") with urllib.request.urlopen(req, timeout=3): return "ok" except Exception: return "unreachable" def _print_banner(port: int) -> None: version = _project_version() git = _git_info() ollama_url = _ollama_url() ollama_status = _smoke_ollama(ollama_url) hr = "─" * 62 print(flush=True) print(f" {hr}") print(f" ┃ Timmy Time — Development Server") print(f" {hr}") print() print(f" Dashboard: http://localhost:{port}") print(f" API docs: http://localhost:{port}/docs") print(f" Health: http://localhost:{port}/health") print() print(f" ── Status ──────────────────────────────────────────────") print(f" Backend: {ollama_url} [{ollama_status}]") print(f" Version: {version}") print(f" Git commit: {git}") print(f" {hr}") print(flush=True) def main() -> None: parser = argparse.ArgumentParser(description="Timmy dev server") parser.add_argument( "--port", type=int, default=DEFAULT_PORT, help=f"Preferred port (default: {DEFAULT_PORT})", ) args = parser.parse_args() port = _find_port(args.port) if port != args.port: print(f" ⚠ Port {args.port} in use — using {port} instead") _print_banner(port) # Set PYTHONPATH so `timmy` CLI inside the tox venv resolves to this source. src_dir = os.path.join(os.path.dirname(__file__), "..", "src") os.environ["PYTHONPATH"] = os.path.abspath(src_dir) # Launch uvicorn with auto-reload cmd = [ sys.executable, "-m", "uvicorn", "dashboard.app:app", "--reload", "--host", "0.0.0.0", "--port", str(port), "--reload-dir", os.path.abspath(src_dir), "--reload-include", "*.html", "--reload-include", "*.css", "--reload-include", "*.js", "--reload-exclude", ".claude", ] try: subprocess.run(cmd, check=True) except KeyboardInterrupt: print("\n Shutting down dev server.") if __name__ == "__main__": main()