Files
automaker/start-automaker.sh
webdevcody aa35eb3d3a feat: implement spec synchronization feature for improved project management
- Added a new `/sync` endpoint to synchronize the project specification with the current codebase and feature state.
- Introduced `syncSpec` function to handle the synchronization logic, updating technology stack, implemented features, and roadmap phases.
- Enhanced the running state management to track synchronization tasks alongside existing generation tasks.
- Updated UI components to support synchronization actions, including loading indicators and status updates.
- Improved logging and error handling for better visibility during sync operations.

These changes enhance project management capabilities by ensuring that the specification remains up-to-date with the latest code and feature developments.
2026-01-17 01:45:45 -05:00

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 package.json (using node for reliable JSON parsing)
if command -v node &> /dev/null; then
VERSION="v$(node -p "require('$SCRIPT_DIR/package.json').version" 2>/dev/null || echo "0.0.0")"
else
VERSION=$(grep '"version"' "$SCRIPT_DIR/package.json" | head -1 | sed 's/.*"version"[^"]*"\([^"]*\)".*/v\1/')
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