@ionyx-apps/cli 0.4.7 → 0.4.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/bin/ionyx.exe +0 -0
  2. package/ionyx-cli/src/templates/angular/src-ionyx/Cargo.toml +30 -0
  3. package/ionyx-cli/src/templates/angular/src-ionyx/build.rs +5 -0
  4. package/ionyx-cli/src/templates/angular/src-ionyx/main.rs +48 -0
  5. package/ionyx-cli/src/templates/angular/src-ionyx/src/lib.rs +267 -0
  6. package/ionyx-cli/src/templates/basic/index.html +3 -2
  7. package/ionyx-cli/src/templates/basic/ionyx-bridge.js +100 -0
  8. package/ionyx-cli/src/templates/basic/ionyx.config.json +3 -0
  9. package/ionyx-cli/src/templates/basic/package.json +6 -1
  10. package/ionyx-cli/src/templates/basic/src/App.tsx +14 -0
  11. package/ionyx-cli/src/templates/basic/src-ionyx/Cargo.toml +14 -12
  12. package/ionyx-cli/src/templates/basic/src-ionyx/main.rs +46 -3
  13. package/ionyx-cli/src/templates/leptos/index.html +2 -1
  14. package/ionyx-cli/src/templates/leptos/ionyx-bridge.js +100 -0
  15. package/ionyx-cli/src/templates/leptos/package.json +6 -1
  16. package/ionyx-cli/src/templates/leptos/src-ionyx/Cargo.toml +14 -8
  17. package/ionyx-cli/src/templates/leptos/src-ionyx/main.rs +46 -3
  18. package/ionyx-cli/src/templates/mod.rs +1 -5
  19. package/ionyx-cli/src/templates/react/index.html +1 -0
  20. package/ionyx-cli/src/templates/react/ionyx-bridge.js +100 -0
  21. package/ionyx-cli/src/templates/react/ionyx.config.json +4 -1
  22. package/ionyx-cli/src/templates/react/package.json +6 -1
  23. package/ionyx-cli/src/templates/react/src-ionyx/Cargo.toml +14 -12
  24. package/ionyx-cli/src/templates/react/src-ionyx/main.rs +46 -3
  25. package/ionyx-cli/src/templates/src-ionyx/Cargo.toml +15 -5
  26. package/ionyx-cli/src/templates/src-ionyx/bridge.rs +380 -0
  27. package/ionyx-cli/src/templates/src-ionyx/fusion.rs +47 -0
  28. package/ionyx-cli/src/templates/src-ionyx/ionyx.js +100 -0
  29. package/ionyx-cli/src/templates/src-ionyx/ipc_bridge.rs +206 -0
  30. package/ionyx-cli/src/templates/src-ionyx/lib.rs +204 -114
  31. package/ionyx-cli/src/templates/src-ionyx/main.rs +268 -3
  32. package/ionyx-cli/src/templates/src-ionyx/protocol.rs +98 -0
  33. package/ionyx-cli/src/templates/src-ionyx/window.rs +13 -0
  34. package/ionyx-cli/src/templates/svelte/index.html +1 -0
  35. package/ionyx-cli/src/templates/svelte/ionyx-bridge.js +100 -0
  36. package/ionyx-cli/src/templates/svelte/package.json +6 -1
  37. package/ionyx-cli/src/templates/svelte/src-ionyx/Cargo.toml +14 -12
  38. package/ionyx-cli/src/templates/svelte/src-ionyx/main.rs +46 -3
  39. package/ionyx-cli/src/templates/vanilla/index.html +1 -0
  40. package/ionyx-cli/src/templates/vanilla/ionyx-bridge.js +100 -0
  41. package/ionyx-cli/src/templates/vanilla/ionyx.config.json +3 -0
  42. package/ionyx-cli/src/templates/vanilla/package.json +6 -1
  43. package/ionyx-cli/src/templates/vanilla/src-ionyx/Cargo.toml +14 -12
  44. package/ionyx-cli/src/templates/vanilla/src-ionyx/main.rs +46 -3
  45. package/ionyx-cli/src/templates/vue/index.html +1 -0
  46. package/ionyx-cli/src/templates/vue/ionyx-bridge.js +100 -0
  47. package/ionyx-cli/src/templates/vue/ionyx.config.json +4 -1
  48. package/ionyx-cli/src/templates/vue/package.json +6 -1
  49. package/ionyx-cli/src/templates/vue/src-ionyx/Cargo.toml +14 -12
  50. package/ionyx-cli/src/templates/vue/src-ionyx/main.rs +46 -3
  51. package/package.json +1 -1
@@ -0,0 +1,380 @@
1
+ use anyhow::Result;
2
+ use serde::{Deserialize, Serialize};
3
+ use serde_json::{json, Value};
4
+ use tracing::{debug, error, info};
5
+ use std::sync::Arc;
6
+
7
+ use crate::config::Config;
8
+ use crate::ipc_bridge::{AppInfo, IpcBridgeManager};
9
+ use crate::security::SecurityManager;
10
+ use rfd::AsyncFileDialog;
11
+
12
+ #[derive(Debug, Clone, Serialize, Deserialize)]
13
+ pub struct IpcRequest {
14
+ pub id: String,
15
+ pub command: String,
16
+ pub payload: Value,
17
+ }
18
+
19
+ use crate::fusion::FusionManager;
20
+
21
+ #[derive(Debug, Clone, Serialize, Deserialize)]
22
+ pub struct IpcResponse {
23
+ pub id: String,
24
+ pub success: bool,
25
+ pub data: Option<Value>,
26
+ pub error: Option<String>,
27
+ }
28
+
29
+ pub struct IpcBridge {
30
+ config: Config,
31
+ security: SecurityManager,
32
+ dispatcher: crate::IpcDispatcher,
33
+ bridge_manager: IpcBridgeManager,
34
+ fusion_manager: Arc<FusionManager>,
35
+ }
36
+
37
+ impl IpcBridge {
38
+ pub fn new(config: Config, dispatcher: crate::IpcDispatcher, fusion_manager: Arc<FusionManager>) -> Self {
39
+ let security = SecurityManager::new(config.clone());
40
+ let bridge_manager = IpcBridgeManager::new(config.clone(), config.app.name.clone());
41
+ Self {
42
+ config,
43
+ security,
44
+ dispatcher,
45
+ bridge_manager,
46
+ fusion_manager,
47
+ }
48
+ }
49
+
50
+ pub async fn handle_request_with_response(&self, request: String) -> Result<()> {
51
+ debug!("Received IPC request: {}", request);
52
+
53
+ let ipc_request: IpcRequest = serde_json::from_str(&request)
54
+ .map_err(|e| anyhow::anyhow!("Failed to parse IPC request: {}", e))?;
55
+
56
+ info!("Processing IPC command: {}", ipc_request.command);
57
+
58
+ // Process the command and get the result
59
+ let response = match self.process_command(&ipc_request).await {
60
+ Ok(data) => {
61
+ info!(
62
+ "IPC command '{}' completed successfully",
63
+ ipc_request.command
64
+ );
65
+ IpcResponse {
66
+ id: ipc_request.id,
67
+ success: true,
68
+ data: Some(data),
69
+ error: None,
70
+ }
71
+ }
72
+ Err(e) => {
73
+ error!("IPC command '{}' failed: {}", ipc_request.command, e);
74
+ IpcResponse {
75
+ id: ipc_request.id,
76
+ success: false,
77
+ data: None,
78
+ error: Some(e.to_string()),
79
+ }
80
+ }
81
+ };
82
+
83
+ debug!("IPC request processed with response: {:?}", response);
84
+
85
+ // Send response back to frontend using dispatcher
86
+ self.dispatcher.resolve_promise(
87
+ &response.id,
88
+ response.success,
89
+ response.data,
90
+ response.error,
91
+ );
92
+
93
+ Ok(())
94
+ }
95
+
96
+ async fn process_command(&self, request: &IpcRequest) -> Result<Value> {
97
+ match request.command.as_str() {
98
+ "fs.readFile" => self.handle_read_file(&request.payload).await,
99
+ "fs.writeFile" => self.handle_write_file(&request.payload).await,
100
+ "fs.exists" => self.handle_file_exists(&request.payload).await,
101
+ "fs.readdir" => self.handle_read_dir(&request.payload).await,
102
+ "os.info" => self.handle_os_info().await,
103
+ "dialog.openFile" => self.handle_open_file_dialog(&request.payload).await,
104
+ "dialog.saveFile" => self.handle_save_file_dialog(&request.payload).await,
105
+ "network.request" => self.handle_network_request(&request.payload).await,
106
+ "app.getVersion" => self.handle_get_version().await,
107
+ "app.getConfig" => self.handle_get_config().await,
108
+ // IPC Bridge commands
109
+ "bridge.registerApp" => self.handle_bridge_register_app(&request.payload).await,
110
+ "bridge.unregisterApp" => self.handle_bridge_unregister_app(&request.payload).await,
111
+ "bridge.getApps" => self.handle_bridge_get_apps(&request.payload).await,
112
+ "bridge.sendToApp" => self.handle_bridge_send_to_app(&request.payload).await,
113
+ "bridge.broadcast" => self.handle_bridge_broadcast(&request.payload).await,
114
+ "fusion.update" => self.handle_fusion_update(&request.payload).await,
115
+ _ => Err(anyhow::anyhow!("Unknown command: {}", request.command)),
116
+ }
117
+ }
118
+
119
+ async fn handle_fusion_update(&self, payload: &Value) -> Result<Value> {
120
+ let key = payload["key"].as_str().ok_or_else(|| anyhow::anyhow!("Missing key"))?;
121
+ let value = payload["value"].clone();
122
+
123
+ info!("Fusion update from JS: {} = {:?}", key, value);
124
+ self.fusion_manager.set(key, value);
125
+
126
+ Ok(json!({ "success": true }))
127
+ }
128
+
129
+ async fn handle_read_file(&self, payload: &Value) -> Result<Value> {
130
+ let path: String = serde_json::from_value(payload["path"].clone())?;
131
+
132
+ self.security.validate_path_operation(&path, "read")?;
133
+ let content = tokio::fs::read_to_string(&path).await?;
134
+ Ok(json!({ "content": content }))
135
+ }
136
+
137
+ async fn handle_write_file(&self, payload: &Value) -> Result<Value> {
138
+ let path: String = serde_json::from_value(payload["path"].clone())?;
139
+ let content: String = serde_json::from_value(payload["content"].clone())?;
140
+
141
+ self.security.validate_path_operation(&path, "write")?;
142
+ tokio::fs::write(&path, content).await?;
143
+ Ok(json!({ "success": true }))
144
+ }
145
+
146
+ async fn handle_file_exists(&self, payload: &Value) -> Result<Value> {
147
+ let path: String = serde_json::from_value(payload["path"].clone())?;
148
+
149
+ self.security.validate_path_operation(&path, "exists")?;
150
+ let exists = tokio::fs::metadata(&path).await.is_ok();
151
+ Ok(json!({ "exists": exists }))
152
+ }
153
+
154
+ async fn handle_read_dir(&self, payload: &Value) -> Result<Value> {
155
+ let path: String = serde_json::from_value(payload["path"].clone())?;
156
+
157
+ self.security.validate_path_operation(&path, "read")?;
158
+ let mut entries = Vec::new();
159
+ let mut dir = tokio::fs::read_dir(&path).await?;
160
+
161
+ while let Some(entry) = dir.next_entry().await? {
162
+ let metadata = entry.metadata().await?;
163
+ entries.push(json!({
164
+ "name": entry.file_name().to_string_lossy(),
165
+ "path": entry.path().to_string_lossy(),
166
+ "isFile": metadata.is_file(),
167
+ "isDir": metadata.is_dir(),
168
+ "size": metadata.len(),
169
+ "modified": metadata.modified()
170
+ .ok()
171
+ .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
172
+ .map(|d| d.as_secs())
173
+ }));
174
+ }
175
+
176
+ Ok(json!({ "entries": entries }))
177
+ }
178
+
179
+ async fn handle_os_info(&self) -> Result<Value> {
180
+ if !self.config.permissions.os_info {
181
+ return Err(anyhow::anyhow!("OS info access is disabled"));
182
+ }
183
+
184
+ Ok(json!({
185
+ "platform": std::env::consts::OS,
186
+ "arch": std::env::consts::ARCH,
187
+ "version": os_info::get().version().to_string(),
188
+ "hostname": gethostname::gethostname().to_string_lossy().to_string()
189
+ }))
190
+ }
191
+
192
+ async fn handle_open_file_dialog(&self, _payload: &Value) -> Result<Value> {
193
+ if !self.config.permissions.dialogs {
194
+ return Err(anyhow::anyhow!("Dialog access is disabled"));
195
+ }
196
+
197
+ let file = AsyncFileDialog::new()
198
+ .set_title("Open File")
199
+ .pick_file()
200
+ .await;
201
+
202
+ match file {
203
+ Some(handle) => Ok(json!({ "filePath": handle.path().to_string_lossy() })),
204
+ None => Ok(json!({ "filePath": null })),
205
+ }
206
+ }
207
+
208
+ async fn handle_save_file_dialog(&self, _payload: &Value) -> Result<Value> {
209
+ if !self.config.permissions.dialogs {
210
+ return Err(anyhow::anyhow!("Dialog access is disabled"));
211
+ }
212
+
213
+ let file = AsyncFileDialog::new()
214
+ .set_title("Save File")
215
+ .save_file()
216
+ .await;
217
+
218
+ match file {
219
+ Some(handle) => Ok(json!({ "filePath": handle.path().to_string_lossy() })),
220
+ None => Ok(json!({ "filePath": null })),
221
+ }
222
+ }
223
+
224
+ async fn handle_network_request(&self, payload: &Value) -> Result<Value> {
225
+ if !self.config.permissions.network {
226
+ return Err(anyhow::anyhow!("Network access is disabled"));
227
+ }
228
+
229
+ let url: String = serde_json::from_value(payload["url"].clone())?;
230
+ let method: String = payload["method"].as_str().unwrap_or("GET").to_string();
231
+
232
+ // Basic HTTP request implementation
233
+ let client = reqwest::Client::new();
234
+ let request = match method.as_str() {
235
+ "GET" => client.get(&url),
236
+ "POST" => {
237
+ let body = payload.get("body").cloned().unwrap_or(json!(null));
238
+ client.post(&url).json(&body)
239
+ }
240
+ "PUT" => {
241
+ let body = payload.get("body").cloned().unwrap_or(json!(null));
242
+ client.put(&url).json(&body)
243
+ }
244
+ "DELETE" => client.delete(&url),
245
+ _ => return Err(anyhow::anyhow!("Unsupported HTTP method: {}", method)),
246
+ };
247
+
248
+ let response = request.send().await?;
249
+ let status = response.status().as_u16();
250
+ let headers = response
251
+ .headers()
252
+ .iter()
253
+ .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
254
+ .collect::<std::collections::HashMap<_, _>>();
255
+ let body = response.text().await?;
256
+
257
+ Ok(json!({
258
+ "status": status,
259
+ "headers": headers,
260
+ "body": body
261
+ }))
262
+ }
263
+
264
+ async fn handle_get_version(&self) -> Result<Value> {
265
+ Ok(json!({
266
+ "name": self.config.app.name.clone(),
267
+ "version": self.config.app.version.clone()
268
+ }))
269
+ }
270
+
271
+ async fn handle_get_config(&self) -> Result<Value> {
272
+ Ok(json!({
273
+ "window": self.config.window,
274
+ "permissions": self.config.permissions
275
+ }))
276
+ }
277
+
278
+ // IPC Bridge methods
279
+ async fn handle_bridge_register_app(&self, payload: &Value) -> Result<Value> {
280
+ let name: String = serde_json::from_value(payload["name"].clone())?;
281
+ let version: String = serde_json::from_value(payload["version"].clone())?;
282
+ let capabilities: Vec<String> = serde_json::from_value(payload["capabilities"].clone())?;
283
+
284
+ info!("Registering app via bridge: {} v{}", name, version);
285
+
286
+ let app_info = AppInfo {
287
+ name: name.clone(),
288
+ version: version.clone(),
289
+ pid: std::process::id(),
290
+ capabilities,
291
+ };
292
+
293
+ self.bridge_manager.register_app(app_info.clone()).await?;
294
+
295
+ Ok(json!({
296
+ "registered": true,
297
+ "app": {
298
+ "name": app_info.name,
299
+ "version": app_info.version,
300
+ "pid": app_info.pid,
301
+ "capabilities": app_info.capabilities,
302
+ "registered_at": chrono::Utc::now().to_rfc3339()
303
+ },
304
+ "message": format!("App '{}' registered successfully", name)
305
+ }))
306
+ }
307
+
308
+ async fn handle_bridge_unregister_app(&self, payload: &Value) -> Result<Value> {
309
+ let name: String = serde_json::from_value(payload["name"].clone())?;
310
+
311
+ info!("Unregistering app via bridge: {}", name);
312
+
313
+ self.bridge_manager.unregister_app(&name).await?;
314
+
315
+ Ok(json!({
316
+ "unregistered": true,
317
+ "app": name,
318
+ "message": format!("App '{}' unregistered successfully", name)
319
+ }))
320
+ }
321
+
322
+ async fn handle_bridge_get_apps(&self, _payload: &Value) -> Result<Value> {
323
+ info!("Getting registered apps via bridge");
324
+
325
+ let apps: Vec<AppInfo> = self.bridge_manager.get_registered_apps().await?;
326
+
327
+ Ok(json!({
328
+ "apps": apps,
329
+ "total_count": apps.len(),
330
+ "message": "Retrieved registered applications"
331
+ }))
332
+ }
333
+
334
+ async fn handle_bridge_send_to_app(&self, payload: &Value) -> Result<Value> {
335
+ let target_app: String = serde_json::from_value(payload["target_app"].clone())?;
336
+ let command: String = serde_json::from_value(payload["command"].clone())?;
337
+ let command_payload: Value = payload["payload"].clone();
338
+
339
+ info!("Sending to app via bridge {}: {}", target_app, command);
340
+
341
+ let request = crate::ipc_bridge::IpcBridgeRequest {
342
+ source_app: self.config.app.name.clone(),
343
+ target_app: target_app.clone(),
344
+ command,
345
+ payload: command_payload,
346
+ id: format!("bridge_msg_{}", chrono::Utc::now().timestamp_millis()),
347
+ };
348
+
349
+ let response = self.bridge_manager.send_to_app(request).await?;
350
+
351
+ Ok(json!({
352
+ "sent": true,
353
+ "delivery": response,
354
+ "message": format!("Message sent to '{}' successfully", target_app)
355
+ }))
356
+ }
357
+
358
+ async fn handle_bridge_broadcast(&self, payload: &Value) -> Result<Value> {
359
+ let message: Value = payload["message"].clone();
360
+
361
+ info!("Broadcasting message via bridge: {:?}", message);
362
+
363
+ let request = crate::ipc_bridge::IpcBridgeRequest {
364
+ source_app: self.config.app.name.clone(),
365
+ target_app: "*".to_string(), // Convention for broadcast
366
+ command: "system.broadcast".to_string(),
367
+ payload: message,
368
+ id: format!("broadcast_{}", chrono::Utc::now().timestamp_millis()),
369
+ };
370
+
371
+ // Use process_bridge_request directly for broadcast logic
372
+ let response = self.bridge_manager.send_to_app_direct(request).await?;
373
+
374
+ Ok(json!({
375
+ "broadcast_sent": true,
376
+ "broadcast": response,
377
+ "message": "Broadcast sent to all recipients successfully"
378
+ }))
379
+ }
380
+ }
@@ -0,0 +1,47 @@
1
+ use serde::{Deserialize, Serialize};
2
+ use serde_json::Value;
3
+ use std::collections::HashMap;
4
+ use std::sync::{Arc, Mutex};
5
+
6
+ #[derive(Debug, Clone, Serialize, Deserialize)]
7
+ pub struct FusionState {
8
+ pub data: HashMap<String, Value>,
9
+ }
10
+
11
+ pub struct FusionManager {
12
+ state: Arc<Mutex<FusionState>>,
13
+ }
14
+
15
+ impl FusionManager {
16
+ pub fn new() -> Self {
17
+ Self {
18
+ state: Arc::new(Mutex::new(FusionState {
19
+ data: HashMap::new(),
20
+ })),
21
+ }
22
+ }
23
+
24
+ pub fn set(&self, key: &str, value: Value) {
25
+ let mut state = self.state.lock().unwrap();
26
+ state.data.insert(key.to_string(), value);
27
+ }
28
+
29
+ #[allow(dead_code)]
30
+ pub fn get(&self, key: &str) -> Option<Value> {
31
+ let state = self.state.lock().unwrap();
32
+ state.data.get(key).cloned()
33
+ }
34
+
35
+ pub fn get_all(&self) -> Value {
36
+ let state = self.state.lock().unwrap();
37
+ serde_json::to_value(&state.data).unwrap_or(Value::Object(serde_json::Map::new()))
38
+ }
39
+
40
+ pub fn generate_sync_script(&self) -> String {
41
+ let data = self.get_all();
42
+ format!(
43
+ "if (window.ionyx) {{ window.ionyx._fusion_sync({}); }}",
44
+ data.to_string()
45
+ )
46
+ }
47
+ }
@@ -0,0 +1,100 @@
1
+ // Ionyx IPC Bridge JavaScript
2
+ (function() {
3
+ const _promises = {};
4
+ let _fusion_data = {};
5
+
6
+ window.ionyx = {
7
+ _promises,
8
+ _fusion_sync: (data) => {
9
+ console.log("🧬 Fusion Sync:", data);
10
+ _fusion_data = data;
11
+ },
12
+ resolveResponse: (responseId, response) => {
13
+ const promiseHandlers = _promises[responseId];
14
+ if (promiseHandlers) {
15
+ console.log("📥 Received IPC response:", response);
16
+ clearTimeout(promiseHandlers.timeoutId);
17
+ if (response.success) {
18
+ promiseHandlers.resolve(response.data);
19
+ } else {
20
+ promiseHandlers.reject(new Error(response.error));
21
+ }
22
+ delete _promises[responseId];
23
+ }
24
+ },
25
+ invoke: (command, payload = {}) => {
26
+ return new Promise((resolve, reject) => {
27
+ const id = `${Date.now()}_${Math.random().toString(36).substr(2, 9)}_${command}`;
28
+ const request = {
29
+ id,
30
+ command,
31
+ payload: payload || {}
32
+ };
33
+
34
+ console.log("🚀 Sending IPC request:", request);
35
+
36
+ const timeoutId = setTimeout(() => {
37
+ console.error("❌ IPC request timeout for:", command);
38
+ reject(new Error("IPC request timeout"));
39
+ delete _promises[id];
40
+ }, 15000);
41
+
42
+ _promises[id] = { resolve, reject, timeoutId };
43
+
44
+ if (window.ipc && typeof window.ipc.postMessage === 'function') {
45
+ console.log("📤 Sending via window.ipc.postMessage");
46
+ window.ipc.postMessage(JSON.stringify(request));
47
+ } else {
48
+ console.error("❌ window.ipc not available");
49
+ clearTimeout(timeoutId);
50
+ reject(new Error("IPC not available natively through window.ipc"));
51
+ delete _promises[id];
52
+ }
53
+ });
54
+ }
55
+ };
56
+
57
+ // Add Tauri-like namespaces
58
+ const invoke = window.ionyx.invoke;
59
+
60
+ window.ionyx.fs = {
61
+ readFile: (path) => invoke("fs.readFile", { path }),
62
+ writeFile: (path, content) => invoke("fs.writeFile", { path, content }),
63
+ exists: (path) => invoke("fs.exists", { path }),
64
+ readDir: (path) => invoke("fs.readdir", { path })
65
+ };
66
+
67
+ window.ionyx.os = {
68
+ info: () => invoke("os.info")
69
+ };
70
+
71
+ window.ionyx.dialog = {
72
+ openFile: () => invoke("dialog.openFile", {}),
73
+ saveFile: () => invoke("dialog.saveFile", {})
74
+ };
75
+
76
+ window.ionyx.app = {
77
+ getVersion: () => invoke("app.getVersion"),
78
+ getConfig: () => invoke("app.getConfig")
79
+ };
80
+
81
+ window.ionyx.network = {
82
+ request: (url, method = "GET", body = null) => invoke("network.request", { url, method, body })
83
+ };
84
+
85
+ // Inspired by NW.js Unified Context:
86
+ // Native Fusion allows direct access to shared state.
87
+ window.fusion = new Proxy({}, {
88
+ get: (target, prop) => {
89
+ return _fusion_data[prop];
90
+ },
91
+ set: (target, prop, value) => {
92
+ _fusion_data[prop] = value;
93
+ invoke("fusion.update", { key: prop, value });
94
+ return true;
95
+ }
96
+ });
97
+
98
+ })();
99
+
100
+ console.log("🔧 Ionyx IPC Bridge injected successfully.");