codex-flow 2.1.13__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. codex_flow/__init__.py +28 -0
  2. codex_flow/__main__.py +9 -0
  3. codex_flow/cli.py +242 -0
  4. codex_flow/data/LICENSE +21 -0
  5. codex_flow/data/README.en.md +303 -0
  6. codex_flow/data/README.md +305 -0
  7. codex_flow/data/VERSION +1 -0
  8. codex_flow/data/apps/chatgpt-mcp/README.md +86 -0
  9. codex_flow/data/apps/chatgpt-mcp/__init__.py +1 -0
  10. codex_flow/data/apps/chatgpt-mcp/adapter.py +458 -0
  11. codex_flow/data/apps/chatgpt-mcp/server.py +358 -0
  12. codex_flow/data/apps/chatgpt-mcp/widget.html +927 -0
  13. codex_flow/data/apps/macos-overlay/README.en.md +121 -0
  14. codex_flow/data/apps/macos-overlay/README.md +123 -0
  15. codex_flow/data/apps/macos-overlay/Sources/Controllers/OverlayRuntimeState.swift +126 -0
  16. codex_flow/data/apps/macos-overlay/Sources/Controllers/OverlayScreenGeometry.swift +82 -0
  17. codex_flow/data/apps/macos-overlay/Sources/Controllers/OverlayWindowController.swift +1052 -0
  18. codex_flow/data/apps/macos-overlay/Sources/Localization.swift +197 -0
  19. codex_flow/data/apps/macos-overlay/Sources/Models/TelemetryData.swift +1557 -0
  20. codex_flow/data/apps/macos-overlay/Sources/Services/AccountSnapshotService.swift +1101 -0
  21. codex_flow/data/apps/macos-overlay/Sources/Services/FlowPilotInstanceLock.swift +153 -0
  22. codex_flow/data/apps/macos-overlay/Sources/Services/IPCServer.swift +298 -0
  23. codex_flow/data/apps/macos-overlay/Sources/Services/TelemetryQueryEngine.swift +800 -0
  24. codex_flow/data/apps/macos-overlay/Sources/Services/TelemetryWatcher.swift +135 -0
  25. codex_flow/data/apps/macos-overlay/Sources/Services/UpdateService.swift +610 -0
  26. codex_flow/data/apps/macos-overlay/Sources/Views/AccountView.swift +610 -0
  27. codex_flow/data/apps/macos-overlay/Sources/Views/AnalyticsView.swift +566 -0
  28. codex_flow/data/apps/macos-overlay/Sources/Views/AutostartView.swift +293 -0
  29. codex_flow/data/apps/macos-overlay/Sources/Views/BubbleView.swift +317 -0
  30. codex_flow/data/apps/macos-overlay/Sources/Views/HistoryView.swift +1124 -0
  31. codex_flow/data/apps/macos-overlay/Sources/Views/HoverRevealText.swift +165 -0
  32. codex_flow/data/apps/macos-overlay/Sources/Views/InspectorSkillsToolsView.swift +121 -0
  33. codex_flow/data/apps/macos-overlay/Sources/Views/LogoView.swift +182 -0
  34. codex_flow/data/apps/macos-overlay/Sources/Views/SleekSwitch.swift +117 -0
  35. codex_flow/data/apps/macos-overlay/Sources/Views/StrategyModeView.swift +561 -0
  36. codex_flow/data/apps/macos-overlay/Sources/Views/SummaryView.swift +1273 -0
  37. codex_flow/data/apps/macos-overlay/Sources/Views/UpdateView.swift +352 -0
  38. codex_flow/data/apps/macos-overlay/Sources/main.swift +340 -0
  39. codex_flow/data/apps/macos-overlay/Tests/OverlayScreenGeometryTests.swift +163 -0
  40. codex_flow/data/apps/macos-overlay/Tests/TelemetryPhase1ContractTests.swift +357 -0
  41. codex_flow/data/apps/macos-overlay/Tests/TelemetryQueryEngineConcurrencyTests.swift +221 -0
  42. codex_flow/data/apps/macos-overlay/Tests/TelemetryQuotaSelectionTests.swift +158 -0
  43. codex_flow/data/apps/macos-overlay/Tests/TelemetryWorkerTokenTests.swift +122 -0
  44. codex_flow/data/apps/macos-overlay/build.sh +75 -0
  45. codex_flow/data/benchmark/corpus.json +103 -0
  46. codex_flow/data/benchmark/manifest.example.json +41 -0
  47. codex_flow/data/benchmark/manifest.schema.json +137 -0
  48. codex_flow/data/benchmark/prices/gpt-5.6-2026-08-30.json +5 -0
  49. codex_flow/data/benchmark/profiles.json +90 -0
  50. codex_flow/data/benchmark/schema.json +77 -0
  51. codex_flow/data/benchmark/tasks.json +50 -0
  52. codex_flow/data/completions/codex-flow.bash +34 -0
  53. codex_flow/data/completions/codex-flow.zsh +52 -0
  54. codex_flow/data/glama.json +6 -0
  55. codex_flow/data/install-release.ps1 +126 -0
  56. codex_flow/data/install-release.sh +155 -0
  57. codex_flow/data/install.ps1 +349 -0
  58. codex_flow/data/install.sh +362 -0
  59. codex_flow/data/policy/benchmark.toml +49 -0
  60. codex_flow/data/policy/defaults.toml +70 -0
  61. codex_flow/data/scripts/analyze-benchmark.py +510 -0
  62. codex_flow/data/scripts/benchmark-local.py +171 -0
  63. codex_flow/data/scripts/check-recommendation.py +277 -0
  64. codex_flow/data/scripts/doctor.py +449 -0
  65. codex_flow/data/scripts/generate-release-manifest.py +74 -0
  66. codex_flow/data/scripts/localization.py +192 -0
  67. codex_flow/data/scripts/manage-hooks.py +448 -0
  68. codex_flow/data/scripts/manage-instructions.py +389 -0
  69. codex_flow/data/scripts/manage-shell.py +151 -0
  70. codex_flow/data/scripts/materialize-corpus.py +193 -0
  71. codex_flow/data/scripts/menu.py +646 -0
  72. codex_flow/data/scripts/migrations/0001_update_settings.py +80 -0
  73. codex_flow/data/scripts/package-release.py +132 -0
  74. codex_flow/data/scripts/render-benchmark-report.py +292 -0
  75. codex_flow/data/scripts/run-benchmark.py +829 -0
  76. codex_flow/data/scripts/strategies/__init__.py +28 -0
  77. codex_flow/data/scripts/strategies/balanced.py +115 -0
  78. codex_flow/data/scripts/strategies/base.py +363 -0
  79. codex_flow/data/scripts/strategies/efficient.py +158 -0
  80. codex_flow/data/scripts/strategies/lifecycle_runtime.py +590 -0
  81. codex_flow/data/scripts/strategies/quality.py +209 -0
  82. codex_flow/data/scripts/strategies/speed.py +108 -0
  83. codex_flow/data/scripts/strategies/task_budget_runtime.py +644 -0
  84. codex_flow/data/scripts/strategies/task_phase_runtime.py +341 -0
  85. codex_flow/data/scripts/strategies/work_unit_runtime.py +421 -0
  86. codex_flow/data/scripts/strategy_runtime.py +1091 -0
  87. codex_flow/data/scripts/telemetry.py +400 -0
  88. codex_flow/data/scripts/telemetry_core/__init__.py +192 -0
  89. codex_flow/data/scripts/telemetry_core/app_server.py +1192 -0
  90. codex_flow/data/scripts/telemetry_core/collector.py +1247 -0
  91. codex_flow/data/scripts/telemetry_core/common.py +421 -0
  92. codex_flow/data/scripts/telemetry_core/latency.py +593 -0
  93. codex_flow/data/scripts/telemetry_core/query.py +427 -0
  94. codex_flow/data/scripts/telemetry_core/quota_ledger.py +598 -0
  95. codex_flow/data/scripts/telemetry_core/render.py +460 -0
  96. codex_flow/data/scripts/telemetry_core/repair.py +223 -0
  97. codex_flow/data/scripts/ui.py +266 -0
  98. codex_flow/data/scripts/update-homebrew-formula.py +146 -0
  99. codex_flow/data/scripts/update_runtime_config.py +134 -0
  100. codex_flow/data/scripts/updater.py +1718 -0
  101. codex_flow/data/smithery.yaml +18 -0
  102. codex_flow/data/templates/agents/worker-explorer.toml +24 -0
  103. codex_flow/data/templates/agents/worker-implementer.toml +49 -0
  104. codex_flow/data/templates/agents/worker-reviewer.toml +25 -0
  105. codex_flow/data/templates/flow-pilot-instructions.md +35 -0
  106. codex_flow/data/templates/skills/flow-pilot/SKILL.md +577 -0
  107. codex_flow/mcp.py +35 -0
  108. codex_flow-2.1.13.dist-info/METADATA +342 -0
  109. codex_flow-2.1.13.dist-info/RECORD +113 -0
  110. codex_flow-2.1.13.dist-info/WHEEL +5 -0
  111. codex_flow-2.1.13.dist-info/entry_points.txt +3 -0
  112. codex_flow-2.1.13.dist-info/licenses/LICENSE +21 -0
  113. codex_flow-2.1.13.dist-info/top_level.txt +1 -0
@@ -0,0 +1,610 @@
1
+ import AppKit
2
+ import Foundation
3
+ import SwiftUI
4
+
5
+ // Shared update state bridge used by both the FlowPilot badge and update popover.
6
+ public struct FlowPilotUpdateSnapshot: Codable, Equatable, Sendable {
7
+ public var schema: Int?
8
+ public var status: String?
9
+ public var currentVersion: String?
10
+ public var latestVersion: String?
11
+ public var channel: String?
12
+ public var notifyCLI: Bool?
13
+ public var notifyApp: Bool?
14
+ public var autoInstall: Bool?
15
+ public var updateAvailable: Bool?
16
+ public var checkedAt: String?
17
+ public var restartRequired: Bool?
18
+ public var flowPilotRestartRequired: Bool?
19
+ public var mandatory: Bool?
20
+ public var releaseURL: String?
21
+ public var releaseNotes: String?
22
+ public var artifactAvailable: Bool?
23
+ public var lastError: String?
24
+ public var progress: Double?
25
+ public var pendingCodexPids: [Int]?
26
+ public var restartReason: String?
27
+
28
+ enum CodingKeys: String, CodingKey {
29
+ case schema, status, channel, mandatory, progress
30
+ case notifyCLI = "notify_cli"
31
+ case notifyApp = "notify_app"
32
+ case autoInstall = "auto_install"
33
+ case currentVersion = "current_version"
34
+ case latestVersion = "latest_version"
35
+ case updateAvailable = "update_available"
36
+ case checkedAt = "checked_at"
37
+ case restartRequired = "restart_required"
38
+ case flowPilotRestartRequired = "flowpilot_restart_required"
39
+ case pendingCodexPids = "pending_codex_pids"
40
+ case restartReason = "restart_reason"
41
+ case releaseURL = "release_url"
42
+ case releaseNotes = "release_notes"
43
+ case artifactAvailable = "artifact_available"
44
+ case lastError = "last_error"
45
+ }
46
+
47
+ public static let empty = FlowPilotUpdateSnapshot()
48
+ }
49
+
50
+ @MainActor
51
+ public final class FlowPilotUpdateService: ObservableObject {
52
+ public static let shared = FlowPilotUpdateService()
53
+
54
+ private static let periodicCheckInterval: TimeInterval = 30 * 60
55
+ private static let lifecycleRefreshInterval: TimeInterval = 5 * 60
56
+
57
+ @Published public private(set) var snapshot: FlowPilotUpdateSnapshot = .empty
58
+ @Published public private(set) var isChecking = false
59
+ @Published public private(set) var isInstalling = false
60
+ @Published public private(set) var isAcknowledgingRestart = false
61
+ @Published public private(set) var isRestartingFlowPilot = false
62
+ @Published public private(set) var isChangingAutoUpdate = false
63
+ @Published public private(set) var isAutoRestartScheduled = false
64
+ @Published public private(set) var actionError: String?
65
+ @Published public private(set) var actionMessage: String?
66
+
67
+ private var periodicCheckTimer: Timer?
68
+ private var lifecycleObservers: [NSObjectProtocol] = []
69
+ private var isBackgroundCheckRunning = false
70
+ private var lastBackgroundCheckRequestedAt: Date?
71
+ private var flowPilotRestartProcess: Process?
72
+
73
+ private init() {
74
+ refreshFromDisk()
75
+ acknowledgeFlowPilotRestartAfterLaunchIfNeeded()
76
+ configureLifecycleObservers()
77
+ configurePeriodicUpdateCheck()
78
+ requestCachedCheck()
79
+ }
80
+
81
+ deinit {
82
+ periodicCheckTimer?.invalidate()
83
+ lifecycleObservers.forEach(NotificationCenter.default.removeObserver)
84
+ lifecycleObservers.forEach(NSWorkspace.shared.notificationCenter.removeObserver)
85
+ }
86
+
87
+ public var hasUpdateBadge: Bool {
88
+ (snapshot.notifyApp ?? true) && (
89
+ (snapshot.updateAvailable ?? false)
90
+ || isRestartRequired
91
+ )
92
+ }
93
+
94
+ // Generic UI badges/icons care whether any process still needs a restart.
95
+ public var isRestartRequired: Bool {
96
+ isCodexRestartRequired || isFlowPilotRestartRequired
97
+ }
98
+
99
+ public var isCodexRestartRequired: Bool {
100
+ snapshot.restartRequired ?? false
101
+ }
102
+
103
+ public var codexRestartReason: String? {
104
+ snapshot.restartReason
105
+ }
106
+
107
+ public var isFlowPilotRestartRequired: Bool {
108
+ snapshot.flowPilotRestartRequired ?? false
109
+ }
110
+
111
+ public var displayVersion: String? {
112
+ if snapshot.updateAvailable == true {
113
+ return snapshot.latestVersion
114
+ }
115
+ return snapshot.currentVersion
116
+ }
117
+
118
+ public var isAutoUpdateEnabled: Bool {
119
+ snapshot.autoInstall ?? true
120
+ }
121
+
122
+ public var statusText: String {
123
+ if isAutoRestartScheduled || isRestartingFlowPilot {
124
+ return L("Restarting FlowPilot…", "正在重启 FlowPilot…")
125
+ }
126
+ if isInstalling {
127
+ return L("Installing update…", "正在安装更新…")
128
+ }
129
+ if isChecking {
130
+ return L("Checking for updates…", "正在检查更新…")
131
+ }
132
+ if isFlowPilotRestartRequired && isCodexRestartRequired {
133
+ return L("Update installed · restart FlowPilot and Codex", "更新已安装 · 请重启 FlowPilot 和 Codex")
134
+ }
135
+ if isFlowPilotRestartRequired {
136
+ return L("Update installed · restart FlowPilot", "更新已安装 · 请重启 FlowPilot")
137
+ }
138
+ if isCodexRestartRequired {
139
+ return L("Update installed · restart Codex", "更新已安装 · 请重启 Codex")
140
+ }
141
+ if snapshot.updateAvailable == true, let latest = snapshot.latestVersion {
142
+ return L("v\(latest) is available", "v\(latest) 可更新")
143
+ }
144
+ if snapshot.status == "latest", let current = snapshot.currentVersion {
145
+ return L("v\(current) is up to date", "v\(current) 已是最新版本")
146
+ }
147
+ if let error = snapshot.lastError, !error.isEmpty {
148
+ return L("Unable to check for updates", "暂时无法检查更新")
149
+ }
150
+ return L("Update status unknown", "更新状态未知")
151
+ }
152
+
153
+ public func refreshFromDisk() {
154
+ let url = updateStateURL
155
+ guard let data = try? Data(contentsOf: url),
156
+ let decoded = try? JSONDecoder().decode(FlowPilotUpdateSnapshot.self, from: data) else {
157
+ return
158
+ }
159
+ if decoded != snapshot {
160
+ snapshot = decoded
161
+ }
162
+ checkPendingRestartHandoff()
163
+ evaluateAutoCodexRestartAcknowledgement()
164
+ }
165
+
166
+ public func scheduleAutoRestart(reason: String, delay: TimeInterval = 1.2) {
167
+ guard !isAutoRestartScheduled, !isRestartingFlowPilot else { return }
168
+ isAutoRestartScheduled = true
169
+ actionMessage = reason
170
+ DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
171
+ guard let self else { return }
172
+ self.isAutoRestartScheduled = false
173
+ self.restartFlowPilot()
174
+ }
175
+ }
176
+
177
+ private func checkPendingRestartHandoff() {
178
+ guard snapshot.flowPilotRestartRequired == true else { return }
179
+ guard !isRestartingFlowPilot, !isAutoRestartScheduled, actionError == nil else { return }
180
+ if TelemetryQueryEngine.shared.loadLatestRun()?.isRunning == true {
181
+ return
182
+ }
183
+ scheduleAutoRestart(
184
+ reason: L("Update installed. Restarting FlowPilot…", "检测到已安装新版本,正在自动重启 FlowPilot…"),
185
+ delay: 2.0
186
+ )
187
+ }
188
+
189
+ public func evaluateAutoUpdate() {
190
+ refreshFromDisk()
191
+ guard isAutoUpdateEnabled else { return }
192
+ guard snapshot.updateAvailable == true, snapshot.artifactAvailable == true else { return }
193
+ guard !isInstalling, !isChecking, !isRestartingFlowPilot, !isAutoRestartScheduled else { return }
194
+ if TelemetryQueryEngine.shared.loadLatestRun()?.isRunning == true {
195
+ return
196
+ }
197
+ installUpdate(isAutomatic: true)
198
+ }
199
+
200
+ public func setAutoUpdateEnabled(_ enabled: Bool) {
201
+ guard !isChangingAutoUpdate else { return }
202
+ isChangingAutoUpdate = true
203
+ actionError = nil
204
+ actionMessage = nil
205
+ guard let executable = codexFlowExecutable else {
206
+ isChangingAutoUpdate = false
207
+ actionError = L("codex-flow CLI was not found.", "未找到 codex-flow CLI。")
208
+ return
209
+ }
210
+
211
+ Task.detached(priority: .utility) {
212
+ let action = enabled ? "enable" : "disable"
213
+ let result = Self.executeUpdater(
214
+ executable: executable,
215
+ arguments: ["update", "auto-install", action, "--quiet"]
216
+ )
217
+ await MainActor.run {
218
+ FlowPilotUpdateService.shared.finishAutoUpdateToggle(enabled: enabled, result: result)
219
+ }
220
+ }
221
+ }
222
+
223
+ private func finishAutoUpdateToggle(enabled: Bool, result: ProcessResult) {
224
+ isChangingAutoUpdate = false
225
+ refreshFromDisk()
226
+ if result.exitCode != 0 {
227
+ actionError = result.output.isEmpty
228
+ ? L("Failed to update auto-update setting.", "更新自动更新设置失败。")
229
+ : result.output
230
+ } else {
231
+ actionMessage = enabled
232
+ ? L("Automatic updates enabled.", "已开启自动更新。")
233
+ : L("Automatic updates disabled.", "已关闭自动更新。")
234
+ if enabled {
235
+ evaluateAutoUpdate()
236
+ }
237
+ }
238
+ }
239
+
240
+ public func requestCachedCheck() {
241
+ requestBackgroundCheck(force: false, minimumInterval: 0)
242
+ }
243
+
244
+ public func checkNow() {
245
+ actionError = nil
246
+ actionMessage = nil
247
+ isChecking = true
248
+ runUpdater(arguments: ["update", "--check", "--force", "--quiet"], mode: .foregroundCheck)
249
+ }
250
+
251
+ public func installUpdate(isAutomatic: Bool = false) {
252
+ guard !isInstalling else { return }
253
+ actionError = nil
254
+ actionMessage = isAutomatic ? L("Auto-updating…", "正在自动更新…") : nil
255
+ isInstalling = true
256
+ runUpdater(arguments: ["update"], mode: .install)
257
+ }
258
+
259
+ // There is no reliable cross-platform way for this helper process to prove
260
+ // that the Codex desktop/CLI host itself has restarted. Keep the reminder
261
+ // explicit and let the user clear it after performing the required restart.
262
+ public func acknowledgeRestart() {
263
+ guard !isAcknowledgingRestart else { return }
264
+ actionError = nil
265
+ actionMessage = nil
266
+ isAcknowledgingRestart = true
267
+ runUpdater(arguments: ["update", "--ack-restart", "--quiet"], mode: .acknowledgeRestart)
268
+ }
269
+
270
+ private func acknowledgeFlowPilotRestartAfterLaunchIfNeeded() {
271
+ guard snapshot.flowPilotRestartRequired == true, let executable = codexFlowExecutable else { return }
272
+ Task.detached(priority: .utility) {
273
+ let result = Self.executeUpdater(
274
+ executable: executable,
275
+ arguments: ["update", "--ack-flowpilot-restart", "--quiet"]
276
+ )
277
+ guard result.exitCode == 0 else { return }
278
+ await MainActor.run {
279
+ FlowPilotUpdateService.shared.refreshFromDisk()
280
+ }
281
+ }
282
+ }
283
+
284
+ // The OTA installer atomically replaces the FlowPilot binary on disk, but
285
+ // the already-running process remains the old executable. Launch the newly
286
+ // installed binary with `restart`; its existing IPC startup path shuts down
287
+ // this process and becomes the new FlowPilot instance.
288
+ public func restartFlowPilot() {
289
+ guard !isRestartingFlowPilot else { return }
290
+ actionError = nil
291
+ actionMessage = nil
292
+ guard let executable = flowPilotExecutable else {
293
+ actionError = L("The updated FlowPilot binary was not found.", "未找到更新后的 FlowPilot 可执行文件。")
294
+ return
295
+ }
296
+ isRestartingFlowPilot = true
297
+ do {
298
+ let process = Process()
299
+ process.executableURL = executable
300
+ process.arguments = ["restart"]
301
+ var environment = ProcessInfo.processInfo.environment
302
+ // This child is an explicit handoff, not a RunAtLoad repair. Do
303
+ // not let a launchd-start marker inherited from the old app make
304
+ // the replacement exit as an expected no-op.
305
+ environment.removeValue(forKey: FlowPilotInstanceLock.launchAgentEnvironmentKey)
306
+ process.environment = environment
307
+ process.standardInput = FileHandle.nullDevice
308
+ process.standardOutput = FileHandle.nullDevice
309
+ process.standardError = FileHandle.nullDevice
310
+ process.terminationHandler = { [weak self] terminatedProcess in
311
+ let terminatedProcessID = ObjectIdentifier(terminatedProcess)
312
+ Task { @MainActor [weak self] in
313
+ guard let self,
314
+ let restartProcess = self.flowPilotRestartProcess,
315
+ ObjectIdentifier(restartProcess) == terminatedProcessID else {
316
+ return
317
+ }
318
+ self.flowPilotRestartProcess = nil
319
+ self.isRestartingFlowPilot = false
320
+ self.actionMessage = nil
321
+ self.actionError = L(
322
+ "FlowPilot restart did not complete: the updated process exited before the handoff finished. Try again.",
323
+ "FlowPilot 重启未完成:更新后的进程在交接完成前退出了。请重试。"
324
+ )
325
+ }
326
+ }
327
+ // Keep the Process object alive so a fast failure can restore the
328
+ // button state while this (old) app is still running.
329
+ flowPilotRestartProcess = process
330
+ try process.run()
331
+ actionMessage = L("Restarting FlowPilot with the updated binary…", "正在使用更新后的程序重启 FlowPilot…")
332
+ } catch {
333
+ flowPilotRestartProcess?.terminationHandler = nil
334
+ flowPilotRestartProcess = nil
335
+ isRestartingFlowPilot = false
336
+ actionError = error.localizedDescription
337
+ }
338
+ }
339
+
340
+ private func configurePeriodicUpdateCheck() {
341
+ let timer = Timer.scheduledTimer(withTimeInterval: Self.periodicCheckInterval, repeats: true) { _ in
342
+ Task { @MainActor in
343
+ FlowPilotUpdateService.shared.requestBackgroundCheck(
344
+ force: true,
345
+ minimumInterval: Self.periodicCheckInterval
346
+ )
347
+ }
348
+ }
349
+ timer.tolerance = 2 * 60
350
+ periodicCheckTimer = timer
351
+ }
352
+
353
+ private func configureLifecycleObservers() {
354
+ let activeObserver = NotificationCenter.default.addObserver(
355
+ forName: NSApplication.didBecomeActiveNotification,
356
+ object: nil,
357
+ queue: .main
358
+ ) { _ in
359
+ Task { @MainActor in
360
+ FlowPilotUpdateService.shared.requestLifecycleRefresh()
361
+ }
362
+ }
363
+ lifecycleObservers.append(activeObserver)
364
+
365
+ let wakeObserver = NSWorkspace.shared.notificationCenter.addObserver(
366
+ forName: NSWorkspace.didWakeNotification,
367
+ object: nil,
368
+ queue: .main
369
+ ) { _ in
370
+ Task { @MainActor in
371
+ FlowPilotUpdateService.shared.requestLifecycleRefresh()
372
+ }
373
+ }
374
+ lifecycleObservers.append(wakeObserver)
375
+
376
+ let termObserver = NSWorkspace.shared.notificationCenter.addObserver(
377
+ forName: NSWorkspace.didTerminateApplicationNotification,
378
+ object: nil,
379
+ queue: .main
380
+ ) { notification in
381
+ Task { @MainActor in
382
+ FlowPilotUpdateService.shared.handleAppTermination(notification: notification)
383
+ }
384
+ }
385
+ lifecycleObservers.append(termObserver)
386
+ }
387
+
388
+ private func handleAppTermination(notification: Notification) {
389
+ guard snapshot.restartRequired == true else { return }
390
+ if let app = notification.userInfo?[NSWorkspace.applicationUserInfoKey] as? NSRunningApplication {
391
+ let name = app.localizedName?.lowercased() ?? ""
392
+ let bundle = app.bundleIdentifier?.lowercased() ?? ""
393
+ if name.contains("codex") || name.contains("chatgpt") || bundle.contains("codex") || bundle.contains("chatgpt") {
394
+ refreshFromDisk()
395
+ }
396
+ }
397
+ }
398
+
399
+ public func evaluateAutoCodexRestartAcknowledgement() {
400
+ guard snapshot.restartRequired == true else { return }
401
+ guard !isAcknowledgingRestart && !isInstalling && !isChecking else { return }
402
+
403
+ if let pids = snapshot.pendingCodexPids, !pids.isEmpty {
404
+ let anyAlive = pids.contains { pid in
405
+ kill(pid_t(pid), 0) == 0 || errno == EPERM
406
+ }
407
+ if !anyAlive {
408
+ acknowledgeRestart()
409
+ }
410
+ }
411
+ }
412
+
413
+ private func requestLifecycleRefresh() {
414
+ refreshFromDisk()
415
+ evaluateAutoUpdate()
416
+ requestBackgroundCheck(force: true, minimumInterval: Self.lifecycleRefreshInterval)
417
+ }
418
+
419
+ private func requestBackgroundCheck(force: Bool, minimumInterval: TimeInterval) {
420
+ refreshFromDisk()
421
+ guard !hasPendingUpdateAction else { return }
422
+ guard !isBackgroundCheckRunning else { return }
423
+
424
+ let now = Date()
425
+ if minimumInterval > 0,
426
+ let lastCheck = mostRecentCheckDate,
427
+ now.timeIntervalSince(lastCheck) < minimumInterval {
428
+ return
429
+ }
430
+
431
+ lastBackgroundCheckRequestedAt = now
432
+ isBackgroundCheckRunning = true
433
+ var arguments = ["update", "--check"]
434
+ if force {
435
+ arguments.append("--force")
436
+ }
437
+ arguments.append("--quiet")
438
+ runUpdater(arguments: arguments, mode: .backgroundCheck)
439
+ }
440
+
441
+ private var hasPendingUpdateAction: Bool {
442
+ (snapshot.updateAvailable ?? false)
443
+ || isRestartRequired
444
+ }
445
+
446
+ private var mostRecentCheckDate: Date? {
447
+ let persisted = snapshot.checkedAt.flatMap(Self.parseISO8601Date)
448
+ switch (lastBackgroundCheckRequestedAt, persisted) {
449
+ case let (runtime?, persisted?):
450
+ return max(runtime, persisted)
451
+ case let (runtime?, nil):
452
+ return runtime
453
+ case let (nil, persisted?):
454
+ return persisted
455
+ case (nil, nil):
456
+ return nil
457
+ }
458
+ }
459
+
460
+ private static func parseISO8601Date(_ value: String) -> Date? {
461
+ let formatter = ISO8601DateFormatter()
462
+ formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
463
+ if let date = formatter.date(from: value) {
464
+ return date
465
+ }
466
+ formatter.formatOptions = [.withInternetDateTime]
467
+ return formatter.date(from: value)
468
+ }
469
+
470
+ private enum RunMode: Sendable {
471
+ case backgroundCheck
472
+ case foregroundCheck
473
+ case install
474
+ case acknowledgeRestart
475
+ }
476
+
477
+ private struct ProcessResult: Sendable {
478
+ let exitCode: Int32
479
+ let output: String
480
+ }
481
+
482
+ private func runUpdater(arguments: [String], mode: RunMode) {
483
+ guard let executable = codexFlowExecutable else {
484
+ if mode == .backgroundCheck {
485
+ isBackgroundCheckRunning = false
486
+ } else {
487
+ actionError = L("codex-flow CLI was not found in the installed bin directory.", "未在安装目录中找到 codex-flow CLI。")
488
+ isChecking = false
489
+ isInstalling = false
490
+ isAcknowledgingRestart = false
491
+ }
492
+ return
493
+ }
494
+
495
+ Task.detached(priority: .utility) {
496
+ let result = Self.executeUpdater(executable: executable, arguments: arguments)
497
+ await MainActor.run {
498
+ FlowPilotUpdateService.shared.finish(mode: mode, result: result)
499
+ }
500
+ }
501
+ }
502
+
503
+ nonisolated private static func executeUpdater(executable: URL, arguments: [String]) -> ProcessResult {
504
+ let process = Process()
505
+ process.executableURL = executable
506
+ process.arguments = arguments
507
+ let pipe = Pipe()
508
+ process.standardOutput = pipe
509
+ process.standardError = pipe
510
+ do {
511
+ try process.run()
512
+ let data = pipe.fileHandleForReading.readDataToEndOfFile()
513
+ process.waitUntilExit()
514
+ let output = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
515
+ return ProcessResult(exitCode: process.terminationStatus, output: output)
516
+ } catch {
517
+ return ProcessResult(exitCode: -1, output: error.localizedDescription)
518
+ }
519
+ }
520
+
521
+ private func finish(mode: RunMode, result: ProcessResult) {
522
+ refreshFromDisk()
523
+ switch mode {
524
+ case .backgroundCheck:
525
+ isBackgroundCheckRunning = false
526
+ evaluateAutoUpdate()
527
+ case .foregroundCheck:
528
+ isChecking = false
529
+ if result.exitCode != 0 {
530
+ actionError = Self.cleanErrorMessage(result.output, fallback: L("Update check failed.", "检查更新失败。"))
531
+ } else {
532
+ actionMessage = statusText
533
+ evaluateAutoUpdate()
534
+ }
535
+ case .install:
536
+ isInstalling = false
537
+ if result.exitCode != 0 {
538
+ actionError = Self.cleanErrorMessage(result.output, fallback: L("Update failed.", "更新失败。"))
539
+ } else {
540
+ let msg = L("Update completed. Restarting FlowPilot…", "更新完成,正在自动重启 FlowPilot…")
541
+ actionMessage = msg
542
+ scheduleAutoRestart(reason: msg, delay: 1.2)
543
+ }
544
+ case .acknowledgeRestart:
545
+ isAcknowledgingRestart = false
546
+ if result.exitCode != 0 {
547
+ actionError = Self.cleanErrorMessage(result.output, fallback: L("Could not clear the restart reminder.", "无法清除重启提醒。"))
548
+ } else {
549
+ actionMessage = L("Restart reminder cleared.", "已清除重启提醒。")
550
+ }
551
+ }
552
+ }
553
+
554
+ private static func cleanErrorMessage(_ output: String, fallback: String) -> String {
555
+ let trimmed = output.trimmingCharacters(in: .whitespacesAndNewlines)
556
+ guard !trimmed.isEmpty else { return fallback }
557
+ let lines = trimmed.components(separatedBy: .newlines)
558
+ .map { $0.trimmingCharacters(in: .whitespaces) }
559
+ .filter { !$0.isEmpty }
560
+ if lines.count <= 3 {
561
+ return lines.joined(separator: "\n")
562
+ }
563
+ return lines.prefix(3).joined(separator: "\n")
564
+ }
565
+
566
+ private var updateStateURL: URL {
567
+ stateDirectory.appendingPathComponent("state/update.json")
568
+ }
569
+
570
+ private var stateDirectory: URL {
571
+ let env = ProcessInfo.processInfo.environment
572
+ if let value = env["CODEX_HOME"], !value.isEmpty {
573
+ return URL(fileURLWithPath: value).appendingPathComponent("codex-flow", isDirectory: true)
574
+ }
575
+ return FileManager.default.homeDirectoryForCurrentUser
576
+ .appendingPathComponent(".codex/codex-flow", isDirectory: true)
577
+ }
578
+
579
+ private var flowPilotExecutable: URL? {
580
+ let fm = FileManager.default
581
+ let stateBin = stateDirectory.appendingPathComponent("bin", isDirectory: true)
582
+ var candidates = [
583
+ stateBin.appendingPathComponent("FlowPilot"),
584
+ stateBin.appendingPathComponent("codex-flow-overlay"),
585
+ ]
586
+ if let firstArg = CommandLine.arguments.first, !firstArg.isEmpty {
587
+ candidates.append(URL(fileURLWithPath: firstArg))
588
+ }
589
+ return candidates.first { fm.isExecutableFile(atPath: $0.path) }
590
+ }
591
+
592
+ private var codexFlowExecutable: URL? {
593
+ let fm = FileManager.default
594
+ let env = ProcessInfo.processInfo.environment
595
+ var candidates: [URL] = []
596
+ if let value = env["CODEX_FLOW_BIN_DIR"], !value.isEmpty {
597
+ candidates.append(URL(fileURLWithPath: value).appendingPathComponent("codex-flow"))
598
+ }
599
+ let binState = stateDirectory.appendingPathComponent("bin_dir")
600
+ if let data = try? Data(contentsOf: binState),
601
+ let value = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines),
602
+ !value.isEmpty {
603
+ candidates.append(URL(fileURLWithPath: value).appendingPathComponent("codex-flow"))
604
+ }
605
+ candidates.append(FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(".local/bin/codex-flow"))
606
+ candidates.append(URL(fileURLWithPath: "/usr/local/bin/codex-flow"))
607
+ candidates.append(URL(fileURLWithPath: "/opt/homebrew/bin/codex-flow"))
608
+ return candidates.first { fm.isExecutableFile(atPath: $0.path) }
609
+ }
610
+ }