@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 CHANGED
@@ -5,9 +5,12 @@
5
5
  ## npm quick start
6
6
 
7
7
  ```bash
8
- npx @mindexec/cli
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 @mindexec/cli --workspace /path/to/workspace
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 @mindexec/cli
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 @mindexec/cli
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.447",
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
- "scripts/",
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
- "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 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",
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
- "tree-sitter-wasms": "^0.1.13"
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
+ }