diff --git a/@packages/inotes/Sources/INoteSync/Reader.swift b/@packages/inotes/Sources/INoteSync/Reader.swift index 05c0cab..be39bee 100644 --- a/@packages/inotes/Sources/INoteSync/Reader.swift +++ b/@packages/inotes/Sources/INoteSync/Reader.swift @@ -140,34 +140,28 @@ public final class NotesReader { return out } - /// Run AppleScript via /usr/bin/osascript subprocess. Returns - /// `(success, stdout-or-stderr)`. We deliberately do NOT use - /// `NSAppleScript` because it blocks the calling thread; `osascript` - /// runs out-of-process and is awaitable. + /// Run AppleScript IN-PROCESS via `NSAppleScript`. Returns + /// `(success, output-or-error)`. + /// + /// We must NOT shell out to `/usr/bin/osascript`: TCC attributes the Apple + /// event a subprocess sends to that subprocess (`osascript`), not to MacSync, + /// so the `MacSync → Notes` Automation grant doesn't apply and every read is + /// denied even after the user allows it. NSAppleScript sends the event from + /// MacSync itself, so the grant is honored. + /// + /// Run on the main actor: `tell application` events need a live run loop to + /// receive their reply, so a detached thread can hang. The Notes read is + /// infrequent (the 600s sync cycle) and brief enough that a menu-bar agent + /// can absorb it on the main thread. + @MainActor private func runAppleScript(_ source: String) async -> (Bool, String) { - await Task.detached { - let process = Process() - process.executableURL = URL(fileURLWithPath: "/usr/bin/osascript") - process.arguments = ["-e", source] - let outPipe = Pipe() - let errPipe = Pipe() - process.standardOutput = outPipe - process.standardError = errPipe - do { - try process.run() - process.waitUntilExit() - let outData = outPipe.fileHandleForReading.readDataToEndOfFile() - let errData = errPipe.fileHandleForReading.readDataToEndOfFile() - let outStr = String(data: outData, encoding: .utf8) ?? "" - let errStr = String(data: errData, encoding: .utf8) ?? "" - if process.terminationStatus == 0 { - return (true, outStr) - } else { - return (false, errStr.isEmpty ? outStr : errStr) - } - } catch { - return (false, error.localizedDescription) - } - }.value + var errorInfo: NSDictionary? + let script = NSAppleScript(source: source) + let result = script?.executeAndReturnError(&errorInfo) + if let errorInfo { + let msg = (errorInfo[NSAppleScript.errorMessage] as? String) ?? "\(errorInfo)" + return (false, msg) + } + return (true, result?.stringValue ?? "") } }