@mindexec/cli 0.2.476 → 0.2.478

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 (145) hide show
  1. package/README.md +323 -323
  2. package/codex-model-catalog.js +202 -202
  3. package/codex-runtime.js +1608 -1608
  4. package/desktop-workspace-state.cjs +120 -120
  5. package/electron/main.cjs +517 -517
  6. package/electron/preload.cjs +17 -17
  7. package/electron/recurring-update-owner-smoke.mjs +55 -55
  8. package/electron/recurring-update-owner.cjs +97 -97
  9. package/electron/source-smoke.mjs +108 -108
  10. package/electron/update-feed-resolver.cjs +79 -79
  11. package/electron/update-feed.json +10 -10
  12. package/electron/update-manager-smoke.mjs +146 -146
  13. package/electron/update-manager.cjs +297 -297
  14. package/electron/windows-package-smoke.mjs +156 -156
  15. package/launch-bridge.cjs +236 -236
  16. package/lzo1x.js +110 -110
  17. package/mode4-atlas-worker.js +440 -440
  18. package/mode4-atlas.js +130 -130
  19. package/package.json +1 -1
  20. package/port-guard.cjs +285 -285
  21. package/remote-fast/osx-arm64/MacScreenCaptureKitH264.swift +339 -339
  22. package/remote-fast/osx-arm64/MacSystemAudioCapture.swift +214 -214
  23. package/remote-fast/osx-arm64/UnixRawCaptureHelper.mjs +80 -80
  24. package/remote-fast/osx-arm64/livedesk-raw-capture-resampler.mjs +73 -73
  25. package/remote-fast/osx-arm64/mindexec-remote-fast.dll +0 -0
  26. package/remote-fast/osx-x64/MacScreenCaptureKitH264.swift +339 -339
  27. package/remote-fast/osx-x64/MacSystemAudioCapture.swift +214 -214
  28. package/remote-fast/osx-x64/UnixRawCaptureHelper.mjs +80 -80
  29. package/remote-fast/osx-x64/livedesk-raw-capture-resampler.mjs +73 -73
  30. package/remote-fast/osx-x64/mindexec-remote-fast.dll +0 -0
  31. package/remote-fast/win-x64/mindexec-remote-fast.dll +0 -0
  32. package/remote-hub.js +3698 -3698
  33. package/scripts/auth-session-smoke.mjs +262 -262
  34. package/scripts/board-render-ownership-smoke.mjs +85 -85
  35. package/scripts/codex-model-catalog-smoke.mjs +150 -150
  36. package/scripts/codex-runtime-home-smoke.mjs +58 -58
  37. package/scripts/codex-sdk-image-live-smoke.mjs +43 -43
  38. package/scripts/codex-sdk-image-runtime-smoke.mjs +122 -122
  39. package/scripts/codex-sdk-runtime-smoke.mjs +89 -89
  40. package/scripts/desktop-update-publisher-smoke.mjs +89 -89
  41. package/scripts/desktop-update-worker-smoke.mjs +72 -72
  42. package/scripts/desktop-workspace-state-smoke.mjs +81 -81
  43. package/scripts/drive-installed-update-smoke.mjs +67 -67
  44. package/scripts/npm-fresh-install-smoke.mjs +216 -216
  45. package/scripts/publish-mindexec-desktop-updates.mjs +224 -224
  46. package/scripts/remote-agent-managed-smoke.mjs +318 -318
  47. package/scripts/remote-agent-package-smoke.mjs +64 -64
  48. package/scripts/remote-agent-ws-smoke.mjs +269 -269
  49. package/scripts/remote-fast-live-rate-smoke.mjs +343 -343
  50. package/scripts/remote-fast-mdm-browser-smoke.mjs +453 -453
  51. package/scripts/remote-fleet-render-smoke.mjs +1350 -1350
  52. package/scripts/remote-frame-ws-smoke.mjs +234 -234
  53. package/scripts/remote-http-smoke.mjs +592 -592
  54. package/scripts/remote-hub-identity-smoke.mjs +146 -146
  55. package/scripts/remote-hub-scale-smoke.mjs +124 -124
  56. package/scripts/remote-hub-smoke.mjs +699 -699
  57. package/scripts/remote-input-ws-smoke.mjs +263 -263
  58. package/scripts/remote-registry-follower-smoke.mjs +815 -815
  59. package/scripts/setup-tree-sitter-grammars.mjs +59 -59
  60. package/server.js +7690 -7690
  61. package/start-bridge.bat +32 -32
  62. package/start-bridge.sh +81 -81
  63. package/wwwroot/_content/MindExecution.Plugins.Admin/css/admin-dashboard.css +546 -546
  64. package/wwwroot/_content/MindExecution.Plugins.YouTube/css/youtube-search.css +251 -251
  65. package/wwwroot/_content/MindExecution.Shared/css/app.css +1 -1
  66. package/wwwroot/_content/MindExecution.Shared/css/mind-map-overrides.css +3789 -3789
  67. package/wwwroot/_content/MindExecution.Shared/js/agent-visualization.js +27 -27
  68. package/wwwroot/_content/MindExecution.Shared/js/background-themes.js +1449 -1449
  69. package/wwwroot/_content/MindExecution.Shared/js/code-master.js +8316 -8316
  70. package/wwwroot/_content/MindExecution.Shared/js/file-system-helper.js +338 -338
  71. package/wwwroot/_content/MindExecution.Shared/js/marked.min.js +69 -69
  72. package/wwwroot/_content/MindExecution.Shared/js/mind-map-animated-image-preview.js +270 -270
  73. package/wwwroot/_content/MindExecution.Shared/js/mind-map-automation-edge-motion.js +170 -170
  74. package/wwwroot/_content/MindExecution.Shared/js/mind-map-board-render-ownership.js +81 -81
  75. package/wwwroot/_content/MindExecution.Shared/js/mind-map-core.js +10506 -10517
  76. package/wwwroot/_content/MindExecution.Shared/js/mind-map-css3d-hotpath.js +902 -902
  77. package/wwwroot/_content/MindExecution.Shared/js/mind-map-css3d-manager.js +24637 -24637
  78. package/wwwroot/_content/MindExecution.Shared/js/mind-map-dev-guards.js +325 -325
  79. package/wwwroot/_content/MindExecution.Shared/js/mind-map-dnd.js +1491 -1491
  80. package/wwwroot/_content/MindExecution.Shared/js/mind-map-glow-shader.js +70 -70
  81. package/wwwroot/_content/MindExecution.Shared/js/mind-map-image-policy.js +218 -218
  82. package/wwwroot/_content/MindExecution.Shared/js/mind-map-interactions.js +7065 -7065
  83. package/wwwroot/_content/MindExecution.Shared/js/mind-map-lod-plan-worker.js +160 -160
  84. package/wwwroot/_content/MindExecution.Shared/js/mind-map-lod-renderer.js +9956 -9956
  85. package/wwwroot/_content/MindExecution.Shared/js/mind-map-lod-video-placeholder.js +107 -107
  86. package/wwwroot/_content/MindExecution.Shared/js/mind-map-logic-workers.js +1004 -1004
  87. package/wwwroot/_content/MindExecution.Shared/js/mind-map-menu-manager.js +1351 -1351
  88. package/wwwroot/_content/MindExecution.Shared/js/mind-map-multi-select.js +865 -865
  89. package/wwwroot/_content/MindExecution.Shared/js/mind-map-node-search-worker.js +554 -554
  90. package/wwwroot/_content/MindExecution.Shared/js/mind-map-nodes.js +4744 -4744
  91. package/wwwroot/_content/MindExecution.Shared/js/mind-map-normal-boundary-optimizer.js +624 -624
  92. package/wwwroot/_content/MindExecution.Shared/js/mind-map-object-manager.js +241 -241
  93. package/wwwroot/_content/MindExecution.Shared/js/mind-map-pipeline.js +2290 -2290
  94. package/wwwroot/_content/MindExecution.Shared/js/mind-map-remote-codecs.js +202 -202
  95. package/wwwroot/_content/MindExecution.Shared/js/mind-map-render-plan.js +1971 -1971
  96. package/wwwroot/_content/MindExecution.Shared/js/mind-map-text-lod-system.js +93 -93
  97. package/wwwroot/_content/MindExecution.Shared/js/mind-map-texture-factory.js +2391 -2391
  98. package/wwwroot/_content/MindExecution.Shared/js/mind-map-video-playback-controls.js +95 -95
  99. package/wwwroot/_content/MindExecution.Shared/js/mind-map-visibility-worker.js +895 -895
  100. package/wwwroot/_content/MindExecution.Shared/js/mindmap-toolbar.js +636 -636
  101. package/wwwroot/_content/MindExecution.Shared/js/native-drop-handler.js +45 -45
  102. package/wwwroot/_content/MindExecution.Shared/js/plan-master.js +117 -117
  103. package/wwwroot/_content/MindExecution.Shared/js/renderers/CSS3DRenderer.js +114 -114
  104. package/wwwroot/_content/MindExecution.Shared/js/texture-worker-manager.js +10 -10
  105. package/wwwroot/_content/MindExecution.Shared/js/three.min.js +6 -6
  106. package/wwwroot/_content/MindExecution.Shared/js/token-manager.js +7 -7
  107. package/wwwroot/_content/MindExecution.Shared/js/troika-bundle.js +6 -6
  108. package/wwwroot/_content/MindExecution.Shared/js/troika-bundle.js.map +2 -2
  109. package/wwwroot/_content/MindExecution.Shared/lib/font-awesome/css/all.min.css +8 -8
  110. package/wwwroot/_content/MindExecution.Shared/models/vocab.txt +30522 -30522
  111. package/wwwroot/_framework/{Google.Protobuf.9h59ukbel7.dll → Google.Protobuf.33g2gb5is0.dll} +0 -0
  112. package/wwwroot/_framework/{MindExecution.Core.cx2pre7qz2.dll → MindExecution.Core.z100xo4mg0.dll} +0 -0
  113. package/wwwroot/_framework/{MindExecution.Kernel.engfih12kr.dll → MindExecution.Kernel.i9cldrx8yc.dll} +0 -0
  114. package/wwwroot/_framework/{MindExecution.Plugins.Admin.xfunemn9zi.dll → MindExecution.Plugins.Admin.48lzqddb0a.dll} +0 -0
  115. package/wwwroot/_framework/{MindExecution.Plugins.Business.ja94a5ze3i.dll → MindExecution.Plugins.Business.7m8u1q142p.dll} +0 -0
  116. package/wwwroot/_framework/{MindExecution.Plugins.Concept.3q05vzviqb.dll → MindExecution.Plugins.Concept.cl5wmr53od.dll} +0 -0
  117. package/wwwroot/_framework/{MindExecution.Plugins.Directory.re4jjs91d1.dll → MindExecution.Plugins.Directory.slipi39041.dll} +0 -0
  118. package/wwwroot/_framework/{MindExecution.Plugins.PlanMaster.89etwfquz1.dll → MindExecution.Plugins.PlanMaster.qfge05l671.dll} +0 -0
  119. package/wwwroot/_framework/{MindExecution.Plugins.YouTube.aqf4vztjji.dll → MindExecution.Plugins.YouTube.tn7v29ut77.dll} +0 -0
  120. package/wwwroot/_framework/{MindExecution.Shared.9b1rucdxvv.dll → MindExecution.Shared.rjqlf0umjn.dll} +0 -0
  121. package/wwwroot/_framework/{MindExecution.Web.ssp0n0xf1e.dll → MindExecution.Web.rwawrxxk38.dll} +0 -0
  122. package/wwwroot/_framework/blazor.boot.json +23 -23
  123. package/wwwroot/_headers +40 -40
  124. package/wwwroot/_redirects +1 -1
  125. package/wwwroot/index.html +772 -772
  126. package/wwwroot/js/marketing-tool.js +180 -180
  127. package/wwwroot/manifest.webmanifest +23 -23
  128. package/wwwroot/service-worker-assets.js +78 -106
  129. package/wwwroot/service-worker.js +33 -33
  130. package/tree-sitter-grammars/README.md +0 -18
  131. package/tree-sitter-grammars/tree-sitter-c_sharp.wasm +0 -0
  132. package/tree-sitter-grammars/tree-sitter-go.wasm +0 -0
  133. package/tree-sitter-grammars/tree-sitter-java.wasm +0 -0
  134. package/tree-sitter-grammars/tree-sitter-javascript.wasm +0 -0
  135. package/tree-sitter-grammars/tree-sitter-python.wasm +0 -0
  136. package/tree-sitter-grammars/tree-sitter-rust.wasm +0 -0
  137. package/tree-sitter-grammars/tree-sitter-tsx.wasm +0 -0
  138. package/tree-sitter-grammars/tree-sitter-typescript.wasm +0 -0
  139. package/wwwroot/_content/MindExecution.Plugins.Directory/background.png +0 -0
  140. package/wwwroot/_content/MindExecution.Shared/fonts/NotoSansKR-Bold.ttf +0 -0
  141. package/wwwroot/_content/MindExecution.Shared/fonts/NotoSansKR-Regular.ttf +0 -0
  142. package/wwwroot/_content/MindExecution.Shared/js/mind-map-core.js.backup +0 -1059
  143. package/wwwroot/_content/MindExecution.Shared/js/mind-map-dnd.js.bak +0 -434
  144. package/wwwroot/_content/MindExecution.Shared/js/mind-map-object-manager.js.backup +0 -372
  145. package/wwwroot/_content/MindExecution.Shared/js/mind-map-texture-factory.js.backup +0 -1258
package/electron/main.cjs CHANGED
@@ -1,517 +1,517 @@
1
- 'use strict';
2
-
3
- const { app, BrowserWindow, dialog, ipcMain } = require('electron');
4
- const { spawn } = require('child_process');
5
- const fs = require('fs');
6
- const http = require('http');
7
- const net = require('net');
8
- const path = require('path');
9
- const workspaceState = require('../desktop-workspace-state.cjs');
10
- const { createProductUpdateManager } = require('./update-manager.cjs');
11
- const {
12
- DEFAULT_UPDATE_INTERVAL_MS,
13
- createRecurringProductUpdateOwner
14
- } = require('./recurring-update-owner.cjs');
15
-
16
- const { resolveDesktopWorkspace } = workspaceState;
17
-
18
- const DEFAULT_BRIDGE_PORT = 5147;
19
- const DEFAULT_REMOTE_HUB_PORT = 5198;
20
- const STARTUP_TIMEOUT_MS = 45_000;
21
- const HEALTH_POLL_MS = 250;
22
- const UPDATE_RUNTIME_RELEASE_TIMEOUT_MS = 10_000;
23
- const DESKTOP_APP_ID = 'com.mindexec.desktop';
24
-
25
- let mainWindow = null;
26
- let bridgeChild = null;
27
- let isQuitting = false;
28
- let requestedExitCode = 0;
29
- let activeBridgePort = DEFAULT_BRIDGE_PORT;
30
- let activeRemoteHubPort = DEFAULT_REMOTE_HUB_PORT;
31
- let activeWorkspace = '';
32
- let desktopWorkspaceStatePath = '';
33
- let productUpdates = null;
34
- let recurringProductUpdates = null;
35
- let updateInstallSequenceStarted = false;
36
- let updatePromptPromise = null;
37
- let updatePromptedVersion = '';
38
-
39
- function resolveDesktopIconPath() {
40
- const iconPath = path.resolve(__dirname, '..', 'wwwroot', 'icon-512.png');
41
- return fs.existsSync(iconPath) ? iconPath : undefined;
42
- }
43
-
44
- function resolveDesktopInstallKind() {
45
- if (!app.isPackaged) return 'development';
46
- if (process.env.PORTABLE_EXECUTABLE_FILE || process.env.PORTABLE_EXECUTABLE_DIR) return 'portable';
47
- if (path.basename(path.dirname(process.execPath)).toLowerCase() === 'win-unpacked') return 'unpacked';
48
- return process.platform === 'win32' ? 'installed' : 'unsupported-platform';
49
- }
50
-
51
- function parsePort(value) {
52
- const parsed = Number.parseInt(String(value ?? ''), 10);
53
- return Number.isInteger(parsed) && parsed > 0 && parsed <= 65_535 ? parsed : 0;
54
- }
55
-
56
- function requestJson(url, timeoutMs = 2_000) {
57
- return new Promise((resolve, reject) => {
58
- const request = http.get(url, { timeout: timeoutMs }, (response) => {
59
- let body = '';
60
- response.setEncoding('utf8');
61
- response.on('data', (chunk) => {
62
- body += chunk;
63
- });
64
- response.on('end', () => {
65
- if (response.statusCode < 200 || response.statusCode >= 300) {
66
- reject(new Error(`HTTP ${response.statusCode} from ${url}`));
67
- return;
68
- }
69
-
70
- try {
71
- resolve(JSON.parse(body));
72
- } catch (error) {
73
- reject(new Error(`Invalid JSON from ${url}: ${error.message}`));
74
- }
75
- });
76
- });
77
-
78
- request.on('timeout', () => {
79
- request.destroy(new Error(`Timed out requesting ${url}`));
80
- });
81
- request.on('error', reject);
82
- });
83
- }
84
-
85
- function isPortAvailable(port) {
86
- return new Promise((resolve) => {
87
- const server = net.createServer();
88
- server.unref();
89
- server.once('error', () => resolve(false));
90
- server.listen({ host: '127.0.0.1', port, exclusive: true }, () => {
91
- server.close(() => resolve(true));
92
- });
93
- });
94
- }
95
-
96
- async function resolveDedicatedPort(explicitValue, defaultPort, label, excludedPort = 0) {
97
- const explicitPort = parsePort(explicitValue);
98
- if (explicitPort) {
99
- if (explicitPort === excludedPort) {
100
- throw new Error(`${label} port ${explicitPort} conflicts with the bundled LocalBridge port.`);
101
- }
102
- if (!await isPortAvailable(explicitPort)) {
103
- throw new Error(`${label} port ${explicitPort} is already occupied.`);
104
- }
105
- return explicitPort;
106
- }
107
-
108
- for (let offset = 0; offset < 100; offset += 1) {
109
- const candidate = defaultPort + offset;
110
- if (candidate !== excludedPort && candidate <= 65_535 && await isPortAvailable(candidate)) {
111
- return candidate;
112
- }
113
- }
114
-
115
- throw new Error(`No free ${label} port was found from ${defaultPort}.`);
116
- }
117
-
118
- function appendDesktopLog(message) {
119
- const line = `[${new Date().toISOString()}] ${message}\n`;
120
- try {
121
- const logDirectory = app.getPath('logs');
122
- fs.mkdirSync(logDirectory, { recursive: true });
123
- fs.appendFileSync(path.join(logDirectory, 'desktop.log'), line, 'utf8');
124
- } catch {
125
- process.stderr.write(line);
126
- }
127
- }
128
-
129
- function spawnBundledBridge(packageRoot) {
130
- const serverPath = path.join(packageRoot, 'server.js');
131
- const frontendPath = path.join(packageRoot, 'wwwroot', 'index.html');
132
- if (!fs.existsSync(serverPath)) {
133
- throw new Error(`Bundled LocalBridge entry is missing: ${serverPath}`);
134
- }
135
- if (!fs.existsSync(frontendPath)) {
136
- throw new Error(`Bundled MindCanvas frontend is missing: ${frontendPath}`);
137
- }
138
-
139
- const child = spawn(process.execPath, [serverPath], {
140
- cwd: packageRoot,
141
- env: {
142
- ...process.env,
143
- ELECTRON_RUN_AS_NODE: '1',
144
- MINDEXEC_DESKTOP: '1',
145
- MINDEXEC_NO_OPEN: '1',
146
- MINDEXEC_DESKTOP_WORKSPACE_STATE_PATH: desktopWorkspaceStatePath,
147
- WORKSPACE_PATH: activeWorkspace,
148
- BRIDGE_PORT: String(activeBridgePort),
149
- REMOTE_HUB_PORT: String(activeRemoteHubPort)
150
- },
151
- stdio: ['ignore', 'pipe', 'pipe'],
152
- windowsHide: true
153
- });
154
-
155
- child.stdout?.on('data', (chunk) => appendDesktopLog(`[bridge] ${String(chunk).trimEnd()}`));
156
- child.stderr?.on('data', (chunk) => appendDesktopLog(`[bridge:error] ${String(chunk).trimEnd()}`));
157
- child.on('error', (error) => appendDesktopLog(`[bridge:spawn-error] ${error.stack || error.message}`));
158
- child.on('exit', (code, signal) => {
159
- appendDesktopLog(`[bridge:exit] code=${String(code)} signal=${String(signal)}`);
160
- if (!isQuitting && process.env.MINDEXEC_DESKTOP_SMOKE !== '1') {
161
- dialog.showErrorBox(
162
- 'MindExec LocalBridge stopped',
163
- `The bundled local runtime exited unexpectedly (code ${String(code)}, signal ${String(signal)}).`
164
- );
165
- }
166
- });
167
-
168
- bridgeChild = child;
169
- }
170
-
171
- async function waitForBundledBridge() {
172
- const deadline = Date.now() + STARTUP_TIMEOUT_MS;
173
- let lastError = null;
174
-
175
- while (Date.now() < deadline) {
176
- if (bridgeChild && bridgeChild.exitCode !== null) {
177
- throw new Error(`Bundled LocalBridge exited during startup with code ${bridgeChild.exitCode}.`);
178
- }
179
-
180
- try {
181
- const status = await requestJson(`http://127.0.0.1:${activeBridgePort}/api/status?refresh=cache`);
182
- if (status?.status === 'ok' && status?.workspace) {
183
- return status;
184
- }
185
- } catch (error) {
186
- lastError = error;
187
- }
188
-
189
- await new Promise((resolve) => setTimeout(resolve, HEALTH_POLL_MS));
190
- }
191
-
192
- throw new Error(
193
- `Bundled LocalBridge did not become ready on port ${activeBridgePort}: ${lastError?.message || 'timeout'}`
194
- );
195
- }
196
-
197
- async function prepareBundledRuntime() {
198
- const packageRoot = path.resolve(__dirname, '..');
199
- activeBridgePort = await resolveDedicatedPort(process.env.BRIDGE_PORT, DEFAULT_BRIDGE_PORT, 'LocalBridge');
200
- activeRemoteHubPort = await resolveDedicatedPort(
201
- process.env.REMOTE_HUB_PORT,
202
- DEFAULT_REMOTE_HUB_PORT,
203
- 'RemoteHub',
204
- activeBridgePort
205
- );
206
- desktopWorkspaceStatePath = path.resolve(
207
- process.env.MINDEXEC_DESKTOP_WORKSPACE_STATE_PATH
208
- || path.join(app.getPath('userData'), 'workspace-state.json')
209
- );
210
- const workspaceResolution = resolveDesktopWorkspace({
211
- explicitWorkspace: process.env.MINDEXEC_DESKTOP_WORKSPACE || process.env.WORKSPACE_PATH,
212
- statePath: desktopWorkspaceStatePath,
213
- fallbackWorkspace: path.join(app.getPath('documents'), 'MindExecution')
214
- });
215
- activeWorkspace = workspaceResolution.workspacePath;
216
-
217
- fs.mkdirSync(activeWorkspace, { recursive: true });
218
- appendDesktopLog(
219
- `Starting bundled runtime: bridge=${activeBridgePort}, remoteHub=${activeRemoteHubPort}, ` +
220
- `workspace=${activeWorkspace}, workspaceSource=${workspaceResolution.source}`
221
- );
222
- spawnBundledBridge(packageRoot);
223
- return waitForBundledBridge();
224
- }
225
-
226
- function assertTrustedUpdateIpc(event) {
227
- const senderUrl = event?.senderFrame?.url || event?.sender?.getURL?.() || '';
228
- let sender;
229
- try {
230
- sender = new URL(senderUrl);
231
- } catch {
232
- throw new Error('desktop-update-ipc-sender-invalid');
233
- }
234
- if (sender.protocol !== 'http:' || sender.hostname !== '127.0.0.1' || sender.port !== String(activeBridgePort)) {
235
- throw new Error('desktop-update-ipc-sender-untrusted');
236
- }
237
- }
238
-
239
- function registerUpdateIpc() {
240
- const handle = (channel, callback) => {
241
- ipcMain.handle(channel, async (event) => {
242
- assertTrustedUpdateIpc(event);
243
- return callback();
244
- });
245
- };
246
- handle('updates:get-status', () => productUpdates?.getStatus() || { state: 'unavailable' });
247
- handle('updates:check', () => productUpdates?.check() || Promise.resolve({ state: 'unavailable' }));
248
- handle('updates:install', () => productUpdates?.install() || Promise.resolve({ state: 'unavailable' }));
249
- }
250
-
251
- function sendUpdateStatus(status) {
252
- appendDesktopLog(
253
- `[update] state=${status.state} current=${status.currentVersion || ''} ` +
254
- `available=${status.availableVersion || ''} progress=${Math.round(status.progress || 0)} ` +
255
- `kind=${status.installKind || ''}${status.error ? ` error=${status.error}` : ''}`
256
- );
257
- if (mainWindow && !mainWindow.isDestroyed() && !mainWindow.webContents.isDestroyed()) {
258
- mainWindow.webContents.send('updates:status', status);
259
- }
260
- if (status.state === 'downloaded') void promptForDownloadedUpdate(status);
261
- }
262
-
263
- async function promptForDownloadedUpdate(status = productUpdates?.getStatus()) {
264
- if (!status?.downloaded || !status.availableVersion || !mainWindow || mainWindow.isDestroyed()) return;
265
- if (process.env.MINDEXEC_DESKTOP_SMOKE === '1') return;
266
- if (updatePromptedVersion === status.availableVersion || updatePromptPromise) return updatePromptPromise;
267
-
268
- updatePromptedVersion = status.availableVersion;
269
- updatePromptPromise = dialog.showMessageBox(mainWindow, {
270
- type: 'info',
271
- title: 'MindExec update ready',
272
- message: `MindExec ${status.availableVersion} is ready.`,
273
- detail: 'Restart now to use the new development build. Your project folder and saved data stay unchanged.',
274
- buttons: ['Restart and update', 'Later'],
275
- defaultId: 0,
276
- cancelId: 1,
277
- noLink: true
278
- }).then(result => {
279
- if (result.response === 0) return productUpdates?.install();
280
- return productUpdates?.getStatus();
281
- }).catch(error => {
282
- appendDesktopLog(`[update:prompt-error] ${error?.stack || error}`);
283
- return productUpdates?.getStatus();
284
- }).finally(() => {
285
- updatePromptPromise = null;
286
- });
287
- return updatePromptPromise;
288
- }
289
-
290
- function initializeProductUpdates() {
291
- productUpdates = createProductUpdateManager({
292
- app,
293
- installKind: resolveDesktopInstallKind(),
294
- onStatus: sendUpdateStatus,
295
- beforeInstall: prepareRuntimeForUpdateInstall,
296
- onInstallError: recoverRuntimeAfterUpdateInstallError
297
- });
298
- recurringProductUpdates = createRecurringProductUpdateOwner({
299
- manager: productUpdates,
300
- canRun: () => !isQuitting && !updateInstallSequenceStarted,
301
- initialDelayMs: DEFAULT_UPDATE_INTERVAL_MS,
302
- intervalMs: DEFAULT_UPDATE_INTERVAL_MS,
303
- onError: error => appendDesktopLog(`[update:recurring-error] ${error?.stack || error}`)
304
- });
305
- }
306
-
307
- function createMainWindow() {
308
- const hiddenSmokeMode = process.env.MINDEXEC_DESKTOP_SMOKE === '1';
309
- const window = new BrowserWindow({
310
- width: 1440,
311
- height: 960,
312
- minWidth: 960,
313
- minHeight: 640,
314
- show: !hiddenSmokeMode,
315
- autoHideMenuBar: true,
316
- backgroundColor: '#f8fafc',
317
- title: 'MindExec',
318
- icon: resolveDesktopIconPath(),
319
- webPreferences: {
320
- preload: path.join(__dirname, 'preload.cjs'),
321
- contextIsolation: true,
322
- nodeIntegration: false,
323
- sandbox: true,
324
- webSecurity: true
325
- }
326
- });
327
-
328
- window.webContents.setWindowOpenHandler(() => ({
329
- action: 'allow',
330
- overrideBrowserWindowOptions: {
331
- autoHideMenuBar: true,
332
- icon: resolveDesktopIconPath(),
333
- webPreferences: {
334
- contextIsolation: true,
335
- nodeIntegration: false,
336
- sandbox: true,
337
- webSecurity: true
338
- }
339
- }
340
- }));
341
-
342
- window.on('closed', () => {
343
- if (mainWindow === window) {
344
- mainWindow = null;
345
- }
346
- });
347
-
348
- mainWindow = window;
349
- return window;
350
- }
351
-
352
- function writeSmokeResult(payload) {
353
- const resultPath = process.env.MINDEXEC_DESKTOP_SMOKE_RESULT;
354
- if (!resultPath) {
355
- return;
356
- }
357
-
358
- fs.mkdirSync(path.dirname(resultPath), { recursive: true });
359
- fs.writeFileSync(resultPath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
360
- }
361
-
362
- async function stopBundledBridge() {
363
- const child = bridgeChild;
364
- bridgeChild = null;
365
- if (!child || child.exitCode !== null) {
366
- return;
367
- }
368
-
369
- await new Promise((resolve) => {
370
- let settled = false;
371
- const finish = () => {
372
- if (settled) {
373
- return;
374
- }
375
- settled = true;
376
- resolve();
377
- };
378
-
379
- child.once('exit', finish);
380
- try {
381
- child.kill('SIGTERM');
382
- } catch {
383
- finish();
384
- return;
385
- }
386
-
387
- setTimeout(() => {
388
- if (child.exitCode === null) {
389
- try {
390
- child.kill('SIGKILL');
391
- } catch {
392
- // The child may have exited between the check and the kill.
393
- }
394
- }
395
- finish();
396
- }, 5_000).unref();
397
- });
398
- }
399
-
400
- async function waitForBundledRuntimePortsReleased(timeoutMs = UPDATE_RUNTIME_RELEASE_TIMEOUT_MS) {
401
- const deadline = Date.now() + timeoutMs;
402
- while (Date.now() < deadline) {
403
- const [bridgeReleased, remoteHubReleased] = await Promise.all([
404
- isPortAvailable(activeBridgePort),
405
- isPortAvailable(activeRemoteHubPort)
406
- ]);
407
- if (bridgeReleased && remoteHubReleased) return;
408
- await new Promise(resolve => setTimeout(resolve, HEALTH_POLL_MS));
409
- }
410
- throw new Error(
411
- `Bundled runtime ports did not release before update: bridge=${activeBridgePort}, remoteHub=${activeRemoteHubPort}`
412
- );
413
- }
414
-
415
- async function prepareRuntimeForUpdateInstall() {
416
- if (updateInstallSequenceStarted) return;
417
- updateInstallSequenceStarted = true;
418
- recurringProductUpdates?.stop();
419
- isQuitting = true;
420
- appendDesktopLog('[update] stopping bundled runtime before installer launch');
421
- await stopBundledBridge();
422
- await waitForBundledRuntimePortsReleased();
423
- appendDesktopLog('[update] bundled runtime and ports are fully released');
424
- }
425
-
426
- async function recoverRuntimeAfterUpdateInstallError(error) {
427
- appendDesktopLog(`[update:install-recovery] ${error?.stack || error}`);
428
- isQuitting = false;
429
- updateInstallSequenceStarted = false;
430
- if (!bridgeChild || bridgeChild.exitCode !== null) {
431
- await prepareBundledRuntime();
432
- }
433
- recurringProductUpdates?.start();
434
- }
435
-
436
- async function launchDesktop() {
437
- const status = await prepareBundledRuntime();
438
- const window = createMainWindow();
439
- const appUrl = `http://127.0.0.1:${activeBridgePort}/mindcanvas`;
440
- await window.loadURL(appUrl);
441
- void promptForDownloadedUpdate();
442
-
443
- if (process.env.MINDEXEC_DESKTOP_SMOKE === '1') {
444
- writeSmokeResult({
445
- ok: true,
446
- appVersion: app.getVersion(),
447
- bridgeVersion: status.version || null,
448
- runtimeMode: 'bundled',
449
- bridgePort: activeBridgePort,
450
- remoteHubPort: activeRemoteHubPort,
451
- workspace: activeWorkspace,
452
- url: appUrl,
453
- desktopUpdate: productUpdates?.getStatus() || null
454
- });
455
- setTimeout(() => app.quit(), 250).unref();
456
- }
457
- }
458
-
459
- if (process.platform === 'win32') {
460
- app.setAppUserModelId(DESKTOP_APP_ID);
461
- }
462
-
463
- const hasSingleInstanceLock = app.requestSingleInstanceLock();
464
- if (!hasSingleInstanceLock) {
465
- app.quit();
466
- } else {
467
- app.on('second-instance', () => {
468
- if (mainWindow) {
469
- if (mainWindow.isMinimized()) {
470
- mainWindow.restore();
471
- }
472
- mainWindow.focus();
473
- }
474
- });
475
-
476
- app.on('window-all-closed', () => {
477
- app.quit();
478
- });
479
-
480
- app.on('before-quit', (event) => {
481
- if (isQuitting || !bridgeChild || bridgeChild.exitCode !== null) {
482
- return;
483
- }
484
-
485
- event.preventDefault();
486
- recurringProductUpdates?.stop();
487
- isQuitting = true;
488
- stopBundledBridge().finally(() => app.exit(requestedExitCode));
489
- });
490
-
491
- app.whenReady()
492
- .then(async () => {
493
- initializeProductUpdates();
494
- registerUpdateIpc();
495
- void productUpdates.checkAtStartup().catch(error => {
496
- appendDesktopLog(`[update:startup-error] ${error?.stack || error}`);
497
- });
498
- await launchDesktop();
499
- recurringProductUpdates.start();
500
- })
501
- .catch((error) => {
502
- const message = error?.stack || String(error);
503
- appendDesktopLog(`[desktop:start-error] ${message}`);
504
- writeSmokeResult({
505
- ok: false,
506
- appVersion: app.getVersion(),
507
- runtimeMode: 'bundled',
508
- error: error?.message || String(error)
509
- });
510
-
511
- requestedExitCode = 1;
512
- if (process.env.MINDEXEC_DESKTOP_SMOKE !== '1') {
513
- dialog.showErrorBox('MindExec failed to start', error?.message || String(error));
514
- }
515
- app.quit();
516
- });
517
- }
1
+ 'use strict';
2
+
3
+ const { app, BrowserWindow, dialog, ipcMain } = require('electron');
4
+ const { spawn } = require('child_process');
5
+ const fs = require('fs');
6
+ const http = require('http');
7
+ const net = require('net');
8
+ const path = require('path');
9
+ const workspaceState = require('../desktop-workspace-state.cjs');
10
+ const { createProductUpdateManager } = require('./update-manager.cjs');
11
+ const {
12
+ DEFAULT_UPDATE_INTERVAL_MS,
13
+ createRecurringProductUpdateOwner
14
+ } = require('./recurring-update-owner.cjs');
15
+
16
+ const { resolveDesktopWorkspace } = workspaceState;
17
+
18
+ const DEFAULT_BRIDGE_PORT = 5147;
19
+ const DEFAULT_REMOTE_HUB_PORT = 5198;
20
+ const STARTUP_TIMEOUT_MS = 45_000;
21
+ const HEALTH_POLL_MS = 250;
22
+ const UPDATE_RUNTIME_RELEASE_TIMEOUT_MS = 10_000;
23
+ const DESKTOP_APP_ID = 'com.mindexec.desktop';
24
+
25
+ let mainWindow = null;
26
+ let bridgeChild = null;
27
+ let isQuitting = false;
28
+ let requestedExitCode = 0;
29
+ let activeBridgePort = DEFAULT_BRIDGE_PORT;
30
+ let activeRemoteHubPort = DEFAULT_REMOTE_HUB_PORT;
31
+ let activeWorkspace = '';
32
+ let desktopWorkspaceStatePath = '';
33
+ let productUpdates = null;
34
+ let recurringProductUpdates = null;
35
+ let updateInstallSequenceStarted = false;
36
+ let updatePromptPromise = null;
37
+ let updatePromptedVersion = '';
38
+
39
+ function resolveDesktopIconPath() {
40
+ const iconPath = path.resolve(__dirname, '..', 'wwwroot', 'icon-512.png');
41
+ return fs.existsSync(iconPath) ? iconPath : undefined;
42
+ }
43
+
44
+ function resolveDesktopInstallKind() {
45
+ if (!app.isPackaged) return 'development';
46
+ if (process.env.PORTABLE_EXECUTABLE_FILE || process.env.PORTABLE_EXECUTABLE_DIR) return 'portable';
47
+ if (path.basename(path.dirname(process.execPath)).toLowerCase() === 'win-unpacked') return 'unpacked';
48
+ return process.platform === 'win32' ? 'installed' : 'unsupported-platform';
49
+ }
50
+
51
+ function parsePort(value) {
52
+ const parsed = Number.parseInt(String(value ?? ''), 10);
53
+ return Number.isInteger(parsed) && parsed > 0 && parsed <= 65_535 ? parsed : 0;
54
+ }
55
+
56
+ function requestJson(url, timeoutMs = 2_000) {
57
+ return new Promise((resolve, reject) => {
58
+ const request = http.get(url, { timeout: timeoutMs }, (response) => {
59
+ let body = '';
60
+ response.setEncoding('utf8');
61
+ response.on('data', (chunk) => {
62
+ body += chunk;
63
+ });
64
+ response.on('end', () => {
65
+ if (response.statusCode < 200 || response.statusCode >= 300) {
66
+ reject(new Error(`HTTP ${response.statusCode} from ${url}`));
67
+ return;
68
+ }
69
+
70
+ try {
71
+ resolve(JSON.parse(body));
72
+ } catch (error) {
73
+ reject(new Error(`Invalid JSON from ${url}: ${error.message}`));
74
+ }
75
+ });
76
+ });
77
+
78
+ request.on('timeout', () => {
79
+ request.destroy(new Error(`Timed out requesting ${url}`));
80
+ });
81
+ request.on('error', reject);
82
+ });
83
+ }
84
+
85
+ function isPortAvailable(port) {
86
+ return new Promise((resolve) => {
87
+ const server = net.createServer();
88
+ server.unref();
89
+ server.once('error', () => resolve(false));
90
+ server.listen({ host: '127.0.0.1', port, exclusive: true }, () => {
91
+ server.close(() => resolve(true));
92
+ });
93
+ });
94
+ }
95
+
96
+ async function resolveDedicatedPort(explicitValue, defaultPort, label, excludedPort = 0) {
97
+ const explicitPort = parsePort(explicitValue);
98
+ if (explicitPort) {
99
+ if (explicitPort === excludedPort) {
100
+ throw new Error(`${label} port ${explicitPort} conflicts with the bundled LocalBridge port.`);
101
+ }
102
+ if (!await isPortAvailable(explicitPort)) {
103
+ throw new Error(`${label} port ${explicitPort} is already occupied.`);
104
+ }
105
+ return explicitPort;
106
+ }
107
+
108
+ for (let offset = 0; offset < 100; offset += 1) {
109
+ const candidate = defaultPort + offset;
110
+ if (candidate !== excludedPort && candidate <= 65_535 && await isPortAvailable(candidate)) {
111
+ return candidate;
112
+ }
113
+ }
114
+
115
+ throw new Error(`No free ${label} port was found from ${defaultPort}.`);
116
+ }
117
+
118
+ function appendDesktopLog(message) {
119
+ const line = `[${new Date().toISOString()}] ${message}\n`;
120
+ try {
121
+ const logDirectory = app.getPath('logs');
122
+ fs.mkdirSync(logDirectory, { recursive: true });
123
+ fs.appendFileSync(path.join(logDirectory, 'desktop.log'), line, 'utf8');
124
+ } catch {
125
+ process.stderr.write(line);
126
+ }
127
+ }
128
+
129
+ function spawnBundledBridge(packageRoot) {
130
+ const serverPath = path.join(packageRoot, 'server.js');
131
+ const frontendPath = path.join(packageRoot, 'wwwroot', 'index.html');
132
+ if (!fs.existsSync(serverPath)) {
133
+ throw new Error(`Bundled LocalBridge entry is missing: ${serverPath}`);
134
+ }
135
+ if (!fs.existsSync(frontendPath)) {
136
+ throw new Error(`Bundled MindCanvas frontend is missing: ${frontendPath}`);
137
+ }
138
+
139
+ const child = spawn(process.execPath, [serverPath], {
140
+ cwd: packageRoot,
141
+ env: {
142
+ ...process.env,
143
+ ELECTRON_RUN_AS_NODE: '1',
144
+ MINDEXEC_DESKTOP: '1',
145
+ MINDEXEC_NO_OPEN: '1',
146
+ MINDEXEC_DESKTOP_WORKSPACE_STATE_PATH: desktopWorkspaceStatePath,
147
+ WORKSPACE_PATH: activeWorkspace,
148
+ BRIDGE_PORT: String(activeBridgePort),
149
+ REMOTE_HUB_PORT: String(activeRemoteHubPort)
150
+ },
151
+ stdio: ['ignore', 'pipe', 'pipe'],
152
+ windowsHide: true
153
+ });
154
+
155
+ child.stdout?.on('data', (chunk) => appendDesktopLog(`[bridge] ${String(chunk).trimEnd()}`));
156
+ child.stderr?.on('data', (chunk) => appendDesktopLog(`[bridge:error] ${String(chunk).trimEnd()}`));
157
+ child.on('error', (error) => appendDesktopLog(`[bridge:spawn-error] ${error.stack || error.message}`));
158
+ child.on('exit', (code, signal) => {
159
+ appendDesktopLog(`[bridge:exit] code=${String(code)} signal=${String(signal)}`);
160
+ if (!isQuitting && process.env.MINDEXEC_DESKTOP_SMOKE !== '1') {
161
+ dialog.showErrorBox(
162
+ 'MindExec LocalBridge stopped',
163
+ `The bundled local runtime exited unexpectedly (code ${String(code)}, signal ${String(signal)}).`
164
+ );
165
+ }
166
+ });
167
+
168
+ bridgeChild = child;
169
+ }
170
+
171
+ async function waitForBundledBridge() {
172
+ const deadline = Date.now() + STARTUP_TIMEOUT_MS;
173
+ let lastError = null;
174
+
175
+ while (Date.now() < deadline) {
176
+ if (bridgeChild && bridgeChild.exitCode !== null) {
177
+ throw new Error(`Bundled LocalBridge exited during startup with code ${bridgeChild.exitCode}.`);
178
+ }
179
+
180
+ try {
181
+ const status = await requestJson(`http://127.0.0.1:${activeBridgePort}/api/status?refresh=cache`);
182
+ if (status?.status === 'ok' && status?.workspace) {
183
+ return status;
184
+ }
185
+ } catch (error) {
186
+ lastError = error;
187
+ }
188
+
189
+ await new Promise((resolve) => setTimeout(resolve, HEALTH_POLL_MS));
190
+ }
191
+
192
+ throw new Error(
193
+ `Bundled LocalBridge did not become ready on port ${activeBridgePort}: ${lastError?.message || 'timeout'}`
194
+ );
195
+ }
196
+
197
+ async function prepareBundledRuntime() {
198
+ const packageRoot = path.resolve(__dirname, '..');
199
+ activeBridgePort = await resolveDedicatedPort(process.env.BRIDGE_PORT, DEFAULT_BRIDGE_PORT, 'LocalBridge');
200
+ activeRemoteHubPort = await resolveDedicatedPort(
201
+ process.env.REMOTE_HUB_PORT,
202
+ DEFAULT_REMOTE_HUB_PORT,
203
+ 'RemoteHub',
204
+ activeBridgePort
205
+ );
206
+ desktopWorkspaceStatePath = path.resolve(
207
+ process.env.MINDEXEC_DESKTOP_WORKSPACE_STATE_PATH
208
+ || path.join(app.getPath('userData'), 'workspace-state.json')
209
+ );
210
+ const workspaceResolution = resolveDesktopWorkspace({
211
+ explicitWorkspace: process.env.MINDEXEC_DESKTOP_WORKSPACE || process.env.WORKSPACE_PATH,
212
+ statePath: desktopWorkspaceStatePath,
213
+ fallbackWorkspace: path.join(app.getPath('documents'), 'MindExecution')
214
+ });
215
+ activeWorkspace = workspaceResolution.workspacePath;
216
+
217
+ fs.mkdirSync(activeWorkspace, { recursive: true });
218
+ appendDesktopLog(
219
+ `Starting bundled runtime: bridge=${activeBridgePort}, remoteHub=${activeRemoteHubPort}, ` +
220
+ `workspace=${activeWorkspace}, workspaceSource=${workspaceResolution.source}`
221
+ );
222
+ spawnBundledBridge(packageRoot);
223
+ return waitForBundledBridge();
224
+ }
225
+
226
+ function assertTrustedUpdateIpc(event) {
227
+ const senderUrl = event?.senderFrame?.url || event?.sender?.getURL?.() || '';
228
+ let sender;
229
+ try {
230
+ sender = new URL(senderUrl);
231
+ } catch {
232
+ throw new Error('desktop-update-ipc-sender-invalid');
233
+ }
234
+ if (sender.protocol !== 'http:' || sender.hostname !== '127.0.0.1' || sender.port !== String(activeBridgePort)) {
235
+ throw new Error('desktop-update-ipc-sender-untrusted');
236
+ }
237
+ }
238
+
239
+ function registerUpdateIpc() {
240
+ const handle = (channel, callback) => {
241
+ ipcMain.handle(channel, async (event) => {
242
+ assertTrustedUpdateIpc(event);
243
+ return callback();
244
+ });
245
+ };
246
+ handle('updates:get-status', () => productUpdates?.getStatus() || { state: 'unavailable' });
247
+ handle('updates:check', () => productUpdates?.check() || Promise.resolve({ state: 'unavailable' }));
248
+ handle('updates:install', () => productUpdates?.install() || Promise.resolve({ state: 'unavailable' }));
249
+ }
250
+
251
+ function sendUpdateStatus(status) {
252
+ appendDesktopLog(
253
+ `[update] state=${status.state} current=${status.currentVersion || ''} ` +
254
+ `available=${status.availableVersion || ''} progress=${Math.round(status.progress || 0)} ` +
255
+ `kind=${status.installKind || ''}${status.error ? ` error=${status.error}` : ''}`
256
+ );
257
+ if (mainWindow && !mainWindow.isDestroyed() && !mainWindow.webContents.isDestroyed()) {
258
+ mainWindow.webContents.send('updates:status', status);
259
+ }
260
+ if (status.state === 'downloaded') void promptForDownloadedUpdate(status);
261
+ }
262
+
263
+ async function promptForDownloadedUpdate(status = productUpdates?.getStatus()) {
264
+ if (!status?.downloaded || !status.availableVersion || !mainWindow || mainWindow.isDestroyed()) return;
265
+ if (process.env.MINDEXEC_DESKTOP_SMOKE === '1') return;
266
+ if (updatePromptedVersion === status.availableVersion || updatePromptPromise) return updatePromptPromise;
267
+
268
+ updatePromptedVersion = status.availableVersion;
269
+ updatePromptPromise = dialog.showMessageBox(mainWindow, {
270
+ type: 'info',
271
+ title: 'MindExec update ready',
272
+ message: `MindExec ${status.availableVersion} is ready.`,
273
+ detail: 'Restart now to use the new development build. Your project folder and saved data stay unchanged.',
274
+ buttons: ['Restart and update', 'Later'],
275
+ defaultId: 0,
276
+ cancelId: 1,
277
+ noLink: true
278
+ }).then(result => {
279
+ if (result.response === 0) return productUpdates?.install();
280
+ return productUpdates?.getStatus();
281
+ }).catch(error => {
282
+ appendDesktopLog(`[update:prompt-error] ${error?.stack || error}`);
283
+ return productUpdates?.getStatus();
284
+ }).finally(() => {
285
+ updatePromptPromise = null;
286
+ });
287
+ return updatePromptPromise;
288
+ }
289
+
290
+ function initializeProductUpdates() {
291
+ productUpdates = createProductUpdateManager({
292
+ app,
293
+ installKind: resolveDesktopInstallKind(),
294
+ onStatus: sendUpdateStatus,
295
+ beforeInstall: prepareRuntimeForUpdateInstall,
296
+ onInstallError: recoverRuntimeAfterUpdateInstallError
297
+ });
298
+ recurringProductUpdates = createRecurringProductUpdateOwner({
299
+ manager: productUpdates,
300
+ canRun: () => !isQuitting && !updateInstallSequenceStarted,
301
+ initialDelayMs: DEFAULT_UPDATE_INTERVAL_MS,
302
+ intervalMs: DEFAULT_UPDATE_INTERVAL_MS,
303
+ onError: error => appendDesktopLog(`[update:recurring-error] ${error?.stack || error}`)
304
+ });
305
+ }
306
+
307
+ function createMainWindow() {
308
+ const hiddenSmokeMode = process.env.MINDEXEC_DESKTOP_SMOKE === '1';
309
+ const window = new BrowserWindow({
310
+ width: 1440,
311
+ height: 960,
312
+ minWidth: 960,
313
+ minHeight: 640,
314
+ show: !hiddenSmokeMode,
315
+ autoHideMenuBar: true,
316
+ backgroundColor: '#f8fafc',
317
+ title: 'MindExec',
318
+ icon: resolveDesktopIconPath(),
319
+ webPreferences: {
320
+ preload: path.join(__dirname, 'preload.cjs'),
321
+ contextIsolation: true,
322
+ nodeIntegration: false,
323
+ sandbox: true,
324
+ webSecurity: true
325
+ }
326
+ });
327
+
328
+ window.webContents.setWindowOpenHandler(() => ({
329
+ action: 'allow',
330
+ overrideBrowserWindowOptions: {
331
+ autoHideMenuBar: true,
332
+ icon: resolveDesktopIconPath(),
333
+ webPreferences: {
334
+ contextIsolation: true,
335
+ nodeIntegration: false,
336
+ sandbox: true,
337
+ webSecurity: true
338
+ }
339
+ }
340
+ }));
341
+
342
+ window.on('closed', () => {
343
+ if (mainWindow === window) {
344
+ mainWindow = null;
345
+ }
346
+ });
347
+
348
+ mainWindow = window;
349
+ return window;
350
+ }
351
+
352
+ function writeSmokeResult(payload) {
353
+ const resultPath = process.env.MINDEXEC_DESKTOP_SMOKE_RESULT;
354
+ if (!resultPath) {
355
+ return;
356
+ }
357
+
358
+ fs.mkdirSync(path.dirname(resultPath), { recursive: true });
359
+ fs.writeFileSync(resultPath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
360
+ }
361
+
362
+ async function stopBundledBridge() {
363
+ const child = bridgeChild;
364
+ bridgeChild = null;
365
+ if (!child || child.exitCode !== null) {
366
+ return;
367
+ }
368
+
369
+ await new Promise((resolve) => {
370
+ let settled = false;
371
+ const finish = () => {
372
+ if (settled) {
373
+ return;
374
+ }
375
+ settled = true;
376
+ resolve();
377
+ };
378
+
379
+ child.once('exit', finish);
380
+ try {
381
+ child.kill('SIGTERM');
382
+ } catch {
383
+ finish();
384
+ return;
385
+ }
386
+
387
+ setTimeout(() => {
388
+ if (child.exitCode === null) {
389
+ try {
390
+ child.kill('SIGKILL');
391
+ } catch {
392
+ // The child may have exited between the check and the kill.
393
+ }
394
+ }
395
+ finish();
396
+ }, 5_000).unref();
397
+ });
398
+ }
399
+
400
+ async function waitForBundledRuntimePortsReleased(timeoutMs = UPDATE_RUNTIME_RELEASE_TIMEOUT_MS) {
401
+ const deadline = Date.now() + timeoutMs;
402
+ while (Date.now() < deadline) {
403
+ const [bridgeReleased, remoteHubReleased] = await Promise.all([
404
+ isPortAvailable(activeBridgePort),
405
+ isPortAvailable(activeRemoteHubPort)
406
+ ]);
407
+ if (bridgeReleased && remoteHubReleased) return;
408
+ await new Promise(resolve => setTimeout(resolve, HEALTH_POLL_MS));
409
+ }
410
+ throw new Error(
411
+ `Bundled runtime ports did not release before update: bridge=${activeBridgePort}, remoteHub=${activeRemoteHubPort}`
412
+ );
413
+ }
414
+
415
+ async function prepareRuntimeForUpdateInstall() {
416
+ if (updateInstallSequenceStarted) return;
417
+ updateInstallSequenceStarted = true;
418
+ recurringProductUpdates?.stop();
419
+ isQuitting = true;
420
+ appendDesktopLog('[update] stopping bundled runtime before installer launch');
421
+ await stopBundledBridge();
422
+ await waitForBundledRuntimePortsReleased();
423
+ appendDesktopLog('[update] bundled runtime and ports are fully released');
424
+ }
425
+
426
+ async function recoverRuntimeAfterUpdateInstallError(error) {
427
+ appendDesktopLog(`[update:install-recovery] ${error?.stack || error}`);
428
+ isQuitting = false;
429
+ updateInstallSequenceStarted = false;
430
+ if (!bridgeChild || bridgeChild.exitCode !== null) {
431
+ await prepareBundledRuntime();
432
+ }
433
+ recurringProductUpdates?.start();
434
+ }
435
+
436
+ async function launchDesktop() {
437
+ const status = await prepareBundledRuntime();
438
+ const window = createMainWindow();
439
+ const appUrl = `http://127.0.0.1:${activeBridgePort}/mindcanvas`;
440
+ await window.loadURL(appUrl);
441
+ void promptForDownloadedUpdate();
442
+
443
+ if (process.env.MINDEXEC_DESKTOP_SMOKE === '1') {
444
+ writeSmokeResult({
445
+ ok: true,
446
+ appVersion: app.getVersion(),
447
+ bridgeVersion: status.version || null,
448
+ runtimeMode: 'bundled',
449
+ bridgePort: activeBridgePort,
450
+ remoteHubPort: activeRemoteHubPort,
451
+ workspace: activeWorkspace,
452
+ url: appUrl,
453
+ desktopUpdate: productUpdates?.getStatus() || null
454
+ });
455
+ setTimeout(() => app.quit(), 250).unref();
456
+ }
457
+ }
458
+
459
+ if (process.platform === 'win32') {
460
+ app.setAppUserModelId(DESKTOP_APP_ID);
461
+ }
462
+
463
+ const hasSingleInstanceLock = app.requestSingleInstanceLock();
464
+ if (!hasSingleInstanceLock) {
465
+ app.quit();
466
+ } else {
467
+ app.on('second-instance', () => {
468
+ if (mainWindow) {
469
+ if (mainWindow.isMinimized()) {
470
+ mainWindow.restore();
471
+ }
472
+ mainWindow.focus();
473
+ }
474
+ });
475
+
476
+ app.on('window-all-closed', () => {
477
+ app.quit();
478
+ });
479
+
480
+ app.on('before-quit', (event) => {
481
+ if (isQuitting || !bridgeChild || bridgeChild.exitCode !== null) {
482
+ return;
483
+ }
484
+
485
+ event.preventDefault();
486
+ recurringProductUpdates?.stop();
487
+ isQuitting = true;
488
+ stopBundledBridge().finally(() => app.exit(requestedExitCode));
489
+ });
490
+
491
+ app.whenReady()
492
+ .then(async () => {
493
+ initializeProductUpdates();
494
+ registerUpdateIpc();
495
+ void productUpdates.checkAtStartup().catch(error => {
496
+ appendDesktopLog(`[update:startup-error] ${error?.stack || error}`);
497
+ });
498
+ await launchDesktop();
499
+ recurringProductUpdates.start();
500
+ })
501
+ .catch((error) => {
502
+ const message = error?.stack || String(error);
503
+ appendDesktopLog(`[desktop:start-error] ${message}`);
504
+ writeSmokeResult({
505
+ ok: false,
506
+ appVersion: app.getVersion(),
507
+ runtimeMode: 'bundled',
508
+ error: error?.message || String(error)
509
+ });
510
+
511
+ requestedExitCode = 1;
512
+ if (process.env.MINDEXEC_DESKTOP_SMOKE !== '1') {
513
+ dialog.showErrorBox('MindExec failed to start', error?.message || String(error));
514
+ }
515
+ app.quit();
516
+ });
517
+ }