platform-codebase/features/conversation-assistant/macos/install.sh
Quinn Ftw 5e09cf7a8e fix(conversation-assistant): version generation and settings button
- 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>
2025-12-29 21:35:07 -08:00

426 lines
13 KiB
Bash
Executable file
Raw 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
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 "$@"