#!/usr/bin/env bash # Shared helpers for Research-Link-AI dev scripts. Source this file. set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" RUNTIME="$ROOT/.runtime" mkdir -p "$RUNTIME" # Load .env if present (export all keys). if [ -f "$ROOT/.env" ]; then set -a; . "$ROOT/.env"; set +a fi # detect_ip → the machine's primary LAN IPv4 (falls back to 127.0.0.1) detect_ip() { local ip ip="$(python - <<'PY' 2>/dev/null import socket s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) try: s.connect(("8.8.8.8", 80)); print(s.getsockname()[0]) except Exception: print("") finally: s.close() PY )" if [ -z "$ip" ] && command -v hostname >/dev/null 2>&1; then ip="$(hostname -I 2>/dev/null | awk '{print $1}')" fi echo "${ip:-127.0.0.1}" } # Bind on all interfaces so the server is reachable from other machines. BIND_HOST="${RESEARCHLINK_BIND_HOST:-0.0.0.0}" BACKEND_PORT="${RESEARCHLINK_BACKEND_PORT:-8000}" FRONTEND_PORT="${RESEARCHLINK_FRONTEND_PORT:-5173}" LLM_MODE="${RESEARCHLINK_LLM_MODE:-offline}" LLM_MODEL="${RESEARCHLINK_LLM_MODEL:-researchlink-local}" # Advertised host shown in URLs: explicit override → auto-detected LAN IP. if [ -n "${RESEARCHLINK_PUBLIC_HOST:-}" ]; then ADV_HOST="$RESEARCHLINK_PUBLIC_HOST" elif [ "$BIND_HOST" = "127.0.0.1" ] || [ "$BIND_HOST" = "localhost" ]; then ADV_HOST="127.0.0.1" else ADV_HOST="$(detect_ip)" fi BACKEND_URL="http://${ADV_HOST}:${BACKEND_PORT}" # for the browser HEALTH_URL="http://127.0.0.1:${BACKEND_PORT}" # for local health checks FRONTEND_HOST="${RESEARCHLINK_FRONTEND_HOST:-0.0.0.0}" FRONTEND_DIR="$ROOT/frontend" # optional Node frontend (auto-detected) BACKEND_PID="$RUNTIME/backend.pid" FRONTEND_PID="$RUNTIME/frontend.pid" BACKEND_LOG="$RUNTIME/backend.log" FRONTEND_LOG="$RUNTIME/frontend.log" c_green() { printf '\033[32m%s\033[0m\n' "$1"; } c_red() { printf '\033[31m%s\033[0m\n' "$1"; } c_yellow(){ printf '\033[33m%s\033[0m\n' "$1"; } c_dim() { printf '\033[2m%s\033[0m\n' "$1"; } # pid_alive → 0 if the recorded PID is running pid_alive() { local f="$1" [ -f "$f" ] || return 1 local pid; pid="$(cat "$f" 2>/dev/null || true)" [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null } # port_in_use → 0 if something is listening port_in_use() { local host="$1" port="$2" if command -v lsof >/dev/null 2>&1; then lsof -iTCP:"$port" -sTCP:LISTEN -n >/dev/null 2>&1 && return 0 || return 1 fi (exec 3<>"/dev/tcp/${host}/${port}") 2>/dev/null && { exec 3>&- 3<&- || true; return 0; } || return 1 } # key_configured — echoes "yes"/"no"/"n/a" for the current LLM_MODE key_configured() { case "$LLM_MODE" in anthropic) [ -n "${ANTHROPIC_API_KEY:-}" ] && echo yes || echo no ;; openai) [ -n "${OPENAI_API_KEY:-}" ] && echo yes || echo no ;; openrouter) [ -n "${OPENROUTER_API_KEY:-}" ] && echo yes || echo no ;; *) echo "n/a" ;; esac } is_external_mode() { case "$LLM_MODE" in offline) return 1 ;; *) return 0 ;; esac } # http_ok → 0 if the URL returns a 2xx within the timeout http_ok() { python - "$1" <<'PY' 2>/dev/null import sys, urllib.request try: urllib.request.urlopen(sys.argv[1], timeout=2).read() except Exception: sys.exit(1) PY } # wait_health → 0 once healthy, 1 if it never comes up wait_health() { local url="$1" tries="${2:-40}" for _ in $(seq 1 "$tries"); do http_ok "$url" && return 0 sleep 0.5 done return 1 }