#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" TMP_DIR="$(cd /tmp && pwd -P)" BACKEND_PORT="${BACKEND_PORT:-3000}" FRONTEND_PORT="${FRONTEND_PORT:-5173}" FRONTEND_HOST="${FRONTEND_HOST:-127.0.0.1}" BACKEND_BIN="${TMP_DIR}/inbox-host" BACKEND_BUILD_TARGET="./cmd/inbox" BACKEND_LOG="${TMP_DIR}/ai-workflow-v2-inbox.log" FRONTEND_LOG="${TMP_DIR}/ai-workflow-v2-dashboard.log" FRONTEND_BIN="${REPO_ROOT}/dashboard/node_modules/.bin/vite" GO_BUILD_CACHE="${TMP_DIR}/ai-workflow-v2-go-build" WORKSPACES_DIR="${REPO_ROOT}/inbox-worktrees" require_cmd() { if ! command -v "$1" >/dev/null 2>&1; then echo "error: missing required command: $1" >&2 exit 1 fi } start_detached() { local cwd="$1" local log_file="$2" shift 2 LAUNCH_CWD="$cwd" LAUNCH_LOG="$log_file" python3 - "$@" <<'PY' import os import subprocess import sys cwd = os.environ["LAUNCH_CWD"] log_path = os.environ["LAUNCH_LOG"] argv = sys.argv[1:] with open(log_path, "ab", buffering=0) as log: proc = subprocess.Popen( argv, cwd=cwd, env=os.environ.copy(), stdin=subprocess.DEVNULL, stdout=log, stderr=subprocess.STDOUT, start_new_session=True, ) print(proc.pid) PY } listening_pid() { local port="$1" lsof -tiTCP:"${port}" -sTCP:LISTEN 2>/dev/null | head -n 1 || true } cwd_for_pid() { local pid="$1" lsof -a -p "$pid" -d cwd -Fn 2>/dev/null | sed -n 's/^n//p' | head -n 1 } txt_for_pid() { local pid="$1" lsof -a -p "$pid" -d txt -Fn 2>/dev/null | sed -n 's/^n//p' | head -n 1 } wait_for_port_to_close() { local port="$1" for _ in $(seq 1 40); do if [ -z "$(listening_pid "$port")" ]; then return 0 fi sleep 0.25 done return 1 } process_exists() { local pid="$1" lsof -p "$pid" >/dev/null 2>&1 } send_signal() { local signal="$1" local pid="$2" local label="$3" local port="$4" local err="" if ! err="$(kill "$signal" "$pid" 2>&1)"; then if [ -z "$(listening_pid "$port")" ]; then return 0 fi echo "error: failed to send ${signal} to ${label} (pid ${pid}): ${err}" >&2 exit 1 fi } stop_known_process() { local pid="$1" local label="$2" local port="$3" echo "stopping ${label} (pid ${pid})..." send_signal "-TERM" "$pid" "$label" "$port" if wait_for_port_to_close "$port"; then return 0 fi echo "${label} did not exit in time, forcing stop..." send_signal "-KILL" "$pid" "$label" "$port" if wait_for_port_to_close "$port"; then return 0 fi echo "error: ${label} is still listening on port ${port} after SIGKILL." >&2 exit 1 } ensure_backend_port_ready() { local pid pid="$(listening_pid "$BACKEND_PORT")" if [ -z "$pid" ]; then return 0 fi local cwd local txt cwd="$(cwd_for_pid "$pid")" txt="$(txt_for_pid "$pid")" if [ "$cwd" = "$REPO_ROOT" ] && [ "$txt" = "$BACKEND_BIN" ]; then stop_known_process "$pid" "backend" "$BACKEND_PORT" return 0 fi echo "error: port ${BACKEND_PORT} is occupied by another process." >&2 echo "pid: ${pid}" >&2 echo "cwd: ${cwd}" >&2 echo "exe: ${txt}" >&2 exit 1 } ensure_frontend_port_ready() { local pid pid="$(listening_pid "$FRONTEND_PORT")" if [ -z "$pid" ]; then return 0 fi local cwd cwd="$(cwd_for_pid "$pid")" if [ "$cwd" = "${REPO_ROOT}/dashboard" ]; then stop_known_process "$pid" "frontend" "$FRONTEND_PORT" return 0 fi local txt txt="$(txt_for_pid "$pid")" echo "error: port ${FRONTEND_PORT} is occupied by another process." >&2 echo "pid: ${pid}" >&2 echo "cwd: ${cwd}" >&2 echo "exe: ${txt}" >&2 exit 1 } wait_for_http() { local url="$1" local label="$2" local log_file="$3" local pid="$4" for _ in $(seq 1 60); do if curl -fsS "$url" >/dev/null 2>&1; then return 0 fi if ! process_exists "$pid"; then echo "error: ${label} exited before becoming ready (pid ${pid})." >&2 if [ -f "$log_file" ]; then echo "--- ${label} log tail ---" >&2 tail -n 40 "$log_file" >&2 || true fi exit 1 fi sleep 0.5 done echo "error: ${label} failed to become ready: ${url}" >&2 if [ -f "$log_file" ]; then echo "--- ${label} log tail ---" >&2 tail -n 40 "$log_file" >&2 || true fi exit 1 } start_backend() { : > "$BACKEND_LOG" start_detached "$REPO_ROOT" "$BACKEND_LOG" \ "$BACKEND_BIN" server \ --workspaces-dir "$WORKSPACES_DIR" \ --port "$BACKEND_PORT" } start_frontend() { : > "$FRONTEND_LOG" start_detached "${REPO_ROOT}/dashboard" "$FRONTEND_LOG" \ "$FRONTEND_BIN" --host "$FRONTEND_HOST" --port "$FRONTEND_PORT" } backend_pid_summary() { listening_pid "$BACKEND_PORT" } frontend_pid_summary() { listening_pid "$FRONTEND_PORT" } main() { require_cmd go require_cmd lsof require_cmd curl require_cmd python3 if [ ! -x "$FRONTEND_BIN" ]; then echo "error: missing dashboard dependencies at ${FRONTEND_BIN}" >&2 echo "run: cd ${REPO_ROOT}/dashboard && npm install" >&2 exit 1 fi echo "building backend binary..." mkdir -p "$GO_BUILD_CACHE" mkdir -p "$WORKSPACES_DIR" ( cd "${REPO_ROOT}/inbox" GOCACHE="$GO_BUILD_CACHE" go build -o "$BACKEND_BIN" "$BACKEND_BUILD_TARGET" ) if [ ! -x "$BACKEND_BIN" ]; then echo "error: built backend binary is not executable: ${BACKEND_BIN}" >&2 file "$BACKEND_BIN" >&2 || true exit 1 fi ensure_backend_port_ready ensure_frontend_port_ready echo "starting backend on http://127.0.0.1:${BACKEND_PORT} ..." local backend_pid backend_pid="$(start_backend)" wait_for_http "http://127.0.0.1:${BACKEND_PORT}/api/v2/workspaces" "backend" "$BACKEND_LOG" "$backend_pid" echo "starting frontend on http://${FRONTEND_HOST}:${FRONTEND_PORT} ..." local frontend_pid frontend_pid="$(start_frontend)" wait_for_http "http://${FRONTEND_HOST}:${FRONTEND_PORT}" "frontend" "$FRONTEND_LOG" "$frontend_pid" echo "" echo "project restarted" echo "backend: http://127.0.0.1:${BACKEND_PORT} (pid $(backend_pid_summary))" echo "frontend: http://${FRONTEND_HOST}:${FRONTEND_PORT} (pid $(frontend_pid_summary))" echo "logs:" echo " ${BACKEND_LOG}" echo " ${FRONTEND_LOG}" } main "$@"