catalyst-core-internal 0.1.3 → 0.1.5
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.
- package/bin/catalyst.js +1 -8
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/BridgeMessageValidator.kt +1 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt +1 -12
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +3 -18
- package/dist/native/buildAppAndroid.js +2 -2
- package/dist/native/buildAppIos.js +17 -10
- package/dist/native/iosnativeWebView/Sources/Core/WebView/NativeBridge.swift +2 -13
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebView.swift +0 -6
- package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/project.pbxproj +0 -4
- package/mcp_v2/conversion-tasks.json +326 -0
- package/mcp_v2/knowledge-base.json +1068 -0
- package/mcp_v2/lib/helpers.js +159 -0
- package/mcp_v2/mcp.js +562 -0
- package/mcp_v2/package.json +13 -0
- package/mcp_v2/schema.sql +88 -0
- package/mcp_v2/setup.js +276 -0
- package/mcp_v2/tools/build.js +686 -0
- package/mcp_v2/tools/config.js +453 -0
- package/mcp_v2/tools/conversion.js +799 -0
- package/mcp_v2/tools/debug.js +113 -0
- package/mcp_v2/tools/knowledge.js +219 -0
- package/mcp_v2/tools/sync.js +23 -0
- package/mcp_v2/tools/tasks.js +945 -0
- package/package.json +2 -3
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/CatalystPlugin.kt +0 -5
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/GeneratedPluginIndex.kt +0 -6
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/PluginBridge.kt +0 -240
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/plugins/PluginBridgeTest.kt +0 -121
- package/dist/native/internal-plugins/device-info-plugin/android/DeviceInfoPlugin.kt +0 -43
- package/dist/native/internal-plugins/device-info-plugin/ios/DeviceInfoPlugin.swift +0 -28
- package/dist/native/internal-plugins/device-info-plugin/manifest.json +0 -19
- package/dist/native/internalPluginUtils.js +0 -1
- package/dist/native/iosnativeWebView/Sources/Core/Plugins/CatalystPlugin.swift +0 -5
- package/dist/native/iosnativeWebView/Sources/Core/Plugins/GeneratedPluginIndex.swift +0 -6
- package/dist/native/iosnativeWebView/Sources/Core/Plugins/PluginBridge.swift +0 -364
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WeakScriptMessageHandler.swift +0 -14
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/PluginBridgeTests.swift +0 -160
- package/dist/native/plugin-bridge/PluginBridge.js +0 -1
- package/dist/native/pluginComposerAndroid.js +0 -9
- package/dist/native/pluginComposerIos.js +0 -7
- package/dist/scripts/plugins.js +0 -1
|
@@ -1,364 +0,0 @@
|
|
|
1
|
-
import Foundation
|
|
2
|
-
import WebKit
|
|
3
|
-
import UIKit
|
|
4
|
-
import os
|
|
5
|
-
|
|
6
|
-
private let pluginBridgeLogger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.app", category: "PluginBridge")
|
|
7
|
-
|
|
8
|
-
private struct PluginBridgeValidationError: LocalizedError {
|
|
9
|
-
let message: String
|
|
10
|
-
let code: String
|
|
11
|
-
|
|
12
|
-
var errorDescription: String? {
|
|
13
|
-
message
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
private struct PluginRequest {
|
|
18
|
-
let pluginId: String
|
|
19
|
-
let command: String
|
|
20
|
-
let data: Any?
|
|
21
|
-
let requestId: String?
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
protocol PluginBridgeMessage {
|
|
25
|
-
var name: String { get }
|
|
26
|
-
var body: Any { get }
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
extension WKScriptMessage: PluginBridgeMessage {}
|
|
30
|
-
|
|
31
|
-
final class PluginBridge: NSObject {
|
|
32
|
-
private weak var webView: WKWebView?
|
|
33
|
-
private weak var viewController: UIViewController?
|
|
34
|
-
private var messageHandlerProxy: WeakScriptMessageHandler?
|
|
35
|
-
private var isRegistered = false
|
|
36
|
-
|
|
37
|
-
private let pluginFactories = GeneratedPluginIndex.pluginFactories
|
|
38
|
-
private let pluginToCommands = GeneratedPluginIndex.pluginToCommands
|
|
39
|
-
|
|
40
|
-
private let bridgeName = "PluginBridge"
|
|
41
|
-
private let errorEvent = "PLUGIN_BRIDGE_ERROR"
|
|
42
|
-
private let systemPluginId = "__bridge__"
|
|
43
|
-
|
|
44
|
-
init(webView: WKWebView, viewController: UIViewController) {
|
|
45
|
-
self.webView = webView
|
|
46
|
-
self.viewController = viewController
|
|
47
|
-
super.init()
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
func register() {
|
|
51
|
-
guard !isRegistered else { return }
|
|
52
|
-
guard let userContentController = webView?.configuration.userContentController else {
|
|
53
|
-
return
|
|
54
|
-
}
|
|
55
|
-
let proxy = WeakScriptMessageHandler(delegate: self)
|
|
56
|
-
userContentController.add(proxy, name: bridgeName)
|
|
57
|
-
messageHandlerProxy = proxy
|
|
58
|
-
isRegistered = true
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
func unregister() {
|
|
62
|
-
guard isRegistered else { return }
|
|
63
|
-
webView?.configuration.userContentController.removeScriptMessageHandler(forName: bridgeName)
|
|
64
|
-
messageHandlerProxy = nil
|
|
65
|
-
isRegistered = false
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
deinit {
|
|
69
|
-
unregister()
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
private func parseRequest(_ message: PluginBridgeMessage) throws -> PluginRequest {
|
|
73
|
-
guard message.name == bridgeName else {
|
|
74
|
-
throw PluginBridgeValidationError(message: "Invalid message handler", code: "INVALID_PAYLOAD")
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
let body: [String: Any]
|
|
78
|
-
if let dictionary = message.body as? [String: Any] {
|
|
79
|
-
body = dictionary
|
|
80
|
-
} else if let dictionary = message.body as? NSDictionary,
|
|
81
|
-
let castedBody = dictionary as? [String: Any] {
|
|
82
|
-
body = castedBody
|
|
83
|
-
} else {
|
|
84
|
-
throw PluginBridgeValidationError(message: "Payload must be an object", code: "INVALID_PAYLOAD")
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
guard JSONSerialization.isValidJSONObject(body) else {
|
|
88
|
-
throw PluginBridgeValidationError(message: "Payload must be JSON-serializable", code: "INVALID_PAYLOAD")
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
let payloadData: Data
|
|
92
|
-
do {
|
|
93
|
-
payloadData = try JSONSerialization.data(withJSONObject: body)
|
|
94
|
-
} catch {
|
|
95
|
-
throw PluginBridgeValidationError(message: "Invalid payload", code: "INVALID_PAYLOAD")
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if payloadData.count > CatalystConstants.Bridge.maxMessageSize {
|
|
99
|
-
throw PluginBridgeValidationError(message: "Payload exceeds maximum size", code: "INVALID_PAYLOAD")
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
let pluginId = (body["pluginId"] as? String ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
|
103
|
-
let command = (body["command"] as? String ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
|
104
|
-
let requestId = (body["requestId"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
105
|
-
|
|
106
|
-
return PluginRequest(
|
|
107
|
-
pluginId: pluginId,
|
|
108
|
-
command: command,
|
|
109
|
-
data: body["data"],
|
|
110
|
-
requestId: requestId?.isEmpty == true ? nil : requestId
|
|
111
|
-
)
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
private func hasPlugin(_ pluginId: String) -> Bool {
|
|
115
|
-
pluginFactories[pluginId] != nil
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
private func hasCommand(pluginId: String, command: String) -> Bool {
|
|
119
|
-
pluginToCommands[pluginId]?.contains(command) ?? false
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
private func sendBridgeError(message: String, code: String, request: PluginRequest?) {
|
|
123
|
-
let bridge = PluginBridgeContext(
|
|
124
|
-
webView: webView,
|
|
125
|
-
viewController: viewController,
|
|
126
|
-
pluginId: systemPluginId,
|
|
127
|
-
command: request?.command,
|
|
128
|
-
requestId: request?.requestId
|
|
129
|
-
)
|
|
130
|
-
var payload: [String: Any] = [
|
|
131
|
-
"message": message,
|
|
132
|
-
"code": code,
|
|
133
|
-
"pluginId": request?.pluginId ?? systemPluginId,
|
|
134
|
-
]
|
|
135
|
-
if let command = request?.command, !command.isEmpty {
|
|
136
|
-
payload["command"] = command
|
|
137
|
-
}
|
|
138
|
-
bridge.callback(eventName: errorEvent, data: payload)
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
func handleMessage(_ message: PluginBridgeMessage) {
|
|
142
|
-
var request: PluginRequest?
|
|
143
|
-
|
|
144
|
-
do {
|
|
145
|
-
let parsedRequest = try parseRequest(message)
|
|
146
|
-
request = parsedRequest
|
|
147
|
-
|
|
148
|
-
if parsedRequest.pluginId.isEmpty {
|
|
149
|
-
sendBridgeError(message: "pluginId is required", code: "INVALID_PAYLOAD", request: parsedRequest)
|
|
150
|
-
return
|
|
151
|
-
}
|
|
152
|
-
if parsedRequest.command.isEmpty {
|
|
153
|
-
sendBridgeError(message: "command is required", code: "INVALID_PAYLOAD", request: parsedRequest)
|
|
154
|
-
return
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
if !hasPlugin(parsedRequest.pluginId) {
|
|
158
|
-
sendBridgeError(
|
|
159
|
-
message: "Unsupported plugin: \(parsedRequest.pluginId)",
|
|
160
|
-
code: "PLUGIN_NOT_FOUND",
|
|
161
|
-
request: parsedRequest
|
|
162
|
-
)
|
|
163
|
-
return
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
if !hasCommand(pluginId: parsedRequest.pluginId, command: parsedRequest.command) {
|
|
167
|
-
sendBridgeError(
|
|
168
|
-
message: "Unsupported command '\(parsedRequest.command)' for plugin '\(parsedRequest.pluginId)'",
|
|
169
|
-
code: "COMMAND_NOT_SUPPORTED",
|
|
170
|
-
request: parsedRequest
|
|
171
|
-
)
|
|
172
|
-
return
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
guard let factory = pluginFactories[parsedRequest.pluginId] else {
|
|
176
|
-
sendBridgeError(
|
|
177
|
-
message: "No plugin registered for id: \(parsedRequest.pluginId)",
|
|
178
|
-
code: "PLUGIN_NOT_REGISTERED",
|
|
179
|
-
request: parsedRequest
|
|
180
|
-
)
|
|
181
|
-
return
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
let plugin = factory()
|
|
185
|
-
let bridge = PluginBridgeContext(
|
|
186
|
-
webView: webView,
|
|
187
|
-
viewController: viewController,
|
|
188
|
-
pluginId: parsedRequest.pluginId,
|
|
189
|
-
command: parsedRequest.command,
|
|
190
|
-
requestId: parsedRequest.requestId
|
|
191
|
-
)
|
|
192
|
-
plugin.handle(command: parsedRequest.command, data: parsedRequest.data, bridge: bridge)
|
|
193
|
-
} catch let error as PluginBridgeValidationError {
|
|
194
|
-
sendBridgeError(message: error.message, code: error.code, request: request)
|
|
195
|
-
} catch {
|
|
196
|
-
pluginBridgeLogger.error("Plugin command failed: \(error.localizedDescription)")
|
|
197
|
-
sendBridgeError(
|
|
198
|
-
message: "Plugin execution failed: \(error.localizedDescription)",
|
|
199
|
-
code: "PLUGIN_EXECUTION_FAILED",
|
|
200
|
-
request: request
|
|
201
|
-
)
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
extension PluginBridge: WKScriptMessageHandler {
|
|
207
|
-
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
|
|
208
|
-
handleMessage(message)
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
final class PluginBridgeContext {
|
|
213
|
-
weak var webView: WKWebView?
|
|
214
|
-
weak var viewController: UIViewController?
|
|
215
|
-
|
|
216
|
-
private let systemPluginId = "__bridge__"
|
|
217
|
-
private let bridgeErrorEvent = "PLUGIN_BRIDGE_ERROR"
|
|
218
|
-
|
|
219
|
-
let pluginId: String
|
|
220
|
-
let command: String?
|
|
221
|
-
let requestId: String?
|
|
222
|
-
|
|
223
|
-
init(
|
|
224
|
-
webView: WKWebView?,
|
|
225
|
-
viewController: UIViewController?,
|
|
226
|
-
pluginId: String,
|
|
227
|
-
command: String?,
|
|
228
|
-
requestId: String?
|
|
229
|
-
) {
|
|
230
|
-
self.webView = webView
|
|
231
|
-
self.viewController = viewController
|
|
232
|
-
self.pluginId = pluginId
|
|
233
|
-
self.command = command
|
|
234
|
-
self.requestId = requestId
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
func callback(
|
|
238
|
-
eventName: String,
|
|
239
|
-
data: Any?,
|
|
240
|
-
command: String? = nil
|
|
241
|
-
) {
|
|
242
|
-
let resolvedRequestId = self.requestId
|
|
243
|
-
let resolvedCommand = command ?? self.command
|
|
244
|
-
let trimmedEventName = eventName.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
245
|
-
guard !trimmedEventName.isEmpty else {
|
|
246
|
-
emitBridgeError(
|
|
247
|
-
message: "Rejected callback with blank event name for plugin \(self.pluginId)",
|
|
248
|
-
code: "INVALID_CALLBACK",
|
|
249
|
-
requestId: resolvedRequestId,
|
|
250
|
-
command: resolvedCommand
|
|
251
|
-
)
|
|
252
|
-
return
|
|
253
|
-
}
|
|
254
|
-
if !dispatchEnvelope(
|
|
255
|
-
pluginId: pluginId,
|
|
256
|
-
eventName: trimmedEventName,
|
|
257
|
-
payload: data,
|
|
258
|
-
requestId: resolvedRequestId,
|
|
259
|
-
command: resolvedCommand
|
|
260
|
-
) {
|
|
261
|
-
if pluginId == systemPluginId && trimmedEventName == bridgeErrorEvent {
|
|
262
|
-
pluginBridgeLogger.error("Failed to dispatch bridge error event for plugin \(self.pluginId)")
|
|
263
|
-
return
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
emitBridgeError(
|
|
267
|
-
message: "Failed to dispatch callback \(trimmedEventName) for plugin \(self.pluginId)",
|
|
268
|
-
code: "PLUGIN_EXECUTION_FAILED",
|
|
269
|
-
requestId: resolvedRequestId,
|
|
270
|
-
command: resolvedCommand
|
|
271
|
-
)
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
private func emitBridgeError(
|
|
276
|
-
message: String,
|
|
277
|
-
code: String,
|
|
278
|
-
requestId: String?,
|
|
279
|
-
command: String?
|
|
280
|
-
) {
|
|
281
|
-
var payload: [String: Any] = [
|
|
282
|
-
"message": message,
|
|
283
|
-
"code": code,
|
|
284
|
-
"pluginId": pluginId,
|
|
285
|
-
]
|
|
286
|
-
|
|
287
|
-
if let command = command, !command.isEmpty {
|
|
288
|
-
payload["command"] = command
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
if !dispatchEnvelope(
|
|
292
|
-
pluginId: systemPluginId,
|
|
293
|
-
eventName: bridgeErrorEvent,
|
|
294
|
-
payload: payload,
|
|
295
|
-
requestId: requestId,
|
|
296
|
-
command: command,
|
|
297
|
-
logFailures: false
|
|
298
|
-
) {
|
|
299
|
-
pluginBridgeLogger.error("Failed to dispatch bridge error for plugin \(self.pluginId): \(message)")
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
private func dispatchEnvelope(
|
|
304
|
-
pluginId: String,
|
|
305
|
-
eventName: String,
|
|
306
|
-
payload: Any?,
|
|
307
|
-
requestId: String?,
|
|
308
|
-
command: String?,
|
|
309
|
-
logFailures: Bool = true
|
|
310
|
-
) -> Bool {
|
|
311
|
-
guard let webView = webView else {
|
|
312
|
-
if logFailures {
|
|
313
|
-
pluginBridgeLogger.error("WebView unavailable for plugin callback \(eventName)")
|
|
314
|
-
}
|
|
315
|
-
return false
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
let envelope: [String: Any] = [
|
|
319
|
-
"pluginId": pluginId,
|
|
320
|
-
"eventName": eventName,
|
|
321
|
-
"payload": payload ?? NSNull(),
|
|
322
|
-
"requestId": requestId ?? NSNull(),
|
|
323
|
-
"command": command ?? NSNull(),
|
|
324
|
-
]
|
|
325
|
-
|
|
326
|
-
guard JSONSerialization.isValidJSONObject(envelope),
|
|
327
|
-
let envelopeData = try? JSONSerialization.data(withJSONObject: envelope),
|
|
328
|
-
let envelopeJson = String(data: envelopeData, encoding: .utf8) else {
|
|
329
|
-
if logFailures {
|
|
330
|
-
pluginBridgeLogger.error("Failed to serialize plugin callback envelope for \(pluginId).\(eventName)")
|
|
331
|
-
}
|
|
332
|
-
return false
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
let envelopeLiteral = javaScriptStringLiteral(envelopeJson)
|
|
336
|
-
let script = "window.PluginBridgeWeb && window.PluginBridgeWeb.dispatch(\(envelopeLiteral));"
|
|
337
|
-
|
|
338
|
-
DispatchQueue.main.async {
|
|
339
|
-
webView.evaluateJavaScript(
|
|
340
|
-
script,
|
|
341
|
-
completionHandler: { _, error in
|
|
342
|
-
if let error = error {
|
|
343
|
-
pluginBridgeLogger.error(
|
|
344
|
-
"Plugin callback JS failed for \(pluginId).\(eventName): \(error.localizedDescription)"
|
|
345
|
-
)
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
)
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
return true
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
private func javaScriptStringLiteral(_ value: String) -> String {
|
|
355
|
-
let escaped = value
|
|
356
|
-
.replacingOccurrences(of: "\\", with: "\\\\")
|
|
357
|
-
.replacingOccurrences(of: "\"", with: "\\\"")
|
|
358
|
-
.replacingOccurrences(of: "\n", with: "\\n")
|
|
359
|
-
.replacingOccurrences(of: "\r", with: "\\r")
|
|
360
|
-
.replacingOccurrences(of: "\u{2028}", with: "\\u2028")
|
|
361
|
-
.replacingOccurrences(of: "\u{2029}", with: "\\u2029")
|
|
362
|
-
return "\"\(escaped)\""
|
|
363
|
-
}
|
|
364
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import WebKit
|
|
2
|
-
|
|
3
|
-
final class WeakScriptMessageHandler: NSObject, WKScriptMessageHandler {
|
|
4
|
-
weak var delegate: WKScriptMessageHandler?
|
|
5
|
-
|
|
6
|
-
init(delegate: WKScriptMessageHandler) {
|
|
7
|
-
self.delegate = delegate
|
|
8
|
-
super.init()
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
|
|
12
|
-
delegate?.userContentController(userContentController, didReceive: message)
|
|
13
|
-
}
|
|
14
|
-
}
|
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
import XCTest
|
|
2
|
-
import WebKit
|
|
3
|
-
import UIKit
|
|
4
|
-
@testable import CatalystCore
|
|
5
|
-
|
|
6
|
-
final class PluginBridgeTests: XCTestCase {
|
|
7
|
-
|
|
8
|
-
private var bridge: PluginBridge!
|
|
9
|
-
private var mockWebView: PluginMockWKWebView!
|
|
10
|
-
private var mockViewController: UIViewController!
|
|
11
|
-
private var testExpectation: XCTestExpectation!
|
|
12
|
-
|
|
13
|
-
@MainActor
|
|
14
|
-
override func setUp() {
|
|
15
|
-
super.setUp()
|
|
16
|
-
mockWebView = PluginMockWKWebView()
|
|
17
|
-
mockViewController = UIViewController()
|
|
18
|
-
bridge = PluginBridge(webView: mockWebView, viewController: mockViewController)
|
|
19
|
-
bridge.register()
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
@MainActor
|
|
23
|
-
override func tearDown() {
|
|
24
|
-
bridge.unregister()
|
|
25
|
-
bridge = nil
|
|
26
|
-
mockWebView = nil
|
|
27
|
-
mockViewController = nil
|
|
28
|
-
testExpectation = nil
|
|
29
|
-
super.tearDown()
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
@MainActor
|
|
33
|
-
func testMessageHandling_InvalidHandler_EmitsInvalidPayloadError() async {
|
|
34
|
-
testExpectation = expectation(description: "Invalid handler should emit bridge error")
|
|
35
|
-
mockWebView.onEvaluateJavaScript = { script in
|
|
36
|
-
if script.contains("PLUGIN_BRIDGE_ERROR") && script.contains("INVALID_PAYLOAD") {
|
|
37
|
-
self.testExpectation.fulfill()
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
let message = PluginMockScriptMessage(
|
|
42
|
-
name: "NativeBridge",
|
|
43
|
-
body: [
|
|
44
|
-
"pluginId": "device-info-plugin",
|
|
45
|
-
"command": "getDeviceInfo"
|
|
46
|
-
]
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
bridge.handleMessage(message)
|
|
50
|
-
|
|
51
|
-
await fulfillment(of: [testExpectation], timeout: 2.0)
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
@MainActor
|
|
55
|
-
func testMessageHandling_UnknownPlugin_EmitsPluginNotFoundError() async {
|
|
56
|
-
testExpectation = expectation(description: "Unknown plugin should emit plugin not found error")
|
|
57
|
-
mockWebView.onEvaluateJavaScript = { script in
|
|
58
|
-
if script.contains("PLUGIN_BRIDGE_ERROR") && script.contains("PLUGIN_NOT_FOUND") {
|
|
59
|
-
self.testExpectation.fulfill()
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
let message = PluginMockScriptMessage(
|
|
64
|
-
name: "PluginBridge",
|
|
65
|
-
body: [
|
|
66
|
-
"pluginId": "missing-plugin",
|
|
67
|
-
"command": "ping",
|
|
68
|
-
"requestId": "req-42"
|
|
69
|
-
]
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
bridge.handleMessage(message)
|
|
73
|
-
|
|
74
|
-
await fulfillment(of: [testExpectation], timeout: 2.0)
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
@MainActor
|
|
78
|
-
func testCallbackHandling_ArbitraryCallback_DispatchesPluginEnvelope() async {
|
|
79
|
-
testExpectation = expectation(description: "Arbitrary callback should dispatch plugin envelope")
|
|
80
|
-
mockWebView.onEvaluateJavaScript = { script in
|
|
81
|
-
if script.contains("PluginBridgeWeb.dispatch") &&
|
|
82
|
-
script.contains("sync-plugin") &&
|
|
83
|
-
script.contains("ON_FAILURE") &&
|
|
84
|
-
script.contains("req-9") &&
|
|
85
|
-
script.contains("syncData") {
|
|
86
|
-
self.testExpectation.fulfill()
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
let context = PluginBridgeContext(
|
|
91
|
-
webView: mockWebView,
|
|
92
|
-
viewController: mockViewController,
|
|
93
|
-
pluginId: "sync-plugin",
|
|
94
|
-
command: "syncData",
|
|
95
|
-
requestId: "req-9"
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
context.callback(eventName: "ON_FAILURE", data: ["reason": "network"])
|
|
99
|
-
|
|
100
|
-
await fulfillment(of: [testExpectation], timeout: 2.0)
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
@MainActor
|
|
104
|
-
func testCallbackHandling_ValidCallback_DispatchesPluginEnvelope() async {
|
|
105
|
-
testExpectation = expectation(description: "Valid callback should dispatch plugin envelope")
|
|
106
|
-
mockWebView.onEvaluateJavaScript = { script in
|
|
107
|
-
if script.contains("PluginBridgeWeb.dispatch") &&
|
|
108
|
-
script.contains("sync-plugin") &&
|
|
109
|
-
script.contains("ON_SUCCESS") &&
|
|
110
|
-
script.contains("req-10") {
|
|
111
|
-
self.testExpectation.fulfill()
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
let context = PluginBridgeContext(
|
|
116
|
-
webView: mockWebView,
|
|
117
|
-
viewController: mockViewController,
|
|
118
|
-
pluginId: "sync-plugin",
|
|
119
|
-
command: "syncData",
|
|
120
|
-
requestId: "req-10"
|
|
121
|
-
)
|
|
122
|
-
|
|
123
|
-
context.callback(eventName: "ON_SUCCESS", data: ["status": "ok"])
|
|
124
|
-
|
|
125
|
-
await fulfillment(of: [testExpectation], timeout: 2.0)
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
private final class PluginMockWKWebView: WKWebView {
|
|
130
|
-
var onEvaluateJavaScript: ((String) -> Void)?
|
|
131
|
-
var evaluatedScripts: [String] = []
|
|
132
|
-
|
|
133
|
-
override init(frame: CGRect, configuration: WKWebViewConfiguration) {
|
|
134
|
-
super.init(frame: frame, configuration: configuration)
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
convenience init() {
|
|
138
|
-
let config = WKWebViewConfiguration()
|
|
139
|
-
self.init(frame: .zero, configuration: config)
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
required init?(coder: NSCoder) {
|
|
143
|
-
fatalError("init(coder:) has not been implemented")
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
@MainActor
|
|
147
|
-
override func evaluateJavaScript(_ javaScriptString: String, completionHandler: ((Any?, Error?) -> Void)? = nil) {
|
|
148
|
-
evaluatedScripts.append(javaScriptString)
|
|
149
|
-
onEvaluateJavaScript?(javaScriptString)
|
|
150
|
-
|
|
151
|
-
DispatchQueue.main.async {
|
|
152
|
-
completionHandler?(nil, nil)
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
private struct PluginMockScriptMessage: PluginBridgeMessage {
|
|
158
|
-
let name: String
|
|
159
|
-
let body: Any
|
|
160
|
-
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.default=void 0;class PluginNativeBridge{constructor(){this.handlers=new Map();this.requestCount=0;this.isInitialized=false;}createRequestId=()=>{this.requestCount+=1;return`plugin_${Date.now()}_${this.requestCount}`;};normalizeRequiredString=(value,fieldName)=>{if(typeof value!=="string"||!value.trim()){throw new Error(`${fieldName} must be a non-empty string`);}return value.trim();};normalizeCommand=command=>{if(command==null){return null;}return this.normalizeRequiredString(command,"command");};getHandlerKey=({pluginId,eventName,command=null}={})=>{return JSON.stringify([this.normalizeRequiredString(pluginId,"pluginId"),this.normalizeRequiredString(eventName,"eventName"),this.normalizeCommand(command)]);};getHandlerSet=(key,create=false)=>{let handlers=this.handlers.get(key);if(!handlers&&create){handlers=new Set();this.handlers.set(key,handlers);}return handlers||null;};addHandler=({pluginId,eventName,command}={},handler)=>{if(typeof handler!=="function"){throw new Error("handler must be a function");}this.getHandlerSet(this.getHandlerKey({pluginId,eventName,command}),true).add(handler);};removeHandler=({pluginId,eventName,command}={},handler)=>{const key=this.getHandlerKey({pluginId,eventName,command});const handlers=this.getHandlerSet(key,false);if(!handlers){return false;}if(handler==null){handlers.clear();this.handlers.delete(key);return true;}const removed=handlers.delete(handler);if(handlers.size===0){this.handlers.delete(key);}return removed;};ensureInitialized=()=>{if(typeof window==="undefined"||this.isInitialized){return this;}window.PluginBridgeWeb=window.PluginBridgeWeb||{};window.PluginBridgeWeb.callback=this.dispatchCallback;window.PluginBridgeWeb.dispatch=message=>{let parsed=message;if(typeof parsed==="string"){try{parsed=JSON.parse(parsed);}catch{return false;}}if(!parsed||typeof parsed!=="object"){return false;}const payload=Object.prototype.hasOwnProperty.call(parsed,"payload")?parsed.payload:null;return this.dispatchCallback(parsed.pluginId,parsed.eventName,payload,parsed.requestId??null,parsed.command??null);};this.isInitialized=true;return this;};hasAndroidBridge=()=>typeof window!=="undefined"&&!!window.PluginBridge;hasIOSBridge=()=>typeof window!=="undefined"&&!!window.webkit?.messageHandlers?.PluginBridge;init=()=>this.ensureInitialized();assertInitialized=()=>{if(!this.isInitialized){throw new Error("PluginBridge.init() must be called before using emit() or register()");}};reportHandlerError=error=>{if(typeof queueMicrotask==="function"){queueMicrotask(()=>{throw error;});return;}setTimeout(()=>{throw error;},0);};emit=({pluginId,command,data=null}={})=>{const normalizedPluginId=this.normalizeRequiredString(pluginId,"pluginId");const normalizedCommand=this.normalizeRequiredString(command,"command");if(typeof window==="undefined"){throw new Error("PluginBridge is not available in this environment");}this.assertInitialized();const payload={pluginId:normalizedPluginId,command:normalizedCommand,data,requestId:this.createRequestId()};if(this.hasAndroidBridge()){window.PluginBridge.emit(JSON.stringify(payload));return;}if(this.hasIOSBridge()){window.webkit.messageHandlers.PluginBridge.postMessage(payload);return;}throw new Error("PluginBridge is not available in this environment");};register=({pluginId,eventName,command,handler}={})=>{this.assertInitialized();this.addHandler({pluginId,eventName,command},handler);return()=>this.removeHandler({pluginId,eventName,command},handler);};unregister=({pluginId,eventName,command,handler}={})=>{if(typeof handler!=="function"){throw new Error("handler must be a function");}return this.removeHandler({pluginId,eventName,command},handler);};dispatchCallback=(pluginId,eventName,payload,requestId=null,command=null)=>{void requestId;const normalizedPluginId=typeof pluginId==="string"&&pluginId.trim()?pluginId.trim():null;const normalizedEventName=typeof eventName==="string"&&eventName.trim()?eventName.trim():null;if(normalizedPluginId==null||normalizedEventName==null){return false;}const normalizedCommand=typeof command==="string"&&command.trim()?command.trim():null;const toCall=new Set();const collectHandlers=commandScope=>{const handlers=this.getHandlerSet(JSON.stringify([normalizedPluginId,normalizedEventName,commandScope]),false);if(!handlers){return;}handlers.forEach(handler=>{toCall.add(handler);});};if(normalizedCommand!=null){collectHandlers(normalizedCommand);}collectHandlers(null);if(toCall.size===0){return false;}const meta={pluginId:normalizedPluginId,eventName:normalizedEventName,command:normalizedCommand};toCall.forEach(handler=>{try{handler(payload,meta);}catch(error){this.reportHandlerError(error);}});return true;};}const pluginNativeBridge=new PluginNativeBridge();var _default=exports.default=pluginNativeBridge;
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
"use strict";const fs=require("fs");const path=require("path");const{discoverInternalPlugins}=require("./internalPluginUtils.js");const PLUGINS_PACKAGE_PARTS=["io","yourname","androidproject","plugins"];function isDir(dirPath){return fs.existsSync(dirPath)&&fs.statSync(dirPath).isDirectory();}function ensureDir(dirPath){if(!fs.existsSync(dirPath)){fs.mkdirSync(dirPath,{recursive:true});}}function sanitizeForPath(value){return value.replace(/[^a-zA-Z0-9_-]/g,"_");}function mustBeNonEmptyString(value,fieldName,sourcePath){if(typeof value!=="string"||!value.trim()){throw new Error(`Invalid '${fieldName}' in ${sourcePath}`);}return value.trim();}function asUniqueSorted(values){return[...new Set(values)].sort();}function parsePluginToggleConfig(pluginConfig){if(pluginConfig==null){return{};}if(typeof pluginConfig!=="object"||Array.isArray(pluginConfig)){throw new Error("'WEBVIEW_CONFIG.plugins' must be an object with boolean values");}const toggles={};for(const[key,value]of Object.entries(pluginConfig)){const normalizedKey=mustBeNonEmptyString(key,"plugins.<key>","WEBVIEW_CONFIG");if(typeof value!=="boolean"){throw new Error(`'WEBVIEW_CONFIG.plugins.${normalizedKey}' must be boolean`);}toggles[normalizedKey]=value;}return toggles;}function selectPluginsByConfig(plugins,pluginConfig,log){const toggles=parsePluginToggleConfig(pluginConfig);const matchedKeys=new Set();const selected=[];for(const plugin of plugins){const selectorKeys=plugin.configKey?[plugin.configKey,plugin.id]:[plugin.id];const matches=[];for(const key of selectorKeys){if(Object.prototype.hasOwnProperty.call(toggles,key)){matches.push({key,value:toggles[key]});matchedKeys.add(key);}}const uniqueValues=[...new Set(matches.map(entry=>entry.value))];if(uniqueValues.length>1){throw new Error(`Conflicting toggle values for plugin '${plugin.id}' across keys: ${matches.map(entry=>`${entry.key}=${entry.value}`).join(", ")}`);}const enabled=matches.length===0?false:matches[0].value;if(enabled){selected.push(plugin);}else{log(`Plugin disabled by config: ${plugin.id}`,"info");}}const unknownKeys=Object.keys(toggles).filter(key=>!matchedKeys.has(key));if(unknownKeys.length>0){throw new Error(`Unknown plugin toggle key(s) in WEBVIEW_CONFIG.plugins: ${unknownKeys.join(", ")}`);}return selected;}function parseDependency(dependency,pluginId){const parts=dependency.split(":");if(parts.length<3){throw new Error(`Dependency '${dependency}' in plugin '${pluginId}' must be in 'group:artifact:version' format`);}return{key:`${parts[0]}:${parts[1]}`,version:parts.slice(2).join(":")};}function validatePlugins(plugins){const pluginIds=new Set();const configKeys=new Set();const dependencies=new Map();const selectorKeys=new Map();for(const plugin of plugins){if(pluginIds.has(plugin.id)){throw new Error(`Duplicate plugin id detected: ${plugin.id}`);}pluginIds.add(plugin.id);if(plugin.configKey){if(configKeys.has(plugin.configKey)){throw new Error(`Duplicate configKey detected: ${plugin.configKey}`);}configKeys.add(plugin.configKey);}for(const[field,selector]of[["id",plugin.id],["configKey",plugin.configKey]]){if(!selector){continue;}const existing=selectorKeys.get(selector);if(existing&&existing.pluginId!==plugin.id){throw new Error(`Plugin selector collision for '${selector}': '${existing.pluginId}' (${existing.field}) conflicts with '${plugin.id}' (${field})`);}selectorKeys.set(selector,{pluginId:plugin.id,field});}if(new Set(plugin.commands).size!==plugin.commands.length){throw new Error(`Duplicate command(s) detected within plugin '${plugin.id}'`);}for(const dependency of plugin.android?.dependencies||[]){const parsed=parseDependency(dependency,plugin.id);const existing=dependencies.get(parsed.key);if(existing&&existing.version!==parsed.version){throw new Error(`Dependency version conflict for '${parsed.key}': '${existing.version}' in '${existing.pluginId}', '${parsed.version}' in '${plugin.id}'`);}if(!existing){dependencies.set(parsed.key,{version:parsed.version,pluginId:plugin.id});}}}}function selectPluginsForPlatform(plugins,platform,log){const selected=[];for(const plugin of plugins){if(plugin.platforms.includes(platform)){selected.push(plugin);continue;}log(`Plugin enabled but not supported on ${platform}: ${plugin.id}`,"info");}return selected;}function walkFiles(rootDir,predicate,results=[]){if(!isDir(rootDir)){return results;}for(const entry of fs.readdirSync(rootDir,{withFileTypes:true})){const fullPath=path.join(rootDir,entry.name);if(entry.isDirectory()){walkFiles(fullPath,predicate,results);continue;}if(predicate(entry.name,fullPath)){results.push(fullPath);}}return results;}function resolvePluginClassSourcePath(plugin){const className=plugin.android.className.split(".").pop();const candidateNames=new Set([`${className}.kt`,`${className}.java`]);const candidates=walkFiles(plugin.android.sourceDir,name=>candidateNames.has(name));if(candidates.length>1){throw new Error(`Declared class '${plugin.android.className}' for selected plugin '${plugin.id}' resolved to multiple source files under ${plugin.android.sourceDir}`);}return candidates[0]||null;}function validateSelectedPluginSources(plugins){for(const plugin of plugins){if(!plugin.android){throw new Error(`Android config missing for selected plugin '${plugin.id}'`);}if(!isDir(plugin.android.sourceDir)){throw new Error(`Android source directory missing for selected plugin '${plugin.id}'`);}const codeFiles=walkFiles(plugin.android.sourceDir,name=>name.endsWith(".kt")||name.endsWith(".java"));if(codeFiles.length===0){throw new Error(`No Android source files found for selected plugin '${plugin.id}'`);}if(!resolvePluginClassSourcePath(plugin)){throw new Error(`Declared class '${plugin.android.className}' for selected plugin '${plugin.id}' was not found as a .kt or .java file under ${plugin.android.sourceDir}`);}}}function copyTree(sourceDir,targetDir){if(!isDir(sourceDir)){return;}ensureDir(targetDir);for(const filePath of walkFiles(sourceDir,()=>true)){const targetPath=path.join(targetDir,path.relative(sourceDir,filePath));ensureDir(path.dirname(targetPath));fs.copyFileSync(filePath,targetPath);}}function copyAndroidPluginSources(plugins,javaRoot,log){const internalRoot=path.join(javaRoot,...PLUGINS_PACKAGE_PARTS,"internal");fs.rmSync(internalRoot,{recursive:true,force:true});ensureDir(internalRoot);let copiedCount=0;for(const plugin of plugins){const pluginOutputDir=path.join(internalRoot,sanitizeForPath(plugin.id));const codeFiles=walkFiles(plugin.android.sourceDir,name=>name.endsWith(".kt")||name.endsWith(".java"));for(const sourcePath of codeFiles){const targetPath=path.join(pluginOutputDir,path.relative(plugin.android.sourceDir,sourcePath));ensureDir(path.dirname(targetPath));fs.copyFileSync(sourcePath,targetPath);copiedCount++;}}log(`Copied ${copiedCount} Android plugin source file(s)`,"info");}function copyPluginAssets(plugins,androidProjectPath,log){const baseAssetsDir=path.join(androidProjectPath,"app","src","main","assets","plugins");fs.rmSync(baseAssetsDir,{recursive:true,force:true});ensureDir(baseAssetsDir);for(const plugin of plugins){const pluginAssetsDir=path.join(baseAssetsDir,sanitizeForPath(plugin.id));copyTree(path.join(plugin.pluginDir,"assets","common"),path.join(pluginAssetsDir,"common"));copyTree(path.join(plugin.pluginDir,"assets","android"),path.join(pluginAssetsDir,"android"));}log("Plugin assets copied to app/src/main/assets/plugins","info");}function formatKotlinMap(entries,emptyLiteral="emptyMap()"){return entries.length===0?emptyLiteral:`mapOf(\n${entries.join(",\n")}\n )`;}function generatePluginRegistryFiles(plugins,javaRoot){const pluginIdToClassName={};const pluginToCommands={};for(const plugin of plugins){pluginIdToClassName[plugin.id]=plugin.android.className;pluginToCommands[plugin.id]=asUniqueSorted(plugin.commands);}const classEntries=Object.keys(pluginIdToClassName).sort().map(pluginId=>` ${JSON.stringify(pluginId)} to ${JSON.stringify(pluginIdToClassName[pluginId])}`);const commandEntries=Object.keys(pluginToCommands).sort().map(pluginId=>{const commands=pluginToCommands[pluginId];const commandSet=commands.length?`setOf(${commands.map(value=>JSON.stringify(value)).join(", ")})`:"emptySet()";return` ${JSON.stringify(pluginId)} to ${commandSet}`;});const indexContent=`package io.yourname.androidproject.plugins
|
|
2
|
-
|
|
3
|
-
object GeneratedPluginIndex {
|
|
4
|
-
val pluginIdToClassName: Map<String, String> = ${formatKotlinMap(classEntries)}
|
|
5
|
-
val pluginToCommands: Map<String, Set<String>> = ${formatKotlinMap(commandEntries)}
|
|
6
|
-
}
|
|
7
|
-
`;const pluginsDir=path.join(javaRoot,...PLUGINS_PACKAGE_PARTS);ensureDir(pluginsDir);fs.writeFileSync(path.join(pluginsDir,"GeneratedPluginIndex.kt"),indexContent);fs.rmSync(path.join(pluginsDir,"GeneratedPluginMeta.kt"),{force:true});}function escapeRegexLiteral(value){return value.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");}function updateAndroidManifestPermissions(manifestPath,selectedPermissions,allKnownPluginPermissions){const uniquePermissions=asUniqueSorted(selectedPermissions);const knownPermissions=asUniqueSorted(allKnownPluginPermissions);let manifest=fs.readFileSync(manifestPath,"utf8");const beginMarker="<!-- CATALYST_PLUGIN_PERMISSIONS_START -->";const endMarker="<!-- CATALYST_PLUGIN_PERMISSIONS_END -->";const markerRegex=/[ \t]*<!-- CATALYST_PLUGIN_PERMISSIONS_START -->[\s\S]*?<!-- CATALYST_PLUGIN_PERMISSIONS_END -->\s*/g;manifest=manifest.replace(markerRegex,"");// Migration cleanup for legacy entries previously written without markers.
|
|
8
|
-
for(const permission of knownPermissions){const escaped=escapeRegexLiteral(permission);const legacyRegex=new RegExp(`^[ \\t]*<uses-permission\\s+android:name="${escaped}"\\s*/>\\s*\\n?`,"gm");manifest=manifest.replace(legacyRegex,"");}if(uniquePermissions.length>0){const permissionLines=uniquePermissions.map(permission=>` <uses-permission android:name="${permission}" />`).join("\n");const managedBlock=` ${beginMarker}\n${permissionLines}\n ${endMarker}\n`;manifest=manifest.replace(/<application\b/,`${managedBlock} <application`);}fs.writeFileSync(manifestPath,manifest);}function findDependenciesBlockRange(gradleText,gradlePath){const headerMatch=gradleText.match(/^dependencies\s*\{/m);if(!headerMatch||headerMatch.index==null){throw new Error(`Could not find dependencies block in ${gradlePath}`);}const openBraceIndex=gradleText.indexOf("{",headerMatch.index);if(openBraceIndex===-1){throw new Error(`Malformed dependencies block in ${gradlePath}`);}let depth=0;for(let index=openBraceIndex;index<gradleText.length;index++){const ch=gradleText[index];if(ch==="{")depth++;if(ch==="}")depth--;if(depth===0){return{openBraceIndex,blockEnd:index};}}throw new Error(`Could not find end of dependencies block in ${gradlePath}`);}function updateGradleDependencies(gradlePath,selectedDependencies,allKnownPluginDependencies){const uniqueDependencies=asUniqueSorted(selectedDependencies);const knownDependencies=asUniqueSorted(allKnownPluginDependencies);let gradle=fs.readFileSync(gradlePath,"utf8");const{openBraceIndex,blockEnd}=findDependenciesBlockRange(gradle,gradlePath);let blockBody=gradle.slice(openBraceIndex+1,blockEnd);const beginMarker="// CATALYST_PLUGIN_DEPENDENCIES_START";const endMarker="// CATALYST_PLUGIN_DEPENDENCIES_END";const markerRegex=/[ \t]*\/\/ CATALYST_PLUGIN_DEPENDENCIES_START[\s\S]*?\/\/ CATALYST_PLUGIN_DEPENDENCIES_END\s*/g;blockBody=blockBody.replace(markerRegex,"");// Migration cleanup for legacy entries previously written without markers.
|
|
9
|
-
for(const dependency of knownDependencies){const escaped=escapeRegexLiteral(dependency);const legacyRegex=new RegExp(`^[ \\t]*implementation\\("${escaped}"\\)\\s*\\n?`,"gm");blockBody=blockBody.replace(legacyRegex,"");}if(uniqueDependencies.length>0){const managedLines=uniqueDependencies.map(dependency=>` implementation("${dependency}")`).join("\n");blockBody=`${blockBody}\n ${beginMarker}\n${managedLines}\n ${endMarker}\n`;}gradle=`${gradle.slice(0,openBraceIndex+1)}${blockBody}${gradle.slice(blockEnd)}`;fs.writeFileSync(gradlePath,gradle);}function updateProguardKeepRules(proguardPath,selectedClassNames,allKnownPluginClassNames){const uniqueClassNames=asUniqueSorted(selectedClassNames);const knownClassNames=asUniqueSorted(allKnownPluginClassNames);let rules=fs.readFileSync(proguardPath,"utf8");const beginMarker="# CATALYST_PLUGIN_KEEP_START";const endMarker="# CATALYST_PLUGIN_KEEP_END";const markerRegex=/[ \t]*# CATALYST_PLUGIN_KEEP_START[\s\S]*?# CATALYST_PLUGIN_KEEP_END\s*/g;rules=rules.replace(markerRegex,"");for(const className of knownClassNames){const escaped=escapeRegexLiteral(className);const legacyRegex=new RegExp(`^[ \\t]*-keep class ${escaped} \\{ \\*; \\}\\s*\\n?`,"gm");rules=rules.replace(legacyRegex,"");}if(uniqueClassNames.length>0){const keepLines=uniqueClassNames.map(className=>`-keep class ${className} { *; }`).join("\n");rules=`${rules.trimEnd()}\n\n${beginMarker}\n${keepLines}\n${endMarker}\n`;}fs.writeFileSync(proguardPath,rules);}function composeAndroidPlugins({corePluginsRoot,androidProjectPath,pluginConfig,log}){const discovered=discoverInternalPlugins(corePluginsRoot,log);validatePlugins(discovered);const enabled=selectPluginsByConfig(discovered,pluginConfig,log);const selected=selectPluginsForPlatform(enabled,"android",log);validateSelectedPluginSources(selected);const javaRoot=path.join(androidProjectPath,"app","src","main","java");const manifestPath=path.join(androidProjectPath,"app","src","main","AndroidManifest.xml");const gradlePath=path.join(androidProjectPath,"app","build.gradle.kts");const proguardPath=path.join(androidProjectPath,"app","proguard-rules.pro");copyAndroidPluginSources(selected,javaRoot,log);copyPluginAssets(selected,androidProjectPath,log);generatePluginRegistryFiles(selected,javaRoot);updateAndroidManifestPermissions(manifestPath,selected.flatMap(plugin=>plugin.android.permissions),discovered.flatMap(plugin=>plugin.android?.permissions||[]));updateGradleDependencies(gradlePath,selected.flatMap(plugin=>plugin.android.dependencies),discovered.flatMap(plugin=>plugin.android?.dependencies||[]));updateProguardKeepRules(proguardPath,selected.map(plugin=>plugin.android?.className).filter(Boolean),discovered.map(plugin=>plugin.android?.className).filter(Boolean));log(`Plugin composition complete (${selected.length} enabled plugin(s))`,"success");return{pluginCount:selected.length,commandCount:selected.reduce((total,plugin)=>total+plugin.commands.length,0)};}module.exports={composeAndroidPlugins};
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
"use strict";const fs=require("fs");const path=require("path");const{discoverInternalPlugins}=require("./internalPluginUtils.js");function isDir(dirPath){return fs.existsSync(dirPath)&&fs.statSync(dirPath).isDirectory();}function ensureDir(dirPath){if(!fs.existsSync(dirPath)){fs.mkdirSync(dirPath,{recursive:true});}}function sanitizeForPath(value){return value.replace(/[^a-zA-Z0-9_-]/g,"_");}function mustBeNonEmptyString(value,fieldName,sourcePath){if(typeof value!=="string"||!value.trim()){throw new Error(`Invalid '${fieldName}' in ${sourcePath}`);}return value.trim();}function asUniqueSorted(values){return[...new Set(values)].sort();}function isPlainObject(value){return!!value&&typeof value==="object"&&!Array.isArray(value);}function deepEqual(left,right){return JSON.stringify(left)===JSON.stringify(right);}function mergeStructuredValues(existing,incoming,fieldName){if(existing===undefined){return JSON.parse(JSON.stringify(incoming));}if(Array.isArray(existing)&&Array.isArray(incoming)){const merged=[];const seen=new Set();for(const value of[...existing,...incoming]){const key=JSON.stringify(value);if(seen.has(key)){continue;}seen.add(key);merged.push(value);}return merged;}if(isPlainObject(existing)&&isPlainObject(incoming)){const merged={...existing};for(const[key,value]of Object.entries(incoming)){merged[key]=mergeStructuredValues(merged[key],value,`${fieldName}.${key}`);}return merged;}if(deepEqual(existing,incoming)){return existing;}throw new Error(`Conflicting values for '${fieldName}' while composing selected iOS plugins`);}function dependencyKey(dependency){return dependency.url;}function requirementKey(dependency){return`${dependency.requirement.type}:${dependency.requirement.version}`;}function packageKey(dependency){return dependency.package;}function resolveManifestPath(pluginDir,relativePath,fieldName){const resolvedPath=path.resolve(pluginDir,relativePath);const normalizedPluginDir=fs.realpathSync(path.resolve(pluginDir));if(resolvedPath!==normalizedPluginDir&&!resolvedPath.startsWith(`${normalizedPluginDir}${path.sep}`)){throw new Error(`'${fieldName}' must stay within plugin directory: ${relativePath}`);}if(!fs.existsSync(resolvedPath)){return resolvedPath;}const realResolvedPath=fs.realpathSync(resolvedPath);if(realResolvedPath!==normalizedPluginDir&&!realResolvedPath.startsWith(`${normalizedPluginDir}${path.sep}`)){throw new Error(`'${fieldName}' resolves outside plugin directory: ${relativePath}`);}return realResolvedPath;}function parsePluginToggleConfig(pluginConfig){if(pluginConfig==null){return{};}if(typeof pluginConfig!=="object"||Array.isArray(pluginConfig)){throw new Error("'WEBVIEW_CONFIG.plugins' must be an object with boolean values");}const toggles={};for(const[key,value]of Object.entries(pluginConfig)){const normalizedKey=mustBeNonEmptyString(key,"plugins.<key>","WEBVIEW_CONFIG");if(typeof value!=="boolean"){throw new Error(`'WEBVIEW_CONFIG.plugins.${normalizedKey}' must be boolean`);}toggles[normalizedKey]=value;}return toggles;}function selectPluginsByConfig(plugins,pluginConfig,log){const toggles=parsePluginToggleConfig(pluginConfig);const matchedKeys=new Set();const enabled=[];for(const plugin of plugins){const selectorKeys=plugin.configKey?[plugin.configKey,plugin.id]:[plugin.id];const matches=[];for(const key of selectorKeys){if(Object.prototype.hasOwnProperty.call(toggles,key)){matches.push({key,value:toggles[key]});matchedKeys.add(key);}}const uniqueValues=[...new Set(matches.map(entry=>entry.value))];if(uniqueValues.length>1){throw new Error(`Conflicting toggle values for plugin '${plugin.id}' across keys: ${matches.map(entry=>`${entry.key}=${entry.value}`).join(", ")}`);}const isEnabled=matches.length===0?false:matches[0].value;if(isEnabled){enabled.push(plugin);}else{log(`Plugin disabled by config: ${plugin.id}`,"info");}}const unknownKeys=Object.keys(toggles).filter(key=>!matchedKeys.has(key));if(unknownKeys.length>0){throw new Error(`Unknown plugin toggle key(s) in WEBVIEW_CONFIG.plugins: ${unknownKeys.join(", ")}`);}return enabled;}function validatePlugins(plugins){const pluginIds=new Set();const configKeys=new Set();const selectorKeys=new Map();const dependencies=new Map();for(const plugin of plugins){if(pluginIds.has(plugin.id)){throw new Error(`Duplicate plugin id detected: ${plugin.id}`);}pluginIds.add(plugin.id);if(plugin.configKey){if(configKeys.has(plugin.configKey)){throw new Error(`Duplicate configKey detected: ${plugin.configKey}`);}configKeys.add(plugin.configKey);}for(const[field,selector]of[["id",plugin.id],["configKey",plugin.configKey]]){if(!selector){continue;}const existing=selectorKeys.get(selector);if(existing&&existing.pluginId!==plugin.id){throw new Error(`Plugin selector collision for '${selector}': '${existing.pluginId}' (${existing.field}) conflicts with '${plugin.id}' (${field})`);}selectorKeys.set(selector,{pluginId:plugin.id,field});}if(new Set(plugin.commands).size!==plugin.commands.length){throw new Error(`Duplicate command(s) detected within plugin '${plugin.id}'`);}if(Object.prototype.hasOwnProperty.call(plugin.ios?.infoPlist||{},"CFBundleURLTypes")){throw new Error(`Plugin '${plugin.id}' must use 'ios.urlSchemes' instead of 'ios.infoPlist.CFBundleURLTypes'`);}if(Object.prototype.hasOwnProperty.call(plugin.ios?.infoPlist||{},"LSApplicationQueriesSchemes")){throw new Error(`Plugin '${plugin.id}' must use 'ios.querySchemes' instead of 'ios.infoPlist.LSApplicationQueriesSchemes'`);}for(const dependency of plugin.ios?.dependencies||[]){const existing=dependencies.get(dependencyKey(dependency));if(existing&&requirementKey(existing.dependency)!==requirementKey(dependency)){throw new Error(`iOS dependency version conflict for '${dependency.url}': '${existing.dependency.requirement.type}:${existing.dependency.requirement.version}' in '${existing.pluginId}', '${dependency.requirement.type}:${dependency.requirement.version}' in '${plugin.id}'`);}if(existing&&packageKey(existing.dependency)!==packageKey(dependency)){throw new Error(`iOS dependency package identity conflict for '${dependency.url}': '${existing.dependency.package}' in '${existing.pluginId}', '${dependency.package}' in '${plugin.id}'`);}if(!existing){dependencies.set(dependencyKey(dependency),{dependency,pluginId:plugin.id});}}for(const resourcePath of plugin.ios?.resources||[]){const resolvedPath=resolveManifestPath(plugin.pluginDir,resourcePath,`ios.resources for '${plugin.id}'`);if(!fs.existsSync(resolvedPath)){throw new Error(`iOS resource path not found for plugin '${plugin.id}': ${resourcePath}`);}}}}function selectPluginsForPlatform(plugins,platform,log){const selected=[];for(const plugin of plugins){if(plugin.platforms.includes(platform)){selected.push(plugin);continue;}log(`Plugin enabled but not supported on ${platform}: ${plugin.id}`,"info");}return selected;}function walkFiles(rootDir,predicate,results=[]){if(!isDir(rootDir)){return results;}for(const entry of fs.readdirSync(rootDir,{withFileTypes:true})){const fullPath=path.join(rootDir,entry.name);if(entry.isDirectory()){walkFiles(fullPath,predicate,results);continue;}if(predicate(entry.name,fullPath)){results.push(fullPath);}}return results;}function resolvePluginClassSourcePath(plugin){const className=plugin.ios.className.split(".").pop();const candidateName=`${className}.swift`;const candidates=walkFiles(plugin.ios.sourceDir,name=>name===candidateName);return candidates[0]||null;}function validateSelectedPluginSources(plugins){for(const plugin of plugins){if(!plugin.ios){throw new Error(`iOS config missing for selected plugin '${plugin.id}'`);}if(!isDir(plugin.ios.sourceDir)){throw new Error(`iOS source directory missing for selected plugin '${plugin.id}'`);}const codeFiles=walkFiles(plugin.ios.sourceDir,name=>name.endsWith(".swift"));if(codeFiles.length===0){throw new Error(`No iOS source files found for selected plugin '${plugin.id}'`);}if(!resolvePluginClassSourcePath(plugin)){throw new Error(`Declared class '${plugin.ios.className}' for selected plugin '${plugin.id}' was not found under ${plugin.ios.sourceDir}`);}}}function collectIosDependencies(plugins){const dependenciesByUrl=new Map();for(const plugin of plugins){for(const dependency of plugin.ios?.dependencies||[]){const existing=dependenciesByUrl.get(dependency.url);if(!existing){dependenciesByUrl.set(dependency.url,{url:dependency.url,package:dependency.package,requirement:dependency.requirement,products:[...dependency.products]});continue;}if(requirementKey(existing)!==requirementKey(dependency)){throw new Error(`iOS dependency version conflict for '${dependency.url}' while composing selected plugins`);}if(packageKey(existing)!==packageKey(dependency)){throw new Error(`iOS dependency package identity conflict for '${dependency.url}' while composing selected plugins`);}existing.products=asUniqueSorted([...existing.products,...dependency.products]);}}return[...dependenciesByUrl.values()].sort((left,right)=>left.url.localeCompare(right.url));}function normalizeResourceRelativePath(plugin,absolutePath){const relativePath=path.relative(plugin.pluginDir,absolutePath);if(relativePath.startsWith(`ios${path.sep}resources${path.sep}`)){return relativePath.slice(`ios${path.sep}resources${path.sep}`.length);}if(relativePath.startsWith(`ios${path.sep}`)){return relativePath.slice(`ios${path.sep}`.length);}return relativePath;}function validateBundleRelativePath(pluginId,bundleRelativePath){const normalizedPath=path.posix.normalize(bundleRelativePath);const expectedPrefix=`PluginResources/${sanitizeForPath(pluginId)}`;if(path.posix.isAbsolute(normalizedPath)||normalizedPath===".."||normalizedPath.startsWith("../")){throw new Error(`Invalid bundled resource path for plugin '${pluginId}': ${bundleRelativePath}`);}if(normalizedPath!==expectedPrefix&&!normalizedPath.startsWith(`${expectedPrefix}/`)){throw new Error(`Bundled resource path escaped managed directory for plugin '${pluginId}': ${bundleRelativePath}`);}return normalizedPath;}function collectIosResources(plugins){const resources=[];for(const plugin of plugins){for(const resourcePath of plugin.ios?.resources||[]){const resolvedPath=resolveManifestPath(plugin.pluginDir,resourcePath,`ios.resources for '${plugin.id}'`);const entries=fs.statSync(resolvedPath).isDirectory()?walkFiles(resolvedPath,()=>true):[resolvedPath];for(const entryPath of entries){const normalizedRelativePath=normalizeResourceRelativePath(plugin,entryPath);const bundleRelativePath=validateBundleRelativePath(plugin.id,path.join("PluginResources",sanitizeForPath(plugin.id),normalizedRelativePath).split(path.sep).join("/"));resources.push({pluginId:plugin.id,sourcePath:entryPath,bundleRelativePath});}}}return resources.sort((left,right)=>left.bundleRelativePath.localeCompare(right.bundleRelativePath));}function collectIosInfoPlist(plugins){let infoPlist={};for(const plugin of plugins){infoPlist=mergeStructuredValues(infoPlist,plugin.ios?.infoPlist||{},`ios.infoPlist for '${plugin.id}'`);}return infoPlist;}function collectIosEntitlements(plugins){let entitlements={};for(const plugin of plugins){entitlements=mergeStructuredValues(entitlements,plugin.ios?.entitlements||{},`ios.entitlements for '${plugin.id}'`);}return entitlements;}function collectIosUrlSchemes(plugins){const entries=[];for(const plugin of plugins){for(const entry of plugin.ios?.urlSchemes||[]){entries.push({name:entry.name||plugin.id,schemes:asUniqueSorted(entry.schemes)});}}return entries;}function collectIosQuerySchemes(plugins){return asUniqueSorted(plugins.flatMap(plugin=>plugin.ios?.querySchemes||[]));}function copyIosPluginSources(plugins,iosProjectPath,log){const internalRoot=path.join(iosProjectPath,"Sources","Core","Plugins","Internal");fs.rmSync(internalRoot,{recursive:true,force:true});ensureDir(internalRoot);let copiedCount=0;for(const plugin of plugins){const pluginOutputDir=path.join(internalRoot,sanitizeForPath(plugin.id));const codeFiles=walkFiles(plugin.ios.sourceDir,name=>name.endsWith(".swift"));for(const sourcePath of codeFiles){const targetPath=path.join(pluginOutputDir,path.relative(plugin.ios.sourceDir,sourcePath));ensureDir(path.dirname(targetPath));fs.copyFileSync(sourcePath,targetPath);copiedCount++;}}log(`Copied ${copiedCount} iOS plugin source file(s)`,"info");}function formatSwiftDictionary(entries,emptyLiteral="[:]"){return entries.length===0?emptyLiteral:`[\n${entries.join(",\n")}\n ]`;}function generatePluginRegistryFiles(plugins,iosProjectPath){const pluginFactories={};const pluginToCommands={};for(const plugin of plugins){pluginFactories[plugin.id]=plugin.ios.className;pluginToCommands[plugin.id]=asUniqueSorted(plugin.commands);}const factoryEntries=Object.keys(pluginFactories).sort().map(pluginId=>` ${JSON.stringify(pluginId)}: { ${pluginFactories[pluginId]}() }`);const commandEntries=Object.keys(pluginToCommands).sort().map(pluginId=>{const commands=pluginToCommands[pluginId];const commandSet=commands.length?`Set([${commands.map(value=>JSON.stringify(value)).join(", ")}])`:"[]";return` ${JSON.stringify(pluginId)}: ${commandSet}`;});const indexContent=`import Foundation
|
|
2
|
-
|
|
3
|
-
enum GeneratedPluginIndex {
|
|
4
|
-
static let pluginFactories: [String: () -> CatalystPlugin] = ${formatSwiftDictionary(factoryEntries)}
|
|
5
|
-
static let pluginToCommands: [String: Set<String>] = ${formatSwiftDictionary(commandEntries)}
|
|
6
|
-
}
|
|
7
|
-
`;const pluginsDir=path.join(iosProjectPath,"Sources","Core","Plugins");ensureDir(pluginsDir);fs.writeFileSync(path.join(pluginsDir,"GeneratedPluginIndex.swift"),indexContent);}function composeIosPlugins({corePluginsRoot,iosProjectPath,pluginConfig,log}){const discovered=discoverInternalPlugins(corePluginsRoot,log);validatePlugins(discovered);const enabled=selectPluginsByConfig(discovered,pluginConfig,log);const selected=selectPluginsForPlatform(enabled,"ios",log);validateSelectedPluginSources(selected);const iosDependencies=collectIosDependencies(selected);const infoPlist=collectIosInfoPlist(selected);const urlSchemes=collectIosUrlSchemes(selected);const querySchemes=collectIosQuerySchemes(selected);const entitlements=collectIosEntitlements(selected);const resources=collectIosResources(selected);copyIosPluginSources(selected,iosProjectPath,log);generatePluginRegistryFiles(selected,iosProjectPath);log(`Plugin composition complete (${selected.length} enabled iOS plugin(s))`,"success");return{pluginCount:selected.length,commandCount:selected.reduce((total,plugin)=>total+plugin.commands.length,0),iosDependencies,infoPlist,urlSchemes,querySchemes,entitlements,resources};}module.exports={composeIosPlugins};
|
package/dist/scripts/plugins.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
"use strict";const fs=require("fs");const path=require("path");const readline=require("node:readline");const{bold,cyan,dim,green,red,yellow}=require("picocolors");const{discoverInternalPlugins,resolveInternalPluginsRoot}=require("../native/internalPluginUtils.js");const CONFIG_PATH=path.join(process.cwd(),"config","config.json");const TICK="x";function detectIndent(rawConfig){const indentMatch=rawConfig.match(/\n([ \t]+)"/);return indentMatch?indentMatch[1]:" ";}function readJsonFile(filePath,label){if(!fs.existsSync(filePath)){throw new Error(`${label} not found at ${filePath}`);}const raw=fs.readFileSync(filePath,"utf8");try{return{raw,json:JSON.parse(raw)};}catch(error){throw new Error(`Invalid JSON in ${filePath}: ${error.message}`);}}function ensureObject(value,label){if(value==null){return{};}if(typeof value!=="object"||Array.isArray(value)){throw new Error(`${label} must be an object`);}return value;}function splitPluginConfig(pluginConfig,plugins){const selectorToPlugin=new Map();const matchesByConfigKey=new Map();const staleToggleKeys=[];plugins.forEach(plugin=>{selectorToPlugin.set(plugin.configKey,plugin);selectorToPlugin.set(plugin.id,plugin);});for(const[key,value]of Object.entries(pluginConfig)){if(typeof value!=="boolean"){throw new Error(`WEBVIEW_CONFIG.plugins.${key} must be boolean`);}const plugin=selectorToPlugin.get(key);if(!plugin){staleToggleKeys.push(key);continue;}const matches=matchesByConfigKey.get(plugin.configKey)||[];matches.push({key,value,plugin});matchesByConfigKey.set(plugin.configKey,matches);}const knownToggles={};for(const plugin of plugins){const matches=matchesByConfigKey.get(plugin.configKey)||[];const uniqueValues=[...new Set(matches.map(entry=>entry.value))];if(uniqueValues.length>1){throw new Error(`Conflicting toggle values for plugin '${plugin.id}' across keys: ${matches.map(entry=>`${entry.key}=${entry.value}`).join(", ")}`);}if(matches.length>0){knownToggles[plugin.configKey]=matches[0].value;}}return{knownToggles,staleToggleKeys};}function createSession({configPath,rawConfig,config,plugins,pluginsRoot}){const webviewConfig=config.WEBVIEW_CONFIG==null?{}:ensureObject(config.WEBVIEW_CONFIG,"WEBVIEW_CONFIG");const pluginConfig=ensureObject(webviewConfig.plugins,"WEBVIEW_CONFIG.plugins");const{knownToggles,staleToggleKeys}=splitPluginConfig(pluginConfig,plugins);return{configPath,config,indent:detectIndent(rawConfig),pluginsRoot,plugins:plugins.map(plugin=>({...plugin,enabled:knownToggles[plugin.configKey]??false})),staleToggleKeys,notice:"",selectedIndex:0};}function buildPersistedPluginConfig(session){const nextConfig={};for(const plugin of session.plugins){nextConfig[plugin.configKey]=plugin.enabled;}return nextConfig;}function renderSession(session){if(typeof console.clear==="function"){console.clear();}console.log(bold("Catalyst Internal Plugins"));console.log(dim(`Config: ${session.configPath}`));console.log(dim(`Catalog: ${session.pluginsRoot}`));console.log("");if(session.staleToggleKeys.length>0){console.log(yellow(`Stale plugin toggle keys will be removed on save: ${session.staleToggleKeys.join(", ")}`));console.log("");}session.plugins.forEach((plugin,index)=>{const isSelected=index===session.selectedIndex;const pointer=isSelected?cyan(">"):" ";const checkbox=plugin.enabled?green(`[${TICK}]`):dim("[ ]");const title=isSelected?bold(plugin.displayName):plugin.displayName;const meta=dim(`(${plugin.configKey})`);console.log(`${pointer} ${checkbox} ${title} ${meta}`);console.log(` ${plugin.description}`);console.log(dim(` category: ${plugin.category} | platforms: ${plugin.platforms.join(", ")}`));console.log(dim(` id: ${plugin.id}`));console.log("");});console.log("Controls: up/down move, space or enter toggle, a enable all, n disable all, s save, q quit");if(session.notice){console.log("");console.log(session.notice);}}function saveSession(session){if(session.config.WEBVIEW_CONFIG==null){session.config.WEBVIEW_CONFIG={};}session.config.WEBVIEW_CONFIG.plugins=buildPersistedPluginConfig(session);fs.writeFileSync(session.configPath,`${JSON.stringify(session.config,null,session.indent)}\n`);}async function runInteractiveSession(session){if(!process.stdin.isTTY||!process.stdout.isTTY){renderSession(session);console.log("");console.log(yellow("Interactive mode requires a TTY. Re-run this command in a terminal session."));return;}const rl=readline.createInterface({input:process.stdin,output:process.stdout});readline.emitKeypressEvents(process.stdin,rl);if(typeof process.stdin.setRawMode==="function"){process.stdin.setRawMode(true);}try{renderSession(session);await new Promise(resolve=>{const finish=()=>{process.stdin.off("keypress",onKeypress);resolve();};const onKeypress=(_,key={})=>{if(key.ctrl&&key.name==="c"){session.notice=yellow("Exited without saving.");renderSession(session);finish();return;}if(key.name==="up"){session.selectedIndex=session.selectedIndex===0?session.plugins.length-1:session.selectedIndex-1;session.notice="";renderSession(session);return;}if(key.name==="down"){session.selectedIndex=session.selectedIndex===session.plugins.length-1?0:session.selectedIndex+1;session.notice="";renderSession(session);return;}if(key.name==="space"||key.name==="return"){const plugin=session.plugins[session.selectedIndex];plugin.enabled=!plugin.enabled;session.notice=green(`${plugin.displayName} is now ${plugin.enabled?"enabled":"disabled"} in the pending config.`);renderSession(session);return;}if(key.name==="a"){session.plugins.forEach(plugin=>{plugin.enabled=true;});session.notice=green("Enabled all discovered plugins.");renderSession(session);return;}if(key.name==="n"){session.plugins.forEach(plugin=>{plugin.enabled=false;});session.notice=yellow("Disabled all discovered plugins.");renderSession(session);return;}if(key.name==="s"){saveSession(session);session.notice=session.staleToggleKeys.length>0?green(`Saved plugin toggles to config/config.json and removed stale keys: ${session.staleToggleKeys.join(", ")}.`):green("Saved plugin toggles to config/config.json.");renderSession(session);finish();return;}if(key.name==="q"){session.notice=yellow("Exited without saving.");renderSession(session);finish();return;}session.notice=red(`Unsupported key: ${key.name||"unknown"}`);renderSession(session);};process.stdin.on("keypress",onKeypress);});}finally{if(typeof process.stdin.setRawMode==="function"){process.stdin.setRawMode(false);}rl.close();}}async function main(){const{raw:rawConfig,json:config}=readJsonFile(CONFIG_PATH,"App config");const catalystCoreRoot=path.dirname(require.resolve("catalyst-core-internal/package.json"));const pluginsRoot=resolveInternalPluginsRoot(catalystCoreRoot);const plugins=discoverInternalPlugins(pluginsRoot);if(plugins.length===0){console.log(yellow(`No internal plugins were discovered at ${pluginsRoot}`));return;}const session=createSession({configPath:CONFIG_PATH,rawConfig,config,plugins,pluginsRoot});await runInteractiveSession(session);}main().catch(error=>{console.error(red(`Plugin manager failed: ${error.message}`));process.exit(1);});
|