From 185d6b3b3150de3f323a8cfbaee377bab3c0a4e7 Mon Sep 17 00:00:00 2001 From: iamtoruk Date: Tue, 18 Aug 2026 07:48:03 -0700 Subject: [PATCH] mac: register the login item with SMAppService The menubar told System Events to make its login item, so macOS asked for Automation access on first launch. SMAppService.mainApp does it in-process with no Automation grant. No AppleScript fallback: a failure must not bring the prompt back. Package floor is macOS 14, so the 13+ API needs no availability guard. Fixes #1026 --- CHANGELOG.md | 1 + mac/Sources/CodeBurnMenubar/CodeBurnApp.swift | 30 +++++-------------- 2 files changed, 9 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e5f9394..66b04297 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ - **One rule for every cache file.** `CODEBURN_CACHE_DIR` when set, otherwise `~/.cache/codeburn`. `XDG_CACHE_HOME` is no longer consulted; the sync ledger, the only file that ever honored it, is merged into the canonical location on first read and the legacy copy is retired, so nothing is re-uploaded after the move. (#972) ### Fixed (Desktop & Menubar) +- **First launch no longer asks to control System Events.** The macOS menubar registered its login item by driving System Events over AppleScript, which made macOS put up an Automation consent dialog the first time the app ran. It now registers itself through `SMAppService.mainApp`, an in-process call that needs no Automation grant; there is no AppleScript fallback, so a failure logs and leaves the login item unset rather than bringing the prompt back. The same `codeburn.loginItemRegistered` guard still limits this to the first launch, so a login item you removed by hand stays removed. (#1026) - **The resident `codeburn serve` child.** The first real panel request is also the cache warm-up, so startup never runs an artificial warm-up query beside a duplicate one-shot child; each served command carries its own read-only option allowlist, and anything outside it falls back to a normal spawn; the child exits when its stdin closes, so it can never outlive the app. Requests whose response exceeds the 16 MiB frame limit still replace the child, but that deliberate kill no longer spends the resident's unexpected-death budget. (#972) ### Fixed diff --git a/mac/Sources/CodeBurnMenubar/CodeBurnApp.swift b/mac/Sources/CodeBurnMenubar/CodeBurnApp.swift index 63144f76..b0400d6e 100644 --- a/mac/Sources/CodeBurnMenubar/CodeBurnApp.swift +++ b/mac/Sources/CodeBurnMenubar/CodeBurnApp.swift @@ -2,6 +2,7 @@ import Foundation import SwiftUI import AppKit import Observation +import ServiceManagement private let refreshIntervalSeconds: UInt64 = 30 private let forceRefreshWatchdogSeconds: TimeInterval = 90 @@ -281,34 +282,19 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate, NSM let key = "codeburn.loginItemRegistered" guard !UserDefaults.standard.bool(forKey: key) else { return } - let appPath = Bundle.main.bundlePath - let script = "tell application \"System Events\" to make login item at end with properties {path:\(appleScriptStringLiteral(appPath)), hidden:false}" - - let process = Process() - process.launchPath = "/usr/bin/osascript" - process.arguments = ["-e", script] - process.standardOutput = FileHandle.nullDevice - process.standardError = FileHandle.nullDevice - + // Registers in-process. The old path told System Events to make the login + // item, which made macOS ask for Automation access on first launch (#1026). + // No AppleScript fallback: a failure here must not bring that prompt back. do { - try process.run() - process.waitUntilExit() - if process.terminationStatus == 0 { - UserDefaults.standard.set(true, forKey: key) + if SMAppService.mainApp.status != .enabled { + try SMAppService.mainApp.register() } + UserDefaults.standard.set(true, forKey: key) } catch { - NSLog("CodeBurn: Login item registration failed: \(error)") + NSLog("CodeBurn: login item registration failed: \(error.localizedDescription)") } } - private func appleScriptStringLiteral(_ value: String) -> String { - var escaped = value.replacingOccurrences(of: "\\", with: "\\\\") - escaped = escaped.replacingOccurrences(of: "\"", with: "\\\"") - escaped = escaped.replacingOccurrences(of: "\r", with: "") - escaped = escaped.replacingOccurrences(of: "\n", with: "") - return "\"\(escaped)\"" - } - private var lastRefreshTime: Date = .distantPast /// Anchors the shallow provider-root snapshot only after a complete usage /// refresh succeeds. It sits beside the cadence anchor so a failed fetch