@mindexec/cli 0.2.133 → 0.2.135

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mindexec/cli",
3
- "version": "0.2.133",
3
+ "version": "0.2.135",
4
4
  "description": "MindExec local runtime and bridge CLI",
5
5
  "main": "server.js",
6
6
  "type": "module",
@@ -21,7 +21,7 @@
21
21
  "scripts": {
22
22
  "start": "node launch-bridge.cjs",
23
23
  "dev": "node launch-bridge.cjs --watch",
24
- "test:syntax": "node --check server.js && node --check remote-hub.js && node --check codex-runtime.js && node --check launch-bridge.cjs && node --check port-guard.cjs && node --check scripts/setup-tree-sitter-grammars.mjs && node --check scripts/auth-session-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-agent-ws-smoke.mjs && node --check scripts/remote-agent-managed-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",
24
+ "test:syntax": "node --check server.js && node --check remote-hub.js && node --check codex-runtime.js && node --check launch-bridge.cjs && node --check port-guard.cjs && node --check scripts/setup-tree-sitter-grammars.mjs && node --check scripts/auth-session-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-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",
25
25
  "test:auth": "node scripts/auth-session-smoke.mjs",
26
26
  "test:remote": "node scripts/remote-hub-smoke.mjs",
27
27
  "test:remote:scale": "node scripts/remote-hub-scale-smoke.mjs",
@@ -30,6 +30,7 @@
30
30
  "test:remote:frame-ws": "node scripts/remote-frame-ws-smoke.mjs",
31
31
  "test:remote:agent-ws": "node scripts/remote-agent-ws-smoke.mjs",
32
32
  "test:remote:managed": "node scripts/remote-agent-managed-smoke.mjs",
33
+ "test:remote:registry": "node scripts/remote-registry-follower-smoke.mjs",
33
34
  "test:remote:package": "node scripts/remote-agent-package-smoke.mjs",
34
35
  "test:remote:live-rate": "node scripts/remote-fast-live-rate-smoke.mjs",
35
36
  "test:remote:mdm-browser": "node scripts/remote-fast-mdm-browser-smoke.mjs",
@@ -161,14 +161,14 @@ async function launchBrowser(chromium) {
161
161
  throw new Error(`Unable to launch a Chromium browser: ${lastError?.message || 'unknown error'}`);
162
162
  }
163
163
 
164
- async function paintRemoteFrameInBrowser(page, wsUrl, deviceId) {
164
+ async function paintRemoteFrameInBrowser(page, wsUrl, deviceId, fps = REQUESTED_FPS) {
165
165
  await page.setContent(`
166
166
  <!doctype html>
167
167
  <meta charset="utf-8">
168
168
  <canvas id="screen" width="320" height="180"></canvas>
169
169
  `);
170
170
 
171
- return await page.evaluate(async ({ wsUrl, deviceId }) => {
171
+ return await page.evaluate(async ({ wsUrl, deviceId, fps }) => {
172
172
  const canvas = document.getElementById('screen');
173
173
  const context = canvas.getContext('2d', { alpha: false, desynchronized: true }) || canvas.getContext('2d');
174
174
  if (!context) {
@@ -203,7 +203,15 @@ async function paintRemoteFrameInBrowser(page, wsUrl, deviceId) {
203
203
  const ws = new WebSocket(wsUrl);
204
204
  ws.binaryType = 'arraybuffer';
205
205
  ws.onopen = () => {
206
- ws.send(JSON.stringify({ type: 'subscribe', deviceIds: [deviceId] }));
206
+ ws.send(JSON.stringify({
207
+ type: 'subscribe',
208
+ deviceIds: [deviceId],
209
+ autoStartLive: true,
210
+ fps,
211
+ maxWidth: 960,
212
+ maxHeight: 540,
213
+ quality: 60
214
+ }));
207
215
  };
208
216
  ws.onerror = () => {
209
217
  clearTimeout(timeout);
@@ -262,7 +270,7 @@ async function paintRemoteFrameInBrowser(page, wsUrl, deviceId) {
262
270
  }
263
271
  };
264
272
  });
265
- }, { wsUrl, deviceId });
273
+ }, { wsUrl, deviceId, fps });
266
274
  }
267
275
 
268
276
  async function main() {
@@ -320,23 +328,11 @@ async function main() {
320
328
  && item.capabilities?.binaryFrames === true) || null;
321
329
  }, 15000, `RemoteFast device registration\n${hostBridge.details()}\n${clientBridge.details()}`);
322
330
 
323
- const live = await fetchJson(`${hostBridge.baseUrl}/api/remote/devices/${encodeURIComponent(device.deviceId)}/live/start`, {
324
- method: 'POST',
325
- body: JSON.stringify({
326
- fps: REQUESTED_FPS,
327
- maxWidth: 960,
328
- maxHeight: 540,
329
- quality: 60
330
- })
331
- });
332
- assert.equal(live.ok, true, JSON.stringify(live.payload));
333
- assert.equal(live.payload?.ok, true, JSON.stringify(live.payload));
334
-
335
331
  const chromium = await loadPlaywrightChromium();
336
332
  browser = await launchBrowser(chromium);
337
333
  const page = await browser.newPage();
338
334
  const wsUrl = `${hostBridge.baseUrl.replace(/^http:/, 'ws:')}/api/remote/frames/ws?token=${encodeURIComponent(BRIDGE_TOKEN)}`;
339
- const painted = await paintRemoteFrameInBrowser(page, wsUrl, device.deviceId);
335
+ const painted = await paintRemoteFrameInBrowser(page, wsUrl, device.deviceId, REQUESTED_FPS);
340
336
  assert.ok(painted.frameSeq > 0, JSON.stringify(painted));
341
337
  assert.equal(painted.fps, REQUESTED_FPS, JSON.stringify(painted));
342
338
  assert.ok(painted.width > 0 && painted.height > 0, JSON.stringify(painted));
@@ -151,7 +151,7 @@ async function main() {
151
151
 
152
152
  const seed = await fetchJson(`${bridge.baseUrl}/api/remote/synthetic/seed`, {
153
153
  method: 'POST',
154
- body: JSON.stringify({ count: 4, connectedRatio: 1, liveRatio: 1 })
154
+ body: JSON.stringify({ count: 4, connectedRatio: 1, thumbnailRatio: 1, liveCount: 0 })
155
155
  });
156
156
  assert.equal(seed.ok, true);
157
157
 
@@ -165,20 +165,22 @@ async function main() {
165
165
  ws.once('open', resolve);
166
166
  ws.once('error', reject);
167
167
  });
168
- ws.send(JSON.stringify({ type: 'subscribe', deviceIds: [device.deviceId] }));
168
+ ws.send(JSON.stringify({
169
+ type: 'subscribe',
170
+ deviceIds: [device.deviceId],
171
+ autoStartLive: true,
172
+ fps: 12,
173
+ maxWidth: 960,
174
+ maxHeight: 540,
175
+ quality: 60
176
+ }));
169
177
 
170
178
  const framePromise = waitForBinaryFrame(ws);
171
- const live = await fetchJson(`${bridge.baseUrl}/api/remote/devices/${encodeURIComponent(device.deviceId)}/live/start`, {
172
- method: 'POST',
173
- body: JSON.stringify({ fps: 20 })
174
- });
175
- assert.equal(live.ok, true);
176
-
177
179
  const frame = await framePromise;
178
180
  assert.equal(frame.metadata.type, 'remote.frame.binary');
179
181
  assert.equal(frame.metadata.kind, 'live');
180
182
  assert.equal(frame.metadata.deviceId, device.deviceId);
181
- assert.equal(frame.metadata.fps, 20);
183
+ assert.equal(frame.metadata.fps, 12);
182
184
  assert.ok(frame.metadata.frameSeq > 0, 'frameSeq should be positive');
183
185
  assert.equal(frame.metadata.mimeType, 'image/png');
184
186
  assert.ok(frame.payload.length > 0, 'payload should be non-empty');
@@ -0,0 +1,322 @@
1
+ #!/usr/bin/env node
2
+
3
+ import assert from 'node:assert/strict';
4
+ import { spawn } from 'node:child_process';
5
+ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
6
+ import { createServer } from 'node:http';
7
+ import net from 'node:net';
8
+ import os from 'node:os';
9
+ import path from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+
12
+ const BRIDGE_TOKEN = 'remote-registry-follower-smoke-token';
13
+ const PAIR_TOKEN = 'remote-registry-follower-pair-token';
14
+ const USER_ID = '11111111-2222-4333-8444-555555555555';
15
+ const SUPABASE_KEY = 'remote-registry-follower-key';
16
+ const ACCESS_TOKEN = 'remote-registry-follower-access-token';
17
+ const LOCAL_BRIDGE_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
18
+
19
+ function wait(ms) {
20
+ return new Promise(resolve => setTimeout(resolve, ms));
21
+ }
22
+
23
+ async function findFreePort() {
24
+ return await new Promise((resolve, reject) => {
25
+ const server = net.createServer();
26
+ server.unref();
27
+ server.once('error', reject);
28
+ server.listen(0, '127.0.0.1', () => {
29
+ const address = server.address();
30
+ const port = typeof address === 'object' && address ? address.port : 0;
31
+ server.close(() => resolve(port));
32
+ });
33
+ });
34
+ }
35
+
36
+ async function fetchJson(url, options = {}) {
37
+ const response = await fetch(url, {
38
+ ...options,
39
+ headers: {
40
+ ...(options.body ? { 'Content-Type': 'application/json' } : {}),
41
+ 'X-Bridge-Token': options.token || BRIDGE_TOKEN,
42
+ ...(options.headers || {})
43
+ }
44
+ });
45
+ let payload = null;
46
+ try {
47
+ payload = await response.json();
48
+ } catch {
49
+ // Empty responses are allowed in failure diagnostics.
50
+ }
51
+ return { ok: response.ok, status: response.status, payload };
52
+ }
53
+
54
+ async function waitFor(predicate, timeoutMs, label) {
55
+ const startedAt = Date.now();
56
+ let lastError = null;
57
+ while (Date.now() - startedAt < timeoutMs) {
58
+ try {
59
+ const value = await predicate();
60
+ if (value) {
61
+ return value;
62
+ }
63
+ } catch (error) {
64
+ lastError = error;
65
+ }
66
+ await wait(100);
67
+ }
68
+ throw new Error(`Timed out waiting for ${label}${lastError ? `: ${lastError.message}` : ''}.`);
69
+ }
70
+
71
+ function createRegistryTarget({ endpoint, endpointCandidates, leaseId, active = true }) {
72
+ return {
73
+ user_id: USER_ID,
74
+ active,
75
+ endpoint,
76
+ endpoint_candidates: endpointCandidates,
77
+ pair_token: PAIR_TOKEN,
78
+ lease_id: leaseId,
79
+ node_id: 'remote-registry-follower-node',
80
+ host_instance_id: `host-${leaseId}`,
81
+ expires_at: new Date(Date.now() + 60_000).toISOString()
82
+ };
83
+ }
84
+
85
+ function startFakeSupabase(getTarget) {
86
+ const requests = [];
87
+ const server = createServer((req, res) => {
88
+ const parsed = new URL(req.url || '/', 'http://127.0.0.1');
89
+ requests.push({
90
+ method: req.method,
91
+ pathname: parsed.pathname,
92
+ authorization: req.headers.authorization || '',
93
+ apikey: req.headers.apikey || ''
94
+ });
95
+
96
+ if (req.method === 'GET' && parsed.pathname === '/rest/v1/remote_host_targets') {
97
+ assert.equal(String(req.headers.apikey || ''), SUPABASE_KEY);
98
+ assert.equal(String(req.headers.authorization || ''), `Bearer ${ACCESS_TOKEN}`);
99
+ res.writeHead(200, {
100
+ 'Content-Type': 'application/json',
101
+ 'Cache-Control': 'no-store'
102
+ });
103
+ const target = getTarget();
104
+ res.end(JSON.stringify(target ? [target] : []));
105
+ return;
106
+ }
107
+
108
+ res.writeHead(404, { 'Content-Type': 'application/json' });
109
+ res.end(JSON.stringify({ error: 'not-found' }));
110
+ });
111
+
112
+ return new Promise((resolve, reject) => {
113
+ server.once('error', reject);
114
+ server.listen(0, '127.0.0.1', () => {
115
+ const address = server.address();
116
+ const port = typeof address === 'object' && address ? address.port : 0;
117
+ resolve({
118
+ url: `http://127.0.0.1:${port}`,
119
+ requests,
120
+ stop: () => new Promise(done => server.close(done))
121
+ });
122
+ });
123
+ });
124
+ }
125
+
126
+ function spawnBridge({ bridgePort, remoteHubPort, workspacePath, authRoot, label, supabaseUrl = '', follower = false }) {
127
+ const child = spawn(process.execPath, ['server.js'], {
128
+ cwd: LOCAL_BRIDGE_DIR,
129
+ stdio: ['ignore', 'pipe', 'pipe'],
130
+ windowsHide: true,
131
+ env: {
132
+ ...process.env,
133
+ BRIDGE_PORT: String(bridgePort),
134
+ BRIDGE_TOKEN,
135
+ BRIDGE_REQUIRE_TOKEN: '1',
136
+ MINDEXEC_REMOTE_HUB: '1',
137
+ REMOTE_HUB_HOST: '127.0.0.1',
138
+ REMOTE_HUB_PORT: String(remoteHubPort),
139
+ REMOTE_HUB_PAIR_TOKEN: PAIR_TOKEN,
140
+ WORKSPACE_PATH: workspacePath,
141
+ MINDEXEC_AUTH_DATA_ROOT: authRoot,
142
+ MINDEXEC_REMOTE_REGISTRY_FOLLOWER: follower ? '1' : '0',
143
+ MINDEXEC_REMOTE_REGISTRY_POLL_MS: '500',
144
+ MINDEXEC_REMOTE_REGISTRY_FAST_RETRY_MS: '250',
145
+ SUPABASE_URL: supabaseUrl,
146
+ SUPABASE_KEY,
147
+ NO_COLOR: '1'
148
+ }
149
+ });
150
+
151
+ let stdout = '';
152
+ let stderr = '';
153
+ child.stdout.on('data', chunk => {
154
+ stdout += chunk.toString();
155
+ });
156
+ child.stderr.on('data', chunk => {
157
+ stderr += chunk.toString();
158
+ });
159
+
160
+ const exitPromise = new Promise(resolve => child.once('exit', resolve));
161
+ const details = () => `${label} stdout=${stdout}\n${label} stderr=${stderr}`;
162
+ const stop = async () => {
163
+ if (child.exitCode === null && !child.killed) {
164
+ child.kill('SIGTERM');
165
+ await Promise.race([exitPromise, wait(5000)]);
166
+ if (child.exitCode === null && !child.killed) {
167
+ child.kill('SIGKILL');
168
+ }
169
+ }
170
+ };
171
+
172
+ return {
173
+ baseUrl: `http://127.0.0.1:${bridgePort}`,
174
+ child,
175
+ details,
176
+ stop
177
+ };
178
+ }
179
+
180
+ async function waitForBridge(bridge) {
181
+ return await waitFor(async () => {
182
+ const result = await fetchJson(`${bridge.baseUrl}/api/status`);
183
+ return result.ok && result.payload?.status === 'ok' ? result.payload : null;
184
+ }, 30000, `bridge startup\n${bridge.details()}`);
185
+ }
186
+
187
+ async function writeSession(authRoot) {
188
+ await mkdir(authRoot, { recursive: true });
189
+ await writeFile(path.join(authRoot, 'supabase-session.json'), JSON.stringify({
190
+ access_token: ACCESS_TOKEN,
191
+ expires_at: Math.floor(Date.now() / 1000) + 3600,
192
+ user: {
193
+ id: USER_ID,
194
+ email: 'remote-registry-follower@example.test'
195
+ }
196
+ }), 'utf8');
197
+ }
198
+
199
+ async function waitForManagedAgent(bridge, managerEndpoint, label) {
200
+ return await waitFor(async () => {
201
+ const result = await fetchJson(`${bridge.baseUrl}/api/remote/agent/status`);
202
+ const agent = result.payload;
203
+ return agent?.running === true
204
+ && agent?.ready === true
205
+ && String(agent.manager || '') === managerEndpoint
206
+ ? agent
207
+ : null;
208
+ }, 20000, `${label} managed agent\n${bridge.details()}`);
209
+ }
210
+
211
+ async function waitForConnectedDevice(bridge, label) {
212
+ return await waitFor(async () => {
213
+ const result = await fetchJson(`${bridge.baseUrl}/api/remote/devices`);
214
+ return result.payload?.devices?.find(device => device.connected === true) || null;
215
+ }, 12000, `${label} connected device\n${bridge.details()}`);
216
+ }
217
+
218
+ async function main() {
219
+ const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'mindexec-remote-registry-smoke-'));
220
+ let fakeSupabase = null;
221
+ let hostA = null;
222
+ let hostB = null;
223
+ let client = null;
224
+
225
+ const hostABridgePort = await findFreePort();
226
+ const hostARemotePort = await findFreePort();
227
+ const hostBBridgePort = await findFreePort();
228
+ const hostBRemotePort = await findFreePort();
229
+ const clientBridgePort = await findFreePort();
230
+ const clientRemotePort = await findFreePort();
231
+ const stalePort = await findFreePort();
232
+ const hostAEndpoint = `127.0.0.1:${hostARemotePort}`;
233
+ const hostBEndpoint = `127.0.0.1:${hostBRemotePort}`;
234
+ const staleEndpoint = `127.0.0.1:${stalePort}`;
235
+ let currentTarget = createRegistryTarget({
236
+ endpoint: hostAEndpoint,
237
+ endpointCandidates: [staleEndpoint, hostAEndpoint],
238
+ leaseId: 'lease-a'
239
+ });
240
+
241
+ try {
242
+ fakeSupabase = await startFakeSupabase(() => currentTarget);
243
+
244
+ const hostAAuth = path.join(tempRoot, 'auth-host-a');
245
+ const hostBAuth = path.join(tempRoot, 'auth-host-b');
246
+ const clientAuth = path.join(tempRoot, 'auth-client');
247
+ await writeSession(clientAuth);
248
+
249
+ hostA = spawnBridge({
250
+ bridgePort: hostABridgePort,
251
+ remoteHubPort: hostARemotePort,
252
+ workspacePath: path.join(tempRoot, 'host-a'),
253
+ authRoot: hostAAuth,
254
+ label: 'host-a'
255
+ });
256
+ hostB = spawnBridge({
257
+ bridgePort: hostBBridgePort,
258
+ remoteHubPort: hostBRemotePort,
259
+ workspacePath: path.join(tempRoot, 'host-b'),
260
+ authRoot: hostBAuth,
261
+ label: 'host-b'
262
+ });
263
+ await waitForBridge(hostA);
264
+ await waitForBridge(hostB);
265
+
266
+ client = spawnBridge({
267
+ bridgePort: clientBridgePort,
268
+ remoteHubPort: clientRemotePort,
269
+ workspacePath: path.join(tempRoot, 'client'),
270
+ authRoot: clientAuth,
271
+ label: 'client',
272
+ supabaseUrl: fakeSupabase.url,
273
+ follower: true
274
+ });
275
+ await waitForBridge(client);
276
+
277
+ const firstAgent = await waitForManagedAgent(client, hostAEndpoint, 'host-a');
278
+ assert.equal(firstAgent.usingNpx, false, JSON.stringify(firstAgent));
279
+ assert.match(String(firstAgent.launcher || ''), /mindexec-remote-fast/i);
280
+ assert.equal(firstAgent.manager, hostAEndpoint);
281
+ assert.equal(firstAgent.managerCandidates?.[0], staleEndpoint, JSON.stringify(firstAgent.managerCandidates));
282
+ await waitForConnectedDevice(hostA, 'host-a');
283
+
284
+ currentTarget = createRegistryTarget({
285
+ endpoint: hostBEndpoint,
286
+ endpointCandidates: [hostBEndpoint],
287
+ leaseId: 'lease-b'
288
+ });
289
+
290
+ const secondAgent = await waitForManagedAgent(client, hostBEndpoint, 'host-b');
291
+ assert.equal(secondAgent.usingNpx, false, JSON.stringify(secondAgent));
292
+ assert.match(String(secondAgent.launcher || ''), /mindexec-remote-fast/i);
293
+ assert.equal(secondAgent.manager, hostBEndpoint);
294
+ assert.equal(secondAgent.leaseId, 'lease-b');
295
+ await waitForConnectedDevice(hostB, 'host-b');
296
+
297
+ currentTarget = {
298
+ ...currentTarget,
299
+ active: false,
300
+ expires_at: new Date(Date.now() - 1000).toISOString()
301
+ };
302
+
303
+ await waitFor(async () => {
304
+ const result = await fetchJson(`${client.baseUrl}/api/remote/agent/status`);
305
+ return result.payload?.running === false
306
+ && String(result.payload?.lastError || '') === 'registry-inactive'
307
+ ? result.payload
308
+ : null;
309
+ }, 15000, `registry inactive stop\n${client.details()}`);
310
+
311
+ assert.ok(fakeSupabase.requests.length >= 2, JSON.stringify(fakeSupabase.requests));
312
+ console.log('Remote registry follower smoke OK');
313
+ } finally {
314
+ if (client) await client.stop();
315
+ if (hostB) await hostB.stop();
316
+ if (hostA) await hostA.stop();
317
+ if (fakeSupabase) await fakeSupabase.stop();
318
+ await rm(tempRoot, { recursive: true, force: true });
319
+ }
320
+ }
321
+
322
+ await main();