@mindexec/cli 0.2.447 → 0.2.448
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 +34 -4
- package/electron/main.cjs +347 -0
- package/electron/source-smoke.mjs +39 -0
- package/electron/windows-package-smoke.mjs +147 -0
- package/package.json +73 -13
package/README.md
CHANGED
|
@@ -5,9 +5,12 @@
|
|
|
5
5
|
## npm quick start
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npx @
|
|
8
|
+
npx -y mindexec@latest
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
`mindexec` is the short official launcher alias. Existing
|
|
12
|
+
`npx -y @mindexec/cli@latest` commands remain compatible.
|
|
13
|
+
|
|
11
14
|
The npm CLI now starts both pieces of the local product:
|
|
12
15
|
|
|
13
16
|
- MindCanvas app: `http://localhost:5147/mindcanvas`
|
|
@@ -23,7 +26,7 @@ run the command. Run it from the folder that contains your `.mindexec` data, or
|
|
|
23
26
|
pass the workspace explicitly:
|
|
24
27
|
|
|
25
28
|
```bash
|
|
26
|
-
npx @
|
|
29
|
+
npx -y mindexec@latest --workspace /path/to/workspace
|
|
27
30
|
```
|
|
28
31
|
|
|
29
32
|
For local frontend development, keep LocalBridge running and refresh the
|
|
@@ -45,13 +48,40 @@ precompressed static files from the npm bundle by default. Use
|
|
|
45
48
|
`-KeepPackagedGalleryImages` or `-KeepPackagedPrecompressedAssets` only when
|
|
46
49
|
testing a full untrimmed local package.
|
|
47
50
|
|
|
51
|
+
## Standalone Electron desktop app
|
|
52
|
+
|
|
53
|
+
The Electron build contains both the published MindCanvas frontend and
|
|
54
|
+
LocalBridge runtime. It does not discover, replace, or reuse a separately
|
|
55
|
+
running `npx` Bridge. Each desktop launch allocates dedicated available
|
|
56
|
+
LocalBridge and RemoteHub ports, starts its bundled runtime, and releases those
|
|
57
|
+
ports when the app exits.
|
|
58
|
+
|
|
59
|
+
Build Windows installer and portable artifacts:
|
|
60
|
+
|
|
61
|
+
```powershell
|
|
62
|
+
cd LocalBridge
|
|
63
|
+
npm run desktop:build:win
|
|
64
|
+
npm run test:desktop:win
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Release artifacts are written to `artifacts/electron`:
|
|
68
|
+
|
|
69
|
+
- `MindExec-Setup-<version>-x64.exe`
|
|
70
|
+
- `MindExec-Portable-<version>-x64.exe`
|
|
71
|
+
- `win-unpacked/MindExec.exe`
|
|
72
|
+
|
|
73
|
+
The default desktop workspace is `Documents/MindExecution`. Set
|
|
74
|
+
`MINDEXEC_DESKTOP_WORKSPACE` to use an explicit workspace. Store signing,
|
|
75
|
+
Partner Center identity, Apple provisioning, and sandbox entitlements are
|
|
76
|
+
release-channel credentials and are intentionally not embedded in source.
|
|
77
|
+
|
|
48
78
|
## Remote Direct quick start
|
|
49
79
|
|
|
50
80
|
LocalBridge also starts a separate RemoteHub listener for direct remote-agent
|
|
51
81
|
experiments. By default it binds to loopback only:
|
|
52
82
|
|
|
53
83
|
```bash
|
|
54
|
-
npx @
|
|
84
|
+
npx -y mindexec@latest
|
|
55
85
|
npx @mindexec/remote connect --manager 127.0.0.1:5198 --pair <pair-token>
|
|
56
86
|
```
|
|
57
87
|
|
|
@@ -67,7 +97,7 @@ LAN/direct mode must be enabled explicitly by changing the RemoteHub bind host:
|
|
|
67
97
|
$env:REMOTE_HUB_HOST="0.0.0.0"
|
|
68
98
|
$env:REMOTE_HUB_PORT="5198"
|
|
69
99
|
$env:REMOTE_HUB_PAIR_TOKEN="<strong-token>"
|
|
70
|
-
npx @
|
|
100
|
+
npx -y mindexec@latest
|
|
71
101
|
```
|
|
72
102
|
|
|
73
103
|
Device inventory is intentionally not paginated:
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { app, BrowserWindow, dialog } = 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
|
+
|
|
10
|
+
const DEFAULT_BRIDGE_PORT = 5147;
|
|
11
|
+
const DEFAULT_REMOTE_HUB_PORT = 5198;
|
|
12
|
+
const STARTUP_TIMEOUT_MS = 45_000;
|
|
13
|
+
const HEALTH_POLL_MS = 250;
|
|
14
|
+
|
|
15
|
+
let mainWindow = null;
|
|
16
|
+
let bridgeChild = null;
|
|
17
|
+
let isQuitting = false;
|
|
18
|
+
let requestedExitCode = 0;
|
|
19
|
+
let activeBridgePort = DEFAULT_BRIDGE_PORT;
|
|
20
|
+
let activeRemoteHubPort = DEFAULT_REMOTE_HUB_PORT;
|
|
21
|
+
let activeWorkspace = '';
|
|
22
|
+
|
|
23
|
+
function parsePort(value) {
|
|
24
|
+
const parsed = Number.parseInt(String(value ?? ''), 10);
|
|
25
|
+
return Number.isInteger(parsed) && parsed > 0 && parsed <= 65_535 ? parsed : 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function requestJson(url, timeoutMs = 2_000) {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
const request = http.get(url, { timeout: timeoutMs }, (response) => {
|
|
31
|
+
let body = '';
|
|
32
|
+
response.setEncoding('utf8');
|
|
33
|
+
response.on('data', (chunk) => {
|
|
34
|
+
body += chunk;
|
|
35
|
+
});
|
|
36
|
+
response.on('end', () => {
|
|
37
|
+
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
38
|
+
reject(new Error(`HTTP ${response.statusCode} from ${url}`));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
resolve(JSON.parse(body));
|
|
44
|
+
} catch (error) {
|
|
45
|
+
reject(new Error(`Invalid JSON from ${url}: ${error.message}`));
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
request.on('timeout', () => {
|
|
51
|
+
request.destroy(new Error(`Timed out requesting ${url}`));
|
|
52
|
+
});
|
|
53
|
+
request.on('error', reject);
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isPortAvailable(port) {
|
|
58
|
+
return new Promise((resolve) => {
|
|
59
|
+
const server = net.createServer();
|
|
60
|
+
server.unref();
|
|
61
|
+
server.once('error', () => resolve(false));
|
|
62
|
+
server.listen({ host: '127.0.0.1', port, exclusive: true }, () => {
|
|
63
|
+
server.close(() => resolve(true));
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function resolveDedicatedPort(explicitValue, defaultPort, label, excludedPort = 0) {
|
|
69
|
+
const explicitPort = parsePort(explicitValue);
|
|
70
|
+
if (explicitPort) {
|
|
71
|
+
if (explicitPort === excludedPort) {
|
|
72
|
+
throw new Error(`${label} port ${explicitPort} conflicts with the bundled LocalBridge port.`);
|
|
73
|
+
}
|
|
74
|
+
if (!await isPortAvailable(explicitPort)) {
|
|
75
|
+
throw new Error(`${label} port ${explicitPort} is already occupied.`);
|
|
76
|
+
}
|
|
77
|
+
return explicitPort;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
for (let offset = 0; offset < 100; offset += 1) {
|
|
81
|
+
const candidate = defaultPort + offset;
|
|
82
|
+
if (candidate !== excludedPort && candidate <= 65_535 && await isPortAvailable(candidate)) {
|
|
83
|
+
return candidate;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
throw new Error(`No free ${label} port was found from ${defaultPort}.`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function appendDesktopLog(message) {
|
|
91
|
+
const line = `[${new Date().toISOString()}] ${message}\n`;
|
|
92
|
+
try {
|
|
93
|
+
const logDirectory = app.getPath('logs');
|
|
94
|
+
fs.mkdirSync(logDirectory, { recursive: true });
|
|
95
|
+
fs.appendFileSync(path.join(logDirectory, 'desktop.log'), line, 'utf8');
|
|
96
|
+
} catch {
|
|
97
|
+
process.stderr.write(line);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function spawnBundledBridge(packageRoot) {
|
|
102
|
+
const serverPath = path.join(packageRoot, 'server.js');
|
|
103
|
+
const frontendPath = path.join(packageRoot, 'wwwroot', 'index.html');
|
|
104
|
+
if (!fs.existsSync(serverPath)) {
|
|
105
|
+
throw new Error(`Bundled LocalBridge entry is missing: ${serverPath}`);
|
|
106
|
+
}
|
|
107
|
+
if (!fs.existsSync(frontendPath)) {
|
|
108
|
+
throw new Error(`Bundled MindCanvas frontend is missing: ${frontendPath}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const child = spawn(process.execPath, [serverPath], {
|
|
112
|
+
cwd: packageRoot,
|
|
113
|
+
env: {
|
|
114
|
+
...process.env,
|
|
115
|
+
ELECTRON_RUN_AS_NODE: '1',
|
|
116
|
+
MINDEXEC_DESKTOP: '1',
|
|
117
|
+
MINDEXEC_NO_OPEN: '1',
|
|
118
|
+
WORKSPACE_PATH: activeWorkspace,
|
|
119
|
+
BRIDGE_PORT: String(activeBridgePort),
|
|
120
|
+
REMOTE_HUB_PORT: String(activeRemoteHubPort)
|
|
121
|
+
},
|
|
122
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
123
|
+
windowsHide: true
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
child.stdout?.on('data', (chunk) => appendDesktopLog(`[bridge] ${String(chunk).trimEnd()}`));
|
|
127
|
+
child.stderr?.on('data', (chunk) => appendDesktopLog(`[bridge:error] ${String(chunk).trimEnd()}`));
|
|
128
|
+
child.on('error', (error) => appendDesktopLog(`[bridge:spawn-error] ${error.stack || error.message}`));
|
|
129
|
+
child.on('exit', (code, signal) => {
|
|
130
|
+
appendDesktopLog(`[bridge:exit] code=${String(code)} signal=${String(signal)}`);
|
|
131
|
+
if (!isQuitting && process.env.MINDEXEC_DESKTOP_SMOKE !== '1') {
|
|
132
|
+
dialog.showErrorBox(
|
|
133
|
+
'MindExec LocalBridge stopped',
|
|
134
|
+
`The bundled local runtime exited unexpectedly (code ${String(code)}, signal ${String(signal)}).`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
bridgeChild = child;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function waitForBundledBridge() {
|
|
143
|
+
const deadline = Date.now() + STARTUP_TIMEOUT_MS;
|
|
144
|
+
let lastError = null;
|
|
145
|
+
|
|
146
|
+
while (Date.now() < deadline) {
|
|
147
|
+
if (bridgeChild && bridgeChild.exitCode !== null) {
|
|
148
|
+
throw new Error(`Bundled LocalBridge exited during startup with code ${bridgeChild.exitCode}.`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const status = await requestJson(`http://127.0.0.1:${activeBridgePort}/api/status?refresh=cache`);
|
|
153
|
+
if (status?.status === 'ok' && status?.workspace) {
|
|
154
|
+
return status;
|
|
155
|
+
}
|
|
156
|
+
} catch (error) {
|
|
157
|
+
lastError = error;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
await new Promise((resolve) => setTimeout(resolve, HEALTH_POLL_MS));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
throw new Error(
|
|
164
|
+
`Bundled LocalBridge did not become ready on port ${activeBridgePort}: ${lastError?.message || 'timeout'}`
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function prepareBundledRuntime() {
|
|
169
|
+
const packageRoot = path.resolve(__dirname, '..');
|
|
170
|
+
activeBridgePort = await resolveDedicatedPort(process.env.BRIDGE_PORT, DEFAULT_BRIDGE_PORT, 'LocalBridge');
|
|
171
|
+
activeRemoteHubPort = await resolveDedicatedPort(
|
|
172
|
+
process.env.REMOTE_HUB_PORT,
|
|
173
|
+
DEFAULT_REMOTE_HUB_PORT,
|
|
174
|
+
'RemoteHub',
|
|
175
|
+
activeBridgePort
|
|
176
|
+
);
|
|
177
|
+
activeWorkspace = path.resolve(
|
|
178
|
+
process.env.MINDEXEC_DESKTOP_WORKSPACE
|
|
179
|
+
|| process.env.WORKSPACE_PATH
|
|
180
|
+
|| path.join(app.getPath('documents'), 'MindExecution')
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
fs.mkdirSync(activeWorkspace, { recursive: true });
|
|
184
|
+
appendDesktopLog(
|
|
185
|
+
`Starting bundled runtime: bridge=${activeBridgePort}, remoteHub=${activeRemoteHubPort}, workspace=${activeWorkspace}`
|
|
186
|
+
);
|
|
187
|
+
spawnBundledBridge(packageRoot);
|
|
188
|
+
return waitForBundledBridge();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function createMainWindow() {
|
|
192
|
+
const hiddenSmokeMode = process.env.MINDEXEC_DESKTOP_SMOKE === '1';
|
|
193
|
+
const window = new BrowserWindow({
|
|
194
|
+
width: 1440,
|
|
195
|
+
height: 960,
|
|
196
|
+
minWidth: 960,
|
|
197
|
+
minHeight: 640,
|
|
198
|
+
show: !hiddenSmokeMode,
|
|
199
|
+
autoHideMenuBar: true,
|
|
200
|
+
backgroundColor: '#0b1020',
|
|
201
|
+
title: 'MindExec',
|
|
202
|
+
webPreferences: {
|
|
203
|
+
contextIsolation: true,
|
|
204
|
+
nodeIntegration: false,
|
|
205
|
+
sandbox: true,
|
|
206
|
+
webSecurity: true
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
window.webContents.setWindowOpenHandler(() => ({
|
|
211
|
+
action: 'allow',
|
|
212
|
+
overrideBrowserWindowOptions: {
|
|
213
|
+
autoHideMenuBar: true,
|
|
214
|
+
webPreferences: {
|
|
215
|
+
contextIsolation: true,
|
|
216
|
+
nodeIntegration: false,
|
|
217
|
+
sandbox: true,
|
|
218
|
+
webSecurity: true
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}));
|
|
222
|
+
|
|
223
|
+
window.on('closed', () => {
|
|
224
|
+
if (mainWindow === window) {
|
|
225
|
+
mainWindow = null;
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
mainWindow = window;
|
|
230
|
+
return window;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function writeSmokeResult(payload) {
|
|
234
|
+
const resultPath = process.env.MINDEXEC_DESKTOP_SMOKE_RESULT;
|
|
235
|
+
if (!resultPath) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
fs.mkdirSync(path.dirname(resultPath), { recursive: true });
|
|
240
|
+
fs.writeFileSync(resultPath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function stopBundledBridge() {
|
|
244
|
+
const child = bridgeChild;
|
|
245
|
+
bridgeChild = null;
|
|
246
|
+
if (!child || child.exitCode !== null) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
await new Promise((resolve) => {
|
|
251
|
+
let settled = false;
|
|
252
|
+
const finish = () => {
|
|
253
|
+
if (settled) {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
settled = true;
|
|
257
|
+
resolve();
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
child.once('exit', finish);
|
|
261
|
+
try {
|
|
262
|
+
child.kill('SIGTERM');
|
|
263
|
+
} catch {
|
|
264
|
+
finish();
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
setTimeout(() => {
|
|
269
|
+
if (child.exitCode === null) {
|
|
270
|
+
try {
|
|
271
|
+
child.kill('SIGKILL');
|
|
272
|
+
} catch {
|
|
273
|
+
// The child may have exited between the check and the kill.
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
finish();
|
|
277
|
+
}, 5_000).unref();
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function launchDesktop() {
|
|
282
|
+
const status = await prepareBundledRuntime();
|
|
283
|
+
const window = createMainWindow();
|
|
284
|
+
const appUrl = `http://127.0.0.1:${activeBridgePort}/mindcanvas`;
|
|
285
|
+
await window.loadURL(appUrl);
|
|
286
|
+
|
|
287
|
+
if (process.env.MINDEXEC_DESKTOP_SMOKE === '1') {
|
|
288
|
+
writeSmokeResult({
|
|
289
|
+
ok: true,
|
|
290
|
+
appVersion: app.getVersion(),
|
|
291
|
+
bridgeVersion: status.version || null,
|
|
292
|
+
runtimeMode: 'bundled',
|
|
293
|
+
bridgePort: activeBridgePort,
|
|
294
|
+
remoteHubPort: activeRemoteHubPort,
|
|
295
|
+
workspace: activeWorkspace,
|
|
296
|
+
url: appUrl
|
|
297
|
+
});
|
|
298
|
+
setTimeout(() => app.quit(), 250).unref();
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const hasSingleInstanceLock = app.requestSingleInstanceLock();
|
|
303
|
+
if (!hasSingleInstanceLock) {
|
|
304
|
+
app.quit();
|
|
305
|
+
} else {
|
|
306
|
+
app.on('second-instance', () => {
|
|
307
|
+
if (mainWindow) {
|
|
308
|
+
if (mainWindow.isMinimized()) {
|
|
309
|
+
mainWindow.restore();
|
|
310
|
+
}
|
|
311
|
+
mainWindow.focus();
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
app.on('window-all-closed', () => {
|
|
316
|
+
app.quit();
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
app.on('before-quit', (event) => {
|
|
320
|
+
if (isQuitting || !bridgeChild || bridgeChild.exitCode !== null) {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
event.preventDefault();
|
|
325
|
+
isQuitting = true;
|
|
326
|
+
stopBundledBridge().finally(() => app.exit(requestedExitCode));
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
app.whenReady()
|
|
330
|
+
.then(launchDesktop)
|
|
331
|
+
.catch((error) => {
|
|
332
|
+
const message = error?.stack || String(error);
|
|
333
|
+
appendDesktopLog(`[desktop:start-error] ${message}`);
|
|
334
|
+
writeSmokeResult({
|
|
335
|
+
ok: false,
|
|
336
|
+
appVersion: app.getVersion(),
|
|
337
|
+
runtimeMode: 'bundled',
|
|
338
|
+
error: error?.message || String(error)
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
requestedExitCode = 1;
|
|
342
|
+
if (process.env.MINDEXEC_DESKTOP_SMOKE !== '1') {
|
|
343
|
+
dialog.showErrorBox('MindExec failed to start', error?.message || String(error));
|
|
344
|
+
}
|
|
345
|
+
app.quit();
|
|
346
|
+
});
|
|
347
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
const electronDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const packageRoot = path.resolve(electronDirectory, '..');
|
|
8
|
+
const packageJson = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
|
|
9
|
+
const mainSource = fs.readFileSync(path.join(electronDirectory, 'main.cjs'), 'utf8');
|
|
10
|
+
|
|
11
|
+
assert.equal(packageJson.build?.appId, 'com.mindexec.desktop', 'Stable desktop appId is required.');
|
|
12
|
+
assert.equal(packageJson.build?.productName, 'MindExec', 'Desktop product name changed unexpectedly.');
|
|
13
|
+
assert.equal(packageJson.build?.extraMetadata?.main, 'electron/main.cjs', 'Packaged Electron entry point is missing.');
|
|
14
|
+
assert.equal(packageJson.build?.asar, false, 'Bundled runtime executables require an unpacked app directory.');
|
|
15
|
+
assert.ok(packageJson.files?.includes('electron/'), 'The npm package must include the Electron source contract.');
|
|
16
|
+
assert.match(packageJson.scripts?.['desktop:build:win'] || '', /electron-builder/, 'Windows build script is missing.');
|
|
17
|
+
assert.match(packageJson.scripts?.['test:desktop:win'] || '', /windows-package-smoke/, 'Packaged smoke test is missing.');
|
|
18
|
+
|
|
19
|
+
const targets = packageJson.build?.win?.target || [];
|
|
20
|
+
assert.ok(targets.some((target) => target.target === 'nsis'), 'NSIS target is required.');
|
|
21
|
+
assert.ok(targets.some((target) => target.target === 'portable'), 'Portable target is required.');
|
|
22
|
+
|
|
23
|
+
assert.match(mainSource, /contextIsolation:\s*true/, 'Electron renderer isolation must remain enabled.');
|
|
24
|
+
assert.match(mainSource, /nodeIntegration:\s*false/, 'Node integration must remain disabled.');
|
|
25
|
+
assert.match(mainSource, /sandbox:\s*true/, 'Electron renderer sandbox must remain enabled.');
|
|
26
|
+
assert.match(mainSource, /runtimeMode:\s*'bundled'/, 'Packaged runtime must report bundled mode.');
|
|
27
|
+
assert.match(mainSource, /spawnBundledBridge/, 'Electron must start its bundled LocalBridge.');
|
|
28
|
+
assert.match(mainSource, /wwwroot.*index\.html/s, 'Electron must verify its bundled frontend.');
|
|
29
|
+
assert.match(mainSource, /resolveDedicatedPort/, 'Electron must allocate dedicated local ports.');
|
|
30
|
+
assert.doesNotMatch(mainSource, /adopted|probeBridge/, 'Electron must not adopt an external LocalBridge.');
|
|
31
|
+
assert.match(mainSource, /MINDEXEC_DESKTOP_SMOKE_RESULT/, 'Packaged runtime smoke contract is missing.');
|
|
32
|
+
|
|
33
|
+
console.log(JSON.stringify({
|
|
34
|
+
ok: true,
|
|
35
|
+
appId: packageJson.build.appId,
|
|
36
|
+
version: packageJson.version,
|
|
37
|
+
runtimeMode: 'bundled',
|
|
38
|
+
targets: targets.map((target) => `${target.target}:${target.arch.join(',')}`)
|
|
39
|
+
}));
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import net from 'node:net';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
const electronDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const packageRoot = path.resolve(electronDirectory, '..');
|
|
11
|
+
const repositoryRoot = path.resolve(packageRoot, '..');
|
|
12
|
+
const packageJson = JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
|
|
13
|
+
const outputDirectory = process.env.MINDEXEC_ELECTRON_OUTPUT
|
|
14
|
+
? path.resolve(process.env.MINDEXEC_ELECTRON_OUTPUT)
|
|
15
|
+
: path.join(repositoryRoot, 'artifacts', 'electron');
|
|
16
|
+
const executablePath = path.join(outputDirectory, 'win-unpacked', 'MindExec.exe');
|
|
17
|
+
|
|
18
|
+
assert.equal(process.platform, 'win32', 'The packaged Electron smoke test requires Windows.');
|
|
19
|
+
assert.ok(fs.existsSync(executablePath), `Packaged Electron executable is missing: ${executablePath}`);
|
|
20
|
+
|
|
21
|
+
function reservePort() {
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
const server = net.createServer();
|
|
24
|
+
server.once('error', reject);
|
|
25
|
+
server.listen({ host: '127.0.0.1', port: 0, exclusive: true }, () => {
|
|
26
|
+
const address = server.address();
|
|
27
|
+
server.close((error) => {
|
|
28
|
+
if (error) {
|
|
29
|
+
reject(error);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
resolve(address.port);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function waitForFile(filePath, child, timeoutMs) {
|
|
39
|
+
const deadline = Date.now() + timeoutMs;
|
|
40
|
+
while (Date.now() < deadline) {
|
|
41
|
+
if (fs.existsSync(filePath)) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (child.exitCode !== null) {
|
|
45
|
+
throw new Error(`Packaged Electron exited before reporting smoke status (code ${child.exitCode}).`);
|
|
46
|
+
}
|
|
47
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
48
|
+
}
|
|
49
|
+
throw new Error(`Timed out waiting for packaged smoke result: ${filePath}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function waitForExit(child, timeoutMs) {
|
|
53
|
+
return new Promise((resolve, reject) => {
|
|
54
|
+
if (child.exitCode !== null) {
|
|
55
|
+
resolve(child.exitCode);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const timeout = setTimeout(() => {
|
|
60
|
+
child.kill('SIGKILL');
|
|
61
|
+
reject(new Error('Packaged Electron did not exit after its smoke run.'));
|
|
62
|
+
}, timeoutMs);
|
|
63
|
+
|
|
64
|
+
child.once('error', (error) => {
|
|
65
|
+
clearTimeout(timeout);
|
|
66
|
+
reject(error);
|
|
67
|
+
});
|
|
68
|
+
child.once('exit', (code) => {
|
|
69
|
+
clearTimeout(timeout);
|
|
70
|
+
resolve(code);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function canBind(port) {
|
|
76
|
+
return new Promise((resolve) => {
|
|
77
|
+
const server = net.createServer();
|
|
78
|
+
server.once('error', () => resolve(false));
|
|
79
|
+
server.listen({ host: '127.0.0.1', port, exclusive: true }, () => {
|
|
80
|
+
server.close(() => resolve(true));
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const bridgePort = await reservePort();
|
|
86
|
+
const remoteHubPort = await reservePort();
|
|
87
|
+
const smokeRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'mindexec-electron-smoke-'));
|
|
88
|
+
const workspace = path.join(smokeRoot, 'workspace');
|
|
89
|
+
const resultPath = path.join(smokeRoot, 'result.json');
|
|
90
|
+
fs.mkdirSync(workspace, { recursive: true });
|
|
91
|
+
|
|
92
|
+
let child;
|
|
93
|
+
try {
|
|
94
|
+
child = spawn(executablePath, [`--user-data-dir=${path.join(smokeRoot, 'electron-profile')}`], {
|
|
95
|
+
cwd: path.dirname(executablePath),
|
|
96
|
+
env: {
|
|
97
|
+
...process.env,
|
|
98
|
+
MINDEXEC_DESKTOP_SMOKE: '1',
|
|
99
|
+
MINDEXEC_DESKTOP_SMOKE_RESULT: resultPath,
|
|
100
|
+
MINDEXEC_DESKTOP_WORKSPACE: workspace,
|
|
101
|
+
MINDEXEC_NO_OPEN: '1',
|
|
102
|
+
BRIDGE_PORT: String(bridgePort),
|
|
103
|
+
REMOTE_HUB_PORT: String(remoteHubPort)
|
|
104
|
+
},
|
|
105
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
106
|
+
windowsHide: true
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
let stdout = '';
|
|
110
|
+
let stderr = '';
|
|
111
|
+
child.stdout?.on('data', (chunk) => {
|
|
112
|
+
stdout += String(chunk);
|
|
113
|
+
});
|
|
114
|
+
child.stderr?.on('data', (chunk) => {
|
|
115
|
+
stderr += String(chunk);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
await waitForFile(resultPath, child, 90_000);
|
|
119
|
+
const result = JSON.parse(fs.readFileSync(resultPath, 'utf8'));
|
|
120
|
+
assert.equal(result.ok, true, `Packaged Electron smoke failed: ${result.error || stderr || stdout}`);
|
|
121
|
+
assert.equal(result.appVersion, packageJson.version, 'Electron app version does not match the npm package version.');
|
|
122
|
+
assert.equal(result.bridgeVersion, packageJson.version, 'Bundled LocalBridge version does not match Electron.');
|
|
123
|
+
assert.equal(result.runtimeMode, 'bundled', 'Electron did not use its bundled runtime.');
|
|
124
|
+
assert.equal(result.bridgePort, bridgePort, 'Packaged shell did not use the requested Bridge port.');
|
|
125
|
+
assert.equal(result.remoteHubPort, remoteHubPort, 'Packaged shell did not use the requested RemoteHub port.');
|
|
126
|
+
|
|
127
|
+
const exitCode = await waitForExit(child, 15_000);
|
|
128
|
+
assert.equal(exitCode, 0, `Packaged Electron smoke exited with code ${exitCode}.`);
|
|
129
|
+
|
|
130
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
131
|
+
assert.equal(await canBind(bridgePort), true, 'Bridge port was not released after Electron exit.');
|
|
132
|
+
assert.equal(await canBind(remoteHubPort), true, 'RemoteHub port was not released after Electron exit.');
|
|
133
|
+
|
|
134
|
+
console.log(JSON.stringify({
|
|
135
|
+
ok: true,
|
|
136
|
+
executablePath,
|
|
137
|
+
version: result.appVersion,
|
|
138
|
+
runtimeMode: result.runtimeMode,
|
|
139
|
+
bridgePortReleased: true,
|
|
140
|
+
remoteHubPortReleased: true
|
|
141
|
+
}));
|
|
142
|
+
} finally {
|
|
143
|
+
if (child && child.exitCode === null) {
|
|
144
|
+
child.kill('SIGKILL');
|
|
145
|
+
}
|
|
146
|
+
fs.rmSync(smokeRoot, { recursive: true, force: true });
|
|
147
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mindexec/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.448",
|
|
4
4
|
"description": "MindExec local runtime and bridge CLI",
|
|
5
5
|
"main": "server.js",
|
|
6
6
|
"type": "module",
|
|
@@ -16,16 +16,21 @@
|
|
|
16
16
|
"codex-runtime.js",
|
|
17
17
|
"codex-model-catalog.js",
|
|
18
18
|
"port-guard.cjs",
|
|
19
|
-
"remote-fast/",
|
|
20
|
-
"wwwroot/",
|
|
21
|
-
"
|
|
19
|
+
"remote-fast/",
|
|
20
|
+
"wwwroot/",
|
|
21
|
+
"electron/",
|
|
22
|
+
"scripts/",
|
|
22
23
|
"tree-sitter-grammars/",
|
|
23
24
|
"README.md"
|
|
24
25
|
],
|
|
25
|
-
"scripts": {
|
|
26
|
-
"start": "node launch-bridge.cjs",
|
|
27
|
-
"dev": "node launch-bridge.cjs --watch",
|
|
28
|
-
"
|
|
26
|
+
"scripts": {
|
|
27
|
+
"start": "node launch-bridge.cjs",
|
|
28
|
+
"dev": "node launch-bridge.cjs --watch",
|
|
29
|
+
"desktop:start": "electron electron/main.cjs",
|
|
30
|
+
"desktop:build:win": "electron-builder --win --x64 --publish never",
|
|
31
|
+
"test:desktop": "node electron/source-smoke.mjs",
|
|
32
|
+
"test:desktop:win": "node electron/windows-package-smoke.mjs",
|
|
33
|
+
"test:syntax": "node --check server.js && node --check remote-hub.js && node --check lzo1x.js && node --check mode4-atlas.js && node --check mode4-atlas-worker.js && node --check codex-runtime.js && node --check codex-model-catalog.js && node --check launch-bridge.cjs && node --check port-guard.cjs && node --check electron/main.cjs && node --check electron/source-smoke.mjs && node --check electron/windows-package-smoke.mjs && node --check scripts/setup-tree-sitter-grammars.mjs && node --check scripts/npm-fresh-install-smoke.mjs && node --check scripts/auth-session-smoke.mjs && node --check scripts/codex-model-catalog-smoke.mjs && node --check scripts/remote-hub-smoke.mjs && node --check scripts/remote-hub-scale-smoke.mjs && node --check scripts/remote-fleet-render-smoke.mjs && node --check scripts/remote-http-smoke.mjs && node --check scripts/remote-frame-ws-smoke.mjs && node --check scripts/remote-input-ws-smoke.mjs && node --check scripts/remote-agent-ws-smoke.mjs && node --check scripts/remote-agent-managed-smoke.mjs && node --check scripts/remote-registry-follower-smoke.mjs && node --check scripts/remote-agent-package-smoke.mjs && node --check scripts/remote-fast-live-rate-smoke.mjs && node --check scripts/remote-fast-mdm-browser-smoke.mjs",
|
|
29
34
|
"test:codex-model-catalog": "node scripts/codex-model-catalog-smoke.mjs",
|
|
30
35
|
"test:codex-model-catalog:live": "node scripts/codex-model-catalog-smoke.mjs --live",
|
|
31
36
|
"test:auth": "node scripts/auth-session-smoke.mjs",
|
|
@@ -53,7 +58,7 @@
|
|
|
53
58
|
"local",
|
|
54
59
|
"ai"
|
|
55
60
|
],
|
|
56
|
-
"author": "",
|
|
61
|
+
"author": "lovecrdm77",
|
|
57
62
|
"license": "MIT",
|
|
58
63
|
"publishConfig": {
|
|
59
64
|
"access": "public"
|
|
@@ -82,7 +87,62 @@
|
|
|
82
87
|
"mindexec": "launch-bridge.cjs",
|
|
83
88
|
"mind-bridge": "launch-bridge.cjs"
|
|
84
89
|
},
|
|
85
|
-
"devDependencies": {
|
|
86
|
-
"
|
|
87
|
-
|
|
88
|
-
|
|
90
|
+
"devDependencies": {
|
|
91
|
+
"electron": "43.2.0",
|
|
92
|
+
"electron-builder": "26.15.3",
|
|
93
|
+
"tree-sitter-wasms": "^0.1.13"
|
|
94
|
+
},
|
|
95
|
+
"build": {
|
|
96
|
+
"appId": "com.mindexec.desktop",
|
|
97
|
+
"productName": "MindExec",
|
|
98
|
+
"copyright": "Copyright © 2026 lovecrdm77",
|
|
99
|
+
"asar": false,
|
|
100
|
+
"extraMetadata": {
|
|
101
|
+
"main": "electron/main.cjs"
|
|
102
|
+
},
|
|
103
|
+
"directories": {
|
|
104
|
+
"output": "../artifacts/electron"
|
|
105
|
+
},
|
|
106
|
+
"files": [
|
|
107
|
+
"*.js",
|
|
108
|
+
"*.cjs",
|
|
109
|
+
"*.json",
|
|
110
|
+
"electron/main.cjs",
|
|
111
|
+
"remote-fast/**/*",
|
|
112
|
+
"scripts/**/*",
|
|
113
|
+
"tree-sitter-grammars/**/*",
|
|
114
|
+
"wwwroot/**/*",
|
|
115
|
+
"node_modules/**/*"
|
|
116
|
+
],
|
|
117
|
+
"win": {
|
|
118
|
+
"icon": "../MindExecution.Web/wwwroot/icon-512.png",
|
|
119
|
+
"executableName": "MindExec",
|
|
120
|
+
"target": [
|
|
121
|
+
{
|
|
122
|
+
"target": "nsis",
|
|
123
|
+
"arch": [
|
|
124
|
+
"x64"
|
|
125
|
+
]
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
"target": "portable",
|
|
129
|
+
"arch": [
|
|
130
|
+
"x64"
|
|
131
|
+
]
|
|
132
|
+
}
|
|
133
|
+
]
|
|
134
|
+
},
|
|
135
|
+
"nsis": {
|
|
136
|
+
"artifactName": "MindExec-Setup-${version}-${arch}.${ext}",
|
|
137
|
+
"oneClick": false,
|
|
138
|
+
"perMachine": false,
|
|
139
|
+
"allowToChangeInstallationDirectory": true,
|
|
140
|
+
"createDesktopShortcut": true,
|
|
141
|
+
"createStartMenuShortcut": true,
|
|
142
|
+
"shortcutName": "MindExec"
|
|
143
|
+
},
|
|
144
|
+
"portable": {
|
|
145
|
+
"artifactName": "MindExec-Portable-${version}-${arch}.${ext}"
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|