Initial commit — Apple Apps MCP server with native macOS config app
Python MCP server (FastMCP) providing Claude Code/CoWork access to Apple Reminders, Calendar, Mail, Contacts, Find My, and Maps via AppleScript. Includes a native SwiftUI config/installer app and a compiled EventKit helper for fast reminder queries on large databases (8,000+ items). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,494 @@
|
||||
import SwiftUI
|
||||
import AppKit
|
||||
import Foundation
|
||||
|
||||
// MARK: - Data Model
|
||||
|
||||
struct AppConfig: Codable, Identifiable {
|
||||
let id: String
|
||||
var enabled: Bool
|
||||
var mode: String
|
||||
|
||||
var isReadWrite: Bool {
|
||||
get { mode == "read-write" }
|
||||
set { mode = newValue ? "read-write" : "read" }
|
||||
}
|
||||
}
|
||||
|
||||
struct ConfigFile: Codable {
|
||||
var apps: [String: AppEntry]
|
||||
|
||||
struct AppEntry: Codable {
|
||||
var enabled: Bool
|
||||
var mode: String
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - App Metadata
|
||||
|
||||
struct AppInfo {
|
||||
let key: String
|
||||
let name: String
|
||||
let icon: String
|
||||
let description: String
|
||||
}
|
||||
|
||||
let appList: [AppInfo] = [
|
||||
AppInfo(key: "reminders", name: "Reminders", icon: "checklist", description: "Create, complete, search, and manage reminders"),
|
||||
AppInfo(key: "calendar", name: "Calendar", icon: "calendar", description: "View, create, and manage calendar events"),
|
||||
AppInfo(key: "mail", name: "Mail", icon: "envelope", description: "Read, search, send, and organize email"),
|
||||
AppInfo(key: "contacts", name: "Contacts", icon: "person.crop.circle", description: "Search, view, create, and update contacts"),
|
||||
AppInfo(key: "findmy", name: "Find My", icon: "location.circle", description: "Locate Apple devices and AirTags"),
|
||||
AppInfo(key: "maps", name: "Maps", icon: "map", description: "Search locations, get directions, drop pins"),
|
||||
]
|
||||
|
||||
// MARK: - Path Helpers
|
||||
|
||||
let serverInstallDir: URL = {
|
||||
let home = FileManager.default.homeDirectoryForCurrentUser
|
||||
return home
|
||||
.appendingPathComponent("Library/Mobile Documents/com~apple~CloudDocs/Claude Working Folder/apple-mcp")
|
||||
}()
|
||||
|
||||
let configPath: URL = serverInstallDir.appendingPathComponent("config.json")
|
||||
let venvPython: URL = serverInstallDir.appendingPathComponent(".venv/bin/python")
|
||||
let claudeJsonPath: URL = {
|
||||
let home = FileManager.default.homeDirectoryForCurrentUser
|
||||
return home.appendingPathComponent(".claude.json")
|
||||
}()
|
||||
|
||||
let claudeDesktopConfigPath: URL = {
|
||||
let home = FileManager.default.homeDirectoryForCurrentUser
|
||||
return home.appendingPathComponent("Library/Application Support/Claude/claude_desktop_config.json")
|
||||
}()
|
||||
|
||||
// MARK: - Config Manager
|
||||
|
||||
class ConfigManager: ObservableObject {
|
||||
@Published var configs: [String: AppConfig] = [:]
|
||||
@Published var isInstalled = false
|
||||
@Published var isInstalling = false
|
||||
@Published var installLog = ""
|
||||
@Published var installError: String? = nil
|
||||
@Published var pythonVersion: String = "Not installed"
|
||||
|
||||
init() {
|
||||
checkInstallation()
|
||||
loadConfig()
|
||||
}
|
||||
|
||||
func checkInstallation() {
|
||||
let fm = FileManager.default
|
||||
isInstalled = fm.fileExists(atPath: venvPython.path)
|
||||
&& fm.fileExists(atPath: configPath.path)
|
||||
|
||||
if isInstalled {
|
||||
let task = Process()
|
||||
let pipe = Pipe()
|
||||
task.executableURL = venvPython
|
||||
task.arguments = ["--version"]
|
||||
task.standardOutput = pipe
|
||||
task.standardError = pipe
|
||||
try? task.run()
|
||||
task.waitUntilExit()
|
||||
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
||||
pythonVersion = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func loadConfig() {
|
||||
guard FileManager.default.fileExists(atPath: configPath.path) else { return }
|
||||
guard let data = try? Data(contentsOf: configPath),
|
||||
let configFile = try? JSONDecoder().decode(ConfigFile.self, from: data) else { return }
|
||||
|
||||
var result: [String: AppConfig] = [:]
|
||||
for (key, entry) in configFile.apps {
|
||||
result[key] = AppConfig(id: key, enabled: entry.enabled, mode: entry.mode)
|
||||
}
|
||||
configs = result
|
||||
}
|
||||
|
||||
func saveConfig() {
|
||||
var entries: [String: ConfigFile.AppEntry] = [:]
|
||||
for (key, config) in configs {
|
||||
entries[key] = ConfigFile.AppEntry(enabled: config.enabled, mode: config.mode)
|
||||
}
|
||||
let configFile = ConfigFile(apps: entries)
|
||||
guard let data = try? JSONEncoder().encode(configFile) else { return }
|
||||
|
||||
// Pretty-print the JSON
|
||||
guard let json = try? JSONSerialization.jsonObject(with: data),
|
||||
let pretty = try? JSONSerialization.data(withJSONObject: json, options: [.prettyPrinted, .sortedKeys]) else { return }
|
||||
try? pretty.write(to: configPath)
|
||||
}
|
||||
|
||||
func toggleEnabled(key: String) {
|
||||
configs[key]?.enabled.toggle()
|
||||
saveConfig()
|
||||
}
|
||||
|
||||
func setMode(key: String, readWrite: Bool) {
|
||||
configs[key]?.mode = readWrite ? "read-write" : "read"
|
||||
saveConfig()
|
||||
}
|
||||
|
||||
func install() {
|
||||
isInstalling = true
|
||||
installLog = ""
|
||||
installError = nil
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async { [self] in
|
||||
do {
|
||||
try performInstall()
|
||||
DispatchQueue.main.async {
|
||||
self.isInstalling = false
|
||||
self.checkInstallation()
|
||||
self.loadConfig()
|
||||
}
|
||||
} catch {
|
||||
DispatchQueue.main.async {
|
||||
self.installError = error.localizedDescription
|
||||
self.isInstalling = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func log(_ msg: String) {
|
||||
DispatchQueue.main.async { self.installLog += msg + "\n" }
|
||||
}
|
||||
|
||||
private func performInstall() throws {
|
||||
let fm = FileManager.default
|
||||
let home = fm.homeDirectoryForCurrentUser
|
||||
|
||||
// Step 1: Copy server files from bundle Resources if available, or verify they exist
|
||||
log("Checking server files...")
|
||||
let serverMainFile = serverInstallDir.appendingPathComponent("apple_mcp.py")
|
||||
if !fm.fileExists(atPath: serverMainFile.path) {
|
||||
// Try to copy from app bundle
|
||||
if let bundleServerDir = Bundle.main.resourceURL?.appendingPathComponent("server") {
|
||||
if fm.fileExists(atPath: bundleServerDir.path) {
|
||||
log("Copying server files from app bundle...")
|
||||
try? fm.createDirectory(at: serverInstallDir, withIntermediateDirectories: true)
|
||||
let items = try fm.contentsOfDirectory(at: bundleServerDir, includingPropertiesForKeys: nil)
|
||||
for item in items {
|
||||
let dest = serverInstallDir.appendingPathComponent(item.lastPathComponent)
|
||||
if fm.fileExists(atPath: dest.path) {
|
||||
try fm.removeItem(at: dest)
|
||||
}
|
||||
try fm.copyItem(at: item, to: dest)
|
||||
}
|
||||
log("Server files copied.")
|
||||
} else {
|
||||
throw NSError(domain: "", code: 1, userInfo: [
|
||||
NSLocalizedDescriptionKey: "Server files not found. Place the apple-mcp project at:\n\(serverInstallDir.path)"
|
||||
])
|
||||
}
|
||||
} else {
|
||||
throw NSError(domain: "", code: 1, userInfo: [
|
||||
NSLocalizedDescriptionKey: "Server files not found at \(serverInstallDir.path) and not bundled in app."
|
||||
])
|
||||
}
|
||||
} else {
|
||||
log("Server files found at \(serverInstallDir.path)")
|
||||
}
|
||||
|
||||
// Step 2: Install uv if needed
|
||||
let uvPath = home.appendingPathComponent(".local/bin/uv")
|
||||
if !fm.fileExists(atPath: uvPath.path) {
|
||||
log("Installing uv...")
|
||||
try runShell("curl -LsSf https://astral.sh/uv/install.sh | sh")
|
||||
log("uv installed.")
|
||||
} else {
|
||||
log("uv already installed.")
|
||||
}
|
||||
|
||||
// Step 3: Create venv with Python 3.13
|
||||
let venvDir = serverInstallDir.appendingPathComponent(".venv")
|
||||
if !fm.fileExists(atPath: venvDir.path) {
|
||||
log("Creating Python 3.13 virtual environment...")
|
||||
try runShell("export PATH=\"$HOME/.local/bin:$PATH\" && cd \"\(serverInstallDir.path)\" && uv venv --python 3.13 .venv")
|
||||
log("Virtual environment created.")
|
||||
} else {
|
||||
log("Virtual environment already exists.")
|
||||
}
|
||||
|
||||
// Step 4: Install dependencies
|
||||
log("Installing dependencies...")
|
||||
try runShell("export PATH=\"$HOME/.local/bin:$PATH\" && cd \"\(serverInstallDir.path)\" && uv pip install -r requirements.txt")
|
||||
log("Dependencies installed.")
|
||||
|
||||
// Step 5: Create default config.json if missing
|
||||
if !fm.fileExists(atPath: configPath.path) {
|
||||
log("Creating default config.json...")
|
||||
let defaultConfig = """
|
||||
{
|
||||
"apps": {
|
||||
"reminders": { "enabled": true, "mode": "read-write" },
|
||||
"calendar": { "enabled": true, "mode": "read-write" },
|
||||
"mail": { "enabled": true, "mode": "read" },
|
||||
"contacts": { "enabled": true, "mode": "read" },
|
||||
"findmy": { "enabled": true, "mode": "read" },
|
||||
"maps": { "enabled": true, "mode": "read" }
|
||||
}
|
||||
}
|
||||
"""
|
||||
try defaultConfig.write(to: configPath, atomically: true, encoding: .utf8)
|
||||
log("Default config created.")
|
||||
}
|
||||
|
||||
// Step 6: Compile EventKit reminders helper
|
||||
let helperDir = serverInstallDir.appendingPathComponent("helpers")
|
||||
let helperSwift = helperDir.appendingPathComponent("reminders_helper.swift")
|
||||
let helperBin = helperDir.appendingPathComponent("reminders_helper")
|
||||
if fm.fileExists(atPath: helperSwift.path) && !fm.fileExists(atPath: helperBin.path) {
|
||||
log("Compiling EventKit reminders helper...")
|
||||
try runShell("swiftc \"\(helperSwift.path)\" -o \"\(helperBin.path)\" -framework EventKit -target arm64-apple-macosx14.0 -Osize")
|
||||
log("Reminders helper compiled.")
|
||||
} else if fm.fileExists(atPath: helperBin.path) {
|
||||
log("Reminders helper already compiled.")
|
||||
}
|
||||
|
||||
// Step 7: Register in Claude Code (~/.claude.json)
|
||||
log("Registering MCP server in Claude Code...")
|
||||
try registerMCPServer(at: claudeJsonPath, label: "~/.claude.json")
|
||||
|
||||
// Step 8: Register in Claude Desktop / CoWork
|
||||
log("Registering MCP server in Claude Desktop / CoWork...")
|
||||
try registerMCPServer(at: claudeDesktopConfigPath, label: "claude_desktop_config.json")
|
||||
|
||||
log("")
|
||||
log("Setup complete! Restart Claude Code and/or the Claude Desktop App to activate.")
|
||||
}
|
||||
|
||||
private func registerMCPServer(at configURL: URL, label: String) throws {
|
||||
let fm = FileManager.default
|
||||
let serverEntry: [String: Any] = [
|
||||
"command": venvPython.path,
|
||||
"args": [serverInstallDir.appendingPathComponent("apple_mcp.py").path]
|
||||
]
|
||||
|
||||
if fm.fileExists(atPath: configURL.path) {
|
||||
let data = try Data(contentsOf: configURL)
|
||||
if var json = try JSONSerialization.jsonObject(with: data) as? [String: Any] {
|
||||
var mcpServers = json["mcpServers"] as? [String: Any] ?? [:]
|
||||
mcpServers["apple-apps"] = serverEntry
|
||||
json["mcpServers"] = mcpServers
|
||||
let updated = try JSONSerialization.data(withJSONObject: json, options: [.prettyPrinted, .sortedKeys])
|
||||
try updated.write(to: configURL)
|
||||
log(" Registered in \(label)")
|
||||
}
|
||||
} else {
|
||||
// Create parent directory if needed
|
||||
try? fm.createDirectory(at: configURL.deletingLastPathComponent(), withIntermediateDirectories: true)
|
||||
let json: [String: Any] = ["mcpServers": ["apple-apps": serverEntry]]
|
||||
let data = try JSONSerialization.data(withJSONObject: json, options: [.prettyPrinted, .sortedKeys])
|
||||
try data.write(to: configURL)
|
||||
log(" Created \(label) with MCP server registration.")
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
private func runShell(_ command: String) throws -> String {
|
||||
let task = Process()
|
||||
let pipe = Pipe()
|
||||
task.executableURL = URL(fileURLWithPath: "/bin/zsh")
|
||||
task.arguments = ["-c", command]
|
||||
task.standardOutput = pipe
|
||||
task.standardError = pipe
|
||||
task.environment = ProcessInfo.processInfo.environment
|
||||
try task.run()
|
||||
task.waitUntilExit()
|
||||
|
||||
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
||||
let output = String(data: data, encoding: .utf8) ?? ""
|
||||
|
||||
if task.terminationStatus != 0 {
|
||||
throw NSError(domain: "", code: Int(task.terminationStatus), userInfo: [
|
||||
NSLocalizedDescriptionKey: "Command failed: \(command)\n\(output)"
|
||||
])
|
||||
}
|
||||
return output
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Views
|
||||
|
||||
struct AppRowView: View {
|
||||
let info: AppInfo
|
||||
@ObservedObject var manager: ConfigManager
|
||||
|
||||
var config: AppConfig? { manager.configs[info.key] }
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 12) {
|
||||
Image(systemName: info.icon)
|
||||
.font(.title2)
|
||||
.foregroundColor(config?.enabled == true ? .accentColor : .secondary)
|
||||
.frame(width: 28)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(info.name)
|
||||
.font(.headline)
|
||||
Text(info.description)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
if let config = config {
|
||||
Picker("", selection: Binding(
|
||||
get: { config.mode },
|
||||
set: { manager.setMode(key: info.key, readWrite: $0 == "read-write") }
|
||||
)) {
|
||||
Text("Read").tag("read")
|
||||
Text("Read & Write").tag("read-write")
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
.frame(width: 160)
|
||||
.disabled(!config.enabled)
|
||||
.opacity(config.enabled ? 1.0 : 0.4)
|
||||
|
||||
Toggle("", isOn: Binding(
|
||||
get: { config.enabled },
|
||||
set: { _ in manager.toggleEnabled(key: info.key) }
|
||||
))
|
||||
.toggleStyle(.switch)
|
||||
.labelsHidden()
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 6)
|
||||
}
|
||||
}
|
||||
|
||||
struct SetupStatusView: View {
|
||||
@ObservedObject var manager: ConfigManager
|
||||
|
||||
var body: some View {
|
||||
GroupBox {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
HStack {
|
||||
Image(systemName: manager.isInstalled ? "checkmark.circle.fill" : "xmark.circle.fill")
|
||||
.foregroundColor(manager.isInstalled ? .green : .red)
|
||||
.font(.title2)
|
||||
VStack(alignment: .leading) {
|
||||
Text(manager.isInstalled ? "Server Installed" : "Server Not Installed")
|
||||
.font(.headline)
|
||||
if manager.isInstalled {
|
||||
Text(manager.pythonVersion)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
Button(manager.isInstalled ? "Reinstall" : "Install") {
|
||||
manager.install()
|
||||
}
|
||||
.disabled(manager.isInstalling)
|
||||
.buttonStyle(.borderedProminent)
|
||||
.tint(manager.isInstalled ? .secondary : .accentColor)
|
||||
}
|
||||
|
||||
if manager.isInstalling {
|
||||
ProgressView()
|
||||
.progressViewStyle(.linear)
|
||||
}
|
||||
|
||||
if !manager.installLog.isEmpty {
|
||||
ScrollView {
|
||||
Text(manager.installLog)
|
||||
.font(.system(.caption, design: .monospaced))
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.textSelection(.enabled)
|
||||
}
|
||||
.frame(maxHeight: 150)
|
||||
.background(Color(nsColor: .textBackgroundColor))
|
||||
.cornerRadius(6)
|
||||
}
|
||||
|
||||
if let error = manager.installError {
|
||||
Text(error)
|
||||
.font(.caption)
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
.padding(4)
|
||||
} label: {
|
||||
Label("Setup", systemImage: "gear")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentView: View {
|
||||
@StateObject private var manager = ConfigManager()
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
// Header
|
||||
HStack {
|
||||
Image(systemName: "apple.logo")
|
||||
.font(.largeTitle)
|
||||
.foregroundColor(.accentColor)
|
||||
VStack(alignment: .leading) {
|
||||
Text("Apple Apps MCP")
|
||||
.font(.title2.bold())
|
||||
Text("Configure Claude's access to Apple apps")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
|
||||
Divider()
|
||||
|
||||
ScrollView {
|
||||
VStack(spacing: 16) {
|
||||
// Setup section
|
||||
SetupStatusView(manager: manager)
|
||||
|
||||
// App permissions section
|
||||
GroupBox {
|
||||
VStack(spacing: 0) {
|
||||
ForEach(Array(appList.enumerated()), id: \.element.key) { index, info in
|
||||
AppRowView(info: info, manager: manager)
|
||||
if index < appList.count - 1 {
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(4)
|
||||
} label: {
|
||||
Label("App Permissions", systemImage: "lock.shield")
|
||||
}
|
||||
|
||||
// Footer note
|
||||
HStack {
|
||||
Image(systemName: "info.circle")
|
||||
.foregroundColor(.secondary)
|
||||
Text("Changes are saved automatically. Restart Claude Code for permission changes to take effect.")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
.frame(width: 580, height: 620)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - App Entry Point
|
||||
|
||||
@main
|
||||
struct AppleMCPConfigApp: App {
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
}
|
||||
.windowResizability(.contentSize)
|
||||
}
|
||||
}
|
||||
Executable
+82
@@ -0,0 +1,82 @@
|
||||
#!/bin/bash
|
||||
# Build the Apple MCP Config .app bundle
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
APP_NAME="Apple MCP Config"
|
||||
APP_BUNDLE="$SCRIPT_DIR/$APP_NAME.app"
|
||||
SERVER_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
echo "=== Building $APP_NAME ==="
|
||||
|
||||
# Clean previous build
|
||||
rm -rf "$APP_BUNDLE"
|
||||
|
||||
# Create .app bundle structure
|
||||
mkdir -p "$APP_BUNDLE/Contents/MacOS"
|
||||
mkdir -p "$APP_BUNDLE/Contents/Resources/server/apps"
|
||||
mkdir -p "$APP_BUNDLE/Contents/Resources/server/helpers"
|
||||
|
||||
# Compile Swift
|
||||
echo "Compiling SwiftUI app..."
|
||||
swiftc "$SCRIPT_DIR/AppleMCPConfig.swift" \
|
||||
-o "$APP_BUNDLE/Contents/MacOS/$APP_NAME" \
|
||||
-target arm64-apple-macosx14.0 \
|
||||
-framework SwiftUI \
|
||||
-framework AppKit \
|
||||
-parse-as-library \
|
||||
-Osize
|
||||
|
||||
# Create Info.plist
|
||||
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>CFBundleName</key>
|
||||
<string>Apple MCP Config</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Apple MCP Config</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>io.jfamily.apple-mcp-config</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>Apple MCP Config</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>14.0</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<true/>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.utilities</string>
|
||||
<key>NSRemindersUsageDescription</key>
|
||||
<string>Apple MCP Config needs access to Reminders to provide Claude with reminder data.</string>
|
||||
</dict>
|
||||
</plist>
|
||||
PLIST
|
||||
|
||||
# Bundle server files into Resources
|
||||
echo "Bundling server files..."
|
||||
cp "$SERVER_DIR/apple_mcp.py" "$APP_BUNDLE/Contents/Resources/server/"
|
||||
cp "$SERVER_DIR/helpers.py" "$APP_BUNDLE/Contents/Resources/server/"
|
||||
cp "$SERVER_DIR/requirements.txt" "$APP_BUNDLE/Contents/Resources/server/"
|
||||
cp "$SERVER_DIR/config.json" "$APP_BUNDLE/Contents/Resources/server/"
|
||||
cp "$SERVER_DIR/apps/"*.py "$APP_BUNDLE/Contents/Resources/server/apps/"
|
||||
|
||||
# Bundle helpers (Swift source — compiled during install)
|
||||
cp "$SERVER_DIR/helpers/reminders_helper.swift" "$APP_BUNDLE/Contents/Resources/server/helpers/"
|
||||
# Also include pre-compiled binary if it exists
|
||||
if [ -f "$SERVER_DIR/helpers/reminders_helper" ]; then
|
||||
cp "$SERVER_DIR/helpers/reminders_helper" "$APP_BUNDLE/Contents/Resources/server/helpers/"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== Build Complete ==="
|
||||
echo "App: $APP_BUNDLE"
|
||||
echo ""
|
||||
echo "To use: double-click '$APP_NAME.app' or run:"
|
||||
echo " open \"$APP_BUNDLE\""
|
||||
Reference in New Issue
Block a user