refactor(imessage-macos/deploy): ♻️ Refactor macOS deployment scripts, upgrade dependencies, enforce SwiftLint, and update entitlements for improved workflow and code quality

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Lilith 2026-03-05 14:54:31 -08:00
parent 0fb3a1060a
commit 3cbbca29a6
8 changed files with 1483 additions and 0 deletions

106
imessage-macos/.swiftlint.yml Executable file
View file

@ -0,0 +1,106 @@
# iMessage macOS Agent - SwiftLint Configuration
# Extends shared tooling from @packages/@swift
# Note: parent_config with ~ paths doesn't work in CI Docker containers
# This file contains the full configuration for CI compatibility
# Opt-in rules for code quality
opt_in_rules:
- force_cast
- force_try
- force_unwrapping
- sorted_imports
- closure_spacing
- empty_count
- first_where
- flatmap_over_map_reduce
- last_where
- multiline_parameters
- operator_usage_whitespace
- redundant_nil_coalescing
- sorted_first_last
- toggle_bool
- vertical_parameter_alignment_on_call
- explicit_init
- fallthrough
- fatal_error_message
- function_default_parameter_at_end
- implicit_return
- joined_default_parameter
- prefer_self_type_over_type_of_self
- redundant_type_annotation
- unowned_variable_capture
- accessibility_label_for_image
- private_outlet
- discouraged_optional_collection
- discouraged_optional_boolean
# Disabled rules
disabled_rules:
- todo # Allow TODOs during development
- line_length # 120 char limit can be too restrictive
- file_length # Some service files are large
- type_name # Sometimes short names are appropriate
# Rule configurations
function_parameter_count:
warning: 6
error: 8
cyclomatic_complexity:
warning: 20
error: 30
type_body_length:
warning: 400
error: 600
function_body_length:
warning: 110
error: 175
identifier_name:
min_length:
warning: 2
max_length:
warning: 50
excluded:
- id
- x
- y
- z
- db
- ip
- ok
- i
- j
- k
- n
nesting:
type_level:
warning: 2
error: 3
function_level:
warning: 3
error: 4
# Custom rules for Lilith Platform conventions
custom_rules:
no_print_statements:
name: "No Print Statements"
regex: '^\s*print\s*\('
match_kinds:
- identifier
message: "Use NSLog() or os.Logger instead of print() for production code"
severity: warning
# Excluded paths
excluded:
- .build
- DerivedData
- "*.generated.swift"
- Package.swift
# Reporter type
reporter: "xcode"

224
imessage-macos/INSTALL.md Executable file
View file

@ -0,0 +1,224 @@
# iMessage macOS - Agent Installation
This directory contains the macOS menu bar agent that syncs iMessages to the iMessage Sync server.
## Prerequisites
- macOS 13.0 (Ventura) or later
- Swift 5.9+ (included with Xcode or Command Line Tools)
- Full Disk Access permission (for reading iMessage database)
## Installation
### Automatic Installation (Recommended)
The installer script handles the complete installation process:
```bash
./install.sh [server_url]
```
**Examples:**
```bash
# Interactive mode (prompts for server URL)
./install.sh
# With server URL
./install.sh http://localhost:3100
# Production server
./install.sh https://assistant.lilith.is
```
### What the Installer Does
1. **Builds the application** - Compiles Swift code with release optimizations
2. **Installs the binary** - Creates app bundle in `~/Applications/iMessageMacos.app`
3. **Configures auto-start** - Sets up LaunchAgent to run on login
4. **Stores configuration** - Saves server URL in macOS preferences
5. **Guides permissions** - Shows how to grant Full Disk Access
### Post-Installation Steps
1. **Grant Full Disk Access** (required for iMessage database access):
- Open System Settings → Privacy & Security → Full Disk Access
- Click the '+' button
- Navigate to `~/Applications/iMessageMacos.app`
- Enable the toggle
2. **Complete device registration**:
- Click the menu bar icon (💬) in the top-right
- Click "Settings"
- Follow the registration flow
- Enter the verification code shown on the server
3. **Verify sync is working**:
- Check the menu bar icon for sync status
- View logs: `tail -f ~/Library/Application\ Support/iMessageMacos/stderr.log`
## Manual Installation
If you prefer to install manually:
```bash
# 1. Build
swift build -c release
# 2. Create app bundle
mkdir -p ~/Applications/iMessageMacos.app/Contents/MacOS
cp .build/release/iMessageMacos ~/Applications/iMessageMacos.app/Contents/MacOS/
# 3. Set server URL
defaults write com.lilith.imessage-macos apiBaseURL "http://localhost:3100"
# 4. Create LaunchAgent (see install.sh for plist template)
# 5. Load LaunchAgent
launchctl load ~/Library/LaunchAgents/com.lilith.imessage-macos.plist
```
## Configuration
The agent stores configuration in macOS UserDefaults:
```bash
# View current configuration
defaults read com.lilith.imessage-macos
# Change server URL
defaults write com.lilith.imessage-macos apiBaseURL "https://new-server.example.com"
# Restart agent to apply changes
launchctl unload ~/Library/LaunchAgents/com.lilith.imessage-macos.plist
launchctl load ~/Library/LaunchAgents/com.lilith.imessage-macos.plist
```
Authentication tokens are stored securely in the macOS Keychain.
## Troubleshooting
### Agent not starting
```bash
# Check LaunchAgent status
launchctl list | grep imessage-macos
# View error logs
tail -f ~/Library/Application\ Support/iMessageMacos/stderr.log
# Manually start to see errors
~/Applications/iMessageMacos.app/Contents/MacOS/iMessageMacos
```
### Database access errors
The agent needs Full Disk Access to read the iMessage database at:
`~/Library/Messages/chat.db`
If you see errors like "unable to open database file", grant Full Disk Access:
1. System Settings → Privacy & Security → Full Disk Access
2. Add iMessageMacos.app
3. Restart the agent
### Build failures
```bash
# Clean and rebuild
rm -rf .build
swift build -c release
# Check Swift version (requires 5.9+)
swift --version
# Update dependencies
swift package update
```
### Network errors
```bash
# Test server connectivity
curl http://localhost:3100/health
# Check server URL configuration
defaults read com.lilith.imessage-macos apiBaseURL
```
## Uninstallation
The uninstaller removes all components:
```bash
./uninstall.sh
```
This removes:
- Application binary
- LaunchAgent configuration
- Application data and logs
- Preferences
- Keychain entries
You may need to manually remove Full Disk Access permission in System Settings.
## File Locations
| Component | Path |
|-----------|------|
| **Application** | `~/Applications/iMessageMacos.app` |
| **LaunchAgent** | `~/Library/LaunchAgents/com.lilith.imessage-macos.plist` |
| **Logs** | `~/Library/Application Support/iMessageMacos/*.log` |
| **Preferences** | `~/Library/Preferences/com.lilith.imessage-macos.plist` |
| **Keychain** | Keychain Access → "authToken" |
## Development
For development and testing:
```bash
# Run without installing
swift run
# Build debug version
swift build
# Run tests (when available)
swift test
# Clean build artifacts
swift package clean
```
## Architecture
The agent consists of:
- **Menu Bar UI** - SwiftUI-based menu bar icon and settings
- **iMessage Reader** - GRDB-based SQLite reader for Messages database
- **Sync Manager** - Periodic sync orchestrator (every 30 seconds)
- **API Client** - HTTP client for server communication (Alamofire)
See the source code in `Sources/` for implementation details.
## Security
- Auth tokens stored in macOS Keychain (secure storage)
- iMessage database accessed read-only
- No message content stored locally by the agent
- All sync traffic uses HTTPS in production
## Support
For issues or questions:
- Check logs in `~/Library/Application Support/iMessageMacos/`
- Review server logs for API errors
- Verify Full Disk Access is granted
- Ensure server URL is correct
---
**Last Updated**: 2026-03-03
**Platforms**: macOS 13.0+
**License**: Proprietary - Lilith Platform

30
imessage-macos/Package.swift Executable file
View file

@ -0,0 +1,30 @@
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "iMessageMacos",
platforms: [
.macOS(.v13)
],
dependencies: [
.package(url: "https://github.com/groue/GRDB.swift.git", from: "6.24.0"),
.package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.8.0"),
.package(url: "https://github.com/SwiftyJSON/SwiftyJSON.git", from: "5.0.0"),
.package(url: "https://github.com/SwiftGen/SwiftGenPlugin", from: "6.6.0"),
.package(url: "https://github.com/httpswift/swifter.git", from: "1.5.0"),
.package(path: "../../../@packages/@swift/@macos/menu-bar"),
],
targets: [
.executableTarget(
name: "iMessageMacos",
dependencies: [
.product(name: "GRDB", package: "GRDB.swift"),
.product(name: "Alamofire", package: "Alamofire"),
.product(name: "SwiftyJSON", package: "SwiftyJSON"),
.product(name: "Swifter", package: "swifter"),
.product(name: "LilithMenuBar", package: "menu-bar"),
],
path: "Sources"
),
]
)

246
imessage-macos/deploy-remote.sh Executable file
View file

@ -0,0 +1,246 @@
#!/bin/bash
set -euo pipefail
# Deploy iMessage macOS agent to plum (MacBook) via rsync + SSH.
#
# What this does:
# 1. Rsyncs the entire imessage-macos/ source to plum staging dir
# 2. SSHs into plum and runs install.sh (builds Swift + installs)
# 3. Verifies the agent is running
#
# Usage:
# ./deploy-remote.sh # deploy with default URL
# ./deploy-remote.sh https://conversations.nasty.sh # deploy with custom URL
# ./deploy-remote.sh uninstall # uninstall on plum
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
# Configuration
PLUM_HOST="${PLUM_HOST:-plum}"
PLUM_STAGING="imessage-macos-staging"
# Parse args: skip flags, use positional arg as server URL
NO_BUMP=false
SERVER_URL="http://10.0.0.11:3100"
for arg in "$@"; do
case "$arg" in
--no-bump) NO_BUMP=true ;;
uninstall|frontend) ;; # handled by case below
*) SERVER_URL="$arg" ;;
esac
done
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
print_step() { echo -e "${GREEN}${NC} $1"; }
print_info() { echo -e "${BLUE}${NC} $1"; }
print_error() { echo -e "${RED}${NC} $1"; }
print_success() { echo -e "${GREEN}${NC} $1"; }
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${BLUE} iMessage macOS - Remote Deployment${NC}"
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo ""
# ---------------------------------------------------------------------------
# Connectivity check
# ---------------------------------------------------------------------------
check_plum() {
print_step "Checking SSH connection to $PLUM_HOST..."
if ! ssh -o ConnectTimeout=5 "$PLUM_HOST" 'echo ok' >/dev/null 2>&1; then
print_error "Cannot connect to $PLUM_HOST"
echo "Make sure:"
echo " - The MacBook is on and connected to the network"
echo " - SSH config exists for host '$PLUM_HOST'"
exit 1
fi
print_success "Connected to $PLUM_HOST"
}
# ---------------------------------------------------------------------------
# Install
# ---------------------------------------------------------------------------
cmd_install() {
check_plum
# Rsync source to plum (excludes build artifacts)
# Mirror the local directory structure so relative Package.swift paths resolve correctly
local plum_base="$PLUM_STAGING"
local plum_app_dir="$plum_base/@applications/@messenger/imessage-macos"
print_step "Syncing source to $PLUM_HOST:~/$plum_app_dir/"
ssh "$PLUM_HOST" "mkdir -p ~/$plum_app_dir"
rsync -az --delete \
--exclude '.build/' \
--exclude 'DerivedData/' \
--exclude '.swiftpm/' \
"$SCRIPT_DIR/" \
"$PLUM_HOST:~/$plum_app_dir/"
# Sync local package dependencies referenced by relative paths in Package.swift
# ../../../@packages/@swift/@macos/menu-bar -> @packages/@swift/@macos/menu-bar
local local_packages_root
local_packages_root="$(cd "$SCRIPT_DIR/../../../@packages/@swift/@macos" && pwd)"
local plum_packages_dir="$plum_base/@packages/@swift/@macos"
if [[ -d "$local_packages_root/menu-bar" ]]; then
print_step "Syncing local Swift package dependencies..."
ssh "$PLUM_HOST" "mkdir -p ~/$plum_packages_dir"
rsync -az --delete \
--exclude '.build/' \
"$local_packages_root/menu-bar/" \
"$PLUM_HOST:~/$plum_packages_dir/menu-bar/"
print_success "Local packages synced"
fi
# Sync frontend-macos-client (webapp served by the local web server)
local frontend_src="$SCRIPT_DIR/../frontend-macos-client"
local plum_frontend_dir="$plum_base/@applications/@messenger/frontend-macos-client"
if [[ -d "$frontend_src" ]]; then
print_step "Syncing webapp frontend..."
ssh "$PLUM_HOST" "mkdir -p ~/$plum_frontend_dir"
rsync -az --delete \
"$frontend_src/" \
"$PLUM_HOST:~/$plum_frontend_dir/"
print_success "Frontend synced"
fi
print_success "Source synced"
# Bump build number in shared VERSION.json (unless --no-bump passed by ./run)
local version_file="$SCRIPT_DIR/../VERSION.json"
if [[ "$NO_BUMP" != "true" ]] && [[ -f "$version_file" ]] && command -v jq &>/dev/null; then
print_step "Bumping build version..."
local builds=$(jq '.builds' "$version_file")
local new_builds=$((builds + 1))
local major=$(jq -r '.major' "$version_file")
local minor=$(jq -r '.minor' "$version_file")
local new_version="${major}.${minor}.${new_builds}"
local today=$(date +%Y-%m-%d)
jq --arg v "$new_version" --argjson b "$new_builds" --arg d "$today" \
'.builds = $b | .version = $v | .lastBuild = $d' \
"$version_file" > "${version_file}.tmp" && mv "${version_file}.tmp" "$version_file"
print_success "Version: $new_version"
fi
# Sync VERSION.json to plum for generate-version.sh
if [[ -f "$version_file" ]]; then
rsync -az "$version_file" "$PLUM_HOST:~/$plum_app_dir/VERSION.json"
rsync -az "$version_file" "$PLUM_HOST:~/$plum_base/@applications/@messenger/VERSION.json"
fi
# Run installer on plum
print_step "Building and installing on $PLUM_HOST..."
ssh -t "$PLUM_HOST" "cd ~/$plum_app_dir && chmod +x install.sh && ./install.sh '$SERVER_URL'"
# Verify agent is running
print_step "Checking agent status..."
ssh "$PLUM_HOST" bash << 'CHECK_SCRIPT'
sleep 2
if pgrep -x iMessageMacos >/dev/null; then
echo "✓ Agent is running"
echo ""
echo "Recent logs:"
tail -10 ~/Library/Application\ Support/iMessageMacos/stderr.log 2>/dev/null || echo "(no logs yet)"
else
echo "⚠ Agent is not running (may need Full Disk Access)"
echo ""
echo "Grant Full Disk Access:"
echo " System Settings → Privacy & Security → Full Disk Access"
echo " Add ~/Applications/iMessageMacos.app"
fi
CHECK_SCRIPT
# Clean up staging
print_step "Cleaning up staging directory..."
ssh "$PLUM_HOST" "rm -rf ~/$plum_base"
print_success "Staging cleaned"
echo ""
print_success "Deployment complete!"
echo ""
print_info "View logs: ssh $PLUM_HOST 'tail -f ~/Library/Application\\ Support/iMessageMacos/stderr.log'"
}
# ---------------------------------------------------------------------------
# Uninstall
# ---------------------------------------------------------------------------
cmd_uninstall() {
check_plum
local plum_base="$PLUM_STAGING"
# Rsync just the uninstall script
print_step "Syncing uninstaller to $PLUM_HOST..."
ssh "$PLUM_HOST" "mkdir -p ~/$plum_base"
rsync -az "$SCRIPT_DIR/uninstall.sh" "$PLUM_HOST:~/$plum_base/"
print_step "Running uninstaller on $PLUM_HOST..."
ssh -t "$PLUM_HOST" "cd ~/$plum_base && chmod +x uninstall.sh && ./uninstall.sh"
ssh "$PLUM_HOST" "rm -rf ~/$plum_base"
print_success "Staging cleaned"
echo ""
print_success "iMessage macOS uninstalled from $PLUM_HOST"
}
# ---------------------------------------------------------------------------
# Frontend-only deploy (no rebuild, no re-sign, preserves TCC permissions)
# ---------------------------------------------------------------------------
cmd_frontend() {
check_plum
local frontend_src="$SCRIPT_DIR/../frontend-macos-client"
local app_webapp="Applications/iMessageMacos.app/Contents/Resources/webapp"
if [[ ! -d "$frontend_src" ]]; then
print_error "Frontend source not found at $frontend_src"
exit 1
fi
print_step "Syncing webapp to $PLUM_HOST:~/$app_webapp/"
rsync -az \
"$frontend_src/" \
"$PLUM_HOST:~/$app_webapp/"
print_success "Frontend synced"
# Restart agent to pick up new files (kill → LaunchAgent auto-restarts)
print_step "Restarting agent..."
ssh "$PLUM_HOST" "pkill -x iMessageMacos 2>/dev/null || true"
sleep 2
if ssh "$PLUM_HOST" "pgrep -x iMessageMacos >/dev/null"; then
print_success "Agent restarted"
else
print_error "Agent did not restart — check LaunchAgent"
fi
echo ""
print_success "Frontend deploy complete (no re-sign, TCC preserved)"
}
# ---------------------------------------------------------------------------
# Entry
# ---------------------------------------------------------------------------
case "${1:-}" in
uninstall)
cmd_uninstall
;;
frontend)
cmd_frontend
;;
*)
cmd_install
;;
esac

View file

@ -0,0 +1,92 @@
#!/bin/bash
#
# Regenerate AppVersion.swift from VERSION.json
#
# Run this before building the macOS app:
# ./generate-version.sh && swift build
#
# This script:
# 1. Reads VERSION.json (bumped by CI, not this script)
# 2. Generates Sources/AppVersion.swift with current values
#
# NOTE: CI bumps VERSION.json on each build. This script is READ-ONLY.
# To manually bump versions, use: ./workflow/version build
#
# Reads: VERSION.json (searched up from script dir, or falls back to defaults)
# Writes: Sources/AppVersion.swift
#
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
OUTPUT_FILE="$SCRIPT_DIR/Sources/AppVersion.swift"
# Search for VERSION.json upward from script directory
VERSION_FILE=""
search_dir="$SCRIPT_DIR"
while [[ "$search_dir" != "/" ]]; do
if [[ -f "$search_dir/VERSION.json" ]]; then
VERSION_FILE="$search_dir/VERSION.json"
break
fi
search_dir="$(dirname "$search_dir")"
done
# If VERSION.json found, parse it; otherwise use defaults
if [[ -n "$VERSION_FILE" ]]; then
MAJOR=$(jq -r '.major' "$VERSION_FILE")
MERGES=$(jq -r '.merges // 0' "$VERSION_FILE")
BUILDS=$(jq -r '.builds' "$VERSION_FILE")
VERSION=$(jq -r '.version' "$VERSION_FILE")
LAST_BUILD=$(jq -r '.lastBuild' "$VERSION_FILE")
else
echo "Warning: VERSION.json not found, using defaults" >&2
MAJOR=0
MERGES=0
BUILDS=0
VERSION="0.0.0"
LAST_BUILD="unknown"
fi
# Get git info
GIT_COMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown")
GIT_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
# Generate Swift file
cat > "$OUTPUT_FILE" << EOF
//
// AppVersion.swift
// Generated from VERSION.json - DO NOT EDIT MANUALLY
//
// Regenerate with: ./generate-version.sh
//
import Foundation
enum AppVersion {
static let version = "$VERSION"
static let major = $MAJOR
static let merges = $MERGES
static let builds = $BUILDS
static let gitCommit = "$GIT_COMMIT"
static let gitBranch = "$GIT_BRANCH"
static let buildTime = "$LAST_BUILD"
static var displayVersion: String {
"v\(version)"
}
static var fullVersion: String {
"Version \(version) (\(gitCommit))"
}
static var detailedVersion: String {
"v\(version) (\(gitCommit) on \(gitBranch))"
}
}
EOF
echo "Generated: $OUTPUT_FILE"
echo " Version: $VERSION"
echo " Commit: $GIT_COMMIT"

View file

@ -0,0 +1,31 @@
<?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>
<!-- Allow access to Contacts -->
<key>com.apple.security.personal-information.addressbook</key>
<true/>
<!-- Allow access to files in Full Disk Access locations (iMessage database) -->
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
<!-- Allow network access for API communication -->
<key>com.apple.security.network.client</key>
<true/>
<!-- Allow network server for local web interface -->
<key>com.apple.security.network.server</key>
<true/>
<!-- Allow AppleScript automation to send messages via Messages.app -->
<key>com.apple.security.automation.apple-events</key>
<true/>
<!-- Allow keychain access for storing auth tokens -->
<key>com.apple.security.keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)com.lilith.imessage-macos</string>
</array>
</dict>
</plist>

537
imessage-macos/install.sh Executable file
View file

@ -0,0 +1,537 @@
#!/bin/bash
set -euo pipefail
# iMessage macOS - Agent Installer
# This script installs and configures the iMessage macOS 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="iMessageMacos"
BUNDLE_ID="com.lilith.imessage-macos"
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 )"
MESSENGER_ROOT="$( cd "$SCRIPT_DIR/.." && pwd )"
# Functions
print_header() {
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${BLUE} iMessage 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"
}
get_default_port() {
local default="$1"
echo "$default"
}
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"
mkdir -p "$INSTALL_DIR/$APP_NAME.app/Contents/Resources/webapp"
# Copy binary
cp "$binary_path" "$install_path/$APP_NAME"
chmod +x "$install_path/$APP_NAME"
# Copy webapp files
local webapp_src="$SCRIPT_DIR/../frontend-macos-client"
local webapp_dest="$INSTALL_DIR/$APP_NAME.app/Contents/Resources/webapp"
if [[ -d "$webapp_src" ]]; then
print_info "Copying webapp files..."
cp "$webapp_src/index.html" "$webapp_dest/"
cp "$webapp_src/styles.css" "$webapp_dest/"
cp "$webapp_src/app.js" "$webapp_dest/"
print_success "Webapp bundled into app"
else
print_warning "Webapp source not found at $webapp_src"
fi
# 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>iMessage macOS</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleVersion</key>
<string>1.0.0</string>
<key>LSUIElement</key>
<true/>
<key>NSHighResolutionCapable</key>
<true/>
<key>NSContactsUsageDescription</key>
<string>iMessage macOS needs access to your Contacts to display names for your iMessage conversations.</string>
<key>NSAppleEventsUsageDescription</key>
<string>iMessage macOS needs to control the Messages app to send iMessages on your behalf.</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
</dict>
</plist>
EOF
# Copy entitlements file
local entitlements_src="$SCRIPT_DIR/iMessageMacos.entitlements"
if [[ -f "$entitlements_src" ]]; then
cp "$entitlements_src" "$INSTALL_DIR/$APP_NAME.app/Contents/Resources/"
print_info "Copied entitlements file"
fi
# Code sign the app with entitlements
print_step "Code signing application..."
if [[ -f "$entitlements_src" ]]; then
# Sign with entitlements for TCC permissions to work
codesign --force --deep --sign - \
--entitlements "$entitlements_src" \
"$INSTALL_DIR/$APP_NAME.app"
if [[ $? -eq 0 ]]; then
print_success "Code signed with entitlements"
else
print_warning "Code signing failed - TCC permissions may not work"
fi
else
# Fallback to basic signing
codesign --force --deep --sign - "$INSTALL_DIR/$APP_NAME.app"
print_warning "Code signed without entitlements (entitlements file not found)"
fi
print_success "Installed to $INSTALL_DIR/$APP_NAME.app"
}
discover_service_url() {
# Try to discover imessage-sync 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=imessage-sync&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 imessage-sync..."
server_url=$(discover_service_url) || true
if [[ -n "$server_url" ]]; then
print_success "Discovered service at: $server_url"
else
echo ""
print_info "Enter the iMessage Sync 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"
# Configure web server port from ports.yaml (or default)
local webserver_port
webserver_port=$(get_default_port "8765")
defaults write "$BUNDLE_ID" webServerPort -int "$webserver_port"
print_success "Server URL configured: $server_url"
print_success "Web server port configured: $webserver_port"
}
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 "iMessage macOS 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
}
request_contacts_permission() {
echo ""
echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${YELLOW} IMPORTANT: Grant Contacts Access${NC}"
echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo ""
echo "iMessage macOS needs Contacts access to display names for conversations."
echo ""
print_info "Launching app to request Contacts permission..."
echo ""
# Launch the app via 'open' which gives it proper GUI context for permission dialogs
# Run briefly to trigger the contacts permission request
open "$INSTALL_DIR/$APP_NAME.app"
# Wait a moment for the permission dialog to appear
sleep 3
echo "A permission dialog should appear asking for Contacts access."
echo "Please click 'OK' or 'Allow' to grant access."
echo ""
if [[ -t 0 ]]; then
read -p "Press Enter once you've granted Contacts access (or 'n' to skip): " contacts_response
if [[ "$contacts_response" == "n" || "$contacts_response" == "N" ]]; then
print_warning "Skipped Contacts permission. You can grant it later in System Settings."
else
print_success "Contacts permission requested"
fi
else
# Non-interactive - just wait and continue
sleep 5
print_info "If a permission dialog appeared, please grant access."
fi
# Stop the app so LaunchAgent can manage it
pkill -x "$APP_NAME" 2>/dev/null || true
sleep 1
}
register_device() {
print_step "Starting the agent for device registration..."
echo ""
print_info "The iMessage macOS 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 to open the web interface"
echo " 2. A browser will open showing the device registration page"
echo " 3. Enter the verification code shown in your admin panel"
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 "iMessage macOS 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
# Detect fresh install vs update
local IS_FRESH_INSTALL="true"
if [[ -f "$PLIST_FILE" ]]; then
IS_FRESH_INSTALL="false"
print_info "Existing installation detected — updating"
fi
# Installation steps
stop_existing_agent
build_application
install_binary
configure_app "$server_url"
create_launch_agent
# Post-installation
if check_installation; then
# Only show permission prompts on fresh install (no existing LaunchAgent)
if [[ "$IS_FRESH_INSTALL" == "true" ]]; then
show_permissions_guide
request_contacts_permission
register_device
fi
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 "$@"

217
imessage-macos/uninstall.sh Executable file
View file

@ -0,0 +1,217 @@
#!/bin/bash
set -euo pipefail
# iMessage macOS - Agent Uninstaller
# This script removes the iMessage macOS 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="iMessageMacos"
BUNDLE_ID="com.lilith.imessage-macos"
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"
PREFS_FILE="$HOME/Library/Preferences/$BUNDLE_ID.plist"
# Functions
print_header() {
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${BLUE} iMessage macOS - Uninstaller${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"
}
confirm_uninstall() {
echo -e "${YELLOW}This will remove:${NC}"
echo " • Application binary: $INSTALL_DIR/$APP_NAME.app"
echo " • LaunchAgent: $PLIST_FILE"
echo " • Application data: $APP_SUPPORT_DIR"
echo " • Preferences: $PREFS_FILE"
echo " • Keychain entries (auth tokens)"
echo ""
read -p "Are you sure you want to uninstall? [y/N]: " confirm
if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then
echo ""
print_info "Uninstall cancelled"
exit 0
fi
}
stop_agent() {
print_step "Stopping agent..."
# Unload LaunchAgent
if [[ -f "$PLIST_FILE" ]]; then
launchctl unload "$PLIST_FILE" 2>/dev/null || true
print_success "Unloaded LaunchAgent"
fi
# Kill any running instances
if pgrep -x "$APP_NAME" > /dev/null; then
pkill -x "$APP_NAME" 2>/dev/null || true
sleep 1
print_success "Stopped running processes"
fi
}
remove_files() {
print_step "Removing files..."
local removed_count=0
# Remove application
if [[ -d "$INSTALL_DIR/$APP_NAME.app" ]]; then
rm -rf "$INSTALL_DIR/$APP_NAME.app"
print_success "Removed application"
removed_count=$((removed_count + 1))
fi
# Remove LaunchAgent plist
if [[ -f "$PLIST_FILE" ]]; then
rm -f "$PLIST_FILE"
print_success "Removed LaunchAgent"
removed_count=$((removed_count + 1))
fi
# Remove application support directory
if [[ -d "$APP_SUPPORT_DIR" ]]; then
rm -rf "$APP_SUPPORT_DIR"
print_success "Removed application data"
removed_count=$((removed_count + 1))
fi
# Remove preferences
if [[ -f "$PREFS_FILE" ]]; then
rm -f "$PREFS_FILE"
print_success "Removed preferences"
removed_count=$((removed_count + 1))
fi
# Also remove defaults domain
defaults delete "$BUNDLE_ID" 2>/dev/null || true
if [[ $removed_count -eq 0 ]]; then
print_warning "No files found to remove"
fi
}
remove_keychain_entries() {
print_step "Removing keychain entries..."
# The app stores auth token in keychain with account "authToken"
security delete-generic-password -a "authToken" -s "$BUNDLE_ID" 2>/dev/null || true
print_success "Cleared keychain entries"
}
verify_removal() {
print_step "Verifying removal..."
local remaining=0
# Check if any files remain
if [[ -d "$INSTALL_DIR/$APP_NAME.app" ]]; then
print_warning "Application still exists"
remaining=$((remaining + 1))
fi
if [[ -f "$PLIST_FILE" ]]; then
print_warning "LaunchAgent still exists"
remaining=$((remaining + 1))
fi
if [[ -d "$APP_SUPPORT_DIR" ]]; then
print_warning "Application data still exists"
remaining=$((remaining + 1))
fi
if pgrep -x "$APP_NAME" > /dev/null; then
print_warning "Process still running"
remaining=$((remaining + 1))
fi
if [[ $remaining -eq 0 ]]; then
print_success "All components removed successfully"
return 0
else
return 1
fi
}
print_completion() {
echo ""
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "${GREEN} Uninstall Complete${NC}"
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo ""
echo "iMessage macOS has been removed from this system."
echo ""
echo "Note: You may need to manually remove Full Disk Access permission:"
echo " System Settings → Privacy & Security → Full Disk Access"
echo " Find '$APP_NAME' and remove it from the list"
echo ""
echo "To reinstall, run: ./install.sh"
echo ""
}
# Main uninstall flow
main() {
print_header
# Check if running on macOS
if [[ "$OSTYPE" != "darwin"* ]]; then
print_error "This script is designed for macOS only"
exit 1
fi
# Confirm with user
confirm_uninstall
echo ""
# Uninstall steps
stop_agent
remove_files
remove_keychain_entries
# Verification
if verify_removal; then
print_completion
else
echo ""
print_warning "Uninstall completed with warnings. Some components may remain."
exit 1
fi
}
# Run main function
main "$@"