364 lines
12 KiB
Bash
Executable file
364 lines
12 KiB
Bash
Executable file
#!/bin/bash
|
||
set -euo pipefail
|
||
|
||
# =============================================================================
|
||
# Messenger Service - Universal Installer
|
||
# =============================================================================
|
||
#
|
||
# Installs the iMessage sync service on any Mac with three modes:
|
||
#
|
||
# --mode shared Use life-manager's PostgreSQL (messenger tables in same DB)
|
||
# --mode local Local PostgreSQL or SQLite, syncs to life-manager on demand
|
||
# --mode standalone Fully standalone, no life-manager connection
|
||
#
|
||
# Examples:
|
||
# ./install.sh --mode shared \
|
||
# --db-url postgresql://lilith@plum:25471/life_manager \
|
||
# --life-manager-url http://plum:3700
|
||
#
|
||
# ./install.sh --mode local \
|
||
# --life-manager-url http://10.0.0.5:3700
|
||
#
|
||
# ./install.sh --mode standalone
|
||
#
|
||
# ./install.sh # Interactive — prompts for everything
|
||
# =============================================================================
|
||
|
||
# Colors
|
||
RED='\033[0;31m'
|
||
GREEN='\033[0;32m'
|
||
YELLOW='\033[1;33m'
|
||
BLUE='\033[0;34m'
|
||
CYAN='\033[0;36m'
|
||
NC='\033[0m'
|
||
|
||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||
CONFIG_DIR="$HOME/.config/messenger-imessage"
|
||
ENV_FILE="$CONFIG_DIR/.env"
|
||
|
||
print_header() {
|
||
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||
echo -e "${CYAN} Messenger Service — Universal Installer${NC}"
|
||
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||
echo ""
|
||
}
|
||
|
||
print_step() { echo -e "${GREEN}▸${NC} $1"; }
|
||
print_info() { echo -e "${BLUE}ℹ${NC} $1"; }
|
||
print_warn() { echo -e "${YELLOW}⚠${NC} $1"; }
|
||
print_error() { echo -e "${RED}✗${NC} $1"; }
|
||
print_ok() { echo -e "${GREEN}✓${NC} $1"; }
|
||
|
||
# ─── Parse Arguments ─────────────────────────────────────────────────────────
|
||
|
||
MODE=""
|
||
DB_URL=""
|
||
LIFE_MANAGER_URL=""
|
||
API_KEY=""
|
||
SERVICE_PORT="3100"
|
||
|
||
while [[ $# -gt 0 ]]; do
|
||
case "$1" in
|
||
--mode) MODE="$2"; shift 2 ;;
|
||
--db-url) DB_URL="$2"; shift 2 ;;
|
||
--life-manager-url) LIFE_MANAGER_URL="$2"; shift 2 ;;
|
||
--api-key) API_KEY="$2"; shift 2 ;;
|
||
--port) SERVICE_PORT="$2"; shift 2 ;;
|
||
--help|-h)
|
||
echo "Usage: ./install.sh [options]"
|
||
echo ""
|
||
echo "Options:"
|
||
echo " --mode <shared|local|standalone> Database mode"
|
||
echo " --db-url <url> PostgreSQL connection URL"
|
||
echo " --life-manager-url <url> Life-manager API URL"
|
||
echo " --api-key <key> Service API key"
|
||
echo " --port <port> Service port (default: 3100)"
|
||
echo ""
|
||
echo "Without options, runs interactively."
|
||
exit 0
|
||
;;
|
||
*)
|
||
print_error "Unknown option: $1"
|
||
exit 1
|
||
;;
|
||
esac
|
||
done
|
||
|
||
# ─── Interactive Mode ────────────────────────────────────────────────────────
|
||
|
||
interactive_setup() {
|
||
echo "Select database mode:"
|
||
echo ""
|
||
echo " ${CYAN}1) shared${NC} — Use life-manager's PostgreSQL"
|
||
echo " (messenger tables alongside life-manager tables)"
|
||
echo ""
|
||
echo " ${CYAN}2) local${NC} — Local Docker PostgreSQL"
|
||
echo " (separate DB, optionally syncs to life-manager)"
|
||
echo ""
|
||
echo " ${CYAN}3) standalone${NC} — Fully standalone, no life-manager"
|
||
echo ""
|
||
read -p "Mode [1/2/3]: " mode_choice
|
||
|
||
case "$mode_choice" in
|
||
1|shared) MODE="shared" ;;
|
||
2|local) MODE="local" ;;
|
||
3|standalone) MODE="standalone" ;;
|
||
*)
|
||
print_error "Invalid choice"
|
||
exit 1
|
||
;;
|
||
esac
|
||
|
||
if [[ "$MODE" == "shared" ]]; then
|
||
echo ""
|
||
read -p "PostgreSQL URL [postgresql://lilith@localhost:25471/life_manager]: " DB_URL
|
||
DB_URL="${DB_URL:-postgresql://lilith@localhost:25471/life_manager}"
|
||
fi
|
||
|
||
if [[ "$MODE" != "standalone" ]]; then
|
||
echo ""
|
||
read -p "Life-manager API URL [http://localhost:3700]: " LIFE_MANAGER_URL
|
||
LIFE_MANAGER_URL="${LIFE_MANAGER_URL:-http://localhost:3700}"
|
||
fi
|
||
|
||
echo ""
|
||
read -p "Service port [$SERVICE_PORT]: " port_input
|
||
SERVICE_PORT="${port_input:-$SERVICE_PORT}"
|
||
|
||
echo ""
|
||
read -p "Service API key (leave blank to generate): " API_KEY
|
||
if [[ -z "$API_KEY" ]]; then
|
||
API_KEY=$(openssl rand -base64 32)
|
||
print_info "Generated API key: $API_KEY"
|
||
fi
|
||
}
|
||
|
||
# ─── Generate .env ───────────────────────────────────────────────────────────
|
||
|
||
generate_env() {
|
||
mkdir -p "$CONFIG_DIR"
|
||
|
||
local db_url="$DB_URL"
|
||
|
||
# Default DB URL based on mode
|
||
if [[ -z "$db_url" ]]; then
|
||
case "$MODE" in
|
||
shared) db_url="postgresql://lilith@localhost:25471/life_manager" ;;
|
||
local) db_url="postgresql://postgres:bnIP7MRFsr7nhfGg7Au16B0Ko7yd5H6ZDWiagRz4@localhost:25433/messenger" ;;
|
||
standalone) db_url="postgresql://postgres:bnIP7MRFsr7nhfGg7Au16B0Ko7yd5H6ZDWiagRz4@localhost:25433/messenger" ;;
|
||
esac
|
||
fi
|
||
|
||
cat > "$ENV_FILE" <<EOF
|
||
# Generated by install.sh — $(date)
|
||
# Mode: $MODE
|
||
|
||
# Database
|
||
DB_MODE=$MODE
|
||
DATABASE_URL=$db_url
|
||
|
||
# Service
|
||
PORT=$SERVICE_PORT
|
||
SERVICE_API_KEY=$API_KEY
|
||
NODE_ENV=production
|
||
|
||
# Life Manager (omit for standalone mode)
|
||
LIFE_MANAGER_API_URL=${LIFE_MANAGER_URL:-}
|
||
EOF
|
||
|
||
chmod 600 "$ENV_FILE"
|
||
print_ok "Generated $ENV_FILE"
|
||
}
|
||
|
||
# ─── Install Backend ─────────────────────────────────────────────────────────
|
||
|
||
install_backend() {
|
||
print_step "Installing backend dependencies..."
|
||
cd "$SCRIPT_DIR/imessage-sync/backend"
|
||
|
||
if command -v bun &>/dev/null; then
|
||
bun install --frozen-lockfile 2>/dev/null || bun install
|
||
print_ok "Dependencies installed (bun)"
|
||
elif command -v npm &>/dev/null; then
|
||
npm ci 2>/dev/null || npm install
|
||
print_ok "Dependencies installed (npm)"
|
||
else
|
||
print_error "Neither bun nor npm found"
|
||
exit 1
|
||
fi
|
||
|
||
# Build
|
||
print_step "Building backend..."
|
||
if command -v bun &>/dev/null; then
|
||
bun run build
|
||
else
|
||
npm run build
|
||
fi
|
||
print_ok "Backend built"
|
||
}
|
||
|
||
# ─── Local Docker (for local/standalone modes) ──────────────────────────────
|
||
|
||
setup_local_docker() {
|
||
if [[ "$MODE" == "shared" ]]; then
|
||
print_info "Shared mode — skipping local Docker setup"
|
||
return
|
||
fi
|
||
|
||
print_step "Starting local Docker containers..."
|
||
cd "$SCRIPT_DIR/imessage-sync"
|
||
docker compose up -d
|
||
print_ok "Local PostgreSQL + Redis running"
|
||
}
|
||
|
||
# ─── Create LaunchDaemon / systemd ───────────────────────────────────────────
|
||
|
||
create_service() {
|
||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||
create_launchd_service
|
||
elif command -v systemctl &>/dev/null; then
|
||
create_systemd_service
|
||
else
|
||
print_warn "No service manager detected. Start manually:"
|
||
print_info " cd $SCRIPT_DIR/imessage-sync/backend && node dist/main.js"
|
||
fi
|
||
}
|
||
|
||
create_launchd_service() {
|
||
local plist_dir="$HOME/Library/LaunchAgents"
|
||
local plist_file="$plist_dir/com.lilith.messenger-imessage.plist"
|
||
local backend_dir="$SCRIPT_DIR/imessage-sync/backend"
|
||
|
||
mkdir -p "$plist_dir"
|
||
|
||
cat > "$plist_file" <<EOF
|
||
<?xml version="1.0" encoding="UTF-8"?>
|
||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||
<plist version="1.0">
|
||
<dict>
|
||
<key>Label</key>
|
||
<string>com.lilith.messenger-imessage</string>
|
||
<key>ProgramArguments</key>
|
||
<array>
|
||
<string>$(which node)</string>
|
||
<string>$backend_dir/dist/main.js</string>
|
||
</array>
|
||
<key>WorkingDirectory</key>
|
||
<string>$backend_dir</string>
|
||
<key>EnvironmentVariables</key>
|
||
<dict>
|
||
<key>NODE_ENV</key>
|
||
<string>production</string>
|
||
<key>ENV_FILE</key>
|
||
<string>$ENV_FILE</string>
|
||
</dict>
|
||
<key>RunAtLoad</key>
|
||
<true/>
|
||
<key>KeepAlive</key>
|
||
<dict>
|
||
<key>SuccessfulExit</key>
|
||
<false/>
|
||
</dict>
|
||
<key>StandardOutPath</key>
|
||
<string>$CONFIG_DIR/stdout.log</string>
|
||
<key>StandardErrorPath</key>
|
||
<string>$CONFIG_DIR/stderr.log</string>
|
||
</dict>
|
||
</plist>
|
||
EOF
|
||
|
||
# Load service
|
||
launchctl unload "$plist_file" 2>/dev/null || true
|
||
launchctl load "$plist_file"
|
||
|
||
print_ok "LaunchAgent created and loaded"
|
||
print_info "Logs: tail -f $CONFIG_DIR/stderr.log"
|
||
}
|
||
|
||
create_systemd_service() {
|
||
local service_file="$HOME/.config/systemd/user/messenger-imessage.service"
|
||
local backend_dir="$SCRIPT_DIR/imessage-sync/backend"
|
||
|
||
mkdir -p "$(dirname "$service_file")"
|
||
|
||
cat > "$service_file" <<EOF
|
||
[Unit]
|
||
Description=Messenger iMessage Sync Service
|
||
After=network.target
|
||
|
||
[Service]
|
||
Type=simple
|
||
WorkingDirectory=$backend_dir
|
||
EnvironmentFile=$ENV_FILE
|
||
ExecStart=$(which node) dist/main.js
|
||
Restart=on-failure
|
||
RestartSec=5
|
||
|
||
[Install]
|
||
WantedBy=default.target
|
||
EOF
|
||
|
||
systemctl --user daemon-reload
|
||
systemctl --user enable messenger-imessage
|
||
systemctl --user start messenger-imessage
|
||
|
||
print_ok "systemd service created and started"
|
||
print_info "Logs: journalctl --user -u messenger-imessage -f"
|
||
}
|
||
|
||
# ─── Verify ──────────────────────────────────────────────────────────────────
|
||
|
||
verify_install() {
|
||
print_step "Verifying installation..."
|
||
sleep 3
|
||
|
||
local health_url="http://localhost:$SERVICE_PORT/health"
|
||
local response
|
||
response=$(curl -s --connect-timeout 5 "$health_url" 2>/dev/null || echo "")
|
||
|
||
if [[ -n "$response" ]]; then
|
||
print_ok "Service is running at http://localhost:$SERVICE_PORT"
|
||
else
|
||
print_warn "Service not responding yet (may still be starting)"
|
||
print_info "Check logs for details"
|
||
fi
|
||
}
|
||
|
||
# ─── Main ────────────────────────────────────────────────────────────────────
|
||
|
||
main() {
|
||
print_header
|
||
|
||
# Interactive if no mode specified
|
||
if [[ -z "$MODE" ]]; then
|
||
interactive_setup
|
||
fi
|
||
|
||
echo ""
|
||
print_info "Mode: $MODE"
|
||
print_info "Port: $SERVICE_PORT"
|
||
[[ -n "$LIFE_MANAGER_URL" ]] && print_info "Life Manager: $LIFE_MANAGER_URL"
|
||
echo ""
|
||
|
||
generate_env
|
||
setup_local_docker
|
||
install_backend
|
||
create_service
|
||
verify_install
|
||
|
||
echo ""
|
||
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||
echo -e "${GREEN} Installation Complete${NC}"
|
||
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||
echo ""
|
||
echo "Config: $ENV_FILE"
|
||
echo "Service: http://localhost:$SERVICE_PORT"
|
||
[[ -n "$LIFE_MANAGER_URL" ]] && echo "Life Manager: $LIFE_MANAGER_URL"
|
||
echo ""
|
||
echo "Next steps:"
|
||
echo " 1. Install the macOS agent: cd imessage-macos && ./install.sh http://localhost:$SERVICE_PORT"
|
||
echo " 2. Grant Full Disk Access to the agent"
|
||
echo ""
|
||
}
|
||
|
||
main
|