@mindexec/cli 0.2.477 → 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.
- package/README.md +323 -323
- package/codex-model-catalog.js +202 -202
- package/codex-runtime.js +1608 -1608
- package/desktop-workspace-state.cjs +120 -120
- package/electron/main.cjs +517 -517
- package/electron/preload.cjs +17 -17
- package/electron/recurring-update-owner-smoke.mjs +55 -55
- package/electron/recurring-update-owner.cjs +97 -97
- package/electron/source-smoke.mjs +108 -108
- package/electron/update-feed-resolver.cjs +79 -79
- package/electron/update-feed.json +10 -10
- package/electron/update-manager-smoke.mjs +146 -146
- package/electron/update-manager.cjs +297 -297
- package/electron/windows-package-smoke.mjs +156 -156
- package/launch-bridge.cjs +236 -236
- package/lzo1x.js +110 -110
- package/mode4-atlas-worker.js +440 -440
- package/mode4-atlas.js +130 -130
- package/package.json +1 -1
- package/port-guard.cjs +285 -285
- package/remote-fast/osx-arm64/MacScreenCaptureKitH264.swift +339 -339
- package/remote-fast/osx-arm64/MacSystemAudioCapture.swift +214 -214
- package/remote-fast/osx-arm64/UnixRawCaptureHelper.mjs +80 -80
- package/remote-fast/osx-arm64/livedesk-raw-capture-resampler.mjs +73 -73
- package/remote-fast/osx-arm64/mindexec-remote-fast.dll +0 -0
- package/remote-fast/osx-x64/MacScreenCaptureKitH264.swift +339 -339
- package/remote-fast/osx-x64/MacSystemAudioCapture.swift +214 -214
- package/remote-fast/osx-x64/UnixRawCaptureHelper.mjs +80 -80
- package/remote-fast/osx-x64/livedesk-raw-capture-resampler.mjs +73 -73
- package/remote-fast/osx-x64/mindexec-remote-fast.dll +0 -0
- package/remote-fast/win-x64/mindexec-remote-fast.dll +0 -0
- package/remote-hub.js +3698 -3698
- package/scripts/auth-session-smoke.mjs +262 -262
- package/scripts/board-render-ownership-smoke.mjs +85 -85
- package/scripts/codex-model-catalog-smoke.mjs +150 -150
- package/scripts/codex-runtime-home-smoke.mjs +58 -58
- package/scripts/codex-sdk-image-live-smoke.mjs +43 -43
- package/scripts/codex-sdk-image-runtime-smoke.mjs +122 -122
- package/scripts/codex-sdk-runtime-smoke.mjs +89 -89
- package/scripts/desktop-update-publisher-smoke.mjs +89 -89
- package/scripts/desktop-update-worker-smoke.mjs +72 -72
- package/scripts/desktop-workspace-state-smoke.mjs +81 -81
- package/scripts/drive-installed-update-smoke.mjs +67 -67
- package/scripts/npm-fresh-install-smoke.mjs +216 -216
- package/scripts/publish-mindexec-desktop-updates.mjs +224 -224
- package/scripts/remote-agent-managed-smoke.mjs +318 -318
- package/scripts/remote-agent-package-smoke.mjs +64 -64
- package/scripts/remote-agent-ws-smoke.mjs +269 -269
- package/scripts/remote-fast-live-rate-smoke.mjs +343 -343
- package/scripts/remote-fast-mdm-browser-smoke.mjs +453 -453
- package/scripts/remote-fleet-render-smoke.mjs +1350 -1350
- package/scripts/remote-frame-ws-smoke.mjs +234 -234
- package/scripts/remote-http-smoke.mjs +592 -592
- package/scripts/remote-hub-identity-smoke.mjs +146 -146
- package/scripts/remote-hub-scale-smoke.mjs +124 -124
- package/scripts/remote-hub-smoke.mjs +699 -699
- package/scripts/remote-input-ws-smoke.mjs +263 -263
- package/scripts/remote-registry-follower-smoke.mjs +815 -815
- package/scripts/setup-tree-sitter-grammars.mjs +59 -59
- package/server.js +7690 -7690
- package/start-bridge.bat +32 -32
- package/start-bridge.sh +81 -81
- package/wwwroot/_content/MindExecution.Plugins.Admin/css/admin-dashboard.css +546 -546
- package/wwwroot/_content/MindExecution.Plugins.YouTube/css/youtube-search.css +251 -251
- package/wwwroot/_content/MindExecution.Shared/css/app.css +1 -1
- package/wwwroot/_content/MindExecution.Shared/css/mind-map-overrides.css +3789 -3789
- package/wwwroot/_content/MindExecution.Shared/js/agent-visualization.js +27 -27
- package/wwwroot/_content/MindExecution.Shared/js/background-themes.js +1449 -1449
- package/wwwroot/_content/MindExecution.Shared/js/code-master.js +8316 -8316
- package/wwwroot/_content/MindExecution.Shared/js/file-system-helper.js +338 -338
- package/wwwroot/_content/MindExecution.Shared/js/marked.min.js +69 -69
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-animated-image-preview.js +270 -270
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-automation-edge-motion.js +170 -170
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-board-render-ownership.js +81 -81
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-core.js +10506 -10506
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-css3d-hotpath.js +902 -902
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-css3d-manager.js +24637 -24637
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-dev-guards.js +325 -325
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-dnd.js +1491 -1491
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-glow-shader.js +70 -70
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-image-policy.js +218 -218
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-interactions.js +7065 -7065
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-lod-plan-worker.js +160 -160
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-lod-renderer.js +9956 -9956
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-lod-video-placeholder.js +107 -107
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-logic-workers.js +1004 -1004
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-menu-manager.js +1351 -1351
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-multi-select.js +865 -865
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-node-search-worker.js +554 -554
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-nodes.js +4744 -4744
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-normal-boundary-optimizer.js +624 -624
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-object-manager.js +241 -241
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-pipeline.js +2290 -2290
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-remote-codecs.js +202 -202
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-render-plan.js +1971 -1971
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-text-lod-system.js +93 -93
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-texture-factory.js +2391 -2391
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-video-playback-controls.js +95 -95
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-visibility-worker.js +895 -895
- package/wwwroot/_content/MindExecution.Shared/js/mindmap-toolbar.js +636 -636
- package/wwwroot/_content/MindExecution.Shared/js/native-drop-handler.js +45 -45
- package/wwwroot/_content/MindExecution.Shared/js/plan-master.js +117 -117
- package/wwwroot/_content/MindExecution.Shared/js/renderers/CSS3DRenderer.js +114 -114
- package/wwwroot/_content/MindExecution.Shared/js/texture-worker-manager.js +10 -10
- package/wwwroot/_content/MindExecution.Shared/js/three.min.js +6 -6
- package/wwwroot/_content/MindExecution.Shared/js/token-manager.js +7 -7
- package/wwwroot/_content/MindExecution.Shared/js/troika-bundle.js +6 -6
- package/wwwroot/_content/MindExecution.Shared/js/troika-bundle.js.map +2 -2
- package/wwwroot/_content/MindExecution.Shared/lib/font-awesome/css/all.min.css +8 -8
- package/wwwroot/_content/MindExecution.Shared/models/vocab.txt +30522 -30522
- package/wwwroot/_framework/{Google.Protobuf.9h59ukbel7.dll → Google.Protobuf.33g2gb5is0.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Core.ty1xewqp36.dll → MindExecution.Core.z100xo4mg0.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Kernel.h7rqjoo69o.dll → MindExecution.Kernel.i9cldrx8yc.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.Admin.wrfrcvhjde.dll → MindExecution.Plugins.Admin.48lzqddb0a.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.Business.csbcyjurdh.dll → MindExecution.Plugins.Business.7m8u1q142p.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.Concept.buomiysqjn.dll → MindExecution.Plugins.Concept.cl5wmr53od.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.Directory.7k00i2v769.dll → MindExecution.Plugins.Directory.slipi39041.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.PlanMaster.85i5ocraro.dll → MindExecution.Plugins.PlanMaster.qfge05l671.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.YouTube.nbolrfakte.dll → MindExecution.Plugins.YouTube.tn7v29ut77.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Shared.nqh4porv7x.dll → MindExecution.Shared.rjqlf0umjn.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Web.211q3bf4mr.dll → MindExecution.Web.rwawrxxk38.dll} +0 -0
- package/wwwroot/_framework/blazor.boot.json +23 -23
- package/wwwroot/_headers +40 -40
- package/wwwroot/_redirects +1 -1
- package/wwwroot/index.html +772 -772
- package/wwwroot/js/marketing-tool.js +180 -180
- package/wwwroot/manifest.webmanifest +23 -23
- package/wwwroot/service-worker-assets.js +78 -106
- package/wwwroot/service-worker.js +33 -33
- package/tree-sitter-grammars/README.md +0 -18
- package/tree-sitter-grammars/tree-sitter-c_sharp.wasm +0 -0
- package/tree-sitter-grammars/tree-sitter-go.wasm +0 -0
- package/tree-sitter-grammars/tree-sitter-java.wasm +0 -0
- package/tree-sitter-grammars/tree-sitter-javascript.wasm +0 -0
- package/tree-sitter-grammars/tree-sitter-python.wasm +0 -0
- package/tree-sitter-grammars/tree-sitter-rust.wasm +0 -0
- package/tree-sitter-grammars/tree-sitter-tsx.wasm +0 -0
- package/tree-sitter-grammars/tree-sitter-typescript.wasm +0 -0
- package/wwwroot/_content/MindExecution.Plugins.Directory/background.png +0 -0
- package/wwwroot/_content/MindExecution.Shared/fonts/NotoSansKR-Bold.ttf +0 -0
- package/wwwroot/_content/MindExecution.Shared/fonts/NotoSansKR-Regular.ttf +0 -0
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-core.js.backup +0 -1059
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-dnd.js.bak +0 -434
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-object-manager.js.backup +0 -372
- 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
|
+
}
|