#!/bin/bash # Automaker TUI Launcher - Interactive menu for launching Automaker in different modes # Supports: Web Browser, Desktop (Electron), Docker Dev, Electron + Docker API # Platforms: Linux, macOS, Windows (Git Bash, WSL, MSYS2, Cygwin) # Features: Terminal responsiveness, history, pre-flight checks, port management set -e # ============================================================================ # CONFIGURATION & CONSTANTS # ============================================================================ APP_NAME="Automaker" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" HISTORY_FILE="${HOME}/.automaker_launcher_history" MIN_TERM_WIDTH=70 MIN_TERM_HEIGHT=20 MENU_BOX_WIDTH=66 MENU_INNER_WIDTH=64 LOGO_WIDTH=52 INPUT_TIMEOUT=30 SELECTED_OPTION=1 MAX_OPTIONS=4 # Platform detection (set early for cross-platform compatibility) IS_WINDOWS=false IS_MACOS=false if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" || "$OSTYPE" == "mingw"* ]]; then IS_WINDOWS=true elif [[ "$OSTYPE" == "darwin"* ]]; then IS_MACOS=true fi # Port configuration DEFAULT_WEB_PORT=3007 DEFAULT_SERVER_PORT=3008 WEB_PORT=$DEFAULT_WEB_PORT SERVER_PORT=$DEFAULT_SERVER_PORT # Extract VERSION from apps/ui/package.json (the actual app version, not monorepo version) if command -v node &> /dev/null; then VERSION="v$(node -p "require('$SCRIPT_DIR/apps/ui/package.json').version" 2>/dev/null || echo "0.11.0")" else VERSION=$(grep '"version"' "$SCRIPT_DIR/apps/ui/package.json" 2>/dev/null | head -1 | sed 's/.*"version"[^"]*"\([^"]*\)".*/v\1/' || echo "v0.11.0") fi # ANSI Color codes (256-color palette) ESC=$(printf '\033') RESET="${ESC}[0m" BOLD="${ESC}[1m" DIM="${ESC}[2m" C_PRI="${ESC}[38;5;51m" # Primary cyan C_SEC="${ESC}[38;5;39m" # Secondary blue C_ACC="${ESC}[38;5;33m" # Accent darker blue C_GREEN="${ESC}[38;5;118m" # Green C_RED="${ESC}[38;5;196m" # Red C_YELLOW="${ESC}[38;5;226m" # Yellow C_GRAY="${ESC}[38;5;240m" # Dark gray C_WHITE="${ESC}[38;5;255m" # White C_MUTE="${ESC}[38;5;248m" # Muted gray # ============================================================================ # ARGUMENT PARSING # ============================================================================ MODE="" USE_COLORS=true CHECK_DEPS=false NO_HISTORY=false PRODUCTION_MODE=false show_help() { cat << 'EOF' Automaker TUI Launcher - Interactive development environment starter USAGE: start-automaker.sh [MODE] [OPTIONS] MODES: web Launch in web browser (localhost:3007) electron Launch as desktop app (Electron) docker Launch in Docker container (dev with live reload) docker-electron Launch Electron with Docker API backend OPTIONS: --help Show this help message --version Show version information --no-colors Disable colored output --check-deps Check dependencies before launching --no-history Don't remember last choice --production Run in production mode (builds first, faster React) EXAMPLES: start-automaker.sh # Interactive menu (development) start-automaker.sh --production # Interactive menu (production) start-automaker.sh web # Launch web mode directly (dev) start-automaker.sh web --production # Launch web mode (production) start-automaker.sh electron # Launch desktop app directly start-automaker.sh docker # Launch Docker dev container start-automaker.sh --version # Show version KEYBOARD SHORTCUTS (in menu): Up/Down arrows Navigate between options Enter Select highlighted option 1-4 Jump to and select mode Q Exit HISTORY: Your last selected mode is remembered in: ~/.automaker_launcher_history Use --no-history to disable this feature PLATFORMS: Linux, macOS, Windows (Git Bash, WSL, MSYS2, Cygwin) EOF } show_version() { echo "Automaker Launcher $VERSION" echo "Node.js: $(node -v 2>/dev/null || echo 'not installed')" echo "Bash: ${BASH_VERSION%.*}" } parse_args() { while [[ $# -gt 0 ]]; do case "$1" in --help) show_help exit 0 ;; --version) show_version exit 0 ;; --no-colors) USE_COLORS=false RESET="" C_PRI="" C_SEC="" C_ACC="" C_GREEN="" C_RED="" C_YELLOW="" C_GRAY="" C_WHITE="" C_MUTE="" ;; --check-deps) CHECK_DEPS=true ;; --no-history) NO_HISTORY=true ;; --production) PRODUCTION_MODE=true ;; web|electron|docker|docker-electron) MODE="$1" ;; *) echo "Unknown option: $1" >&2 echo "Use --help for usage information" >&2 exit 1 ;; esac shift done } # ============================================================================ # PRE-FLIGHT CHECKS # ============================================================================ check_platform() { # Platform already detected at script start # This function is kept for any additional platform-specific checks if [ "$IS_WINDOWS" = true ]; then # Check if running in a proper terminal if [ -z "$TERM" ]; then echo "${C_YELLOW}Warning:${RESET} Running on Windows without proper terminal." echo "For best experience, use Git Bash, WSL, or Windows Terminal." fi fi } check_required_commands() { local missing=() # Check for required commands for cmd in node npm tput; do if ! command -v "$cmd" &> /dev/null; then missing+=("$cmd") fi done if [ ${#missing[@]} -gt 0 ]; then echo "${C_RED}Error:${RESET} Missing required commands: ${missing[*]}" echo "" echo "Please install:" for cmd in "${missing[@]}"; do case "$cmd" in node|npm) echo " - Node.js (includes npm) from https://nodejs.org/" ;; tput) echo " - ncurses package (usually pre-installed on Unix systems)" ;; esac done exit 1 fi } DOCKER_CMD="docker" check_docker() { if ! command -v docker &> /dev/null; then echo "${C_RED}Error:${RESET} Docker is not installed or not in PATH" echo "Please install Docker from https://docs.docker.com/get-docker/" return 1 fi if ! docker info &> /dev/null 2>&1; then if sg docker -c "docker info" &> /dev/null 2>&1; then DOCKER_CMD="sg docker -c" else echo "${C_RED}Error:${RESET} Docker daemon is not running or not accessible" echo "" echo "To fix, run:" echo " sudo usermod -aG docker \$USER" echo "" echo "Then either log out and back in, or run:" echo " newgrp docker" return 1 fi fi export DOCKER_CMD return 0 } check_running_electron() { local electron_pids="" if [ "$IS_WINDOWS" = true ]; then # Windows: look for electron.exe or Automaker.exe electron_pids=$(tasklist 2>/dev/null | grep -iE "electron|automaker" | awk '{print $2}' | tr '\n' ' ' || true) else # Unix: look for electron or Automaker processes electron_pids=$(pgrep -f "electron.*automaker|Automaker" 2>/dev/null | tr '\n' ' ' || true) fi if [ -n "$electron_pids" ] && [ "$electron_pids" != " " ]; then get_term_size echo "" center_print "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" "$C_GRAY" center_print "Running Electron App Detected" "$C_YELLOW" center_print "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" "$C_GRAY" echo "" center_print "Electron process(es): $electron_pids" "$C_MUTE" echo "" center_print "What would you like to do?" "$C_WHITE" echo "" center_print "[K] Kill Electron and continue" "$C_GREEN" center_print "[I] Ignore and continue anyway" "$C_MUTE" center_print "[C] Cancel" "$C_RED" echo "" while true; do local choice_pad=$(( (TERM_COLS - 20) / 2 )) printf "%${choice_pad}s" "" read -r -p "Choice: " choice case "$choice" in [kK]|[kK][iI][lL][lL]) echo "" center_print "Killing Electron processes..." "$C_YELLOW" if [ "$IS_WINDOWS" = true ]; then taskkill //F //IM "electron.exe" 2>/dev/null || true taskkill //F //IM "Automaker.exe" 2>/dev/null || true else pkill -f "electron.*automaker" 2>/dev/null || true pkill -f "Automaker" 2>/dev/null || true fi sleep 1 center_print "✓ Electron stopped" "$C_GREEN" echo "" return 0 ;; [iI]|[iI][gG][nN][oO][rR][eE]) echo "" center_print "Continuing without stopping Electron..." "$C_MUTE" echo "" return 0 ;; [cC]|[cC][aA][nN][cC][eE][lL]) echo "" center_print "Cancelled." "$C_MUTE" echo "" exit 0 ;; *) center_print "Invalid choice. Please enter K, I, or C." "$C_RED" ;; esac done fi return 0 } check_running_containers() { local compose_file="$1" local running_containers="" # Get list of running automaker containers if [ "$DOCKER_CMD" = "sg docker -c" ]; then running_containers=$(sg docker -c "docker ps --filter 'name=automaker-dev' --format '{{{{Names}}}}'" 2>/dev/null | tr '\n' ' ' || true) else running_containers=$($DOCKER_CMD ps --filter "name=automaker-dev" --format "{{.Names}}" 2>/dev/null | tr '\n' ' ' || true) fi if [ -n "$running_containers" ] && [ "$running_containers" != " " ]; then get_term_size echo "" center_print "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" "$C_GRAY" center_print "Existing Containers Detected" "$C_YELLOW" center_print "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" "$C_GRAY" echo "" center_print "Running containers: $running_containers" "$C_MUTE" echo "" center_print "What would you like to do?" "$C_WHITE" echo "" center_print "[S] Stop containers and start fresh" "$C_GREEN" center_print "[R] Restart containers (rebuild)" "$C_MUTE" center_print "[A] Attach to existing containers" "$C_MUTE" center_print "[C] Cancel" "$C_RED" echo "" while true; do local choice_pad=$(( (TERM_COLS - 20) / 2 )) printf "%${choice_pad}s" "" read -r -p "Choice: " choice case "$choice" in [sS]|[sS][tT][oO][pP]) echo "" center_print "Stopping existing containers..." "$C_YELLOW" if [ "$DOCKER_CMD" = "sg docker -c" ]; then sg docker -c "docker compose -f '$compose_file' down" 2>/dev/null || true sg docker -c "docker ps --filter 'name=automaker-dev' -q" 2>/dev/null | xargs -r sg docker -c "docker stop" 2>/dev/null || true else $DOCKER_CMD compose -f "$compose_file" down 2>/dev/null || true $DOCKER_CMD ps --filter "name=automaker-dev" -q 2>/dev/null | xargs -r $DOCKER_CMD stop 2>/dev/null || true fi center_print "✓ Containers stopped" "$C_GREEN" echo "" return 0 # Continue with fresh start ;; [rR]|[rR][eE][sS][tT][aA][rR][tT]) echo "" center_print "Stopping and rebuilding containers..." "$C_YELLOW" if [ "$DOCKER_CMD" = "sg docker -c" ]; then sg docker -c "docker compose -f '$compose_file' down" 2>/dev/null || true else $DOCKER_CMD compose -f "$compose_file" down 2>/dev/null || true fi center_print "✓ Ready to rebuild" "$C_GREEN" echo "" return 0 # Continue with rebuild ;; [aA]|[aA][tT][tT][aA][cC][hH]) echo "" center_print "Attaching to existing containers..." "$C_GREEN" echo "" return 2 # Special code for attach ;; [cC]|[cC][aA][nN][cC][eE][lL]) echo "" center_print "Cancelled." "$C_MUTE" echo "" exit 0 ;; *) center_print "Invalid choice. Please enter S, R, A, or C." "$C_RED" ;; esac done fi return 0 # No containers running, continue normally } check_dependencies() { if [ "$CHECK_DEPS" = false ]; then return 0 fi echo "${C_MUTE}Checking project dependencies...${RESET}" if [ ! -d "node_modules" ]; then echo "${C_YELLOW}⚠${RESET} node_modules not found. Run 'npm install' before launching." return 1 fi if [ ! -f "package-lock.json" ]; then echo "${C_YELLOW}⚠${RESET} package-lock.json not found." fi return 0 } # ============================================================================ # PORT MANAGEMENT (Cross-platform) # ============================================================================ get_pids_on_port() { local port=$1 if [ "$IS_WINDOWS" = true ]; then # Windows: use netstat netstat -ano 2>/dev/null | grep ":$port " | grep "LISTENING" | awk '{print $5}' | sort -u | tr '\n' ' ' || true else # Unix: use lsof lsof -ti:"$port" 2>/dev/null || true fi } is_port_in_use() { local port=$1 local pids pids=$(get_pids_on_port "$port") [ -n "$pids" ] && [ "$pids" != " " ] } kill_port() { local port=$1 local pids pids=$(get_pids_on_port "$port") if [ -z "$pids" ] || [ "$pids" = " " ]; then echo "${C_GREEN}✓${RESET} Port $port is available" return 0 fi echo "${C_YELLOW}Killing process(es) on port $port: $pids${RESET}" if [ "$IS_WINDOWS" = true ]; then # Windows: use taskkill for pid in $pids; do taskkill //F //PID "$pid" 2>/dev/null || true done else # Unix: use kill echo "$pids" | xargs kill -9 2>/dev/null || true fi # Wait for port to be freed local i=0 while [ $i -lt 10 ]; do sleep 0.5 2>/dev/null || sleep 1 if ! is_port_in_use "$port"; then echo "${C_GREEN}✓${RESET} Port $port is now free" return 0 fi i=$((i + 1)) done echo "${C_RED}Warning:${RESET} Port $port may still be in use" return 1 } check_ports() { show_cursor stty echo icanon 2>/dev/null || true local web_in_use=false local server_in_use=false if is_port_in_use "$DEFAULT_WEB_PORT"; then web_in_use=true fi if is_port_in_use "$DEFAULT_SERVER_PORT"; then server_in_use=true fi if [ "$web_in_use" = true ] || [ "$server_in_use" = true ]; then echo "" if [ "$web_in_use" = true ]; then local pids pids=$(get_pids_on_port "$DEFAULT_WEB_PORT") echo "${C_YELLOW}⚠${RESET} Port $DEFAULT_WEB_PORT is in use by process(es): $pids" fi if [ "$server_in_use" = true ]; then local pids pids=$(get_pids_on_port "$DEFAULT_SERVER_PORT") echo "${C_YELLOW}⚠${RESET} Port $DEFAULT_SERVER_PORT is in use by process(es): $pids" fi echo "" while true; do read -r -p "What would you like to do? (k)ill processes, (u)se different ports, or (c)ancel: " choice case "$choice" in [kK]|[kK][iI][lL][lL]) if [ "$web_in_use" = true ]; then kill_port "$DEFAULT_WEB_PORT" else echo "${C_GREEN}✓${RESET} Port $DEFAULT_WEB_PORT is available" fi if [ "$server_in_use" = true ]; then kill_port "$DEFAULT_SERVER_PORT" else echo "${C_GREEN}✓${RESET} Port $DEFAULT_SERVER_PORT is available" fi break ;; [uU]|[uU][sS][eE]) read -r -p "Enter web port (default $DEFAULT_WEB_PORT): " input_web WEB_PORT=${input_web:-$DEFAULT_WEB_PORT} read -r -p "Enter server port (default $DEFAULT_SERVER_PORT): " input_server SERVER_PORT=${input_server:-$DEFAULT_SERVER_PORT} echo "${C_GREEN}Using ports: Web=$WEB_PORT, Server=$SERVER_PORT${RESET}" break ;; [cC]|[cC][aA][nN][cC][eE][lL]) echo "${C_MUTE}Cancelled.${RESET}" exit 0 ;; *) echo "${C_RED}Invalid choice. Please enter k, u, or c.${RESET}" ;; esac done echo "" else echo "${C_GREEN}✓${RESET} Port $DEFAULT_WEB_PORT is available" echo "${C_GREEN}✓${RESET} Port $DEFAULT_SERVER_PORT is available" fi hide_cursor stty -echo -icanon 2>/dev/null || true } validate_terminal_size() { if [ "$USE_COLORS" = false ]; then return 0 fi local term_width term_height term_width=$(tput cols 2>/dev/null || echo 80) term_height=$(tput lines 2>/dev/null || echo 24) if [ "$term_width" -lt "$MIN_TERM_WIDTH" ] || [ "$term_height" -lt "$MIN_TERM_HEIGHT" ]; then echo "${C_YELLOW}⚠${RESET} Terminal size ${term_width}x${term_height} is smaller than recommended ${MIN_TERM_WIDTH}x${MIN_TERM_HEIGHT}" echo " Some elements may not display correctly." echo "" return 1 fi } # ============================================================================ # CURSOR & CLEANUP # ============================================================================ hide_cursor() { [ "$USE_COLORS" = true ] && printf "${ESC}[?25l" } show_cursor() { [ "$USE_COLORS" = true ] && printf "${ESC}[?25h" } cleanup() { show_cursor # Restore terminal settings (echo and canonical mode) stty echo icanon 2>/dev/null || true # Kill server process if running in production mode if [ -n "${SERVER_PID:-}" ]; then kill $SERVER_PID 2>/dev/null || true fi printf "${RESET}\n" } trap cleanup EXIT INT TERM # ============================================================================ # TERMINAL SIZE & UI UTILITIES # ============================================================================ get_term_size() { TERM_COLS=$(tput cols 2>/dev/null || echo 80) TERM_LINES=$(tput lines 2>/dev/null || echo 24) } center_text() { local text="$1" local len=${#text} local pad=$(( (TERM_COLS - len) / 2 )) printf "%${pad}s%s\n" "" "$text" } draw_line() { local char="${1:-─}" local color="${2:-$C_GRAY}" local width="${3:-58}" printf "${color}" for ((i=0; i/dev/null || echo "") if [ -n "$last_mode" ]; then local hint_text="(Last: $last_mode)" local h_pad=$(( (TERM_COLS - ${#hint_text}) / 2 )) printf "%${h_pad}s" "" echo -e "${DIM}${hint_text}${RESET}" fi fi } # ============================================================================ # SPINNER & INITIALIZATION # ============================================================================ spinner() { local text="$1" local -a frames=('⠋' '⠙' '⠹' '⠸' '⠼' '⠴' '⠦' '⠧' '⠇' '⠏') local i=0 local count=0 local max_frames=20 # Max 2 seconds # Ensure TERM_COLS is set [ -z "$TERM_COLS" ] && TERM_COLS=80 while [ $count -lt $max_frames ]; do local len=${#text} local pad_left=$(( (TERM_COLS - len - 4) / 2 )) [ $pad_left -lt 0 ] && pad_left=0 printf "\r%${pad_left}s${C_PRI}${frames[$i]}${RESET} ${C_WHITE}%s${RESET}" "" "$text" i=$(( (i + 1) % ${#frames[@]} )) count=$((count + 1)) sleep 0.1 2>/dev/null || sleep 1 done local len=${#text} local pad_left=$(( (TERM_COLS - len - 4) / 2 )) [ $pad_left -lt 0 ] && pad_left=0 printf "\r%${pad_left}s${C_GREEN}✓${RESET} ${C_WHITE}%s${RESET} \n" "" "$text" } center_print() { local text="$1" local color="${2:-}" local len=${#text} local pad=$(( (TERM_COLS - len) / 2 )) [ $pad -lt 0 ] && pad=0 printf "%${pad}s${color}%s${RESET}\n" "" "$text" } resolve_port_conflicts() { # Ensure terminal is in proper state for input show_cursor stty echo icanon 2>/dev/null || true local web_in_use=false local server_in_use=false local web_pids="" local server_pids="" if is_port_in_use "$DEFAULT_WEB_PORT"; then web_in_use=true web_pids=$(get_pids_on_port "$DEFAULT_WEB_PORT") fi if is_port_in_use "$DEFAULT_SERVER_PORT"; then server_in_use=true server_pids=$(get_pids_on_port "$DEFAULT_SERVER_PORT") fi if [ "$web_in_use" = true ] || [ "$server_in_use" = true ]; then echo "" if [ "$web_in_use" = true ]; then center_print "⚠ Port $DEFAULT_WEB_PORT is in use by process(es): $web_pids" "$C_YELLOW" fi if [ "$server_in_use" = true ]; then center_print "⚠ Port $DEFAULT_SERVER_PORT is in use by process(es): $server_pids" "$C_YELLOW" fi echo "" # Show options center_print "What would you like to do?" "$C_WHITE" echo "" center_print "[K] Kill processes and continue" "$C_GREEN" center_print "[U] Use different ports" "$C_MUTE" center_print "[C] Cancel" "$C_RED" echo "" while true; do local choice_pad=$(( (TERM_COLS - 20) / 2 )) printf "%${choice_pad}s" "" read -r -p "Choice: " choice case "$choice" in [kK]|[kK][iI][lL][lL]) echo "" if [ "$web_in_use" = true ]; then center_print "Killing process(es) on port $DEFAULT_WEB_PORT..." "$C_YELLOW" kill_port "$DEFAULT_WEB_PORT" > /dev/null 2>&1 || true center_print "✓ Port $DEFAULT_WEB_PORT is now free" "$C_GREEN" fi if [ "$server_in_use" = true ]; then center_print "Killing process(es) on port $DEFAULT_SERVER_PORT..." "$C_YELLOW" kill_port "$DEFAULT_SERVER_PORT" > /dev/null 2>&1 || true center_print "✓ Port $DEFAULT_SERVER_PORT is now free" "$C_GREEN" fi break ;; [uU]|[uU][sS][eE]) echo "" local input_pad=$(( (TERM_COLS - 40) / 2 )) printf "%${input_pad}s" "" read -r -p "Enter web port (default $DEFAULT_WEB_PORT): " input_web WEB_PORT=${input_web:-$DEFAULT_WEB_PORT} printf "%${input_pad}s" "" read -r -p "Enter server port (default $DEFAULT_SERVER_PORT): " input_server SERVER_PORT=${input_server:-$DEFAULT_SERVER_PORT} center_print "Using ports: Web=$WEB_PORT, Server=$SERVER_PORT" "$C_GREEN" break ;; [cC]|[cC][aA][nN][cC][eE][lL]) echo "" center_print "Cancelled." "$C_MUTE" echo "" exit 0 ;; *) center_print "Invalid choice. Please enter K, U, or C." "$C_RED" ;; esac done else center_print "✓ Port $DEFAULT_WEB_PORT is available" "$C_GREEN" center_print "✓ Port $DEFAULT_SERVER_PORT is available" "$C_GREEN" fi # Restore terminal state hide_cursor stty -echo -icanon 2>/dev/null || true } launch_sequence() { local mode_name="$1" # Ensure terminal size is available get_term_size echo "" # Show port checking for modes that use local ports if [[ "$MODE" == "web" || "$MODE" == "electron" ]]; then center_print "Checking ports ${DEFAULT_WEB_PORT} and ${DEFAULT_SERVER_PORT}..." "$C_MUTE" resolve_port_conflicts echo "" fi spinner "Initializing environment..." spinner "Starting $mode_name..." echo "" local msg="Automaker is ready!" local pad=$(( (TERM_COLS - ${#msg}) / 2 )) printf "%${pad}s${C_GREEN}${BOLD}%s${RESET}\n" "" "$msg" case "$MODE" in web) local url="http://localhost:$WEB_PORT" local upad=$(( (TERM_COLS - ${#url} - 10) / 2 )) echo "" printf "%${upad}s${DIM}Opening ${C_SEC}%s${RESET}\n" "" "$url" ;; docker|docker-electron) echo "" local ui_msg="UI: http://localhost:$DEFAULT_WEB_PORT" local api_msg="API: http://localhost:$DEFAULT_SERVER_PORT" center_text "${DIM}${ui_msg}${RESET}" center_text "${DIM}${api_msg}${RESET}" ;; esac echo "" } # ============================================================================ # HISTORY MANAGEMENT # ============================================================================ save_mode_to_history() { if [ "$NO_HISTORY" = false ]; then echo "$1" > "$HISTORY_FILE" fi } get_last_mode_from_history() { if [ -f "$HISTORY_FILE" ] && [ "$NO_HISTORY" = false ]; then cat "$HISTORY_FILE" fi } # ============================================================================ # PRODUCTION BUILD # ============================================================================ build_for_production() { echo "" center_print "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" "$C_GRAY" center_print "Building for Production" "$C_PRI" center_print "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" "$C_GRAY" echo "" center_print "Building shared packages..." "$C_YELLOW" if ! npm run build:packages; then center_print "✗ Failed to build packages" "$C_RED" exit 1 fi center_print "✓ Packages built" "$C_GREEN" echo "" center_print "Building server..." "$C_YELLOW" if ! npm run build --workspace=apps/server; then center_print "✗ Failed to build server" "$C_RED" exit 1 fi center_print "✓ Server built" "$C_GREEN" echo "" center_print "Building UI..." "$C_YELLOW" if ! npm run build --workspace=apps/ui; then center_print "✗ Failed to build UI" "$C_RED" exit 1 fi center_print "✓ UI built" "$C_GREEN" echo "" center_print "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" "$C_GRAY" center_print "Build Complete" "$C_GREEN" center_print "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" "$C_GRAY" echo "" } # Ensure production env is applied consistently for builds and runtime apply_production_env() { if [ "$PRODUCTION_MODE" = true ]; then export NODE_ENV="production" fi } # ============================================================================ # MAIN EXECUTION # ============================================================================ parse_args "$@" apply_production_env # Pre-flight checks check_platform check_required_commands validate_terminal_size if [ "$CHECK_DEPS" = true ]; then check_dependencies || true fi hide_cursor # Disable echo and line buffering for single-key input stty -echo -icanon 2>/dev/null || true # Function to read a single key, handling escape sequences for arrows # Note: bash 3.2 (macOS) doesn't support fractional timeouts, so we use a different approach read_key() { local key local escape_seq="" if [ -n "$ZSH_VERSION" ]; then read -k 1 -s -t "$INPUT_TIMEOUT" key 2>/dev/null || key="" else # Use IFS= to preserve special characters IFS= read -n 1 -s -t "$INPUT_TIMEOUT" -r key 2>/dev/null || key="" fi # Check for escape sequence (arrow keys send ESC [ A/B/C/D) if [[ "$key" == $'\x1b' ]]; then # Read the rest of the escape sequence without timeout # Arrow keys send 3 bytes: ESC [ A/B/C/D IFS= read -n 1 -s -r escape_seq 2>/dev/null || escape_seq="" if [[ "$escape_seq" == "[" ]] || [[ "$escape_seq" == "O" ]]; then IFS= read -n 1 -s -r escape_seq 2>/dev/null || escape_seq="" case "$escape_seq" in A) echo "UP"; return ;; B) echo "DOWN"; return ;; C) echo "RIGHT"; return ;; D) echo "LEFT"; return ;; esac fi # Just ESC key pressed echo "ESC" return fi echo "$key" } # Interactive menu if no mode specified if [ -z "$MODE" ]; then while true; do show_header show_menu key=$(read_key) case $key in UP) SELECTED_OPTION=$((SELECTED_OPTION - 1)) [ $SELECTED_OPTION -lt 1 ] && SELECTED_OPTION=$MAX_OPTIONS ;; DOWN) SELECTED_OPTION=$((SELECTED_OPTION + 1)) [ $SELECTED_OPTION -gt $MAX_OPTIONS ] && SELECTED_OPTION=1 ;; 1) SELECTED_OPTION=1; MODE="web"; break ;; 2) SELECTED_OPTION=2; MODE="electron"; break ;; 3) SELECTED_OPTION=3; MODE="docker"; break ;; 4) SELECTED_OPTION=4; MODE="docker-electron"; break ;; ""|$'\n'|$'\r') # Enter key - select current option case $SELECTED_OPTION in 1) MODE="web" ;; 2) MODE="electron" ;; 3) MODE="docker" ;; 4) MODE="docker-electron" ;; esac break ;; q|Q) echo "" echo "" center_text "${C_MUTE}Goodbye! See you soon.${RESET}" echo "" exit 0 ;; *) ;; esac done fi # Validate mode case $MODE in web) MODE_NAME="Web Browser" ;; electron) MODE_NAME="Desktop App" ;; docker) MODE_NAME="Docker Dev" ;; docker-electron) MODE_NAME="Electron + Docker" ;; *) echo "${C_RED}Error:${RESET} Invalid mode '$MODE'" echo "Valid modes: web, electron, docker, docker-electron" exit 1 ;; esac # Check Docker for Docker modes if [[ "$MODE" == "docker" || "$MODE" == "docker-electron" ]]; then show_cursor stty echo icanon 2>/dev/null || true if ! check_docker; then exit 1 fi hide_cursor stty -echo -icanon 2>/dev/null || true fi # Save to history save_mode_to_history "$MODE" # Launch sequence launch_sequence "$MODE_NAME" # Restore terminal state before running npm show_cursor stty echo icanon 2>/dev/null || true # Build for production if needed if [ "$PRODUCTION_MODE" = true ]; then build_for_production fi # Execute the appropriate command case $MODE in web) export TEST_PORT="$WEB_PORT" export VITE_SERVER_URL="http://$HOSTNAME:$SERVER_PORT" export PORT="$SERVER_PORT" export DATA_DIR="$SCRIPT_DIR/data" export CORS_ORIGIN="http://localhost:$WEB_PORT,http://$HOSTNAME:$WEB_PORT,http://127.0.0.1:$WEB_PORT" export VITE_APP_MODE="1" if [ "$PRODUCTION_MODE" = true ]; then # Production: run built server and UI preview concurrently echo "" center_print "Starting server on port $SERVER_PORT..." "$C_YELLOW" npm run start --workspace=apps/server & SERVER_PID=$! # Wait for server to be healthy echo "" center_print "Waiting for server to be ready..." "$C_YELLOW" max_retries=30 server_ready=false for ((i=0; i /dev/null 2>&1; then server_ready=true break fi sleep 1 done if [ "$server_ready" = false ]; then center_print "✗ Server failed to start" "$C_RED" kill $SERVER_PID 2>/dev/null || true exit 1 fi center_print "✓ Server is ready!" "$C_GREEN" echo "" # Start UI preview center_print "Starting UI preview on port $WEB_PORT..." "$C_YELLOW" npm run preview --workspace=apps/ui -- --port "$WEB_PORT" # Cleanup server on exit kill $SERVER_PID 2>/dev/null || true else # Development: build packages, start server, then start UI with Vite dev server echo "" center_print "Building shared packages..." "$C_YELLOW" npm run build:packages center_print "✓ Packages built" "$C_GREEN" echo "" # Start backend server in dev mode (background) center_print "Starting backend server on port $SERVER_PORT..." "$C_YELLOW" npm run _dev:server & SERVER_PID=$! # Wait for server to be healthy center_print "Waiting for server to be ready..." "$C_YELLOW" max_retries=30 server_ready=false for ((i=0; i /dev/null 2>&1; then server_ready=true break fi sleep 1 printf "." done echo "" if [ "$server_ready" = false ]; then center_print "✗ Server failed to start" "$C_RED" kill $SERVER_PID 2>/dev/null || true exit 1 fi center_print "✓ Server is ready!" "$C_GREEN" echo "" center_print "The application will be available at: http://localhost:$WEB_PORT" "$C_GREEN" echo "" # Start web app with Vite dev server (HMR enabled) export VITE_APP_MODE="1" npm run _dev:web fi ;; electron) # Set environment variables for Electron (it starts its own server) export TEST_PORT="$WEB_PORT" export PORT="$SERVER_PORT" export VITE_SERVER_URL="http://localhost:$SERVER_PORT" export CORS_ORIGIN="http://localhost:$WEB_PORT,http://127.0.0.1:$WEB_PORT" export VITE_APP_MODE="2" if [ "$PRODUCTION_MODE" = true ]; then # For production electron, we'd normally use the packaged app # For now, run in dev mode but with production-built packages center_print "Note: For production Electron, use the packaged app" "$C_YELLOW" center_print "Running with production-built packages..." "$C_MUTE" echo "" fi center_print "Launching Desktop Application..." "$C_YELLOW" center_print "(Electron will start its own backend server)" "$C_MUTE" echo "" npm run dev:electron ;; docker) # Check for running Electron (user might be switching from option 4) check_running_electron # Check for running containers check_running_containers "docker-compose.dev.yml" container_check=$? if [ $container_check -eq 2 ]; then # Attach to existing containers center_print "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" "$C_GRAY" center_print "Attaching to Docker Dev Containers" "$C_PRI" center_print "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" "$C_GRAY" echo "" center_print "UI: http://localhost:$DEFAULT_WEB_PORT" "$C_GREEN" center_print "API: http://localhost:$DEFAULT_SERVER_PORT" "$C_GREEN" center_print "Press Ctrl+C to detach" "$C_MUTE" echo "" if [ "$DOCKER_CMD" = "sg docker -c" ]; then if [ -f "docker-compose.override.yml" ]; then sg docker -c "docker compose -f 'docker-compose.dev.yml' -f 'docker-compose.override.yml' logs -f" else sg docker -c "docker compose -f 'docker-compose.dev.yml' logs -f" fi else if [ -f "docker-compose.override.yml" ]; then $DOCKER_CMD compose -f docker-compose.dev.yml -f docker-compose.override.yml logs -f else $DOCKER_CMD compose -f docker-compose.dev.yml logs -f fi fi else echo "" center_print "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" "$C_GRAY" center_print "Docker Development Mode" "$C_PRI" center_print "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" "$C_GRAY" echo "" center_print "Starting UI + Server containers..." "$C_MUTE" center_print "Source code is volume mounted for live reload" "$C_MUTE" echo "" center_print "UI: http://localhost:$DEFAULT_WEB_PORT" "$C_GREEN" center_print "API: http://localhost:$DEFAULT_SERVER_PORT" "$C_GREEN" echo "" center_print "First run may take several minutes (building image + npm install)" "$C_YELLOW" center_print "Press Ctrl+C to stop" "$C_MUTE" echo "" center_print "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" "$C_GRAY" echo "" if [ "$DOCKER_CMD" = "sg docker -c" ]; then if [ -f "docker-compose.override.yml" ]; then sg docker -c "docker compose -f 'docker-compose.dev.yml' -f 'docker-compose.override.yml' up --build" else sg docker -c "docker compose -f 'docker-compose.dev.yml' up --build" fi else if [ -f "docker-compose.override.yml" ]; then $DOCKER_CMD compose -f docker-compose.dev.yml -f docker-compose.override.yml up --build else $DOCKER_CMD compose -f docker-compose.dev.yml up --build fi fi fi ;; docker-electron) # Check for running Electron (user might be switching from option 2) check_running_electron # Check for running containers check_running_containers "docker-compose.dev-server.yml" container_check=$? echo "" center_print "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" "$C_GRAY" center_print "Electron + Docker API Mode" "$C_PRI" center_print "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" "$C_GRAY" echo "" center_print "Server runs in Docker container" "$C_MUTE" center_print "Electron runs locally on your machine" "$C_MUTE" echo "" center_print "API: http://localhost:$DEFAULT_SERVER_PORT (Docker)" "$C_GREEN" echo "" # If attaching to existing, skip the build if [ $container_check -eq 2 ]; then center_print "Using existing server container..." "$C_MUTE" else center_print "First run may take several minutes (building image + npm install)" "$C_YELLOW" fi echo "" center_print "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" "$C_GRAY" echo "" # Start docker in background (or skip if attaching) if [ $container_check -eq 2 ]; then center_print "Checking if server is healthy..." "$C_MUTE" DOCKER_PID="" else center_print "Starting Docker server container..." "$C_MUTE" echo "" if [ "$DOCKER_CMD" = "sg docker -c" ]; then if [ -f "docker-compose.override.yml" ]; then sg docker -c "docker compose -f 'docker-compose.dev-server.yml' -f 'docker-compose.override.yml' up --build" & else sg docker -c "docker compose -f 'docker-compose.dev-server.yml' up --build" & fi else if [ -f "docker-compose.override.yml" ]; then $DOCKER_CMD compose -f docker-compose.dev-server.yml -f docker-compose.override.yml up --build & else $DOCKER_CMD compose -f docker-compose.dev-server.yml up --build & fi fi DOCKER_PID=$! fi # Wait for server to be healthy echo "" center_print "Waiting for server to become healthy..." "$C_YELLOW" center_print "(This may take a while on first run)" "$C_MUTE" echo "" max_retries=180 server_ready=false dots="" for ((i=0; i /dev/null 2>&1; then server_ready=true break fi sleep 1 if (( i > 0 && i % 10 == 0 )); then dots="${dots}." center_print "Still waiting${dots}" "$C_MUTE" fi done echo "" if [ "$server_ready" = false ]; then center_print "✗ Server container failed to become healthy" "$C_RED" center_print "Check Docker logs above for errors" "$C_MUTE" [ -n "$DOCKER_PID" ] && kill $DOCKER_PID 2>/dev/null || true exit 1 fi center_print "✓ Server is healthy!" "$C_GREEN" echo "" center_print "Building packages and launching Electron..." "$C_MUTE" echo "" # Build packages and launch Electron npm run build:packages SKIP_EMBEDDED_SERVER=true PORT=$DEFAULT_SERVER_PORT VITE_SERVER_URL="http://localhost:$DEFAULT_SERVER_PORT" VITE_APP_MODE="4" npm run _dev:electron # Cleanup docker when electron exits echo "" center_print "Shutting down Docker container..." "$C_MUTE" [ -n "$DOCKER_PID" ] && kill $DOCKER_PID 2>/dev/null || true if [ "$DOCKER_CMD" = "sg docker -c" ]; then sg docker -c "docker compose -f 'docker-compose.dev-server.yml' down" 2>/dev/null || true else $DOCKER_CMD compose -f docker-compose.dev-server.yml down 2>/dev/null || true fi center_print "Done!" "$C_GREEN" ;; esac