@mindexec/cli 0.2.478 → 0.2.480

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 (146) 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 +7510 -7510
  61. package/start-bridge.bat +32 -32
  62. package/start-bridge.sh +81 -81
  63. package/tree-sitter-grammars/README.md +18 -0
  64. package/tree-sitter-grammars/tree-sitter-c_sharp.wasm +0 -0
  65. package/tree-sitter-grammars/tree-sitter-go.wasm +0 -0
  66. package/tree-sitter-grammars/tree-sitter-java.wasm +0 -0
  67. package/tree-sitter-grammars/tree-sitter-javascript.wasm +0 -0
  68. package/tree-sitter-grammars/tree-sitter-python.wasm +0 -0
  69. package/tree-sitter-grammars/tree-sitter-rust.wasm +0 -0
  70. package/tree-sitter-grammars/tree-sitter-tsx.wasm +0 -0
  71. package/tree-sitter-grammars/tree-sitter-typescript.wasm +0 -0
  72. package/wwwroot/_content/MindExecution.Plugins.Admin/css/admin-dashboard.css +546 -546
  73. package/wwwroot/_content/MindExecution.Plugins.Directory/background.png +0 -0
  74. package/wwwroot/_content/MindExecution.Plugins.YouTube/css/youtube-search.css +251 -251
  75. package/wwwroot/_content/MindExecution.Shared/css/mind-map-overrides.css +3789 -3789
  76. package/wwwroot/_content/MindExecution.Shared/fonts/NotoSansKR-Bold.ttf +0 -0
  77. package/wwwroot/_content/MindExecution.Shared/fonts/NotoSansKR-Regular.ttf +0 -0
  78. package/wwwroot/_content/MindExecution.Shared/js/agent-visualization.js +27 -27
  79. package/wwwroot/_content/MindExecution.Shared/js/background-themes.js +1449 -1449
  80. package/wwwroot/_content/MindExecution.Shared/js/code-master.js +8316 -8316
  81. package/wwwroot/_content/MindExecution.Shared/js/file-system-helper.js +338 -338
  82. package/wwwroot/_content/MindExecution.Shared/js/marked.min.js +69 -69
  83. package/wwwroot/_content/MindExecution.Shared/js/mind-map-animated-image-preview.js +270 -270
  84. package/wwwroot/_content/MindExecution.Shared/js/mind-map-automation-edge-motion.js +170 -170
  85. package/wwwroot/_content/MindExecution.Shared/js/mind-map-board-render-ownership.js +81 -81
  86. package/wwwroot/_content/MindExecution.Shared/js/mind-map-core.js +10528 -10506
  87. package/wwwroot/_content/MindExecution.Shared/js/mind-map-core.js.backup +1059 -0
  88. package/wwwroot/_content/MindExecution.Shared/js/mind-map-css3d-hotpath.js +902 -902
  89. package/wwwroot/_content/MindExecution.Shared/js/mind-map-css3d-manager.js +24637 -24637
  90. package/wwwroot/_content/MindExecution.Shared/js/mind-map-dev-guards.js +325 -325
  91. package/wwwroot/_content/MindExecution.Shared/js/mind-map-dnd.js +1491 -1491
  92. package/wwwroot/_content/MindExecution.Shared/js/mind-map-dnd.js.bak +434 -0
  93. package/wwwroot/_content/MindExecution.Shared/js/mind-map-glow-shader.js +70 -70
  94. package/wwwroot/_content/MindExecution.Shared/js/mind-map-image-policy.js +218 -218
  95. package/wwwroot/_content/MindExecution.Shared/js/mind-map-interactions.js +7081 -7087
  96. package/wwwroot/_content/MindExecution.Shared/js/mind-map-lod-plan-worker.js +160 -160
  97. package/wwwroot/_content/MindExecution.Shared/js/mind-map-lod-renderer.js +9960 -9958
  98. package/wwwroot/_content/MindExecution.Shared/js/mind-map-lod-video-placeholder.js +107 -107
  99. package/wwwroot/_content/MindExecution.Shared/js/mind-map-logic-workers.js +1004 -1004
  100. package/wwwroot/_content/MindExecution.Shared/js/mind-map-menu-manager.js +1351 -1351
  101. package/wwwroot/_content/MindExecution.Shared/js/mind-map-multi-select.js +865 -865
  102. package/wwwroot/_content/MindExecution.Shared/js/mind-map-node-search-worker.js +554 -554
  103. package/wwwroot/_content/MindExecution.Shared/js/mind-map-nodes.js +4771 -4744
  104. package/wwwroot/_content/MindExecution.Shared/js/mind-map-normal-boundary-optimizer.js +624 -624
  105. package/wwwroot/_content/MindExecution.Shared/js/mind-map-object-manager.js +241 -241
  106. package/wwwroot/_content/MindExecution.Shared/js/mind-map-object-manager.js.backup +372 -0
  107. package/wwwroot/_content/MindExecution.Shared/js/mind-map-pipeline.js +2290 -2290
  108. package/wwwroot/_content/MindExecution.Shared/js/mind-map-remote-codecs.js +202 -202
  109. package/wwwroot/_content/MindExecution.Shared/js/mind-map-render-plan.js +1971 -1971
  110. package/wwwroot/_content/MindExecution.Shared/js/mind-map-text-lod-system.js +93 -93
  111. package/wwwroot/_content/MindExecution.Shared/js/mind-map-texture-factory.js +2391 -2391
  112. package/wwwroot/_content/MindExecution.Shared/js/mind-map-texture-factory.js.backup +1258 -0
  113. package/wwwroot/_content/MindExecution.Shared/js/mind-map-video-playback-controls.js +95 -95
  114. package/wwwroot/_content/MindExecution.Shared/js/mind-map-visibility-worker.js +895 -895
  115. package/wwwroot/_content/MindExecution.Shared/js/mindmap-toolbar.js +636 -636
  116. package/wwwroot/_content/MindExecution.Shared/js/native-drop-handler.js +45 -45
  117. package/wwwroot/_content/MindExecution.Shared/js/plan-master.js +117 -117
  118. package/wwwroot/_content/MindExecution.Shared/js/renderers/CSS3DRenderer.js +114 -114
  119. package/wwwroot/_content/MindExecution.Shared/js/texture-worker-manager.js +10 -10
  120. package/wwwroot/_content/MindExecution.Shared/js/three.min.js +6 -6
  121. package/wwwroot/_content/MindExecution.Shared/js/token-manager.js +7 -7
  122. package/wwwroot/_content/MindExecution.Shared/js/troika-bundle.js +6 -6
  123. package/wwwroot/_content/MindExecution.Shared/js/troika-bundle.js.map +2 -2
  124. package/wwwroot/_content/MindExecution.Shared/lib/font-awesome/css/all.min.css +8 -8
  125. package/wwwroot/_content/MindExecution.Shared/models/vocab.txt +30522 -30522
  126. package/wwwroot/_framework/{Google.Protobuf.33g2gb5is0.dll → Google.Protobuf.9h59ukbel7.dll} +0 -0
  127. package/wwwroot/_framework/{MindExecution.Core.z100xo4mg0.dll → MindExecution.Core.gnj3fo0pr7.dll} +0 -0
  128. package/wwwroot/_framework/{MindExecution.Kernel.i9cldrx8yc.dll → MindExecution.Kernel.1qygzf8o02.dll} +0 -0
  129. package/wwwroot/_framework/{MindExecution.Plugins.Admin.48lzqddb0a.dll → MindExecution.Plugins.Admin.n3uxh144wo.dll} +0 -0
  130. package/wwwroot/_framework/{MindExecution.Plugins.Business.7m8u1q142p.dll → MindExecution.Plugins.Business.vl5dximn25.dll} +0 -0
  131. package/wwwroot/_framework/{MindExecution.Plugins.Concept.cl5wmr53od.dll → MindExecution.Plugins.Concept.rutru9qeoi.dll} +0 -0
  132. package/wwwroot/_framework/{MindExecution.Plugins.Directory.slipi39041.dll → MindExecution.Plugins.Directory.fqbmq85het.dll} +0 -0
  133. package/wwwroot/_framework/{MindExecution.Plugins.PlanMaster.qfge05l671.dll → MindExecution.Plugins.PlanMaster.ko11cc9pll.dll} +0 -0
  134. package/wwwroot/_framework/{MindExecution.Plugins.YouTube.tn7v29ut77.dll → MindExecution.Plugins.YouTube.mt39q0to2j.dll} +0 -0
  135. package/wwwroot/_framework/MindExecution.Shared.umgls362or.dll +0 -0
  136. package/wwwroot/_framework/{MindExecution.Web.rwawrxxk38.dll → MindExecution.Web.1dwoarj2hg.dll} +0 -0
  137. package/wwwroot/_framework/{System.Collections.23yxgetbju.dll → System.Collections.zyvbaq6mpd.dll} +0 -0
  138. package/wwwroot/_framework/blazor.boot.json +25 -25
  139. package/wwwroot/_headers +40 -40
  140. package/wwwroot/_redirects +1 -1
  141. package/wwwroot/index.html +772 -772
  142. package/wwwroot/js/marketing-tool.js +180 -180
  143. package/wwwroot/manifest.webmanifest +23 -23
  144. package/wwwroot/service-worker-assets.js +109 -81
  145. package/wwwroot/service-worker.js +33 -33
  146. package/wwwroot/_framework/MindExecution.Shared.rjqlf0umjn.dll +0 -0
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
+ }