@ericsanchezok/meta-synergy 1.1.24 → 1.1.26

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.
@@ -1,12 +1,48 @@
1
1
  import AppKit
2
2
  import Foundation
3
3
 
4
+ private struct CLIEnvelope<Payload: Decodable>: Decodable {
5
+ let ok: Bool
6
+ let data: Payload?
7
+ }
8
+
9
+ private struct StatusPayload: Decodable {
10
+ let auth: AuthPayload
11
+ let state: StatePayload
12
+ let session: SessionPayload?
13
+ let envID: String?
14
+ let service: ServicePayload
15
+ }
16
+
17
+ private struct AuthPayload: Decodable {
18
+ let loggedIn: Bool
19
+ let agentID: String?
20
+ }
21
+
22
+ private struct StatePayload: Decodable {
23
+ let collaborationEnabled: Bool
24
+ let connectionStatus: String
25
+ let currentSession: SessionPayload?
26
+ let envID: String?
27
+ }
28
+
29
+ private struct SessionPayload: Decodable {
30
+ let sessionID: String?
31
+ let remoteAgentID: String?
32
+ }
33
+
34
+ private struct ServicePayload: Decodable {
35
+ let running: Bool
36
+ let runtimeStatus: String
37
+ let pid: Int?
38
+ }
39
+
4
40
  final class AppDelegate: NSObject, NSApplicationDelegate {
5
41
  private var statusItem: NSStatusItem!
6
42
  private var menu: NSMenu!
7
- private var hostProcess: Process?
8
43
  private var pollTimer: Timer?
9
44
  private var statusImage: NSImage?
45
+ private var lastStatus: StatusPayload?
10
46
 
11
47
  private let agentItem = NSMenuItem(title: "Agent ID: unknown", action: #selector(copyAgentID), keyEquivalent: "")
12
48
  private let envItem = NSMenuItem(title: "Env ID: unknown", action: #selector(copyEnvID), keyEquivalent: "")
@@ -19,7 +55,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
19
55
 
20
56
  private var currentAgentID: String?
21
57
  private var currentEnvID: String?
22
- private var hostStarted = false
23
58
 
24
59
  private lazy var runtimeURL: URL? = {
25
60
  Bundle.main.resourceURL?.appendingPathComponent("meta-synergy-runtime")
@@ -57,7 +92,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
57
92
 
58
93
  statusItem.menu = menu
59
94
 
60
- startHostIfPossible()
95
+ startServiceIfPossible()
61
96
  refreshStatus()
62
97
 
63
98
  pollTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { [weak self] _ in
@@ -67,106 +102,110 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
67
102
 
68
103
  @objc private func login() {
69
104
  _ = runCommand(["login"])
70
- startHostIfPossible()
105
+ startServiceIfPossible()
71
106
  refreshStatus()
72
107
  }
73
108
 
74
109
  @objc private func reconnect() {
75
- if hostProcess == nil {
76
- startHostIfPossible()
77
- } else {
110
+ let status = lastStatus ?? loadStatus()
111
+ let serviceRunning = status?.service.running ?? false
112
+ if serviceRunning {
78
113
  _ = runCommand(["reconnect"])
114
+ } else {
115
+ startServiceIfPossible()
79
116
  }
80
117
  refreshStatus()
81
118
  }
82
119
 
83
120
  @objc private func refreshStatus() {
84
- guard let output = runCommand(["status"]) else {
121
+ guard let status = loadStatus() else {
122
+ lastStatus = nil
123
+ currentAgentID = nil
124
+ currentEnvID = nil
125
+ agentItem.isHidden = true
126
+ envItem.isHidden = true
127
+ loginItem.isHidden = false
128
+ connectionItem.isHidden = false
85
129
  connectionItem.title = "Holos: unavailable"
86
130
  connectionItem.isEnabled = false
87
131
  collaborationItem.isHidden = true
88
132
  terminateItem.isHidden = true
89
- refreshItem.isHidden = true
133
+ refreshItem.isHidden = false
134
+ applyStatusAppearance(textFallback: "Meta", toolTip: "MetaSynergy")
90
135
  return
91
136
  }
92
137
 
93
- guard
94
- let data = output.data(using: .utf8),
95
- let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
96
- else {
97
- connectionItem.title = "Holos: unparsable"
98
- connectionItem.isEnabled = false
99
- return
100
- }
138
+ lastStatus = status
139
+
140
+ let loggedIn = status.auth.loggedIn
141
+ let agentID = status.auth.agentID
142
+ let envID = status.envID ?? status.state.envID
143
+ let connectionStatus = status.state.connectionStatus
144
+ let collaborationEnabled = status.state.collaborationEnabled
145
+ let session = status.session ?? status.state.currentSession
146
+ let serviceRunning = status.service.running
147
+ let serviceTransitioning = status.service.runtimeStatus == "starting" || status.service.runtimeStatus == "stopping"
148
+
149
+ currentAgentID = loggedIn ? agentID : nil
150
+ currentEnvID = loggedIn ? envID : nil
101
151
 
102
- let auth = json["auth"] as? [String: Any]
103
- let state = json["state"] as? [String: Any]
104
- let session = json["session"] as? [String: Any]
105
- let envID = json["envID"] as? String
106
- let agentID = auth?["agentID"] as? String
107
- let agentSecret = auth?["agentSecret"] as? String
108
- let hasAuth = !(agentID?.isEmpty ?? true) && !(agentSecret?.isEmpty ?? true)
109
- currentAgentID = hasAuth ? agentID : nil
110
- currentEnvID = hasAuth ? envID : nil
111
-
112
- let connected = state?["connectionStatus"] as? String ?? "disconnected"
113
- let collaborationEnabled = (state?["collaborationEnabled"] as? Bool) ?? true
114
- let isConnected = connected == "connected"
115
-
116
- agentItem.isHidden = !hasAuth
117
- envItem.isHidden = !hasAuth
118
- loginItem.isHidden = hasAuth
119
-
120
- if hasAuth {
152
+ agentItem.isHidden = !loggedIn
153
+ envItem.isHidden = !loggedIn
154
+ loginItem.isHidden = loggedIn
155
+
156
+ if loggedIn {
121
157
  agentItem.title = "Agent ID: \(truncateID(agentID))"
122
158
  envItem.title = "Env ID: \(truncateID(envID))"
123
159
  }
124
160
 
125
- connectionItem.isHidden = !hasAuth
126
- collaborationItem.isHidden = !hasAuth
127
- terminateItem.isHidden = !hasAuth
128
- refreshItem.isHidden = !hasAuth
161
+ connectionItem.isHidden = !loggedIn
162
+ collaborationItem.isHidden = !loggedIn
163
+ terminateItem.isHidden = !loggedIn
164
+ refreshItem.isHidden = false
165
+
166
+ let connectionSummary = serviceRunning ? connectionStatus : "service \(status.service.runtimeStatus)"
167
+ connectionItem.title = "Holos: \(connectionSummary)"
168
+ connectionItem.isEnabled = loggedIn && !serviceTransitioning && (!serviceRunning || connectionStatus != "connected")
129
169
 
130
- connectionItem.title = "Holos: \(connected)"
131
- connectionItem.isEnabled = hasAuth && !isConnected
132
170
  collaborationItem.title = collaborationEnabled ? "Collaboration: on" : "Collaboration: off"
133
- collaborationItem.isEnabled = hasAuth
171
+ collaborationItem.isEnabled = loggedIn
134
172
 
135
- if let sessionID = session?["sessionID"] as? String,
136
- let remoteAgentID = session?["remoteAgentID"] as? String {
173
+ if let sessionID = session?.sessionID,
174
+ let remoteAgentID = session?.remoteAgentID {
137
175
  terminateItem.title = "Terminate Collaboration (\(truncateID(remoteAgentID)), \(sessionID.prefix(8)))"
138
176
  terminateItem.isEnabled = true
139
177
  applyStatusAppearance(textFallback: "Meta Busy", toolTip: "MetaSynergy: busy")
140
178
  } else {
141
179
  terminateItem.title = "Terminate Collaboration"
142
180
  terminateItem.isEnabled = false
143
- if hasAuth {
181
+
182
+ if !loggedIn {
183
+ applyStatusAppearance(textFallback: "Meta", toolTip: "MetaSynergy")
184
+ } else if !serviceRunning {
185
+ applyStatusAppearance(textFallback: "Meta", toolTip: "MetaSynergy: service \(status.service.runtimeStatus)")
186
+ } else {
144
187
  applyStatusAppearance(
145
188
  textFallback: collaborationEnabled ? "Meta" : "Meta Off",
146
- toolTip: collaborationEnabled ? "MetaSynergy: collaboration on" : "MetaSynergy: collaboration off",
189
+ toolTip: collaborationEnabled
190
+ ? "MetaSynergy: Holos \(connectionStatus)"
191
+ : "MetaSynergy: collaboration off"
147
192
  )
148
- } else {
149
- applyStatusAppearance(textFallback: "Meta", toolTip: "MetaSynergy")
150
193
  }
151
194
  }
152
195
  }
153
196
 
154
197
  @objc private func toggleCollaboration() {
155
- guard let output = runCommand(["status"]),
156
- let data = output.data(using: .utf8),
157
- let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
158
- let state = json["state"] as? [String: Any],
159
- let enabled = state["collaborationEnabled"] as? Bool
160
- else {
198
+ let enabled = lastStatus?.state.collaborationEnabled ?? loadStatus()?.state.collaborationEnabled
199
+ guard let enabled else {
161
200
  return
162
201
  }
163
202
 
164
- _ = runCommand([enabled ? "disable" : "enable"])
203
+ _ = runCommand(["collaboration", enabled ? "disable" : "enable"])
165
204
  refreshStatus()
166
205
  }
167
206
 
168
207
  @objc private func terminateCollaboration() {
169
- _ = runCommand(["kick"])
208
+ _ = runCommand(["session", "kick"])
170
209
  refreshStatus()
171
210
  }
172
211
 
@@ -179,45 +218,21 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
179
218
  }
180
219
 
181
220
  @objc private func quitApp() {
182
- hostProcess?.terminate()
221
+ _ = runCommand(["stop"])
183
222
  NSApplication.shared.terminate(nil)
184
223
  }
185
224
 
186
- private func startHostIfPossible() {
187
- guard hostProcess == nil, !hostStarted else { return }
225
+ private func startServiceIfPossible() {
188
226
  guard hasAuth() else { return }
189
- guard let runtimeURL else { return }
190
-
191
- let process = Process()
192
- process.executableURL = runtimeURL
193
- process.arguments = ["start"]
194
- process.terminationHandler = { [weak self] _ in
195
- DispatchQueue.main.async {
196
- self?.hostStarted = false
197
- self?.hostProcess = nil
198
- }
199
- }
200
-
201
- do {
202
- try process.run()
203
- hostProcess = process
204
- hostStarted = true
205
- } catch {
206
- connectionItem.title = "Holos: failed to start host"
227
+ let status = lastStatus ?? loadStatus()
228
+ if status?.service.running == true {
229
+ return
207
230
  }
231
+ _ = runCommand(["start"])
208
232
  }
209
233
 
210
234
  private func hasAuth() -> Bool {
211
- guard let output = runCommand(["status"]),
212
- let data = output.data(using: .utf8),
213
- let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
214
- let auth = json["auth"] as? [String: Any],
215
- let agentID = auth["agentID"] as? String,
216
- let agentSecret = auth["agentSecret"] as? String
217
- else {
218
- return false
219
- }
220
- return !agentID.isEmpty && !agentSecret.isEmpty
235
+ (lastStatus ?? loadStatus())?.auth.loggedIn == true
221
236
  }
222
237
 
223
238
  @discardableResult
@@ -241,6 +256,23 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
241
256
  }
242
257
  }
243
258
 
259
+ private func loadStatus() -> StatusPayload? {
260
+ runJSONCommand(["status"], as: StatusPayload.self)
261
+ }
262
+
263
+ private func runJSONCommand<Payload: Decodable>(_ args: [String], as type: Payload.Type) -> Payload? {
264
+ guard let output = runCommand(args + ["--json"]),
265
+ let data = output.data(using: .utf8),
266
+ let envelope = try? JSONDecoder().decode(CLIEnvelope<Payload>.self, from: data),
267
+ envelope.ok,
268
+ let payload = envelope.data
269
+ else {
270
+ return nil
271
+ }
272
+
273
+ return payload
274
+ }
275
+
244
276
  private func copyValue(_ value: String?) {
245
277
  guard let value, !value.isEmpty else { return }
246
278
  let pasteboard = NSPasteboard.general
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@ericsanchezok/meta-synergy",
4
- "version": "1.1.24",
4
+ "version": "1.1.26",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "bin": {