2026-05-15 17:05:13 -07:00
|
|
|
|
#!/bin/bash
|
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
|
|
|
|
|
|
|
# MacSync — macOS client installer.
|
|
|
|
|
|
# Builds Swift app, creates app bundle, installs LaunchAgent com.lilith.mac-sync.
|
|
|
|
|
|
#
|
|
|
|
|
|
# Usage (run on plum):
|
|
|
|
|
|
# ./install.sh [SERVER_URL]
|
|
|
|
|
|
# SERVER_URL defaults to http://10.0.0.11:3201
|
|
|
|
|
|
|
|
|
|
|
|
APP_NAME="MacSyncApp"
|
|
|
|
|
|
BUNDLE_ID="com.lilith.mac-sync"
|
|
|
|
|
|
DISPLAY_NAME="MacSync"
|
|
|
|
|
|
INSTALL_DIR="$HOME/Applications"
|
|
|
|
|
|
APP_BUNDLE="$INSTALL_DIR/$DISPLAY_NAME.app"
|
|
|
|
|
|
APP_SUPPORT_DIR="$HOME/Library/Application Support/$DISPLAY_NAME"
|
|
|
|
|
|
LAUNCH_AGENTS_DIR="$HOME/Library/LaunchAgents"
|
|
|
|
|
|
PLIST_FILE="$LAUNCH_AGENTS_DIR/$BUNDLE_ID.plist"
|
|
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
|
|
|
MAC_SYNC_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
|
|
|
|
|
|
|
|
|
|
RED='\033[0;31m'
|
|
|
|
|
|
GREEN='\033[0;32m'
|
|
|
|
|
|
YELLOW='\033[1;33m'
|
|
|
|
|
|
BLUE='\033[0;34m'
|
|
|
|
|
|
NC='\033[0m'
|
|
|
|
|
|
|
|
|
|
|
|
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"; }
|
|
|
|
|
|
|
|
|
|
|
|
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
|
|
|
|
|
echo -e "${BLUE} MacSync — Unified macOS Sync Agent Installer${NC}"
|
|
|
|
|
|
echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
|
|
|
|
|
echo ""
|
|
|
|
|
|
|
|
|
|
|
|
SERVER_URL="${1:-http://10.0.0.11:3201}"
|
|
|
|
|
|
|
|
|
|
|
|
check_macos() {
|
|
|
|
|
|
if [[ "$OSTYPE" != "darwin"* ]]; then
|
|
|
|
|
|
print_error "This script is macOS only"
|
|
|
|
|
|
exit 1
|
|
|
|
|
|
fi
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
check_swift() {
|
|
|
|
|
|
if ! command -v swift &>/dev/null; then
|
|
|
|
|
|
print_error "Swift not found — install Xcode or Command Line Tools: xcode-select --install"
|
|
|
|
|
|
exit 1
|
|
|
|
|
|
fi
|
|
|
|
|
|
print_info "Swift: $(swift --version | head -n1)"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
stop_existing_agent() {
|
|
|
|
|
|
print_step "Stopping existing agent (if running)..."
|
|
|
|
|
|
if [[ -f "$PLIST_FILE" ]]; then
|
|
|
|
|
|
launchctl unload "$PLIST_FILE" 2>/dev/null || true
|
|
|
|
|
|
fi
|
|
|
|
|
|
pkill -x "$APP_NAME" 2>/dev/null || true
|
|
|
|
|
|
sleep 1
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
build_application() {
|
|
|
|
|
|
print_step "Building Swift application (release)..."
|
|
|
|
|
|
cd "$MAC_SYNC_ROOT"
|
|
|
|
|
|
rm -rf .build
|
|
|
|
|
|
if swift build -c release; then
|
|
|
|
|
|
print_success "Build successful"
|
|
|
|
|
|
else
|
|
|
|
|
|
print_error "Swift build failed"
|
|
|
|
|
|
exit 1
|
|
|
|
|
|
fi
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
install_binary() {
|
|
|
|
|
|
print_step "Installing app bundle..."
|
|
|
|
|
|
|
|
|
|
|
|
local binary_src="$MAC_SYNC_ROOT/.build/release/$APP_NAME"
|
|
|
|
|
|
if [[ ! -f "$binary_src" ]]; then
|
|
|
|
|
|
print_error "Binary not found: $binary_src"
|
|
|
|
|
|
exit 1
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
local macos_dir="$APP_BUNDLE/Contents/MacOS"
|
|
|
|
|
|
local resources_dir="$APP_BUNDLE/Contents/Resources"
|
|
|
|
|
|
local webapp_dir="$resources_dir/webapp"
|
|
|
|
|
|
|
|
|
|
|
|
mkdir -p "$macos_dir" "$resources_dir" "$webapp_dir"
|
|
|
|
|
|
# Clean stale resource bundles from previous installs
|
|
|
|
|
|
rm -rf "$macos_dir"/*.bundle "$resources_dir"/*.bundle
|
|
|
|
|
|
|
|
|
|
|
|
cp "$binary_src" "$macos_dir/$APP_NAME"
|
|
|
|
|
|
chmod +x "$macos_dir/$APP_NAME"
|
|
|
|
|
|
|
|
|
|
|
|
# Copy SPM resource bundles (e.g. tray-resources_LilithTrayResources.bundle)
|
|
|
|
|
|
local build_dir="$MAC_SYNC_ROOT/.build/release"
|
|
|
|
|
|
for bundle in "$build_dir"/*.bundle; do
|
|
|
|
|
|
[[ -d "$bundle" ]] || continue
|
|
|
|
|
|
cp -R "$bundle" "$resources_dir/$(basename "$bundle")"
|
|
|
|
|
|
print_info "Bundled: $(basename "$bundle")"
|
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
|
|
# Bundle Vite webapp dist — pre-built by deploy-remote.sh, or build locally
|
|
|
|
|
|
local web_dist="$MAC_SYNC_ROOT/web/dist"
|
|
|
|
|
|
if [[ -d "$web_dist" ]]; then
|
|
|
|
|
|
cp -R "$web_dist/"* "$webapp_dir/"
|
|
|
|
|
|
print_success "Webapp bundled"
|
|
|
|
|
|
else
|
|
|
|
|
|
print_warning "web/dist not found — webapp not bundled (run: cd web && bun run build)"
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
# Write Info.plist from template (substitute BUILD + VERSION from VERSION.json)
|
|
|
|
|
|
local version="1.0.0"
|
|
|
|
|
|
local build_num="1"
|
|
|
|
|
|
local version_file="$MAC_SYNC_ROOT/VERSION.json"
|
|
|
|
|
|
if [[ -f "$version_file" ]] && command -v python3 &>/dev/null; then
|
|
|
|
|
|
version=$(python3 -c "import json,sys; d=json.load(open('$version_file')); print(d.get('version','1.0.0'))" 2>/dev/null || echo "1.0.0")
|
|
|
|
|
|
build_num=$(python3 -c "import json,sys; d=json.load(open('$version_file')); print(d.get('builds',1))" 2>/dev/null || echo "1")
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
local template="$MAC_SYNC_ROOT/src/client/Resources/Info.plist.template"
|
|
|
|
|
|
if [[ -f "$template" ]]; then
|
|
|
|
|
|
sed -e "s/{{VERSION}}/$version/g" -e "s/{{BUILD}}/$build_num/g" \
|
|
|
|
|
|
"$template" > "$APP_BUNDLE/Contents/Info.plist"
|
|
|
|
|
|
else
|
|
|
|
|
|
cat > "$APP_BUNDLE/Contents/Info.plist" <<PLIST
|
|
|
|
|
|
<?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>$DISPLAY_NAME</string>
|
|
|
|
|
|
<key>CFBundlePackageType</key>
|
|
|
|
|
|
<string>APPL</string>
|
|
|
|
|
|
<key>CFBundleVersion</key>
|
|
|
|
|
|
<string>$build_num</string>
|
|
|
|
|
|
<key>CFBundleShortVersionString</key>
|
|
|
|
|
|
<string>$version</string>
|
|
|
|
|
|
<key>LSUIElement</key>
|
|
|
|
|
|
<true/>
|
|
|
|
|
|
<key>NSHighResolutionCapable</key>
|
|
|
|
|
|
<true/>
|
|
|
|
|
|
<key>NSContactsUsageDescription</key>
|
|
|
|
|
|
<string>MacSync syncs your contacts to your personal server.</string>
|
|
|
|
|
|
<key>NSPhotoLibraryUsageDescription</key>
|
|
|
|
|
|
<string>MacSync reads your photo library to sync metadata and originals to your personal server.</string>
|
|
|
|
|
|
<key>NSAppleEventsUsageDescription</key>
|
|
|
|
|
|
<string>MacSync reads Mail.app messages and sends iMessages to sync to your personal server.</string>
|
|
|
|
|
|
<key>NSAppTransportSecurity</key>
|
|
|
|
|
|
<dict>
|
|
|
|
|
|
<key>NSAllowsLocalNetworking</key>
|
|
|
|
|
|
<true/>
|
|
|
|
|
|
</dict>
|
|
|
|
|
|
</dict>
|
|
|
|
|
|
</plist>
|
|
|
|
|
|
PLIST
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
print_success "Installed to $APP_BUNDLE"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-15 17:05:39 -07:00
|
|
|
|
# Stable code-signing identity for TCC-grant persistence across rebuilds.
|
|
|
|
|
|
# Preference order:
|
|
|
|
|
|
# 1. $MAC_SYNC_SIGNING_IDENTITY if set
|
|
|
|
|
|
# 2. Self-signed identity with CN="Quinn Norton" if present in keychain
|
|
|
|
|
|
# 3. Auto-create the "Quinn Norton" self-signed identity (requires interactive session)
|
|
|
|
|
|
# Never pick up Apple Development certs automatically — those may carry a
|
|
|
|
|
|
# legacy Apple ID / email we don't want showing up in `codesign -dv`.
|
|
|
|
|
|
SIGNING_IDENTITY_PREFERRED="${MAC_SYNC_SIGNING_IDENTITY:-}"
|
|
|
|
|
|
SIGNING_IDENTITY_FALLBACK="Quinn Norton"
|
|
|
|
|
|
SIGNING_IDENTITY=""
|
|
|
|
|
|
# Dedicated keychain so deployments over SSH don't need the user's login
|
|
|
|
|
|
# password. The password below is embedded intentionally — this keychain only
|
|
|
|
|
|
# ever holds the self-signed code-signing cert and nothing secret.
|
|
|
|
|
|
MAC_SYNC_KEYCHAIN_PW="quinn-unlock"
|
|
|
|
|
|
MAC_SYNC_KEYCHAIN_PATH="$HOME/Library/Keychains/macsync-signing.keychain-db"
|
|
|
|
|
|
|
|
|
|
|
|
ensure_signing_identity() {
|
|
|
|
|
|
# 1. Prefer an explicit identity passed via $MAC_SYNC_SIGNING_IDENTITY env var.
|
|
|
|
|
|
if [[ -n "$SIGNING_IDENTITY_PREFERRED" ]] && \
|
|
|
|
|
|
security find-identity -p codesigning -v 2>/dev/null | grep -q "$SIGNING_IDENTITY_PREFERRED"; then
|
|
|
|
|
|
SIGNING_IDENTITY="$SIGNING_IDENTITY_PREFERRED"
|
|
|
|
|
|
print_success "Using preferred signing identity: $SIGNING_IDENTITY"
|
|
|
|
|
|
return 0
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
# 2. Prefer the self-signed "Quinn Norton" identity.
|
|
|
|
|
|
SIGNING_IDENTITY="$SIGNING_IDENTITY_FALLBACK"
|
2026-05-17 23:41:30 -07:00
|
|
|
|
# Count how many signing identities are in the dedicated keychain.
|
|
|
|
|
|
# NOTE: we deliberately omit -v (valid-only filter). The self-signed
|
|
|
|
|
|
# cert is not in System trust settings — find-identity -v returns 0
|
|
|
|
|
|
# for it, which would loop-regenerate the cert every install and
|
|
|
|
|
|
# invalidate TCC grants (FDA, AppleEvents, etc.) bound to the old
|
|
|
|
|
|
# cert hash. Without -v we still match the cert by label and identifier.
|
2026-05-15 17:05:39 -07:00
|
|
|
|
local identity_count
|
2026-05-17 23:41:30 -07:00
|
|
|
|
identity_count=$(security find-identity -p codesigning "$MAC_SYNC_KEYCHAIN_PATH" 2>/dev/null | grep -c "\"$SIGNING_IDENTITY\"" || true)
|
2026-05-15 17:05:39 -07:00
|
|
|
|
|
|
|
|
|
|
if [[ "$identity_count" -eq 1 ]]; then
|
|
|
|
|
|
print_success "Code-signing identity '$SIGNING_IDENTITY' already in keychain"
|
|
|
|
|
|
return 0
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
print_step "Creating self-signed code-signing identity '$SIGNING_IDENTITY'..."
|
|
|
|
|
|
|
|
|
|
|
|
# Recreate the keychain to remove duplicates or start fresh (identity count was 0 or >1).
|
|
|
|
|
|
if [[ -f "$MAC_SYNC_KEYCHAIN_PATH" ]]; then
|
|
|
|
|
|
security delete-keychain "$MAC_SYNC_KEYCHAIN_PATH" 2>/dev/null || true
|
|
|
|
|
|
rm -f "$MAC_SYNC_KEYCHAIN_PATH"
|
|
|
|
|
|
fi
|
|
|
|
|
|
security create-keychain -p "$MAC_SYNC_KEYCHAIN_PW" "macsync-signing.keychain"
|
|
|
|
|
|
security unlock-keychain -p "$MAC_SYNC_KEYCHAIN_PW" "$MAC_SYNC_KEYCHAIN_PATH"
|
|
|
|
|
|
# No auto-lock timeout for this keychain (holds only a public self-signed cert).
|
|
|
|
|
|
# Requires interactive context on some macOS versions — best-effort.
|
|
|
|
|
|
security set-keychain-settings -t 0 -l "$MAC_SYNC_KEYCHAIN_PATH" 2>/dev/null || \
|
|
|
|
|
|
print_info "(skipped set-keychain-settings — keychain will auto-lock on timeout)"
|
|
|
|
|
|
# Add to the user search list so codesign can find the identity.
|
|
|
|
|
|
local existing_keychains
|
|
|
|
|
|
existing_keychains=$(security list-keychains -d user | tr -d '"' | xargs)
|
|
|
|
|
|
if ! echo "$existing_keychains" | grep -q "macsync-signing.keychain"; then
|
|
|
|
|
|
security list-keychains -d user -s $existing_keychains "$MAC_SYNC_KEYCHAIN_PATH"
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
local tmp_dir
|
|
|
|
|
|
tmp_dir=$(mktemp -d)
|
|
|
|
|
|
trap "rm -rf '$tmp_dir'" RETURN
|
|
|
|
|
|
cat > "$tmp_dir/cert.conf" <<EOF
|
|
|
|
|
|
[req]
|
|
|
|
|
|
distinguished_name = req_dn
|
|
|
|
|
|
prompt = no
|
|
|
|
|
|
x509_extensions = v3_req
|
|
|
|
|
|
[req_dn]
|
|
|
|
|
|
CN = $SIGNING_IDENTITY
|
|
|
|
|
|
O = $SIGNING_IDENTITY
|
|
|
|
|
|
emailAddress = quinn90210@pm.me
|
|
|
|
|
|
[v3_req]
|
|
|
|
|
|
basicConstraints = CA:false
|
|
|
|
|
|
keyUsage = digitalSignature
|
|
|
|
|
|
extendedKeyUsage = codeSigning
|
|
|
|
|
|
EOF
|
|
|
|
|
|
openssl req -x509 -newkey rsa:2048 \
|
|
|
|
|
|
-keyout "$tmp_dir/key.pem" -out "$tmp_dir/cert.pem" \
|
|
|
|
|
|
-days 3650 -nodes -config "$tmp_dir/cert.conf" 2>&1 | grep -v '\-\-\-\-\-' || true
|
|
|
|
|
|
# Separate imports — LibreSSL's p12 output fails MAC verification in
|
|
|
|
|
|
# macOS's `security import`, so we skip pkcs12.
|
|
|
|
|
|
# -A allows all apps to access without ACL prompts.
|
|
|
|
|
|
# Order matters: import the PRIVATE KEY first, then the cert. macOS pairs
|
|
|
|
|
|
# them at cert-import time by matching the cert's public key against
|
|
|
|
|
|
# already-imported keys. Reversed order leaves "Imported Private Key" with
|
|
|
|
|
|
# no cert attached → `security find-identity` won't see it as an identity,
|
|
|
|
|
|
# and codesign fails with errSecInternalComponent.
|
|
|
|
|
|
# Keep unlocked with long timeout so imports + partition-list run without prompts.
|
|
|
|
|
|
security unlock-keychain -p "$MAC_SYNC_KEYCHAIN_PW" "$MAC_SYNC_KEYCHAIN_PATH"
|
|
|
|
|
|
security set-keychain-settings -t 3600 "$MAC_SYNC_KEYCHAIN_PATH" 2>/dev/null || true
|
|
|
|
|
|
security import "$tmp_dir/key.pem" -k "$MAC_SYNC_KEYCHAIN_PATH" -A 2>/dev/null || true
|
|
|
|
|
|
security import "$tmp_dir/cert.pem" -k "$MAC_SYNC_KEYCHAIN_PATH" -A
|
|
|
|
|
|
# Grant codesign access to the private key — must happen while keychain is unlocked.
|
|
|
|
|
|
security unlock-keychain -p "$MAC_SYNC_KEYCHAIN_PW" "$MAC_SYNC_KEYCHAIN_PATH"
|
|
|
|
|
|
security set-key-partition-list \
|
|
|
|
|
|
-S "apple-tool:,apple:,codesign:" \
|
|
|
|
|
|
-s -k "$MAC_SYNC_KEYCHAIN_PW" \
|
|
|
|
|
|
"$MAC_SYNC_KEYCHAIN_PATH" >/dev/null 2>&1 || true
|
|
|
|
|
|
print_success "Identity '$SIGNING_IDENTITY' created in dedicated keychain"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-15 17:05:13 -07:00
|
|
|
|
code_sign() {
|
2026-05-15 17:05:39 -07:00
|
|
|
|
ensure_signing_identity
|
2026-05-21 22:03:45 -07:00
|
|
|
|
# Add the signing keychain to the search list so `codesign` can resolve
|
|
|
|
|
|
# the identity by name (the --keychain flag alone is not enough). It is
|
|
|
|
|
|
# removed again after signing — left in the search list it hangs the
|
|
|
|
|
|
# agent's own startup keychain reads, since it auto-locks.
|
|
|
|
|
|
security list-keychains -d user -s "$HOME/Library/Keychains/login.keychain-db" "$MAC_SYNC_KEYCHAIN_PATH"
|
2026-05-15 17:05:39 -07:00
|
|
|
|
# Keep the signing keychain unlocked through the codesign call.
|
|
|
|
|
|
security unlock-keychain -p "$MAC_SYNC_KEYCHAIN_PW" "$MAC_SYNC_KEYCHAIN_PATH" 2>/dev/null || true
|
|
|
|
|
|
security set-keychain-settings -t 300 "$MAC_SYNC_KEYCHAIN_PATH" 2>/dev/null || true
|
|
|
|
|
|
|
2026-05-15 17:05:13 -07:00
|
|
|
|
print_step "Code signing with entitlements..."
|
|
|
|
|
|
local entitlements="$MAC_SYNC_ROOT/src/client/Resources/LilithMacSync.entitlements"
|
|
|
|
|
|
if [[ ! -f "$entitlements" ]]; then
|
|
|
|
|
|
print_error "Entitlements file not found: $entitlements"
|
|
|
|
|
|
exit 1
|
|
|
|
|
|
fi
|
2026-05-15 17:05:39 -07:00
|
|
|
|
# Sign directly using the dedicated keychain (explicitly unlocked above).
|
|
|
|
|
|
# No launchctl asuser needed — codesign --keychain works over SSH as long
|
|
|
|
|
|
# as the keychain is unlocked and the partition list grants codesign access.
|
2026-05-21 22:03:45 -07:00
|
|
|
|
local sign_ok=true
|
2026-05-15 17:05:39 -07:00
|
|
|
|
if codesign --force --deep --sign "$SIGNING_IDENTITY" \
|
|
|
|
|
|
--keychain "$MAC_SYNC_KEYCHAIN_PATH" \
|
|
|
|
|
|
--entitlements "$entitlements" \
|
|
|
|
|
|
"$APP_BUNDLE" 2>&1; then
|
|
|
|
|
|
print_success "Signed with '$SIGNING_IDENTITY' (stable identity — TCC grants persist)"
|
|
|
|
|
|
else
|
2026-05-21 22:03:45 -07:00
|
|
|
|
sign_ok=false
|
|
|
|
|
|
fi
|
|
|
|
|
|
# Restore the search list to just the login keychain (whether or not
|
|
|
|
|
|
# signing succeeded) — leaving the signing keychain in it hangs the
|
|
|
|
|
|
# agent's startup keychain reads.
|
|
|
|
|
|
security list-keychains -d user -s "$HOME/Library/Keychains/login.keychain-db"
|
|
|
|
|
|
print_info "Signing keychain removed from search list"
|
|
|
|
|
|
if [[ "$sign_ok" != "true" ]]; then
|
2026-05-15 17:05:39 -07:00
|
|
|
|
print_error "Code signing failed — TCC grants will be lost. Check keychain."
|
|
|
|
|
|
exit 1
|
|
|
|
|
|
fi
|
2026-05-15 17:05:13 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
configure_app() {
|
|
|
|
|
|
print_step "Configuring app defaults..."
|
|
|
|
|
|
mkdir -p "$APP_SUPPORT_DIR"
|
|
|
|
|
|
|
|
|
|
|
|
if [[ ! "$SERVER_URL" =~ ^https?:// ]]; then
|
|
|
|
|
|
print_error "SERVER_URL must start with http:// or https://"
|
|
|
|
|
|
exit 1
|
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
defaults write "$BUNDLE_ID" serverURL "$SERVER_URL"
|
|
|
|
|
|
defaults write "$BUNDLE_ID" webServerPort -int 8765
|
|
|
|
|
|
print_success "serverURL: $SERVER_URL"
|
|
|
|
|
|
print_success "webServerPort: 8765"
|
|
|
|
|
|
|
|
|
|
|
|
# Drop a discoverable config file at ~/.config/com.lilith.mac-sync/config.json
|
|
|
|
|
|
# so users can see + edit settings in plain text. App reads this at launch
|
|
|
|
|
|
# and merges into UserDefaults (ConfigFile.load()), so this file wins over
|
|
|
|
|
|
# prior `defaults write` values.
|
|
|
|
|
|
local config_dir="$HOME/.config/com.lilith.mac-sync"
|
|
|
|
|
|
local config_file="$config_dir/config.json"
|
|
|
|
|
|
if [[ ! -f "$config_file" ]]; then
|
|
|
|
|
|
mkdir -p "$config_dir"
|
|
|
|
|
|
cat > "$config_file" <<JSON
|
|
|
|
|
|
{
|
|
|
|
|
|
"serverURL": "$SERVER_URL",
|
|
|
|
|
|
"webServerPort": 8765
|
|
|
|
|
|
}
|
|
|
|
|
|
JSON
|
|
|
|
|
|
print_success "config: $config_file"
|
|
|
|
|
|
else
|
|
|
|
|
|
print_info "config: $config_file (preserved — edit to change settings)"
|
|
|
|
|
|
fi
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
create_launch_agent() {
|
|
|
|
|
|
print_step "Creating LaunchAgent $BUNDLE_ID..."
|
|
|
|
|
|
mkdir -p "$LAUNCH_AGENTS_DIR"
|
|
|
|
|
|
cat > "$PLIST_FILE" <<PLIST
|
|
|
|
|
|
<?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>$APP_BUNDLE/Contents/MacOS/$APP_NAME</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>
|
|
|
|
|
|
PLIST
|
|
|
|
|
|
launchctl load "$PLIST_FILE"
|
|
|
|
|
|
print_success "LaunchAgent loaded — auto-starts on login"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
verify_install() {
|
|
|
|
|
|
print_step "Verifying installation..."
|
|
|
|
|
|
local errors=0
|
|
|
|
|
|
[[ -f "$APP_BUNDLE/Contents/MacOS/$APP_NAME" ]] && print_success "Binary present" || { print_error "Binary missing"; errors=$((errors+1)); }
|
|
|
|
|
|
[[ -f "$PLIST_FILE" ]] && print_success "LaunchAgent present" || { print_error "LaunchAgent missing"; errors=$((errors+1)); }
|
|
|
|
|
|
pgrep -x "$APP_NAME" >/dev/null && print_success "Agent running" || print_warning "Agent not yet running (may need TCC permissions)"
|
|
|
|
|
|
return $errors
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
show_permissions_guide() {
|
|
|
|
|
|
echo ""
|
|
|
|
|
|
echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
|
|
|
|
|
echo -e "${YELLOW} Grant macOS Permissions${NC}"
|
|
|
|
|
|
echo -e "${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
|
|
|
|
|
echo ""
|
|
|
|
|
|
echo "Open System Settings → Privacy & Security and grant $DISPLAY_NAME:"
|
|
|
|
|
|
echo " • Full Disk Access (required for iMessage chat.db)"
|
|
|
|
|
|
echo " • Contacts"
|
|
|
|
|
|
echo " • Photos"
|
|
|
|
|
|
echo " • Automation → Messages + Mail"
|
|
|
|
|
|
echo ""
|
|
|
|
|
|
echo "App bundle: $APP_BUNDLE"
|
|
|
|
|
|
echo ""
|
|
|
|
|
|
if [[ -t 0 ]]; then
|
|
|
|
|
|
read -rp "Press Enter to open System Settings → Full Disk Access (or 'n' to skip): " resp
|
|
|
|
|
|
if [[ "${resp:-}" != "n" && "${resp:-}" != "N" ]]; then
|
|
|
|
|
|
open "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles" 2>/dev/null || \
|
|
|
|
|
|
open "/System/Applications/System Settings.app"
|
|
|
|
|
|
fi
|
|
|
|
|
|
fi
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
print_completion() {
|
|
|
|
|
|
echo ""
|
|
|
|
|
|
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
|
|
|
|
|
echo -e "${GREEN} Installation complete${NC}"
|
|
|
|
|
|
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
|
|
|
|
|
echo ""
|
|
|
|
|
|
echo " Logs: tail -f $APP_SUPPORT_DIR/stderr.log"
|
|
|
|
|
|
echo " Restart: launchctl unload $PLIST_FILE && launchctl load $PLIST_FILE"
|
|
|
|
|
|
echo " Status: http://localhost:8765/"
|
|
|
|
|
|
echo ""
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
main() {
|
|
|
|
|
|
check_macos
|
|
|
|
|
|
check_swift
|
|
|
|
|
|
|
|
|
|
|
|
local IS_FRESH_INSTALL="true"
|
|
|
|
|
|
[[ -f "$PLIST_FILE" ]] && IS_FRESH_INSTALL="false" && print_info "Updating existing install"
|
|
|
|
|
|
|
|
|
|
|
|
stop_existing_agent
|
|
|
|
|
|
build_application
|
|
|
|
|
|
install_binary
|
|
|
|
|
|
code_sign
|
|
|
|
|
|
configure_app
|
|
|
|
|
|
create_launch_agent
|
|
|
|
|
|
|
|
|
|
|
|
if verify_install; then
|
|
|
|
|
|
[[ "$IS_FRESH_INSTALL" == "true" ]] && show_permissions_guide
|
|
|
|
|
|
print_completion
|
|
|
|
|
|
else
|
|
|
|
|
|
echo ""
|
|
|
|
|
|
print_error "Installation completed with errors — review output above"
|
|
|
|
|
|
exit 1
|
|
|
|
|
|
fi
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
main "$@"
|