- Add ./generate-version.sh call in install.sh before swift build
to ensure AppVersion.swift is regenerated with latest VERSION.json
- Fix openSettings() selector syntax: remove extra parentheses from
Selector(("...")) and add fallback for macOS 12 and earlier
- Remove test comment
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
426 lines
13 KiB
Bash
Executable file
426 lines
13 KiB
Bash
Executable file
#!/bin/bash
|
||
set -euo pipefail
|
||
|
||
# Conversation Assistant - macOS Agent Installer
|
||
# This script installs and configures the Conversation Assistant menu bar agent
|
||
|
||
# Colors for output
|
||
RED='\033[0;31m'
|
||
GREEN='\033[0;32m'
|
||
YELLOW='\033[1;33m'
|
||
BLUE='\033[0;34m'
|
||
NC='\033[0m' # No Color
|
||
|
||
# Configuration
|
||
APP_NAME="ConversationAssistant"
|
||
BUNDLE_ID="com.lilith.conversation-assistant"
|
||
INSTALL_DIR="$HOME/Applications"
|
||
APP_SUPPORT_DIR="$HOME/Library/Application Support/$APP_NAME"
|
||
LAUNCH_AGENTS_DIR="$HOME/Library/LaunchAgents"
|
||
PLIST_FILE="$LAUNCH_AGENTS_DIR/$BUNDLE_ID.plist"
|
||
|
||
# Script directory (where this script lives)
|
||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||
|
||
# Functions
|
||
print_header() {
|
||
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||
echo -e "${BLUE} Conversation Assistant - macOS Agent Installer${NC}"
|
||
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||
echo ""
|
||
}
|
||
|
||
print_step() {
|
||
echo -e "${GREEN}▸${NC} $1"
|
||
}
|
||
|
||
print_info() {
|
||
echo -e "${BLUE}ℹ${NC} $1"
|
||
}
|
||
|
||
print_warning() {
|
||
echo -e "${YELLOW}⚠${NC} $1"
|
||
}
|
||
|
||
print_error() {
|
||
echo -e "${RED}✗${NC} $1"
|
||
}
|
||
|
||
print_success() {
|
||
echo -e "${GREEN}✓${NC} $1"
|
||
}
|
||
|
||
check_macos() {
|
||
if [[ "$OSTYPE" != "darwin"* ]]; then
|
||
print_error "This script is designed for macOS only"
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
check_swift() {
|
||
if ! command -v swift &> /dev/null; then
|
||
print_error "Swift is not installed. Please install Xcode or Command Line Tools."
|
||
echo ""
|
||
echo "To install Command Line Tools:"
|
||
echo " xcode-select --install"
|
||
exit 1
|
||
fi
|
||
|
||
local swift_version=$(swift --version | head -n1)
|
||
print_info "Found: $swift_version"
|
||
}
|
||
|
||
stop_existing_agent() {
|
||
print_step "Stopping existing agent (if running)..."
|
||
|
||
# Unload LaunchAgent if it exists
|
||
if [[ -f "$PLIST_FILE" ]]; then
|
||
launchctl unload "$PLIST_FILE" 2>/dev/null || true
|
||
print_info "Stopped LaunchAgent"
|
||
fi
|
||
|
||
# Kill any running instances
|
||
pkill -x "$APP_NAME" 2>/dev/null || true
|
||
sleep 1
|
||
}
|
||
|
||
build_application() {
|
||
print_step "Building Swift application..."
|
||
|
||
cd "$SCRIPT_DIR"
|
||
|
||
# Generate version file from VERSION.json
|
||
print_info "Generating version info..."
|
||
if [[ -x "./generate-version.sh" ]]; then
|
||
./generate-version.sh
|
||
else
|
||
print_warning "generate-version.sh not found or not executable, skipping version generation"
|
||
fi
|
||
|
||
# Clean previous builds
|
||
if [[ -d ".build" ]]; then
|
||
rm -rf .build
|
||
fi
|
||
|
||
# Build release version
|
||
print_info "Compiling with optimizations (this may take a minute)..."
|
||
if swift build -c release; then
|
||
print_success "Build successful"
|
||
else
|
||
print_error "Build failed"
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
install_binary() {
|
||
print_step "Installing application..."
|
||
|
||
# Create installation directory
|
||
mkdir -p "$INSTALL_DIR"
|
||
|
||
# Copy binary
|
||
local binary_path="$SCRIPT_DIR/.build/release/$APP_NAME"
|
||
local install_path="$INSTALL_DIR/$APP_NAME.app/Contents/MacOS"
|
||
|
||
if [[ ! -f "$binary_path" ]]; then
|
||
print_error "Binary not found at $binary_path"
|
||
exit 1
|
||
fi
|
||
|
||
# Create app bundle structure
|
||
mkdir -p "$install_path"
|
||
mkdir -p "$INSTALL_DIR/$APP_NAME.app/Contents/Resources"
|
||
|
||
# Copy binary
|
||
cp "$binary_path" "$install_path/$APP_NAME"
|
||
chmod +x "$install_path/$APP_NAME"
|
||
|
||
# Create Info.plist
|
||
cat > "$INSTALL_DIR/$APP_NAME.app/Contents/Info.plist" <<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>CFBundleExecutable</key>
|
||
<string>$APP_NAME</string>
|
||
<key>CFBundleIdentifier</key>
|
||
<string>$BUNDLE_ID</string>
|
||
<key>CFBundleName</key>
|
||
<string>Conversation Assistant</string>
|
||
<key>CFBundlePackageType</key>
|
||
<string>APPL</string>
|
||
<key>CFBundleVersion</key>
|
||
<string>1.0.0</string>
|
||
<key>LSUIElement</key>
|
||
<true/>
|
||
<key>NSHighResolutionCapable</key>
|
||
<true/>
|
||
</dict>
|
||
</plist>
|
||
EOF
|
||
|
||
print_success "Installed to $INSTALL_DIR/$APP_NAME.app"
|
||
}
|
||
|
||
discover_service_url() {
|
||
# Try to discover conversation-assistant service from service-registry
|
||
local registry_url="${SERVICE_REGISTRY_URL:-http://localhost:30000}"
|
||
|
||
# Try to discover the service
|
||
local discovery_response
|
||
discovery_response=$(curl -s --connect-timeout 2 "$registry_url/registry/discover?serviceName=conversation-assistant&healthy=true" 2>/dev/null || echo "")
|
||
|
||
if [[ -n "$discovery_response" && "$discovery_response" != *"error"* ]]; then
|
||
# Parse the first instance's URL from JSON response
|
||
local service_url
|
||
service_url=$(echo "$discovery_response" | python3 -c "
|
||
import sys, json
|
||
try:
|
||
data = json.load(sys.stdin)
|
||
if data.get('instances') and len(data['instances']) > 0:
|
||
inst = data['instances'][0]
|
||
ip = inst.get('ipAddress', 'localhost')
|
||
port = inst.get('port', 3105)
|
||
print(f'http://{ip}:{port}')
|
||
except:
|
||
pass
|
||
" 2>/dev/null)
|
||
|
||
if [[ -n "$service_url" ]]; then
|
||
echo "$service_url"
|
||
return 0
|
||
fi
|
||
fi
|
||
|
||
return 1
|
||
}
|
||
|
||
configure_app() {
|
||
print_step "Configuring application..."
|
||
|
||
# Create application support directory
|
||
mkdir -p "$APP_SUPPORT_DIR"
|
||
|
||
# Get server URL from: 1) argument, 2) service-registry, 3) user input, 4) default
|
||
local server_url="${1:-}"
|
||
|
||
if [[ -z "$server_url" ]]; then
|
||
print_info "Checking service registry for conversation-assistant..."
|
||
server_url=$(discover_service_url) || true
|
||
|
||
if [[ -n "$server_url" ]]; then
|
||
print_success "Discovered service at: $server_url"
|
||
else
|
||
echo ""
|
||
print_info "Enter the Conversation Assistant server URL"
|
||
print_info "(Production: https://conversations.nasty.sh)"
|
||
read -p "Server URL [https://conversations.nasty.sh]: " server_url
|
||
server_url=${server_url:-https://conversations.nasty.sh}
|
||
fi
|
||
fi
|
||
|
||
# Validate URL format
|
||
if [[ ! "$server_url" =~ ^https?:// ]]; then
|
||
print_error "Invalid URL format. Must start with http:// or https://"
|
||
exit 1
|
||
fi
|
||
|
||
# Store in UserDefaults via defaults command
|
||
defaults write "$BUNDLE_ID" apiBaseURL "$server_url"
|
||
|
||
print_success "Server URL configured: $server_url"
|
||
}
|
||
|
||
create_launch_agent() {
|
||
print_step "Creating LaunchAgent for auto-start..."
|
||
|
||
mkdir -p "$LAUNCH_AGENTS_DIR"
|
||
|
||
local binary_path="$INSTALL_DIR/$APP_NAME.app/Contents/MacOS/$APP_NAME"
|
||
|
||
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>$BUNDLE_ID</string>
|
||
<key>ProgramArguments</key>
|
||
<array>
|
||
<string>$binary_path</string>
|
||
</array>
|
||
<key>RunAtLoad</key>
|
||
<true/>
|
||
<key>KeepAlive</key>
|
||
<dict>
|
||
<key>SuccessfulExit</key>
|
||
<false/>
|
||
<key>Crashed</key>
|
||
<true/>
|
||
</dict>
|
||
<key>StandardOutPath</key>
|
||
<string>$APP_SUPPORT_DIR/stdout.log</string>
|
||
<key>StandardErrorPath</key>
|
||
<string>$APP_SUPPORT_DIR/stderr.log</string>
|
||
<key>EnvironmentVariables</key>
|
||
<dict>
|
||
<key>PATH</key>
|
||
<string>/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
|
||
</dict>
|
||
</dict>
|
||
</plist>
|
||
EOF
|
||
|
||
# Load the LaunchAgent
|
||
launchctl load "$PLIST_FILE"
|
||
|
||
print_success "LaunchAgent created and loaded"
|
||
print_info "The agent will start automatically on login"
|
||
}
|
||
|
||
show_permissions_guide() {
|
||
echo ""
|
||
echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||
echo -e "${YELLOW} IMPORTANT: Grant Full Disk Access${NC}"
|
||
echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||
echo ""
|
||
echo "The Conversation Assistant requires Full Disk Access to read the iMessage database."
|
||
echo ""
|
||
echo "To grant access:"
|
||
echo " 1. System Settings will open to Full Disk Access"
|
||
echo " 2. Click the '+' button"
|
||
echo " 3. Navigate to: $INSTALL_DIR/$APP_NAME.app"
|
||
echo " 4. Select the app and click 'Open'"
|
||
echo " 5. Enable the toggle next to $APP_NAME"
|
||
echo ""
|
||
|
||
# Check if running interactively (has a TTY)
|
||
if [[ -t 0 ]]; then
|
||
read -p "Press Enter to open System Settings, or 'n' to skip: " open_settings
|
||
if [[ "$open_settings" != "n" && "$open_settings" != "N" ]]; then
|
||
open_fda_settings
|
||
fi
|
||
else
|
||
# Non-interactive mode (SSH batch) - auto-open settings
|
||
print_info "Opening System Settings → Full Disk Access..."
|
||
open_fda_settings
|
||
fi
|
||
}
|
||
|
||
open_fda_settings() {
|
||
# Open System Settings to Full Disk Access pane (macOS Ventura+)
|
||
# Use osascript to ensure it opens in the user's GUI session (works via SSH)
|
||
osascript -e 'tell application "System Settings"
|
||
activate
|
||
delay 0.5
|
||
tell application "System Events"
|
||
keystroke "Full Disk Access"
|
||
delay 0.3
|
||
key code 36
|
||
end tell
|
||
end tell' 2>/dev/null || \
|
||
open "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles" 2>/dev/null || \
|
||
open "/System/Applications/System Settings.app" 2>/dev/null
|
||
}
|
||
|
||
register_device() {
|
||
print_step "Starting the agent for device registration..."
|
||
|
||
echo ""
|
||
print_info "The Conversation Assistant agent is now running in your menu bar."
|
||
print_info "Look for the message bubble icon (💬) in the top-right of your screen."
|
||
echo ""
|
||
print_info "To complete setup:"
|
||
echo " 1. Click the menu bar icon"
|
||
echo " 2. Click 'Settings'"
|
||
echo " 3. Follow the device registration flow"
|
||
echo " 4. Enter the verification code shown on the server"
|
||
echo ""
|
||
}
|
||
|
||
check_installation() {
|
||
print_step "Verifying installation..."
|
||
|
||
local errors=0
|
||
|
||
# Check binary exists
|
||
if [[ -f "$INSTALL_DIR/$APP_NAME.app/Contents/MacOS/$APP_NAME" ]]; then
|
||
print_success "Binary installed"
|
||
else
|
||
print_error "Binary not found"
|
||
errors=$((errors + 1))
|
||
fi
|
||
|
||
# Check LaunchAgent exists
|
||
if [[ -f "$PLIST_FILE" ]]; then
|
||
print_success "LaunchAgent configured"
|
||
else
|
||
print_error "LaunchAgent not found"
|
||
errors=$((errors + 1))
|
||
fi
|
||
|
||
# Check if agent is running
|
||
if pgrep -x "$APP_NAME" > /dev/null; then
|
||
print_success "Agent is running"
|
||
else
|
||
print_warning "Agent is not running yet (may need permissions)"
|
||
fi
|
||
|
||
return $errors
|
||
}
|
||
|
||
print_completion() {
|
||
echo ""
|
||
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||
echo -e "${GREEN} Installation Complete${NC}"
|
||
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||
echo ""
|
||
echo "The Conversation Assistant has been successfully installed."
|
||
echo ""
|
||
echo "Next steps:"
|
||
echo " 1. Grant Full Disk Access (see instructions above)"
|
||
echo " 2. Click the menu bar icon and complete device registration"
|
||
echo " 3. Your iMessages will automatically sync to the server"
|
||
echo ""
|
||
echo "Useful commands:"
|
||
echo " View logs: tail -f $APP_SUPPORT_DIR/stderr.log"
|
||
echo " Restart agent: launchctl unload $PLIST_FILE && launchctl load $PLIST_FILE"
|
||
echo " Uninstall: ./uninstall.sh"
|
||
echo ""
|
||
}
|
||
|
||
# Main installation flow
|
||
main() {
|
||
print_header
|
||
|
||
# Parse arguments
|
||
local server_url=""
|
||
if [[ $# -gt 0 ]]; then
|
||
server_url="$1"
|
||
fi
|
||
|
||
# Pre-flight checks
|
||
check_macos
|
||
check_swift
|
||
|
||
# Installation steps
|
||
stop_existing_agent
|
||
build_application
|
||
install_binary
|
||
configure_app "$server_url"
|
||
create_launch_agent
|
||
|
||
# Post-installation
|
||
if check_installation; then
|
||
show_permissions_guide
|
||
register_device
|
||
print_completion
|
||
else
|
||
echo ""
|
||
print_error "Installation completed with errors. Please review the output above."
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
# Run main function with all arguments
|
||
main "$@"
|