start-server.sh 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. #!/usr/bin/env bash
  2. # Start the brainstorm server and output connection info
  3. # Usage: start-server.sh [--project-dir <path>] [--host <bind-host>] [--url-host <display-host>] [--foreground] [--background]
  4. #
  5. # Starts server on a random high port, outputs JSON with URL.
  6. # Each session gets its own directory to avoid conflicts.
  7. #
  8. # Options:
  9. # --project-dir <path> Store session files under <path>/.superpowers/brainstorm/
  10. # instead of /tmp. Files persist after server stops.
  11. # --host <bind-host> Host/interface to bind (default: 127.0.0.1).
  12. # Use 0.0.0.0 in remote/containerized environments.
  13. # --url-host <host> Hostname shown in returned URL JSON.
  14. # --idle-timeout-minutes <n> Shut down after n minutes idle (default 240 = 4h).
  15. # --open Auto-open the browser on the first screen (use only
  16. # after the user approves the visual companion).
  17. # --foreground Run server in the current terminal (no backgrounding).
  18. # --background Force background mode (overrides Codex auto-foreground).
  19. SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
  20. # Parse arguments
  21. PROJECT_DIR=""
  22. FOREGROUND="false"
  23. FORCE_BACKGROUND="false"
  24. BIND_HOST="127.0.0.1"
  25. URL_HOST=""
  26. IDLE_TIMEOUT_MINUTES=""
  27. while [[ $# -gt 0 ]]; do
  28. case "$1" in
  29. --project-dir)
  30. PROJECT_DIR="$2"
  31. shift 2
  32. ;;
  33. --host)
  34. BIND_HOST="$2"
  35. shift 2
  36. ;;
  37. --url-host)
  38. URL_HOST="$2"
  39. shift 2
  40. ;;
  41. --idle-timeout-minutes)
  42. IDLE_TIMEOUT_MINUTES="$2"
  43. shift 2
  44. ;;
  45. --open)
  46. export BRAINSTORM_OPEN=1
  47. shift
  48. ;;
  49. --foreground|--no-daemon)
  50. FOREGROUND="true"
  51. shift
  52. ;;
  53. --background|--daemon)
  54. FORCE_BACKGROUND="true"
  55. shift
  56. ;;
  57. *)
  58. echo "{\"error\": \"Unknown argument: $1\"}"
  59. exit 1
  60. ;;
  61. esac
  62. done
  63. if [[ -z "$URL_HOST" ]]; then
  64. if [[ "$BIND_HOST" == "127.0.0.1" || "$BIND_HOST" == "localhost" ]]; then
  65. URL_HOST="localhost"
  66. else
  67. URL_HOST="$BIND_HOST"
  68. fi
  69. fi
  70. if [[ -n "$IDLE_TIMEOUT_MINUTES" ]]; then
  71. if ! [[ "$IDLE_TIMEOUT_MINUTES" =~ ^[0-9]+$ ]] || [[ "$IDLE_TIMEOUT_MINUTES" -lt 1 ]]; then
  72. echo "{\"error\": \"--idle-timeout-minutes must be a positive integer\"}"
  73. exit 1
  74. fi
  75. export BRAINSTORM_IDLE_TIMEOUT_MS=$(( IDLE_TIMEOUT_MINUTES * 60 * 1000 ))
  76. fi
  77. is_windows_like_shell() {
  78. case "${OSTYPE:-}" in
  79. msys*|cygwin*|mingw*) return 0 ;;
  80. esac
  81. if [[ -n "${MSYSTEM:-}" ]]; then
  82. return 0
  83. fi
  84. local uname_s
  85. uname_s="$(uname -s 2>/dev/null || true)"
  86. case "$uname_s" in
  87. MSYS*|MINGW*|CYGWIN*) return 0 ;;
  88. esac
  89. return 1
  90. }
  91. # Some environments reap detached/background processes. Auto-foreground when detected.
  92. if [[ -n "${CODEX_CI:-}" && "$FOREGROUND" != "true" && "$FORCE_BACKGROUND" != "true" ]]; then
  93. FOREGROUND="true"
  94. fi
  95. # Windows/Git Bash reaps nohup background processes. Auto-foreground when detected.
  96. if [[ "$FOREGROUND" != "true" && "$FORCE_BACKGROUND" != "true" ]]; then
  97. if is_windows_like_shell; then
  98. FOREGROUND="true"
  99. fi
  100. fi
  101. # Session files (server.log, server-info, .last-token) embed the session key —
  102. # keep everything this script and the server create owner-only.
  103. umask 077
  104. # Generate unique session directory
  105. SESSION_ID="$$-$(date +%s)"
  106. if [[ -n "$PROJECT_DIR" ]]; then
  107. SESSION_DIR="${PROJECT_DIR}/.superpowers/brainstorm/${SESSION_ID}"
  108. # Persist the bound port and key per project so a restart reuses them and an
  109. # already-open browser tab reconnects to the same URL with a valid cookie.
  110. export BRAINSTORM_PORT_FILE="${PROJECT_DIR}/.superpowers/brainstorm/.last-port"
  111. export BRAINSTORM_TOKEN_FILE="${PROJECT_DIR}/.superpowers/brainstorm/.last-token"
  112. else
  113. SESSION_DIR="/tmp/brainstorm-${SESSION_ID}"
  114. fi
  115. STATE_DIR="${SESSION_DIR}/state"
  116. PID_FILE="${STATE_DIR}/server.pid"
  117. LOG_FILE="${STATE_DIR}/server.log"
  118. SERVER_ID_FILE="${STATE_DIR}/server-instance-id"
  119. # Create fresh session directory with content and state peers
  120. mkdir -p "${SESSION_DIR}/content" "$STATE_DIR"
  121. SERVER_ID=""
  122. if [[ -r /dev/urandom ]]; then
  123. SERVER_ID="$(od -An -N24 -tx1 /dev/urandom 2>/dev/null | tr -d ' \n' || true)"
  124. fi
  125. if ! [[ "$SERVER_ID" =~ ^[A-Za-z0-9_-]{32,64}$ ]]; then
  126. SERVER_ID="$(printf '%08x%08x%08x%08x' "$$" "$(date +%s)" "${RANDOM:-0}" "${RANDOM:-0}")"
  127. fi
  128. printf '%s\n' "$SERVER_ID" > "$SERVER_ID_FILE"
  129. chmod 600 "$SERVER_ID_FILE" 2>/dev/null || true
  130. # Kill any existing server
  131. if [[ -f "$PID_FILE" ]]; then
  132. old_pid=$(cat "$PID_FILE")
  133. kill "$old_pid" 2>/dev/null
  134. rm -f "$PID_FILE"
  135. fi
  136. cd "$SCRIPT_DIR" || exit 1
  137. # Resolve the harness PID (grandparent of this script).
  138. # $PPID is the ephemeral shell the harness spawned to run us — it dies
  139. # when this script exits. The harness itself is $PPID's parent.
  140. OWNER_PID="$(ps -o ppid= -p "$PPID" 2>/dev/null | tr -d ' ')"
  141. if [[ -z "$OWNER_PID" || "$OWNER_PID" == "1" ]]; then
  142. OWNER_PID="$PPID"
  143. fi
  144. # Windows/MSYS2: Node.js cannot see POSIX PIDs from the MSYS2 namespace.
  145. # Passing a PID node cannot verify causes server to log owner-pid-invalid
  146. # and self-terminate at the 60-second lifecycle check. Clear it so the
  147. # watchdog is disabled and the idle timeout becomes the only shutdown trigger.
  148. if is_windows_like_shell; then
  149. OWNER_PID=""
  150. fi
  151. # Foreground mode for environments that reap detached/background processes.
  152. if [[ "$FOREGROUND" == "true" ]]; then
  153. env BRAINSTORM_DIR="$SESSION_DIR" BRAINSTORM_HOST="$BIND_HOST" BRAINSTORM_URL_HOST="$URL_HOST" BRAINSTORM_OWNER_PID="$OWNER_PID" node server.cjs "--brainstorm-server-id=$SERVER_ID" &
  154. SERVER_PID=$!
  155. echo "$SERVER_PID" > "$PID_FILE"
  156. wait "$SERVER_PID"
  157. exit $?
  158. fi
  159. # Start server, capturing output to log file
  160. # Use nohup to survive shell exit; disown to remove from job table
  161. nohup env BRAINSTORM_DIR="$SESSION_DIR" BRAINSTORM_HOST="$BIND_HOST" BRAINSTORM_URL_HOST="$URL_HOST" BRAINSTORM_OWNER_PID="$OWNER_PID" node server.cjs "--brainstorm-server-id=$SERVER_ID" > "$LOG_FILE" 2>&1 &
  162. SERVER_PID=$!
  163. disown "$SERVER_PID" 2>/dev/null
  164. echo "$SERVER_PID" > "$PID_FILE"
  165. # Wait for server-started message (check log file)
  166. for _ in {1..50}; do
  167. if grep -q "server-started" "$LOG_FILE" 2>/dev/null; then
  168. # Verify server is still alive after a short window (catches process reapers)
  169. alive="true"
  170. for _ in {1..20}; do
  171. if ! kill -0 "$SERVER_PID" 2>/dev/null; then
  172. alive="false"
  173. break
  174. fi
  175. sleep 0.1
  176. done
  177. if [[ "$alive" != "true" ]]; then
  178. echo "{\"error\": \"Server started but was killed. Retry in a persistent terminal with: $SCRIPT_DIR/start-server.sh${PROJECT_DIR:+ --project-dir $PROJECT_DIR} --host $BIND_HOST --url-host $URL_HOST --foreground\"}"
  179. exit 1
  180. fi
  181. grep "server-started" "$LOG_FILE" | head -1
  182. exit 0
  183. fi
  184. sleep 0.1
  185. done
  186. # Timeout - server didn't start
  187. echo '{"error": "Server failed to start within 5 seconds"}'
  188. exit 1