Files
Pulse/scripts/hot-dev.sh

653 lines
24 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/bash
# hot-dev.sh - Development server with hot-reload for Pulse
#
# This script runs a local development environment with:
# - Go backend with auto-rebuild on file changes (via inotifywait)
# - Vite frontend dev server with HMR
# - Auto-detection of pulse-pro module for Pro features
# - Snapshot watcher (if scripts/watch-snapshot.sh exists)
#
# Environment Variables:
# HOT_DEV_USE_PROD_DATA=true Use /etc/pulse for data (sessions, config, etc.)
# HOT_DEV_USE_PRO=true Build Pro binary (default: true if module available)
# PULSE_MOCK_MODE=true Use isolated mock data directory
# PULSE_DATA_DIR=/path Override data directory
# PULSE_DEV_API_PORT=7655 Backend API port (default: 7655)
# FRONTEND_DEV_PORT=5173 Frontend dev server port (default: 5173)
#
# Pro Features Mode:
# When pulse-enterprise repo exists and HOT_DEV_USE_PRO is not "false",
# the script builds the Pro binary which includes:
# - SQLite-based persistent audit logging
# - RBAC (Role-Based Access Control)
# - HMAC event signing for tamper detection
#
# Usage:
# ./scripts/hot-dev.sh # Standard dev mode
# HOT_DEV_USE_PROD_DATA=true ./scripts/hot-dev.sh # Use production data
#
set -euo pipefail
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
ROOT_DIR=$(cd "${SCRIPT_DIR}/.." && pwd)
SCRIPT_PATH="${SCRIPT_DIR}/$(basename "${BASH_SOURCE[0]}")"
SCRIPT_MTIME=$(stat -c %Y "${SCRIPT_PATH}" 2>/dev/null || stat -f %m "${SCRIPT_PATH}")
# --- Helper Functions ---
log_info() { printf "\033[0;34m[hot-dev]\033[0m %s\n" "$1"; }
log_warn() { printf "\033[0;33m[hot-dev] WARNING:\033[0m %s\n" "$1"; }
log_error() { printf "\033[0;31m[hot-dev] ERROR:\033[0m %s\n" "$1"; }
detect_encrypted_files() {
local data_dir=$1
local patterns=(nodes.enc* email.enc* webhooks.enc* oidc.enc* ai.enc*)
local files=()
for pattern in "${patterns[@]}"; do
for path in "${data_dir}/${pattern}"; do
if [[ -s "${path}" ]]; then
files+=("$(basename "${path}")")
fi
done
done
printf '%s\n' "${files[@]}"
}
check_dependencies() {
local missing=0
for cmd in go npm lsof; do
if ! command -v $cmd >/dev/null 2>&1; then
log_error "$cmd is not installed but is required."
missing=1
fi
done
if [[ $missing -eq 1 ]]; then
exit 1
fi
}
# --- Configuration & Environment ---
load_env_file() {
local env_file=$1
if [[ -f ${env_file} ]]; then
log_info "Loading ${env_file}"
set +u
set -a
# shellcheck disable=SC1090
source "${env_file}"
set +a
set -u
fi
}
load_env_file "${ROOT_DIR}/.env"
load_env_file "${ROOT_DIR}/.env.local"
load_env_file "${ROOT_DIR}/.env.dev"
FRONTEND_PORT=${FRONTEND_PORT:-${PORT:-5173}}
PORT=${PORT:-${FRONTEND_PORT}}
# Detect LAN IP
if [[ -z ${LAN_IP:-} ]]; then
if command -v hostname >/dev/null 2>&1 && hostname -I >/dev/null 2>&1; then
LAN_IP=$(hostname -I | awk '{print $1}')
fi
if [[ -z ${LAN_IP:-} ]]; then
LAN_IP=$(ipconfig getifaddr en0 2>/dev/null || echo "")
fi
if [[ -z ${LAN_IP:-} ]]; then
LAN_IP="0.0.0.0"
fi
fi
FRONTEND_DEV_HOST=${FRONTEND_DEV_HOST:-0.0.0.0}
FRONTEND_DEV_PORT=${FRONTEND_DEV_PORT:-${FRONTEND_PORT}}
PULSE_DEV_API_HOST=${PULSE_DEV_API_HOST:-${LAN_IP}}
PULSE_DEV_API_PORT=${PULSE_DEV_API_PORT:-7655}
if [[ -z ${PULSE_DEV_API_URL:-} ]]; then
PULSE_DEV_API_URL="http://${PULSE_DEV_API_HOST}:${PULSE_DEV_API_PORT}"
fi
if [[ -z ${PULSE_DEV_WS_URL:-} ]]; then
if [[ ${PULSE_DEV_API_URL} == http://* ]]; then
PULSE_DEV_WS_URL="ws://${PULSE_DEV_API_URL#http://}"
elif [[ ${PULSE_DEV_API_URL} == https://* ]]; then
PULSE_DEV_WS_URL="wss://${PULSE_DEV_API_URL#https://}"
else
PULSE_DEV_WS_URL=${PULSE_DEV_API_URL}
fi
fi
# Set specific allowed origin for CORS with credentials
# Use the frontend dev URL so cross-port SSE requests work with auth cookies
# Added localhost and 127.0.0.1 by default for flexibility (both 7655 and 5173)
ALLOWED_ORIGINS="http://${PULSE_DEV_API_HOST:-127.0.0.1}:${FRONTEND_DEV_PORT:-7655}"
ALLOWED_ORIGINS="${ALLOWED_ORIGINS},http://localhost:${FRONTEND_DEV_PORT:-7655},http://127.0.0.1:${FRONTEND_DEV_PORT:-7655}"
ALLOWED_ORIGINS="${ALLOWED_ORIGINS},http://localhost:5173,http://127.0.0.1:5173"
# Detect and add all system IPs (V4)
if [[ "$(uname -s)" == "Darwin" ]]; then
# macOS: get IPs from ifconfig
ALL_IPS=$(ifconfig 2>/dev/null | grep "inet " | awk '{print $2}' | grep -v "^127\.")
else
# Linux: use hostname -I
ALL_IPS=$(hostname -I 2>/dev/null || echo "")
fi
for ip in $ALL_IPS; do
if [[ "${ip}" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
if [[ "${ip}" != "127.0.0.1" ]]; then
ALLOWED_ORIGINS="${ALLOWED_ORIGINS},http://${ip}:${FRONTEND_DEV_PORT:-7655}"
ALLOWED_ORIGINS="${ALLOWED_ORIGINS},http://${ip}:5173"
fi
fi
done
export FRONTEND_PORT PORT
export FRONTEND_DEV_HOST FRONTEND_DEV_PORT
export PULSE_DEV_API_HOST PULSE_DEV_API_PORT PULSE_DEV_API_URL PULSE_DEV_WS_URL
export ALLOWED_ORIGINS
EXTRA_CLEANUP_PORT=$((PULSE_DEV_API_PORT + 1))
# --- Startup Checks ---
check_dependencies
cat <<BANNER
=========================================
Starting HOT-RELOAD development mode
=========================================
Frontend: http://${FRONTEND_DEV_HOST}:${FRONTEND_DEV_PORT} (Local)
http://${LAN_IP}:${FRONTEND_DEV_PORT} (LAN)
Backend API: ${PULSE_DEV_API_URL}
Dev Credentials: admin / admin
Mock Mode: ${PULSE_MOCK_MODE:-false}
Toggle mock mode: npm run mock:on / npm run mock:off
Mock config: npm run mock:edit
Frontend: Edit files and see changes instantly!
Backend: Auto-rebuilds when .go files change!
Press Ctrl+C to stop
=========================================
BANNER
kill_port() {
local port=$1
log_info "Cleaning up port ${port}..."
lsof -i :"${port}" 2>/dev/null | awk 'NR>1 {print $2}' | xargs -r kill -9 2>/dev/null || true
}
log_info "Cleaning up existing processes..."
# OS-Specific Cleanup
# Note: When running under systemd (INVOCATION_ID is set), skip stopping our own service
OS_NAME=$(uname -s)
if [[ "$OS_NAME" == "Linux" ]] && [[ -z "${INVOCATION_ID:-}" ]]; then
# Only stop pulse-dev if we're NOT running under systemd
sudo systemctl stop pulse-dev 2>/dev/null || true
fi
pkill -f "backend-watch.sh" 2>/dev/null || true
# Only kill vite/npm processes that look like ours (simple check)
pkill -f "vite" 2>/dev/null || true
pkill -f "npm run dev" 2>/dev/null || true
pkill -x "pulse" 2>/dev/null || true
sleep 1
pkill -9 -x "pulse" 2>/dev/null || true
kill_port "${FRONTEND_DEV_PORT}"
kill_port "${PULSE_DEV_API_PORT}"
kill_port "${EXTRA_CLEANUP_PORT}"
# Truncate debug log
:> /tmp/pulse-debug.log
sleep 2
# Verify ports are free
set +o pipefail
for port in "${FRONTEND_DEV_PORT}" "${PULSE_DEV_API_PORT}"; do
if lsof -i :"${port}" 2>/dev/null | grep -q LISTEN; then
log_error "Port ${port} is still in use after cleanup!"
kill_port "${port}"
sleep 2
if lsof -i :"${port}" 2>/dev/null | grep -q LISTEN; then
log_error "FATAL: Cannot free port ${port}. Please manually kill the process:"
lsof -i :"${port}"
exit 1
fi
fi
done
set -o pipefail
log_info "Ports are clean!"
# Self-restart check
check_script_change() {
local current_mtime
current_mtime=$(stat -c %Y "${SCRIPT_PATH}" 2>/dev/null || stat -f %m "${SCRIPT_PATH}")
if [[ "${current_mtime}" != "${SCRIPT_MTIME}" ]]; then
log_warn "hot-dev.sh script changed! Restarting..."
exec "$0" "$@"
fi
}
# --- Config Setup ---
if [[ -f "${ROOT_DIR}/mock.env" ]]; then
load_env_file "${ROOT_DIR}/mock.env"
if [[ -f "${ROOT_DIR}/mock.env.local" ]]; then
load_env_file "${ROOT_DIR}/mock.env.local"
log_info "Loaded mock.env.local overrides"
fi
if [[ ${PULSE_MOCK_MODE:-false} == "true" ]]; then
TOTAL_GUESTS=$((PULSE_MOCK_NODES * (PULSE_MOCK_VMS_PER_NODE + PULSE_MOCK_LXCS_PER_NODE)))
echo "Mock mode ENABLED with ${PULSE_MOCK_NODES} nodes (${TOTAL_GUESTS} total guests)"
else
echo "Syncing production configuration..."
DEV_DIR="${ROOT_DIR}/tmp/dev-config" "${ROOT_DIR}/scripts/sync-production-config.sh"
fi
fi
if [[ -f /etc/pulse/.env ]] && [[ -r /etc/pulse/.env ]]; then
load_env_file "/etc/pulse/.env"
echo "Auth configuration loaded from /etc/pulse/.env"
fi
# --- Start Backend ---
log_info "Starting backend on port ${PULSE_DEV_API_PORT}..."
cd "${ROOT_DIR}"
mkdir -p internal/api/frontend-modern/dist
touch internal/api/frontend-modern/dist/index.html
# Check if Pro module is available and use it for full audit logging support
# Use PULSE_REPOS_DIR env var or default to ~/Development/pulse/repos
PULSE_REPOS_DIR="${PULSE_REPOS_DIR:-$HOME/Development/pulse/repos}"
PRO_MODULE_DIR="${PULSE_REPOS_DIR}/pulse-enterprise"
if [[ -d "${PRO_MODULE_DIR}" ]] && [[ ${HOT_DEV_USE_PRO:-true} == "true" ]]; then
log_info "Building Pro binary (includes persistent audit logging)..."
cd "${PRO_MODULE_DIR}"
go build -buildvcs=false -o "${ROOT_DIR}/pulse" ./cmd/pulse-enterprise 2>/dev/null || {
log_warn "Pro build failed, falling back to standard binary"
cd "${ROOT_DIR}"
go build -o pulse ./cmd/pulse
}
cd "${ROOT_DIR}"
# Note: PULSE_AUDIT_DIR is set after PULSE_DATA_DIR is determined below
PRO_BUILD_SUCCESS=true
else
PRO_BUILD_SUCCESS=false
go build -o pulse ./cmd/pulse
fi
FRONTEND_PORT=${PULSE_DEV_API_PORT}
PORT=${PULSE_DEV_API_PORT}
PULSE_DEV=true # Enable development mode features (needed for admin bypass etc)
ALLOW_ADMIN_BYPASS=1 # Allow X-Admin-Bypass header in dev mode
# Dev credentials: admin/admin (bcrypt hash of 'admin')
PULSE_AUTH_USER="admin"
PULSE_AUTH_PASS='$2a$12$j9/pl2RCHGVGvtv4wocrx.FGBczUw97ZAeO8im0.Ty.fXDGFOviWS'
export FRONTEND_PORT PULSE_DEV_API_PORT PORT PULSE_DEV ALLOW_ADMIN_BYPASS PULSE_AUTH_USER PULSE_AUTH_PASS
# Data Directory Setup
if [[ ${PULSE_MOCK_MODE:-false} == "true" ]]; then
export PULSE_DATA_DIR="${ROOT_DIR}/tmp/mock-data"
mkdir -p "$PULSE_DATA_DIR"
log_info "Mock mode: Using isolated data directory: ${PULSE_DATA_DIR}"
# Set audit dir for Pro features (must be after PULSE_DATA_DIR is set)
if [[ ${PRO_BUILD_SUCCESS:-false} == "true" ]]; then
export PULSE_AUDIT_DIR="${PULSE_DATA_DIR}"
log_info "Pro audit logging enabled (SQLite storage in ${PULSE_AUDIT_DIR})"
fi
else
if [[ -n ${PULSE_DATA_DIR:-} ]]; then
log_info "Using preconfigured data directory: ${PULSE_DATA_DIR}"
elif [[ ${HOT_DEV_USE_PROD_DATA:-false} == "true" ]]; then
export PULSE_DATA_DIR=/etc/pulse
log_info "HOT_DEV_USE_PROD_DATA=true using production data directory: ${PULSE_DATA_DIR}"
else
DEV_CONFIG_DIR="${ROOT_DIR}/tmp/dev-config"
mkdir -p "$DEV_CONFIG_DIR"
export PULSE_DATA_DIR="${DEV_CONFIG_DIR}"
log_info "Production mode: Using dev config directory: ${PULSE_DATA_DIR}"
fi
# Auto-restore encryption key from backup if missing
if [[ ! -f "${PULSE_DATA_DIR}/.encryption.key" ]]; then
BACKUP_KEY=$(find "${PULSE_DATA_DIR}" -maxdepth 1 -name '.encryption.key.bak*' -type f 2>/dev/null | head -1)
if [[ -n "${BACKUP_KEY}" ]] && [[ -f "${BACKUP_KEY}" ]]; then
echo ""
log_error "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
log_error "!! ENCRYPTION KEY WAS MISSING - AUTO-RESTORING FROM BACKUP !!"
log_error "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
log_error "!! Backup used: ${BACKUP_KEY}"
log_error "!! "
log_error "!! To find out what deleted the key:"
log_error "!! Linux: sudo journalctl -u encryption-key-watcher -n 100"
log_error "!! macOS: check /tmp/pulse-debug.log"
log_error "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
echo ""
cp -f "${BACKUP_KEY}" "${PULSE_DATA_DIR}/.encryption.key"
chmod 600 "${PULSE_DATA_DIR}/.encryption.key"
log_info "Restored encryption key from backup"
fi
fi
if [[ -z ${PULSE_ENCRYPTION_KEY:-} ]]; then
if [[ -f "${PULSE_DATA_DIR}/.encryption.key" ]]; then
export PULSE_ENCRYPTION_KEY="$(<"${PULSE_DATA_DIR}/.encryption.key")"
log_info "Loaded encryption key from ${PULSE_DATA_DIR}/.encryption.key"
elif [[ ${PULSE_DATA_DIR} == "${ROOT_DIR}/tmp/dev-config" ]]; then
DEV_KEY_FILE="${PULSE_DATA_DIR}/.encryption.key"
if [[ ! -f "${DEV_KEY_FILE}" ]]; then
mapfile -t ENCRYPTED_FILES < <(detect_encrypted_files "${PULSE_DATA_DIR}")
if [[ ${#ENCRYPTED_FILES[@]} -gt 0 ]]; then
log_error "Encryption key is missing but encrypted data exists."
log_error "Restore ${DEV_KEY_FILE} from backup before continuing."
log_error "Encrypted files: ${ENCRYPTED_FILES[*]}"
exit 1
fi
openssl rand -base64 32 > "${DEV_KEY_FILE}"
chmod 600 "${DEV_KEY_FILE}"
log_info "Generated dev encryption key at ${DEV_KEY_FILE}"
fi
export PULSE_ENCRYPTION_KEY="$(<"${DEV_KEY_FILE}")"
elif [[ ${HOT_DEV_USE_PROD_DATA:-false} == "true" ]]; then
# Production data mode but no key - generate one to prevent orphaned encrypted data
mapfile -t ENCRYPTED_FILES < <(detect_encrypted_files "${PULSE_DATA_DIR}")
if [[ ${#ENCRYPTED_FILES[@]} -gt 0 ]]; then
log_error "Encryption key is missing but encrypted data exists."
log_error "Restore ${PULSE_DATA_DIR}/.encryption.key from backup before continuing."
log_error "Encrypted files: ${ENCRYPTED_FILES[*]}"
exit 1
fi
log_warn "No encryption key found for ${PULSE_DATA_DIR}. Generating new key..."
openssl rand -base64 32 > "${PULSE_DATA_DIR}/.encryption.key"
chmod 600 "${PULSE_DATA_DIR}/.encryption.key"
export PULSE_ENCRYPTION_KEY="$(<"${PULSE_DATA_DIR}/.encryption.key")"
log_info "Generated new encryption key at ${PULSE_DATA_DIR}/.encryption.key"
else
log_warn "No encryption key found for ${PULSE_DATA_DIR}. Encrypted config may fail to load."
fi
fi
# Set audit dir for Pro features (must be after PULSE_DATA_DIR is set)
if [[ ${PRO_BUILD_SUCCESS:-false} == "true" ]]; then
export PULSE_AUDIT_DIR="${PULSE_DATA_DIR}"
log_info "Pro audit logging enabled (SQLite storage in ${PULSE_AUDIT_DIR})"
fi
fi
LOG_LEVEL="${LOG_LEVEL:-debug}" \
FRONTEND_PORT="${PULSE_DEV_API_PORT:-7655}" \
PORT="${PULSE_DEV_API_PORT:-7655}" \
PULSE_DATA_DIR="${PULSE_DATA_DIR:-}" \
PULSE_ENCRYPTION_KEY="${PULSE_ENCRYPTION_KEY:-}" \
ALLOW_ADMIN_BYPASS="${ALLOW_ADMIN_BYPASS:-1}" \
PULSE_DEV="${PULSE_DEV:-true}" \
PULSE_AUTH_USER="${PULSE_AUTH_USER:-}" \
PULSE_AUTH_PASS="${PULSE_AUTH_PASS:-}" \
ALLOWED_ORIGINS="${ALLOWED_ORIGINS:-}" \
LOG_FILE="/tmp/pulse-debug.log" \
LOG_MAX_SIZE="50" \
./pulse > /dev/null 2>&1 &
BACKEND_PID=$!
sleep 2
if ! kill -0 "${BACKEND_PID}" 2>/dev/null; then
log_error "Backend failed to start!"
exit 1
fi
# --- Backend Health Monitor ---
# Restarts Pulse if it dies unexpectedly (not from file watcher rebuild)
# IMPORTANT: Enforces single-instance - kills duplicates if found
log_info "Starting backend health monitor..."
(
while true; do
sleep 10
PULSE_COUNT=$(pgrep -f "^\./pulse$" 2>/dev/null | wc -l | tr -d ' ')
if [[ "$PULSE_COUNT" -eq 0 ]]; then
log_warn "⚠️ Pulse died unexpectedly, restarting..."
LOG_LEVEL="${LOG_LEVEL:-debug}" \
FRONTEND_PORT="${PULSE_DEV_API_PORT:-7655}" \
PORT="${PULSE_DEV_API_PORT:-7655}" \
PULSE_DATA_DIR="${PULSE_DATA_DIR:-}" \
PULSE_ENCRYPTION_KEY="${PULSE_ENCRYPTION_KEY:-}" \
ALLOW_ADMIN_BYPASS="${ALLOW_ADMIN_BYPASS:-1}" \
PULSE_DEV="${PULSE_DEV:-true}" \
PULSE_AUTH_USER="${PULSE_AUTH_USER:-}" \
PULSE_AUTH_PASS="${PULSE_AUTH_PASS:-}" \
ALLOWED_ORIGINS="${ALLOWED_ORIGINS:-}" \
LOG_FILE="/tmp/pulse-debug.log" \
LOG_MAX_SIZE="50" \
./pulse > /dev/null 2>&1 &
NEW_PID=$!
sleep 2
if kill -0 "$NEW_PID" 2>/dev/null; then
log_info "✓ Backend auto-restarted (PID: $NEW_PID)"
else
log_error "✗ Backend failed to auto-restart!"
fi
elif [[ "$PULSE_COUNT" -gt 1 ]]; then
log_error "⚠️ Multiple Pulse processes detected ($PULSE_COUNT), killing all and restarting..."
pkill -9 -f "^\./pulse$" 2>/dev/null || true
sleep 2
LOG_LEVEL="${LOG_LEVEL:-debug}" \
FRONTEND_PORT="${PULSE_DEV_API_PORT:-7655}" \
PORT="${PULSE_DEV_API_PORT:-7655}" \
PULSE_DATA_DIR="${PULSE_DATA_DIR:-}" \
PULSE_ENCRYPTION_KEY="${PULSE_ENCRYPTION_KEY:-}" \
ALLOW_ADMIN_BYPASS="${ALLOW_ADMIN_BYPASS:-1}" \
PULSE_DEV="${PULSE_DEV:-true}" \
PULSE_AUTH_USER="${PULSE_AUTH_USER:-}" \
PULSE_AUTH_PASS="${PULSE_AUTH_PASS:-}" \
ALLOWED_ORIGINS="${ALLOWED_ORIGINS:-}" \
LOG_FILE="/tmp/pulse-debug.log" \
LOG_MAX_SIZE="50" \
./pulse > /dev/null 2>&1 &
NEW_PID=$!
sleep 2
if kill -0 "$NEW_PID" 2>/dev/null; then
log_info "✓ Backend restarted after killing duplicates (PID: $NEW_PID)"
else
log_error "✗ Backend failed to restart after killing duplicates!"
fi
fi
done
) &
HEALTH_MONITOR_PID=$!
# --- File Watcher ---
log_info "Starting backend file watcher..."
(
cd "${ROOT_DIR}"
restart_backend() {
log_info "Restarting backend..."
# Kill ALL pulse processes (not just one) to prevent duplicates
pkill -f "^\./pulse$" 2>/dev/null || true
sleep 1
# Force kill any remaining
pkill -9 -f "^\./pulse$" 2>/dev/null || true
sleep 1
LOG_LEVEL="${LOG_LEVEL:-debug}" \
FRONTEND_PORT="${PULSE_DEV_API_PORT:-7655}" \
PORT="${PULSE_DEV_API_PORT:-7655}" \
PULSE_DATA_DIR="${PULSE_DATA_DIR:-}" \
PULSE_ENCRYPTION_KEY="${PULSE_ENCRYPTION_KEY:-}" \
ALLOW_ADMIN_BYPASS="${ALLOW_ADMIN_BYPASS:-1}" \
PULSE_DEV="${PULSE_DEV:-true}" \
PULSE_AUTH_USER="${PULSE_AUTH_USER:-}" \
PULSE_AUTH_PASS="${PULSE_AUTH_PASS:-}" \
ALLOWED_ORIGINS="${ALLOWED_ORIGINS:-}" \
LOG_FILE="/tmp/pulse-debug.log" \
LOG_MAX_SIZE="50" \
./pulse > /dev/null 2>&1 &
NEW_PID=$!
sleep 1
# Verify exactly one process is running
PULSE_COUNT=$(pgrep -f "^\./pulse$" 2>/dev/null | wc -l | tr -d ' ')
if [[ "$PULSE_COUNT" -eq 1 ]] && kill -0 "$NEW_PID" 2>/dev/null; then
log_info "✓ Backend restarted (PID: $NEW_PID)"
elif [[ "$PULSE_COUNT" -gt 1 ]]; then
log_error "✗ Multiple processes after restart ($PULSE_COUNT) - killing all"
pkill -9 -f "^\./pulse$" 2>/dev/null || true
else
log_error "✗ Backend failed to start! Check /tmp/pulse-debug.log"
fi
}
rebuild_backend() {
local changed_file=$1
echo ""
log_info "🔄 Change detected: $(basename "$changed_file")"
log_info "Rebuilding backend..."
# Use the same build logic as the initial build
local build_success=0
PULSE_REPOS_DIR="${PULSE_REPOS_DIR:-$HOME/Development/pulse/repos}"
PRO_MODULE_DIR="${PULSE_REPOS_DIR}/pulse-enterprise"
if [[ -d "${PRO_MODULE_DIR}" ]] && [[ ${HOT_DEV_USE_PRO:-true} == "true" ]]; then
log_info "Building Pro binary..."
if (cd "${PRO_MODULE_DIR}" && go build -buildvcs=false -o "${ROOT_DIR}/pulse" ./cmd/pulse-enterprise 2>&1 | grep -v "^#"); then
build_success=1
fi
fi
if [[ $build_success -eq 0 ]]; then
if go build -o pulse ./cmd/pulse 2>&1 | grep -v "^#"; then
build_success=1
fi
fi
if [[ $build_success -eq 1 ]]; then
restart_backend
else
log_error "✗ Build failed!"
fi
log_info "Watching for changes..."
}
if command -v inotifywait >/dev/null 2>&1; then
# Linux: inotifywait
# Watch source directories AND the pulse binary itself (so manual builds trigger restart)
inotifywait -r -m -e modify,create,delete,move,attrib \
--exclude '(vendor/|node_modules/|\.git/|\.swp$|\.tmp$|~$)' \
--format '%e %w%f' \
"${ROOT_DIR}/cmd" "${ROOT_DIR}/internal" "${ROOT_DIR}/pkg" "${ROOT_DIR}/pulse" 2>/dev/null | \
while read -r event changed_file; do
check_script_change "$@"
if [[ "$changed_file" == "${ROOT_DIR}/pulse" ]]; then
log_info "🚀 Manual build detected (pulse binary changed), restarting..."
restart_backend
elif [[ "$changed_file" == *.go ]] || [[ "$event" =~ CREATE|DELETE|MOVED|ATTRIB ]]; then
rebuild_backend "$changed_file"
fi
done
elif command -v fswatch >/dev/null 2>&1; then
# macOS: fswatch
log_info "Using fswatch for file monitoring"
fswatch -r --event Created --event Updated --event Removed --event Renamed \
--exclude '\.git/' --exclude 'vendor/' --exclude 'node_modules/' \
--include '\.go$' \
"${ROOT_DIR}/cmd" "${ROOT_DIR}/internal" "${ROOT_DIR}/pkg" 2>/dev/null | \
while read -r changed_file; do
# fswatch sends absolute paths, simple check for .go extension
if [[ "$changed_file" == *.go ]]; then
rebuild_backend "$changed_file"
fi
done
else
log_warn "No supported file watcher found (inotifywait or fswatch). Auto-rebuild disabled."
sleep 3600
fi
) &
WATCHER_PID=$!
# --- Cleanup Handler ---
cleanup() {
echo ""
log_info "Stopping services..."
# Kill Health Monitor
if [[ -n ${HEALTH_MONITOR_PID:-} ]] && kill -0 "${HEALTH_MONITOR_PID}" 2>/dev/null; then
kill "${HEALTH_MONITOR_PID}" 2>/dev/null || true
fi
# Kill Watcher
if [[ -n ${WATCHER_PID:-} ]] && kill -0 "${WATCHER_PID}" 2>/dev/null; then
kill "${WATCHER_PID}" 2>/dev/null || true
fi
# Kill Backend
# We re-find the PID because it might have changed during restart
CURRENT_BACKEND_PID=$(pgrep -f "^\./pulse$" || true)
if [[ -n ${CURRENT_BACKEND_PID} ]]; then
kill "${CURRENT_BACKEND_PID}" 2>/dev/null || true
sleep 1
if kill -0 "${CURRENT_BACKEND_PID}" 2>/dev/null; then
kill -9 "${CURRENT_BACKEND_PID}" 2>/dev/null || true
fi
fi
# Kill Frontend (Vite)
if [[ -n ${VITE_PID:-} ]] && kill -0 "${VITE_PID}" 2>/dev/null; then
kill "${VITE_PID}" 2>/dev/null || true
fi
# Kill file backup watcher
if [[ -n ${BACKUP_WATCHER_PID:-} ]] && kill -0 "${BACKUP_WATCHER_PID}" 2>/dev/null; then
kill "${BACKUP_WATCHER_PID}" 2>/dev/null || true
fi
# Fallback cleanup
pkill -f "inotifywait.*pulse" 2>/dev/null || true
pkill -f "fswatch.*pulse" 2>/dev/null || true
pkill -f "watch-snapshot.sh" 2>/dev/null || true
log_info "Hot-dev stopped."
}
trap cleanup INT TERM EXIT
# --- Start File Backup Watcher (optional) ---
SNAPSHOT_SCRIPT="${ROOT_DIR}/scripts/watch-snapshot.sh"
if [[ -x "${SNAPSHOT_SCRIPT}" ]]; then
log_info "Starting snapshot watcher..."
"${SNAPSHOT_SCRIPT}" > /tmp/pulse-watch-snapshot.log 2>&1 &
BACKUP_WATCHER_PID=$!
log_info "Snapshots: ~/.pulse-snapshots (PID: ${BACKUP_WATCHER_PID})"
fi
# --- Start Frontend ---
log_info "Starting frontend with hot-reload on port ${FRONTEND_DEV_PORT}..."
cd "${ROOT_DIR}/frontend-modern"
# Run Vite in background and wait for it, so we can trap signals properly
npx vite --config vite.config.ts --host "${FRONTEND_DEV_HOST}" --port "${FRONTEND_DEV_PORT}" --clearScreen false &
VITE_PID=$!
wait "$VITE_PID"