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:
parent
0fb3a1060a
commit
3cbbca29a6
8 changed files with 1483 additions and 0 deletions
106
imessage-macos/.swiftlint.yml
Executable file
106
imessage-macos/.swiftlint.yml
Executable 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
224
imessage-macos/INSTALL.md
Executable 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
30
imessage-macos/Package.swift
Executable 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
246
imessage-macos/deploy-remote.sh
Executable 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
|
||||
92
imessage-macos/generate-version.sh
Executable file
92
imessage-macos/generate-version.sh
Executable 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"
|
||||
31
imessage-macos/iMessageMacos.entitlements
Executable file
31
imessage-macos/iMessageMacos.entitlements
Executable 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
537
imessage-macos/install.sh
Executable 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
217
imessage-macos/uninstall.sh
Executable 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 "$@"
|
||||
Loading…
Add table
Reference in a new issue