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.
- codex_flow/__init__.py +28 -0
- codex_flow/__main__.py +9 -0
- codex_flow/cli.py +242 -0
- codex_flow/data/LICENSE +21 -0
- codex_flow/data/README.en.md +303 -0
- codex_flow/data/README.md +305 -0
- codex_flow/data/VERSION +1 -0
- codex_flow/data/apps/chatgpt-mcp/README.md +86 -0
- codex_flow/data/apps/chatgpt-mcp/__init__.py +1 -0
- codex_flow/data/apps/chatgpt-mcp/adapter.py +458 -0
- codex_flow/data/apps/chatgpt-mcp/server.py +358 -0
- codex_flow/data/apps/chatgpt-mcp/widget.html +927 -0
- codex_flow/data/apps/macos-overlay/README.en.md +121 -0
- codex_flow/data/apps/macos-overlay/README.md +123 -0
- codex_flow/data/apps/macos-overlay/Sources/Controllers/OverlayRuntimeState.swift +126 -0
- codex_flow/data/apps/macos-overlay/Sources/Controllers/OverlayScreenGeometry.swift +82 -0
- codex_flow/data/apps/macos-overlay/Sources/Controllers/OverlayWindowController.swift +1052 -0
- codex_flow/data/apps/macos-overlay/Sources/Localization.swift +197 -0
- codex_flow/data/apps/macos-overlay/Sources/Models/TelemetryData.swift +1557 -0
- codex_flow/data/apps/macos-overlay/Sources/Services/AccountSnapshotService.swift +1101 -0
- codex_flow/data/apps/macos-overlay/Sources/Services/FlowPilotInstanceLock.swift +153 -0
- codex_flow/data/apps/macos-overlay/Sources/Services/IPCServer.swift +298 -0
- codex_flow/data/apps/macos-overlay/Sources/Services/TelemetryQueryEngine.swift +800 -0
- codex_flow/data/apps/macos-overlay/Sources/Services/TelemetryWatcher.swift +135 -0
- codex_flow/data/apps/macos-overlay/Sources/Services/UpdateService.swift +610 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/AccountView.swift +610 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/AnalyticsView.swift +566 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/AutostartView.swift +293 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/BubbleView.swift +317 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/HistoryView.swift +1124 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/HoverRevealText.swift +165 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/InspectorSkillsToolsView.swift +121 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/LogoView.swift +182 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/SleekSwitch.swift +117 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/StrategyModeView.swift +561 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/SummaryView.swift +1273 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/UpdateView.swift +352 -0
- codex_flow/data/apps/macos-overlay/Sources/main.swift +340 -0
- codex_flow/data/apps/macos-overlay/Tests/OverlayScreenGeometryTests.swift +163 -0
- codex_flow/data/apps/macos-overlay/Tests/TelemetryPhase1ContractTests.swift +357 -0
- codex_flow/data/apps/macos-overlay/Tests/TelemetryQueryEngineConcurrencyTests.swift +221 -0
- codex_flow/data/apps/macos-overlay/Tests/TelemetryQuotaSelectionTests.swift +158 -0
- codex_flow/data/apps/macos-overlay/Tests/TelemetryWorkerTokenTests.swift +122 -0
- codex_flow/data/apps/macos-overlay/build.sh +75 -0
- codex_flow/data/benchmark/corpus.json +103 -0
- codex_flow/data/benchmark/manifest.example.json +41 -0
- codex_flow/data/benchmark/manifest.schema.json +137 -0
- codex_flow/data/benchmark/prices/gpt-5.6-2026-08-30.json +5 -0
- codex_flow/data/benchmark/profiles.json +90 -0
- codex_flow/data/benchmark/schema.json +77 -0
- codex_flow/data/benchmark/tasks.json +50 -0
- codex_flow/data/completions/codex-flow.bash +34 -0
- codex_flow/data/completions/codex-flow.zsh +52 -0
- codex_flow/data/glama.json +6 -0
- codex_flow/data/install-release.ps1 +126 -0
- codex_flow/data/install-release.sh +155 -0
- codex_flow/data/install.ps1 +349 -0
- codex_flow/data/install.sh +362 -0
- codex_flow/data/policy/benchmark.toml +49 -0
- codex_flow/data/policy/defaults.toml +70 -0
- codex_flow/data/scripts/analyze-benchmark.py +510 -0
- codex_flow/data/scripts/benchmark-local.py +171 -0
- codex_flow/data/scripts/check-recommendation.py +277 -0
- codex_flow/data/scripts/doctor.py +449 -0
- codex_flow/data/scripts/generate-release-manifest.py +74 -0
- codex_flow/data/scripts/localization.py +192 -0
- codex_flow/data/scripts/manage-hooks.py +448 -0
- codex_flow/data/scripts/manage-instructions.py +389 -0
- codex_flow/data/scripts/manage-shell.py +151 -0
- codex_flow/data/scripts/materialize-corpus.py +193 -0
- codex_flow/data/scripts/menu.py +646 -0
- codex_flow/data/scripts/migrations/0001_update_settings.py +80 -0
- codex_flow/data/scripts/package-release.py +132 -0
- codex_flow/data/scripts/render-benchmark-report.py +292 -0
- codex_flow/data/scripts/run-benchmark.py +829 -0
- codex_flow/data/scripts/strategies/__init__.py +28 -0
- codex_flow/data/scripts/strategies/balanced.py +115 -0
- codex_flow/data/scripts/strategies/base.py +363 -0
- codex_flow/data/scripts/strategies/efficient.py +158 -0
- codex_flow/data/scripts/strategies/lifecycle_runtime.py +590 -0
- codex_flow/data/scripts/strategies/quality.py +209 -0
- codex_flow/data/scripts/strategies/speed.py +108 -0
- codex_flow/data/scripts/strategies/task_budget_runtime.py +644 -0
- codex_flow/data/scripts/strategies/task_phase_runtime.py +341 -0
- codex_flow/data/scripts/strategies/work_unit_runtime.py +421 -0
- codex_flow/data/scripts/strategy_runtime.py +1091 -0
- codex_flow/data/scripts/telemetry.py +400 -0
- codex_flow/data/scripts/telemetry_core/__init__.py +192 -0
- codex_flow/data/scripts/telemetry_core/app_server.py +1192 -0
- codex_flow/data/scripts/telemetry_core/collector.py +1247 -0
- codex_flow/data/scripts/telemetry_core/common.py +421 -0
- codex_flow/data/scripts/telemetry_core/latency.py +593 -0
- codex_flow/data/scripts/telemetry_core/query.py +427 -0
- codex_flow/data/scripts/telemetry_core/quota_ledger.py +598 -0
- codex_flow/data/scripts/telemetry_core/render.py +460 -0
- codex_flow/data/scripts/telemetry_core/repair.py +223 -0
- codex_flow/data/scripts/ui.py +266 -0
- codex_flow/data/scripts/update-homebrew-formula.py +146 -0
- codex_flow/data/scripts/update_runtime_config.py +134 -0
- codex_flow/data/scripts/updater.py +1718 -0
- codex_flow/data/smithery.yaml +18 -0
- codex_flow/data/templates/agents/worker-explorer.toml +24 -0
- codex_flow/data/templates/agents/worker-implementer.toml +49 -0
- codex_flow/data/templates/agents/worker-reviewer.toml +25 -0
- codex_flow/data/templates/flow-pilot-instructions.md +35 -0
- codex_flow/data/templates/skills/flow-pilot/SKILL.md +577 -0
- codex_flow/mcp.py +35 -0
- codex_flow-2.1.13.dist-info/METADATA +342 -0
- codex_flow-2.1.13.dist-info/RECORD +113 -0
- codex_flow-2.1.13.dist-info/WHEEL +5 -0
- codex_flow-2.1.13.dist-info/entry_points.txt +3 -0
- codex_flow-2.1.13.dist-info/licenses/LICENSE +21 -0
- 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
|
+
}
|