@mindexec/cli 0.2.447 → 0.2.449

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. package/README.md +34 -4
  2. package/codex-runtime.js +65 -7
  3. package/electron/main.cjs +347 -0
  4. package/electron/source-smoke.mjs +39 -0
  5. package/electron/windows-package-smoke.mjs +147 -0
  6. package/package.json +76 -13
  7. package/scripts/codex-sdk-runtime-smoke.mjs +89 -0
  8. package/wwwroot/_content/MindExecution.Shared/css/app.css +1 -1
  9. package/wwwroot/_content/MindExecution.Shared/js/mind-map-lod-renderer.js +12 -98
  10. package/wwwroot/_content/MindExecution.Shared/js/mind-map-lod-video-placeholder.js +107 -0
  11. package/wwwroot/_framework/MindExecution.Core.ocmk8xd0dx.dll +0 -0
  12. package/wwwroot/_framework/{MindExecution.Kernel.zupeldfptg.dll → MindExecution.Kernel.fxnztqgsp3.dll} +0 -0
  13. package/wwwroot/_framework/{MindExecution.Plugins.Admin.hq0vyqnqtn.dll → MindExecution.Plugins.Admin.vtp5nvymq9.dll} +0 -0
  14. package/wwwroot/_framework/{MindExecution.Plugins.Business.pucw3o7rno.dll → MindExecution.Plugins.Business.o0bonn1p2f.dll} +0 -0
  15. package/wwwroot/_framework/{MindExecution.Plugins.Concept.557tzzkrrx.dll → MindExecution.Plugins.Concept.1omsw5caki.dll} +0 -0
  16. package/wwwroot/_framework/{MindExecution.Plugins.Directory.zh1ddg1fn6.dll → MindExecution.Plugins.Directory.imwo19wy9c.dll} +0 -0
  17. package/wwwroot/_framework/{MindExecution.Plugins.PlanMaster.0z9pzynpu9.dll → MindExecution.Plugins.PlanMaster.hakp8m5rxg.dll} +0 -0
  18. package/wwwroot/_framework/{MindExecution.Plugins.YouTube.y8g77r54fa.dll → MindExecution.Plugins.YouTube.2imuchw4ww.dll} +0 -0
  19. package/wwwroot/_framework/{MindExecution.Shared.1ymsdbwid2.dll → MindExecution.Shared.9podjbu7w7.dll} +0 -0
  20. package/wwwroot/_framework/{MindExecution.Web.bb1txupuv0.dll → MindExecution.Web.rck7kmuixn.dll} +0 -0
  21. package/wwwroot/_framework/blazor.boot.json +21 -21
  22. package/wwwroot/index.html +2 -1
  23. package/wwwroot/service-worker-assets.js +29 -25
  24. package/wwwroot/service-worker.js +1 -1
  25. package/wwwroot/_framework/MindExecution.Core.mswl4wlkm9.dll +0 -0
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:
package/codex-runtime.js CHANGED
@@ -247,12 +247,28 @@ function makeEmptyResult(overrides = {}) {
247
247
  changedFiles: [],
248
248
  commandLogs: [],
249
249
  approvalEvents: [],
250
+ toolEvents: [],
250
251
  rawEventCount: 0,
251
252
  ...overrides
252
253
  };
253
254
  }
254
255
 
255
- function collectItem(result, item) {
256
+ function upsertToolEvent(result, event) {
257
+ const id = String(event?.id || '').trim();
258
+ const type = String(event?.type || '').trim();
259
+ const name = String(event?.name || '').trim();
260
+ const key = id || `${type}:${name}`;
261
+ const index = result.toolEvents.findIndex(existing =>
262
+ (String(existing?.id || '').trim() || `${existing?.type || ''}:${existing?.name || ''}`) === key);
263
+ if (index >= 0) {
264
+ result.toolEvents[index] = event;
265
+ return;
266
+ }
267
+
268
+ result.toolEvents.push(event);
269
+ }
270
+
271
+ function collectItem(result, item, eventType = '') {
256
272
  if (!item || typeof item !== 'object') {
257
273
  return;
258
274
  }
@@ -295,6 +311,51 @@ function collectItem(result, item) {
295
311
  exitCode: Number.isFinite(Number(item.exit_code)) ? Number(item.exit_code) : null,
296
312
  output: truncateLog(item.aggregated_output)
297
313
  });
314
+ upsertToolEvent(result, {
315
+ id: String(item.id || ''),
316
+ type: 'command_execution',
317
+ name: String(item.command || '').trim() || 'Shell',
318
+ status: String(item.status || eventType || ''),
319
+ summary: truncateLog(item.aggregated_output, 1200)
320
+ });
321
+ return;
322
+ }
323
+
324
+ if (item.type === 'web_search') {
325
+ upsertToolEvent(result, {
326
+ id: String(item.id || ''),
327
+ type: 'web_search',
328
+ name: 'WebSearch',
329
+ status: eventType === 'item.completed' ? 'completed' : 'running',
330
+ summary: truncateLog(item.query, 1200)
331
+ });
332
+ return;
333
+ }
334
+
335
+ if (item.type === 'mcp_tool_call') {
336
+ const server = String(item.server || '').trim();
337
+ const tool = String(item.tool || '').trim();
338
+ upsertToolEvent(result, {
339
+ id: String(item.id || ''),
340
+ type: 'mcp_tool_call',
341
+ name: [server, tool].filter(Boolean).join('.') || 'MCP',
342
+ status: String(item.status || eventType || ''),
343
+ summary: item.error?.message
344
+ ? truncateLog(item.error.message, 1200)
345
+ : truncateLog(JSON.stringify(item.arguments ?? {}), 1200)
346
+ });
347
+ return;
348
+ }
349
+
350
+ if (item.type === 'todo_list') {
351
+ const items = Array.isArray(item.items) ? item.items : [];
352
+ upsertToolEvent(result, {
353
+ id: String(item.id || ''),
354
+ type: 'todo_list',
355
+ name: 'Plan',
356
+ status: eventType === 'item.completed' ? 'completed' : 'running',
357
+ summary: truncateLog(items.map(entry => `${entry?.completed ? '[x]' : '[ ]'} ${entry?.text || ''}`).join('\n'), 1200)
358
+ });
298
359
  return;
299
360
  }
300
361
 
@@ -422,6 +483,8 @@ function serializeThreadOptionsForStatus(options) {
422
483
  sandboxMode: API_SANDBOX_BY_SDK_VALUE[options?.sandboxMode] || options?.sandboxMode || null,
423
484
  reasoningEffort: options?.modelReasoningEffort || null,
424
485
  approvalPolicy: options?.approvalPolicy || null,
486
+ networkAccessEnabled: options?.networkAccessEnabled ?? null,
487
+ webSearchMode: options?.webSearchMode || null,
425
488
  workingDirectory: options?.workingDirectory || null
426
489
  };
427
490
  }
@@ -548,11 +611,6 @@ export function createCodexRuntime(options) {
548
611
  return PROVIDER_KIND.legacyExec;
549
612
  }
550
613
 
551
- const fallbackLegacy = legacy || await checkLegacyAvailability();
552
- if (requested === PROVIDER_KIND.typeScriptSdk && fallbackLegacy?.available) {
553
- return PROVIDER_KIND.legacyExec;
554
- }
555
-
556
614
  return PROVIDER_KIND.unavailable;
557
615
  }
558
616
 
@@ -711,7 +769,7 @@ export function createCodexRuntime(options) {
711
769
  result.threadId = record.officialThreadId;
712
770
  threads.set(record.officialThreadId, record);
713
771
  } else if (event.type === 'item.completed' || event.type === 'item.updated' || event.type === 'item.started') {
714
- collectItem(result, event.item);
772
+ collectItem(result, event.item, event.type);
715
773
  } else if (event.type === 'turn.failed') {
716
774
  result.error = event.error?.message || 'Codex turn failed.';
717
775
  result.status = 'failed';
@@ -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
+ }));