@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.
- package/README.md +34 -4
- package/codex-runtime.js +65 -7
- package/electron/main.cjs +347 -0
- package/electron/source-smoke.mjs +39 -0
- package/electron/windows-package-smoke.mjs +147 -0
- package/package.json +76 -13
- package/scripts/codex-sdk-runtime-smoke.mjs +89 -0
- package/wwwroot/_content/MindExecution.Shared/css/app.css +1 -1
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-lod-renderer.js +12 -98
- package/wwwroot/_content/MindExecution.Shared/js/mind-map-lod-video-placeholder.js +107 -0
- package/wwwroot/_framework/MindExecution.Core.ocmk8xd0dx.dll +0 -0
- package/wwwroot/_framework/{MindExecution.Kernel.zupeldfptg.dll → MindExecution.Kernel.fxnztqgsp3.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.Admin.hq0vyqnqtn.dll → MindExecution.Plugins.Admin.vtp5nvymq9.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.Business.pucw3o7rno.dll → MindExecution.Plugins.Business.o0bonn1p2f.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.Concept.557tzzkrrx.dll → MindExecution.Plugins.Concept.1omsw5caki.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.Directory.zh1ddg1fn6.dll → MindExecution.Plugins.Directory.imwo19wy9c.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.PlanMaster.0z9pzynpu9.dll → MindExecution.Plugins.PlanMaster.hakp8m5rxg.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Plugins.YouTube.y8g77r54fa.dll → MindExecution.Plugins.YouTube.2imuchw4ww.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Shared.1ymsdbwid2.dll → MindExecution.Shared.9podjbu7w7.dll} +0 -0
- package/wwwroot/_framework/{MindExecution.Web.bb1txupuv0.dll → MindExecution.Web.rck7kmuixn.dll} +0 -0
- package/wwwroot/_framework/blazor.boot.json +21 -21
- package/wwwroot/index.html +2 -1
- package/wwwroot/service-worker-assets.js +29 -25
- package/wwwroot/service-worker.js +1 -1
- 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 @
|
|
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:
|
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
|
|
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
|
+
}));
|