mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
The start-automaker.sh script now correctly sources the app version (0.12.0) from apps/ui/package.json instead of the monorepo version (1.0.0) from the root package.json. This ensures the launcher displays the correct Automaker application version.
1291 lines
45 KiB
Bash
Executable File
1291 lines
45 KiB
Bash
Executable File
#!/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
|
|
}
|
|
|
|
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; then
|
|
echo "${C_RED}Error:${RESET} Docker daemon is not running"
|
|
echo "Please start Docker and try again"
|
|
return 1
|
|
fi
|
|
|
|
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
|
|
running_containers=$(docker ps --filter "name=automaker-dev" --format "{{.Names}}" 2>/dev/null | tr '\n' ' ')
|
|
|
|
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"
|
|
docker compose -f "$compose_file" down 2>/dev/null || true
|
|
# Also try stopping any orphaned containers
|
|
docker ps --filter "name=automaker-dev" -q 2>/dev/null | xargs -r docker stop 2>/dev/null || true
|
|
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"
|
|
docker compose -f "$compose_file" down 2>/dev/null || true
|
|
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<width; i++)); do printf "%s" "$char"; done
|
|
printf "${RESET}"
|
|
}
|
|
|
|
# ============================================================================
|
|
# UI DISPLAY FUNCTIONS
|
|
# ============================================================================
|
|
|
|
show_header() {
|
|
clear
|
|
get_term_size
|
|
|
|
# Top padding
|
|
local top_pad=$(( TERM_LINES / 6 ))
|
|
for ((i=0; i<top_pad; i++)); do echo ""; done
|
|
|
|
# Automaker ASCII art logo
|
|
local l1=" █▀▀█ █ █ ▀▀█▀▀ █▀▀█ █▀▄▀█ █▀▀█ █ █ █▀▀ █▀▀█ "
|
|
local l2=" █▄▄█ █ █ █ █ █ █ ▀ █ █▄▄█ █▀▄ █▀▀ █▄▄▀ "
|
|
local l3=" ▀ ▀ ▀▀▀ ▀ ▀▀▀▀ ▀ ▀ ▀ ▀ ▀ ▀ ▀▀▀ ▀ ▀▀ "
|
|
|
|
local pad_left=$(( (TERM_COLS - LOGO_WIDTH) / 2 ))
|
|
local pad=$(printf "%${pad_left}s" "")
|
|
|
|
echo -e "${pad}${C_PRI}${l1}${RESET}"
|
|
echo -e "${pad}${C_SEC}${l2}${RESET}"
|
|
echo -e "${pad}${C_ACC}${l3}${RESET}"
|
|
|
|
echo ""
|
|
local mode_indicator=""
|
|
if [ "$PRODUCTION_MODE" = true ]; then
|
|
mode_indicator="${C_GREEN}[PRODUCTION]${RESET}"
|
|
else
|
|
mode_indicator="${C_YELLOW}[DEVELOPMENT]${RESET}"
|
|
fi
|
|
local sub_display_len=60
|
|
local sub_pad=$(( (TERM_COLS - sub_display_len) / 2 ))
|
|
printf "%${sub_pad}s" ""
|
|
echo -e "${C_MUTE}Autonomous AI Development Studio${RESET} ${C_GRAY}│${RESET} ${C_GREEN}${VERSION}${RESET} ${mode_indicator}"
|
|
|
|
echo ""
|
|
echo ""
|
|
}
|
|
|
|
show_menu() {
|
|
local pad_left=$(( (TERM_COLS - MENU_BOX_WIDTH) / 2 ))
|
|
local pad=$(printf "%${pad_left}s" "")
|
|
local border="${C_GRAY}│${RESET}"
|
|
|
|
printf "%s${C_GRAY}╭" "$pad"
|
|
draw_line "─" "$C_GRAY" "$MENU_INNER_WIDTH"
|
|
printf "╮${RESET}\n"
|
|
|
|
# Menu items with selection indicator
|
|
local sel1="" sel2="" sel3="" sel4=""
|
|
local txt1="${C_MUTE}" txt2="${C_MUTE}" txt3="${C_MUTE}" txt4="${C_MUTE}"
|
|
|
|
case $SELECTED_OPTION in
|
|
1) sel1="${C_ACC}▸${RESET} ${C_PRI}"; txt1="${C_WHITE}" ;;
|
|
2) sel2="${C_ACC}▸${RESET} ${C_PRI}"; txt2="${C_WHITE}" ;;
|
|
3) sel3="${C_ACC}▸${RESET} ${C_PRI}"; txt3="${C_WHITE}" ;;
|
|
4) sel4="${C_ACC}▸${RESET} ${C_PRI}"; txt4="${C_WHITE}" ;;
|
|
esac
|
|
|
|
# Default non-selected prefix
|
|
[[ -z "$sel1" ]] && sel1=" ${C_MUTE}"
|
|
[[ -z "$sel2" ]] && sel2=" ${C_MUTE}"
|
|
[[ -z "$sel3" ]] && sel3=" ${C_MUTE}"
|
|
[[ -z "$sel4" ]] && sel4=" ${C_MUTE}"
|
|
|
|
printf "%s${border}${sel1}[1]${RESET} 🌐 ${txt1}Web App${RESET} ${C_MUTE}Server + Browser (localhost:$WEB_PORT)${RESET} ${border}\n" "$pad"
|
|
printf "%s${border}${sel2}[2]${RESET} 🖥 ${txt2}Electron${RESET} ${DIM}Desktop App (embedded server)${RESET} ${border}\n" "$pad"
|
|
printf "%s${border}${sel3}[3]${RESET} 🐳 ${txt3}Docker${RESET} ${DIM}Full Stack (live reload)${RESET} ${border}\n" "$pad"
|
|
printf "%s${border}${sel4}[4]${RESET} 🔗 ${txt4}Electron & Docker${RESET} ${DIM}Desktop + Docker Server${RESET} ${border}\n" "$pad"
|
|
|
|
printf "%s${C_GRAY}├" "$pad"
|
|
draw_line "─" "$C_GRAY" "$MENU_INNER_WIDTH"
|
|
printf "┤${RESET}\n"
|
|
|
|
printf "%s${border} ${C_RED}[Q]${RESET} ⏻ ${C_MUTE}Exit${RESET} ${border}\n" "$pad"
|
|
|
|
printf "%s${C_GRAY}╰" "$pad"
|
|
draw_line "─" "$C_GRAY" "$MENU_INNER_WIDTH"
|
|
printf "╯${RESET}\n"
|
|
|
|
echo ""
|
|
local footer_text="[↑↓] Navigate [Enter] Select [1-4] Quick Select [Q] Exit"
|
|
local f_pad=$(( (TERM_COLS - ${#footer_text}) / 2 ))
|
|
printf "%${f_pad}s" ""
|
|
echo -e "${DIM}${footer_text}${RESET}"
|
|
|
|
if [ -f "$HISTORY_FILE" ]; then
|
|
local last_mode
|
|
last_mode=$(cat "$HISTORY_FILE" 2>/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 CORS_ORIGIN="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<max_retries; i++)); do
|
|
if curl -s "http://$HOSTNAME:$SERVER_PORT/api/health" > /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<max_retries; i++)); do
|
|
if curl -s "http://localhost:$SERVER_PORT/api/health" > /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 [ -f "docker-compose.override.yml" ]; then
|
|
docker compose -f docker-compose.dev.yml -f docker-compose.override.yml logs -f
|
|
else
|
|
docker compose -f docker-compose.dev.yml logs -f
|
|
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 [ -f "docker-compose.override.yml" ]; then
|
|
docker compose -f docker-compose.dev.yml -f docker-compose.override.yml up --build
|
|
else
|
|
docker compose -f docker-compose.dev.yml up --build
|
|
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 [ -f "docker-compose.override.yml" ]; then
|
|
docker compose -f docker-compose.dev-server.yml -f docker-compose.override.yml up --build &
|
|
else
|
|
docker compose -f docker-compose.dev-server.yml up --build &
|
|
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<max_retries; i++)); do
|
|
if curl -s "http://localhost:$DEFAULT_SERVER_PORT/api/health" > /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
|
|
docker compose -f docker-compose.dev-server.yml down 2>/dev/null || true
|
|
center_print "Done!" "$C_GREEN"
|
|
;;
|
|
esac
|