@mindexec/cli 0.2.460 → 0.2.462

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 (28) hide show
  1. package/electron/main.cjs +147 -3
  2. package/electron/preload.cjs +17 -0
  3. package/electron/recurring-update-owner-smoke.mjs +38 -0
  4. package/electron/recurring-update-owner.cjs +93 -0
  5. package/electron/source-smoke.mjs +32 -0
  6. package/electron/update-feed-resolver.cjs +79 -0
  7. package/electron/update-feed.json +10 -0
  8. package/electron/update-manager-smoke.mjs +146 -0
  9. package/electron/update-manager.cjs +297 -0
  10. package/electron/windows-package-smoke.mjs +3 -0
  11. package/package.json +26 -15
  12. package/scripts/desktop-update-publisher-smoke.mjs +89 -0
  13. package/scripts/desktop-update-worker-smoke.mjs +72 -0
  14. package/scripts/drive-installed-update-smoke.mjs +67 -0
  15. package/scripts/publish-mindexec-desktop-updates.mjs +224 -0
  16. package/wwwroot/_framework/{MindExecution.Core.oqju650dkd.dll → MindExecution.Core.0b8jdcyhj8.dll} +0 -0
  17. package/wwwroot/_framework/{MindExecution.Kernel.7zjugdfmfg.dll → MindExecution.Kernel.hm6hmoblm6.dll} +0 -0
  18. package/wwwroot/_framework/{MindExecution.Plugins.Admin.qln7lkmsnn.dll → MindExecution.Plugins.Admin.oztzw186ns.dll} +0 -0
  19. package/wwwroot/_framework/{MindExecution.Plugins.Business.rd6flxuebm.dll → MindExecution.Plugins.Business.a1e73rkgjv.dll} +0 -0
  20. package/wwwroot/_framework/{MindExecution.Plugins.Concept.0qrgx3epss.dll → MindExecution.Plugins.Concept.rgwn0b8m2o.dll} +0 -0
  21. package/wwwroot/_framework/{MindExecution.Plugins.Directory.4prauy9d1z.dll → MindExecution.Plugins.Directory.2qeqqundtn.dll} +0 -0
  22. package/wwwroot/_framework/{MindExecution.Plugins.PlanMaster.cobfta1p3l.dll → MindExecution.Plugins.PlanMaster.mlf2c4st5u.dll} +0 -0
  23. package/wwwroot/_framework/{MindExecution.Plugins.YouTube.99bahbgkkr.dll → MindExecution.Plugins.YouTube.acobc6tmxc.dll} +0 -0
  24. package/wwwroot/_framework/{MindExecution.Shared.sevaa4rgkp.dll → MindExecution.Shared.6lbogo6eek.dll} +0 -0
  25. package/wwwroot/_framework/{MindExecution.Web.coqh2ccnuk.dll → MindExecution.Web.y97l6vu05i.dll} +0 -0
  26. package/wwwroot/_framework/blazor.boot.json +21 -21
  27. package/wwwroot/service-worker-assets.js +22 -22
  28. package/wwwroot/service-worker.js +1 -1
@@ -0,0 +1,297 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { resolveDesktopUpdateFeed } = require('./update-feed-resolver.cjs');
6
+
7
+ function createProductUpdateManager({
8
+ app,
9
+ onStatus = () => undefined,
10
+ beforeInstall = async () => undefined,
11
+ onInstallError = async () => undefined,
12
+ updater = null,
13
+ platform = process.platform,
14
+ arch = process.arch,
15
+ installKind = 'installed',
16
+ startupCheckTimeoutMs = 2_500,
17
+ updateFeedConfig = null,
18
+ updateChannel = '',
19
+ overrideUrl = process.env.MINDEXEC_UPDATE_URL || ''
20
+ } = {}) {
21
+ if (!app) throw new TypeError('desktop-update-app-required');
22
+ if (!updater) updater = require('electron-updater').autoUpdater;
23
+
24
+ let feedConfig = updateFeedConfig;
25
+ let packageMetadata = {};
26
+ try {
27
+ packageMetadata = JSON.parse(fs.readFileSync(path.join(app.getAppPath(), 'package.json'), 'utf8'));
28
+ } catch {
29
+ packageMetadata = {};
30
+ }
31
+ if (!feedConfig) {
32
+ try {
33
+ feedConfig = JSON.parse(fs.readFileSync(path.join(app.getAppPath(), 'electron', 'update-feed.json'), 'utf8'));
34
+ } catch {
35
+ feedConfig = {};
36
+ }
37
+ }
38
+
39
+ const packagedChannel = String(updateChannel || packageMetadata?.desktopUpdateChannel || '')
40
+ .trim()
41
+ .toLowerCase();
42
+ if (packagedChannel) feedConfig = { ...feedConfig, channel: packagedChannel };
43
+ const feed = resolveDesktopUpdateFeed({ config: feedConfig, overrideUrl, platform, arch });
44
+ const updateSupported = Boolean(app.isPackaged && installKind === 'installed' && platform === 'win32');
45
+
46
+ let status = {
47
+ state: 'idle',
48
+ currentVersion: app.getVersion(),
49
+ availableVersion: '',
50
+ downloaded: false,
51
+ progress: 0,
52
+ error: '',
53
+ configured: feed.configured,
54
+ feedUrl: feed.url,
55
+ channel: feed.channel,
56
+ platform: feed.platform,
57
+ arch: feed.arch,
58
+ installKind,
59
+ updateSupported,
60
+ startupCheck: 'idle'
61
+ };
62
+ let installPromise = null;
63
+ let checkPromise = null;
64
+ let downloadPromise = null;
65
+ let cyclePromise = null;
66
+ let startupPromise = null;
67
+ let checkOutcome = 'idle';
68
+ let installRuntimePreparationStarted = false;
69
+ let installRecoveryPromise = null;
70
+
71
+ const publish = patch => {
72
+ status = { ...status, ...patch };
73
+ onStatus({ ...status });
74
+ };
75
+ const snapshot = () => ({ ...status, packaged: Boolean(app.isPackaged) });
76
+ const recoverPreparedRuntime = error => {
77
+ if (!installRuntimePreparationStarted) return Promise.resolve();
78
+ installRuntimePreparationStarted = false;
79
+ if (!installRecoveryPromise) {
80
+ installRecoveryPromise = Promise.resolve(onInstallError(error))
81
+ .catch(() => undefined)
82
+ .finally(() => {
83
+ installRecoveryPromise = null;
84
+ if (status.state === 'error') installPromise = null;
85
+ });
86
+ }
87
+ return installRecoveryPromise;
88
+ };
89
+
90
+ updater.autoDownload = false;
91
+ updater.autoInstallOnAppQuit = false;
92
+ if (feed.url) updater.setFeedURL({ provider: 'generic', url: feed.url });
93
+
94
+ updater.on('checking-for-update', () => {
95
+ checkOutcome = 'checking';
96
+ publish({ state: 'checking', error: '' });
97
+ });
98
+ updater.on('update-available', info => {
99
+ checkOutcome = 'available';
100
+ publish({
101
+ state: 'available',
102
+ availableVersion: String(info?.version || ''),
103
+ downloaded: false,
104
+ progress: 0,
105
+ error: ''
106
+ });
107
+ });
108
+ updater.on('update-not-available', () => {
109
+ checkOutcome = 'current';
110
+ publish({ state: 'current', availableVersion: '', downloaded: false, progress: 0, error: '' });
111
+ });
112
+ updater.on('download-progress', progress => {
113
+ publish({ state: 'downloading', progress: Number(progress?.percent || 0), error: '' });
114
+ });
115
+ updater.on('update-downloaded', info => {
116
+ publish({
117
+ state: 'downloaded',
118
+ downloaded: true,
119
+ progress: 100,
120
+ availableVersion: String(info?.version || status.availableVersion || ''),
121
+ error: ''
122
+ });
123
+ });
124
+ updater.on('error', error => {
125
+ checkOutcome = 'error';
126
+ publish({ state: 'error', error: String(error?.message || error) });
127
+ void recoverPreparedRuntime(error);
128
+ });
129
+
130
+ const blockedStatus = () => {
131
+ if (!app.isPackaged) {
132
+ return { ...snapshot(), state: 'development', error: 'desktop-updates-require-packaged-app' };
133
+ }
134
+ if (!updateSupported) {
135
+ return { ...snapshot(), state: 'unsupported', error: 'desktop-updates-require-installed-windows-app' };
136
+ }
137
+ return null;
138
+ };
139
+ const unavailableStatus = () => ({
140
+ ...snapshot(),
141
+ state: 'unavailable',
142
+ error: feed.error || 'desktop-update-feed-not-configured'
143
+ });
144
+
145
+ const checkForUpdates = async () => {
146
+ const blocked = blockedStatus();
147
+ if (blocked) return blocked;
148
+ if (!feed.url) return unavailableStatus();
149
+ if (checkPromise) return checkPromise;
150
+
151
+ const operation = (async () => {
152
+ checkOutcome = 'checking';
153
+ publish({ state: 'checking', error: '' });
154
+ try {
155
+ const result = await updater.checkForUpdates();
156
+ if (checkOutcome === 'checking') {
157
+ const availableVersion = String(result?.updateInfo?.version || status.availableVersion || '');
158
+ const updateAvailable = Boolean(availableVersion && availableVersion !== app.getVersion());
159
+ checkOutcome = updateAvailable ? 'available' : 'current';
160
+ publish({
161
+ state: checkOutcome,
162
+ availableVersion: updateAvailable ? availableVersion : '',
163
+ downloaded: updateAvailable ? status.downloaded : false,
164
+ progress: updateAvailable ? status.progress : 0,
165
+ error: ''
166
+ });
167
+ }
168
+ } catch (error) {
169
+ checkOutcome = 'error';
170
+ publish({ state: 'error', error: String(error?.message || error) });
171
+ }
172
+ return snapshot();
173
+ })();
174
+ checkPromise = operation;
175
+ try {
176
+ return await operation;
177
+ } finally {
178
+ if (checkPromise === operation) checkPromise = null;
179
+ }
180
+ };
181
+
182
+ const downloadUpdate = async () => {
183
+ const blocked = blockedStatus();
184
+ if (blocked) return blocked;
185
+ if (!feed.url) return unavailableStatus();
186
+ if (status.downloaded) return snapshot();
187
+ if (downloadPromise) return downloadPromise;
188
+
189
+ const operation = (async () => {
190
+ try {
191
+ publish({ state: 'downloading', progress: Math.max(0, Number(status.progress || 0)), error: '' });
192
+ await updater.downloadUpdate();
193
+ if (!status.downloaded) {
194
+ publish({ state: 'downloaded', downloaded: true, progress: 100, error: '' });
195
+ }
196
+ } catch (error) {
197
+ publish({ state: 'error', error: String(error?.message || error) });
198
+ }
199
+ return snapshot();
200
+ })();
201
+ downloadPromise = operation;
202
+ try {
203
+ return await operation;
204
+ } finally {
205
+ if (downloadPromise === operation) downloadPromise = null;
206
+ }
207
+ };
208
+
209
+ const checkAndDownload = async () => {
210
+ if (cyclePromise) return cyclePromise;
211
+ const operation = (async () => {
212
+ const checked = await checkForUpdates();
213
+ if (checked.state === 'available') return downloadUpdate();
214
+ return checked;
215
+ })();
216
+ cyclePromise = operation;
217
+ try {
218
+ return await operation;
219
+ } finally {
220
+ if (cyclePromise === operation) cyclePromise = null;
221
+ }
222
+ };
223
+
224
+ return {
225
+ getStatus() {
226
+ return snapshot();
227
+ },
228
+ async check() {
229
+ return checkAndDownload();
230
+ },
231
+ async checkAtStartup() {
232
+ if (startupPromise) return startupPromise;
233
+ const operation = (async () => {
234
+ const blocked = blockedStatus();
235
+ if (blocked || !feed.url) return checkForUpdates();
236
+
237
+ publish({ startupCheck: 'checking' });
238
+ const background = checkAndDownload().finally(() => {
239
+ publish({ startupCheck: 'complete' });
240
+ });
241
+ let timeoutId;
242
+ const timeout = new Promise(resolve => {
243
+ timeoutId = setTimeout(
244
+ () => resolve('timeout'),
245
+ Math.max(10, Number(startupCheckTimeoutMs) || 2_500)
246
+ );
247
+ timeoutId.unref?.();
248
+ });
249
+ const result = await Promise.race([background, timeout]);
250
+ clearTimeout(timeoutId);
251
+ if (result === 'timeout') {
252
+ publish({ startupCheck: 'timed-out' });
253
+ return snapshot();
254
+ }
255
+ return result;
256
+ })();
257
+ startupPromise = operation;
258
+ try {
259
+ return await operation;
260
+ } finally {
261
+ if (startupPromise === operation) startupPromise = null;
262
+ }
263
+ },
264
+ async install() {
265
+ const blocked = blockedStatus();
266
+ if (blocked) return blocked;
267
+ if (!feed.url) return unavailableStatus();
268
+ if (installPromise) return installPromise;
269
+
270
+ const operation = (async () => {
271
+ try {
272
+ if (!status.downloaded) {
273
+ const downloadResult = await downloadUpdate();
274
+ if (!downloadResult.downloaded) return downloadResult;
275
+ }
276
+ installRuntimePreparationStarted = true;
277
+ await beforeInstall();
278
+ publish({ state: 'installing', error: '' });
279
+ updater.quitAndInstall(true, true);
280
+ return snapshot();
281
+ } catch (error) {
282
+ await recoverPreparedRuntime(error);
283
+ publish({ state: 'error', error: String(error?.message || error) });
284
+ return snapshot();
285
+ }
286
+ })();
287
+ installPromise = operation;
288
+ try {
289
+ return await operation;
290
+ } finally {
291
+ if (installPromise === operation && status.state !== 'installing') installPromise = null;
292
+ }
293
+ }
294
+ };
295
+ }
296
+
297
+ module.exports = { createProductUpdateManager };
@@ -129,6 +129,8 @@ try {
129
129
  assert.equal(result.bridgePort, bridgePort, 'Packaged shell did not use the requested Bridge port.');
130
130
  assert.equal(result.remoteHubPort, remoteHubPort, 'Packaged shell did not use the requested RemoteHub port.');
131
131
  assert.equal(path.resolve(result.workspace), path.resolve(workspace), 'Packaged Electron did not restore the saved workspace.');
132
+ assert.equal(result.desktopUpdate?.installKind, 'unpacked', 'The unpacked EXE must identify its non-self-updating package kind.');
133
+ assert.equal(result.desktopUpdate?.updateSupported, false, 'The unpacked EXE must never replace itself.');
132
134
 
133
135
  const exitCode = await waitForExit(child, 15_000);
134
136
  assert.equal(exitCode, 0, `Packaged Electron smoke exited with code ${exitCode}.`);
@@ -142,6 +144,7 @@ try {
142
144
  executablePath,
143
145
  version: result.appVersion,
144
146
  runtimeMode: result.runtimeMode,
147
+ desktopUpdateKind: result.desktopUpdate.installKind,
145
148
  bridgePortReleased: true,
146
149
  remoteHubPortReleased: true
147
150
  }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindexec/cli",
3
- "version": "0.2.460",
3
+ "version": "0.2.462",
4
4
  "description": "MindExec local runtime and bridge CLI",
5
5
  "main": "server.js",
6
6
  "type": "module",
@@ -27,11 +27,16 @@
27
27
  "scripts": {
28
28
  "start": "node launch-bridge.cjs",
29
29
  "dev": "node launch-bridge.cjs --watch",
30
- "desktop:start": "electron electron/main.cjs",
31
- "desktop:build:win": "electron-builder --win --x64 --publish never",
32
- "test:desktop": "node electron/source-smoke.mjs && node scripts/desktop-workspace-state-smoke.mjs",
30
+ "desktop:start": "electron electron/main.cjs",
31
+ "desktop:build:win": "electron-builder --win --x64 --publish never",
32
+ "desktop:inspect:update:dev": "node scripts/publish-mindexec-desktop-updates.mjs --channel dev --platform windows-x64 --dir ../artifacts/electron --allow-unsigned",
33
+ "desktop:publish:update:dev": "node scripts/publish-mindexec-desktop-updates.mjs --channel dev --platform windows-x64 --dir ../artifacts/electron --allow-unsigned --publish",
34
+ "desktop:deploy:update-worker": "node ../r2-auth-proxy/node_modules/wrangler/wrangler-dist/cli.js deploy --config ../workers/mindexec-desktop-updates/wrangler.jsonc",
35
+ "desktop:check:update-worker": "node ../r2-auth-proxy/node_modules/wrangler/wrangler-dist/cli.js deploy --dry-run --config ../workers/mindexec-desktop-updates/wrangler.jsonc",
36
+ "test:desktop:update": "node --test electron/update-manager-smoke.mjs electron/recurring-update-owner-smoke.mjs scripts/desktop-update-worker-smoke.mjs scripts/desktop-update-publisher-smoke.mjs",
37
+ "test:desktop": "node electron/source-smoke.mjs && node scripts/desktop-workspace-state-smoke.mjs && npm run test:desktop:update",
33
38
  "test:desktop:win": "node electron/windows-package-smoke.mjs",
34
- "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 desktop-workspace-state.cjs && node --check electron/main.cjs && node --check electron/source-smoke.mjs && node --check electron/windows-package-smoke.mjs && node --check scripts/desktop-workspace-state-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/codex-sdk-runtime-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",
39
+ "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 desktop-workspace-state.cjs && node --check electron/main.cjs && node --check electron/preload.cjs && node --check electron/update-manager.cjs && node --check electron/update-feed-resolver.cjs && node --check electron/recurring-update-owner.cjs && node --check electron/source-smoke.mjs && node --check electron/windows-package-smoke.mjs && node --check electron/update-manager-smoke.mjs && node --check electron/recurring-update-owner-smoke.mjs && node --check scripts/desktop-workspace-state-smoke.mjs && node --check scripts/desktop-update-worker-smoke.mjs && node --check scripts/desktop-update-publisher-smoke.mjs && node --check scripts/publish-mindexec-desktop-updates.mjs && node --check ../workers/mindexec-desktop-updates/src/index.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/codex-sdk-runtime-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",
35
40
  "test:codex-sdk": "node scripts/codex-sdk-runtime-smoke.mjs",
36
41
  "test:codex-sdk:live": "node scripts/codex-sdk-runtime-smoke.mjs --live",
37
42
  "test:codex-sdk:live-tools": "node scripts/codex-sdk-runtime-smoke.mjs --live-tools",
@@ -78,8 +83,9 @@
78
83
  "cors": "^2.8.5",
79
84
  "express": "^4.18.2",
80
85
  "ffmpeg-static": "^5.3.0",
81
- "multer": "^2.2.0",
82
- "playwright-core": "^1.53.0",
86
+ "multer": "^2.2.0",
87
+ "electron-updater": "^6.8.9",
88
+ "playwright-core": "^1.53.0",
83
89
  "sharp": "^0.35.3",
84
90
  "web-tree-sitter": "^0.22.6",
85
91
  "ws": "^8.16.0"
@@ -101,8 +107,9 @@
101
107
  "productName": "MindExec",
102
108
  "copyright": "Copyright © 2026 lovecrdm77",
103
109
  "asar": false,
104
- "extraMetadata": {
105
- "main": "electron/main.cjs"
110
+ "extraMetadata": {
111
+ "main": "electron/main.cjs",
112
+ "desktopUpdateChannel": "dev"
106
113
  },
107
114
  "directories": {
108
115
  "output": "../artifacts/electron"
@@ -111,7 +118,7 @@
111
118
  "*.js",
112
119
  "*.cjs",
113
120
  "*.json",
114
- "electron/main.cjs",
121
+ "electron/**/*",
115
122
  "remote-fast/**/*",
116
123
  "scripts/**/*",
117
124
  "tree-sitter-grammars/**/*",
@@ -136,8 +143,8 @@
136
143
  }
137
144
  ]
138
145
  },
139
- "nsis": {
140
- "artifactName": "MindExec-Setup-${version}-${arch}.${ext}",
146
+ "nsis": {
147
+ "artifactName": "MindExec-${version}-dev-${arch}.${ext}",
141
148
  "oneClick": false,
142
149
  "perMachine": false,
143
150
  "allowToChangeInstallationDirectory": true,
@@ -145,8 +152,12 @@
145
152
  "createStartMenuShortcut": true,
146
153
  "shortcutName": "MindExec"
147
154
  },
148
- "portable": {
149
- "artifactName": "MindExec-Portable-${version}-${arch}.${ext}"
150
- }
155
+ "portable": {
156
+ "artifactName": "MindExec-Portable-${version}-${arch}.${ext}"
157
+ },
158
+ "publish": {
159
+ "provider": "generic",
160
+ "url": "https://mindexec.lovecrdm.workers.dev/dev/windows/x64"
161
+ }
151
162
  }
152
163
  }
@@ -0,0 +1,89 @@
1
+ import assert from 'node:assert/strict';
2
+ import { createHash } from 'node:crypto';
3
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import test from 'node:test';
7
+ import {
8
+ createWranglerPutCommand,
9
+ parseUpdateManifest,
10
+ readReleasePlan
11
+ } from './publish-mindexec-desktop-updates.mjs';
12
+
13
+ test('publisher launches Wrangler directly and targets the private update bucket', () => {
14
+ const command = createWranglerPutCommand({
15
+ key: 'dev/windows/x64/MindExec-0.2.461-dev-x64.exe',
16
+ filePath: 'C:\\release\\MindExec-0.2.461-dev-x64.exe',
17
+ immutable: true
18
+ });
19
+ assert.equal(command.executable, process.execPath);
20
+ assert.match(command.args[0], /wrangler(?:-dist)?[\\/]cli\.js$/i);
21
+ assert.ok(command.args.includes('mindexec-desktop-updates/dev/windows/x64/MindExec-0.2.461-dev-x64.exe'));
22
+ assert.equal(command.args.includes('npx.cmd'), false);
23
+ assert.equal(command.args.at(-1), '--remote');
24
+ });
25
+
26
+ test('publisher validates version, size, and SHA-512 before upload', async () => {
27
+ const directory = await mkdtemp(join(tmpdir(), 'mindexec-update-publisher-'));
28
+ try {
29
+ const artifact = 'MindExec-1.2.3-dev-x64.exe';
30
+ const bytes = Buffer.from('unsigned-development-artifact');
31
+ const sha512 = createHash('sha512').update(bytes).digest('base64');
32
+ await writeFile(join(directory, artifact), bytes);
33
+ await writeFile(join(directory, 'latest.yml'), [
34
+ 'version: 1.2.3',
35
+ 'files:',
36
+ ` - url: ${artifact}`,
37
+ ` sha512: ${sha512}`,
38
+ ` size: ${bytes.length}`,
39
+ `path: ${artifact}`,
40
+ `sha512: ${sha512}`,
41
+ 'releaseDate: 2026-08-16T00:00:00.000Z',
42
+ ''
43
+ ].join('\n'));
44
+
45
+ const parsed = parseUpdateManifest(await (await import('node:fs/promises')).readFile(join(directory, 'latest.yml'), 'utf8'));
46
+ assert.equal(parsed.version, '1.2.3');
47
+ assert.equal(parsed.files[0].name, artifact);
48
+ assert.throws(
49
+ () => readReleasePlan({
50
+ platform: 'windows-x64',
51
+ directory,
52
+ packageVersion: '1.2.3',
53
+ channel: 'dev'
54
+ }),
55
+ /desktop-update-dev-unsigned-ack-required/
56
+ );
57
+ const plan = readReleasePlan({
58
+ platform: 'windows-x64',
59
+ directory,
60
+ packageVersion: '1.2.3',
61
+ channel: 'dev',
62
+ allowUnsigned: true
63
+ });
64
+ assert.equal(plan.prefix, 'dev/windows/x64');
65
+ assert.deepEqual(plan.artifacts.map(item => item.fileName), [artifact]);
66
+ assert.throws(
67
+ () => readReleasePlan({
68
+ platform: 'windows-x64',
69
+ directory,
70
+ packageVersion: '1.2.4',
71
+ channel: 'dev',
72
+ allowUnsigned: true
73
+ }),
74
+ /desktop-update-version-mismatch/
75
+ );
76
+ assert.throws(
77
+ () => readReleasePlan({
78
+ platform: 'windows-x64',
79
+ directory,
80
+ packageVersion: '1.2.3',
81
+ channel: 'stable',
82
+ allowUnsigned: true
83
+ }),
84
+ /desktop-update-stable-unsigned-forbidden/
85
+ );
86
+ } finally {
87
+ await rm(directory, { recursive: true, force: true });
88
+ }
89
+ });
@@ -0,0 +1,72 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import worker, { resolveUpdateObjectKey } from '../../workers/mindexec-desktop-updates/src/index.mjs';
4
+
5
+ function storedObject(body = 'release') {
6
+ const bytes = new TextEncoder().encode(body);
7
+ return {
8
+ body: new ReadableStream({ start(controller) { controller.enqueue(bytes); controller.close(); } }),
9
+ size: bytes.byteLength,
10
+ httpEtag: '"release-etag"',
11
+ writeHttpMetadata(headers) { headers.set('content-type', 'application/octet-stream'); }
12
+ };
13
+ }
14
+
15
+ test('only fixed Windows stable and development update paths are public', () => {
16
+ assert.equal(resolveUpdateObjectKey('/stable/windows/x64/latest.yml'), 'stable/windows/x64/latest.yml');
17
+ assert.equal(resolveUpdateObjectKey('/dev/windows/x64/latest.yml'), 'dev/windows/x64/latest.yml');
18
+ assert.equal(
19
+ resolveUpdateObjectKey('/dev/windows/x64/MindExec-0.2.461-dev-x64.exe'),
20
+ 'dev/windows/x64/MindExec-0.2.461-dev-x64.exe'
21
+ );
22
+ assert.equal(resolveUpdateObjectKey('/private/secret.txt'), '');
23
+ assert.equal(resolveUpdateObjectKey('/dev/windows/x64/../secret.txt'), '');
24
+ assert.equal(resolveUpdateObjectKey('/preview/windows/x64/latest.yml'), '');
25
+ assert.equal(resolveUpdateObjectKey('/dev/macos/arm64/latest-mac.yml'), '');
26
+ });
27
+
28
+ test('worker exposes health but rejects writes and bucket listing', async () => {
29
+ const env = { UPDATE_BUCKET: { head: async () => null, get: async () => null } };
30
+ assert.equal((await worker.fetch(new Request('https://updates.example/healthz'), env)).status, 200);
31
+ assert.equal((await worker.fetch(new Request('https://updates.example/', { method: 'PUT' }), env)).status, 405);
32
+ assert.equal((await worker.fetch(new Request('https://updates.example/'), env)).status, 404);
33
+ });
34
+
35
+ test('worker streams a manifest without caching it', async () => {
36
+ const object = storedObject('version: 0.2.461');
37
+ let observedOptions = null;
38
+ const env = {
39
+ UPDATE_BUCKET: {
40
+ head: async () => object,
41
+ get: async (_key, options) => { observedOptions = options; return object; }
42
+ }
43
+ };
44
+ const response = await worker.fetch(new Request('https://updates.example/dev/windows/x64/latest.yml'), env);
45
+ assert.equal(response.status, 200);
46
+ assert.equal('range' in observedOptions, false);
47
+ assert.equal(await response.text(), 'version: 0.2.461');
48
+ assert.equal(response.headers.get('cache-control'), 'no-store, max-age=0');
49
+ assert.equal(response.headers.get('etag'), '"release-etag"');
50
+ });
51
+
52
+ test('worker preserves resumable byte-range responses for large installers', async () => {
53
+ const object = storedObject('part');
54
+ object.size = 20;
55
+ object.range = { offset: 8, length: 4 };
56
+ const env = {
57
+ UPDATE_BUCKET: {
58
+ head: async () => object,
59
+ get: async (_key, options) => {
60
+ assert.ok(options.range, 'an explicit Range request must reach R2');
61
+ return object;
62
+ }
63
+ }
64
+ };
65
+ const response = await worker.fetch(new Request(
66
+ 'https://updates.example/dev/windows/x64/MindExec-0.2.461-dev-x64.exe',
67
+ { headers: { range: 'bytes=8-11' } }
68
+ ), env);
69
+ assert.equal(response.status, 206);
70
+ assert.equal(response.headers.get('content-range'), 'bytes 8-11/20');
71
+ assert.equal(response.headers.get('content-length'), '4');
72
+ });
@@ -0,0 +1,67 @@
1
+ import { chromium } from 'playwright-core';
2
+
3
+ function readArgument(name, fallback = '') {
4
+ const index = process.argv.indexOf(name);
5
+ return index >= 0 ? String(process.argv[index + 1] || fallback) : fallback;
6
+ }
7
+
8
+ const debugPort = Number.parseInt(readArgument('--debug-port'), 10);
9
+ const expectedVersion = readArgument('--expected-version');
10
+ const timeoutMs = Number.parseInt(readArgument('--timeout-ms', '300000'), 10);
11
+ if (!Number.isInteger(debugPort) || debugPort <= 0 || !expectedVersion) {
12
+ throw new Error('usage: --debug-port <port> --expected-version <version> [--timeout-ms <ms>]');
13
+ }
14
+
15
+ const endpoint = `http://127.0.0.1:${debugPort}`;
16
+ const deadline = Date.now() + timeoutMs;
17
+ let browser = null;
18
+ let page = null;
19
+ let lastError = null;
20
+
21
+ while (Date.now() < deadline && !page) {
22
+ try {
23
+ browser = await chromium.connectOverCDP(endpoint);
24
+ const contexts = browser.contexts();
25
+ page = contexts.flatMap(context => context.pages()).find(candidate =>
26
+ candidate.url().startsWith('http://127.0.0.1:')) || contexts[0]?.pages()[0] || null;
27
+ if (!page) throw new Error('desktop-page-not-ready');
28
+ await page.waitForFunction(() => Boolean(window.mindExecDesktop?.updates), null, { timeout: 5_000 });
29
+ } catch (error) {
30
+ lastError = error;
31
+ page = null;
32
+ await new Promise(resolve => setTimeout(resolve, 500));
33
+ }
34
+ }
35
+ if (!page) throw new Error(`desktop-cdp-not-ready:${lastError?.message || 'timeout'}`);
36
+
37
+ await page.evaluate(() => window.mindExecDesktop.updates.check());
38
+ let status = null;
39
+ while (Date.now() < deadline) {
40
+ status = await page.evaluate(() => window.mindExecDesktop.updates.getStatus());
41
+ if (status?.state === 'error') throw new Error(`desktop-update-check-failed:${status.error}`);
42
+ if (status?.downloaded && status?.availableVersion === expectedVersion) break;
43
+ await new Promise(resolve => setTimeout(resolve, 500));
44
+ }
45
+ if (!status?.downloaded || status?.availableVersion !== expectedVersion) {
46
+ throw new Error(`desktop-update-not-downloaded:${JSON.stringify(status)}`);
47
+ }
48
+
49
+ let rendererClosedDuringInstall = false;
50
+ try {
51
+ await page.evaluate(() => window.mindExecDesktop.updates.install());
52
+ } catch (error) {
53
+ const message = String(error?.message || error);
54
+ if (!/Target page, context or browser has been closed|Execution context was destroyed|Protocol error|closed/i.test(message)) {
55
+ throw error;
56
+ }
57
+ rendererClosedDuringInstall = true;
58
+ }
59
+
60
+ console.log(JSON.stringify({
61
+ ok: true,
62
+ downloadedVersion: status.availableVersion,
63
+ progress: status.progress,
64
+ installInvoked: true,
65
+ rendererClosedDuringInstall
66
+ }));
67
+ process.exit(0);