Spaces:
Paused
Paused
| import AppKit | |
| import Foundation | |
| import OSLog | |
| final class CLIInstallPrompter { | |
| static let shared = CLIInstallPrompter() | |
| private let logger = Logger(subsystem: "ai.openclaw", category: "cli.prompt") | |
| private var isPrompting = false | |
| func checkAndPromptIfNeeded(reason: String) { | |
| guard self.shouldPrompt() else { return } | |
| guard let version = Self.appVersion() else { return } | |
| self.isPrompting = true | |
| UserDefaults.standard.set(version, forKey: cliInstallPromptedVersionKey) | |
| let alert = NSAlert() | |
| alert.messageText = "Install OpenClaw CLI?" | |
| alert.informativeText = "Local mode needs the CLI so launchd can run the gateway." | |
| alert.addButton(withTitle: "Install CLI") | |
| alert.addButton(withTitle: "Not now") | |
| alert.addButton(withTitle: "Open Settings") | |
| let response = alert.runModal() | |
| switch response { | |
| case .alertFirstButtonReturn: | |
| Task { await self.installCLI() } | |
| case .alertThirdButtonReturn: | |
| self.openSettings(tab: .general) | |
| default: | |
| break | |
| } | |
| self.logger.debug("cli install prompt handled reason=\(reason, privacy: .public)") | |
| self.isPrompting = false | |
| } | |
| private func shouldPrompt() -> Bool { | |
| guard !self.isPrompting else { return false } | |
| guard AppStateStore.shared.onboardingSeen else { return false } | |
| guard AppStateStore.shared.connectionMode == .local else { return false } | |
| guard CLIInstaller.installedLocation() == nil else { return false } | |
| guard let version = Self.appVersion() else { return false } | |
| let lastPrompt = UserDefaults.standard.string(forKey: cliInstallPromptedVersionKey) | |
| return lastPrompt != version | |
| } | |
| private func installCLI() async { | |
| let status = StatusBox() | |
| await CLIInstaller.install { message in | |
| await status.set(message) | |
| } | |
| if let message = await status.get() { | |
| let alert = NSAlert() | |
| alert.messageText = "CLI install finished" | |
| alert.informativeText = message | |
| alert.runModal() | |
| } | |
| } | |
| private func openSettings(tab: SettingsTab) { | |
| SettingsTabRouter.request(tab) | |
| SettingsWindowOpener.shared.open() | |
| DispatchQueue.main.async { | |
| NotificationCenter.default.post(name: .openclawSelectSettingsTab, object: tab) | |
| } | |
| } | |
| private static func appVersion() -> String? { | |
| Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String | |
| } | |
| } | |
| private actor StatusBox { | |
| private var value: String? | |
| func set(_ value: String) { | |
| self.value = value | |
| } | |
| func get() -> String? { | |
| self.value | |
| } | |
| } | |