@deeeed/metamask-harness 0.17.3 → 0.17.5

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 (114) hide show
  1. package/CHANGELOG.md +6 -96
  2. package/README.md +136 -96
  3. package/adapters/extension/console-tail.mjs +24 -51
  4. package/adapters/extension/ensure-browser.sh +7 -1
  5. package/adapters/extension/inject.mjs +15 -5
  6. package/adapters/extension/launch-browser.cjs +4 -3
  7. package/adapters/extension/launch.sh +11 -8
  8. package/adapters/extension/lib/chrome-args.cjs +24 -0
  9. package/adapters/extension/live.sh +1 -2
  10. package/adapters/extension/reattach.sh +0 -1
  11. package/adapters/extension/seed-fixture.sh +12 -4
  12. package/adapters/extension/sidepanel-toggle.sh +1 -4
  13. package/adapters/extension/start-watch.sh +4 -17
  14. package/adapters/extension/stop-viewers.sh +1 -1
  15. package/adapters/extension/verify.sh +1 -1
  16. package/adapters/manifest.json +45 -53
  17. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +0 -16
  18. package/adapters/mobile/bridge-runtime/console-forwarder.cjs +1 -1
  19. package/adapters/mobile/bridge-runtime/lib/match-bridge-target.cjs +1 -3
  20. package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +3 -2
  21. package/adapters/mobile/cleanup.sh +176 -16
  22. package/adapters/mobile/inject.sh +671 -90
  23. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +185 -0
  24. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +1662 -0
  25. package/adapters/mobile/overlay/compat/README.md +28 -0
  26. package/adapters/mobile/overlay/compat/rn81-message-event-source.patch +42 -0
  27. package/adapters/mobile/start-metro.sh +41 -0
  28. package/adapters/mobile/verify.sh +61 -4
  29. package/adapters/mobile/wait-for-bridge.sh +8 -22
  30. package/adapters/shared/harness-source-fingerprint.mjs +49 -0
  31. package/adapters/shared/install-repo-deps.sh +1 -5
  32. package/adapters/shared/open-debug.mjs +184 -103
  33. package/adapters/shared/resolve-farmslot-ports-core.mjs +3 -0
  34. package/adapters/shared/resolve-farmslot-ports.mjs +5 -0
  35. package/adapters/shared/resolve-farmslot-ports.sh +7 -0
  36. package/adapters/shared/resolve-slot-ports-core.mjs +6 -0
  37. package/adapters/shared/resolve-slot-ports.sh +7 -2
  38. package/bin/mm-harness +0 -7
  39. package/dist/adapters/extension/harness-freshness.js +39 -0
  40. package/dist/adapters/extension/runtime-decision.js +4 -73
  41. package/dist/adapters/extension/surface.js +2 -3
  42. package/dist/adapters/mobile/perps-env.js +3 -43
  43. package/dist/adapters/mobile/prepare.js +7 -22
  44. package/dist/adapters/mobile/source-freshness.js +26 -41
  45. package/dist/adapters/mobile/surface.js +1 -1
  46. package/dist/adapters/resolve-slot-ports.js +2 -0
  47. package/dist/adapters/slot-ports.js +0 -16
  48. package/dist/adapters.js +5 -12
  49. package/dist/cli-commands.js +1 -1
  50. package/dist/cli.js +12 -6
  51. package/dist/commands/call.js +22 -56
  52. package/dist/commands/check.js +3 -9
  53. package/dist/commands/checklist.js +4 -1
  54. package/dist/commands/debug.js +31 -38
  55. package/dist/commands/device-target.js +12 -27
  56. package/dist/commands/doctor.js +23 -77
  57. package/dist/commands/fixtures.js +17 -27
  58. package/dist/commands/launch/extension.js +2 -0
  59. package/dist/commands/launch/index.js +59 -167
  60. package/dist/commands/logs.js +6 -8
  61. package/dist/commands/manifest.js +25 -151
  62. package/dist/commands/parse-args.js +0 -2
  63. package/dist/commands/provision.js +3 -10
  64. package/dist/commands/run-engine.js +69 -71
  65. package/dist/commands/run-report.js +3 -12
  66. package/dist/commands/run.js +35 -245
  67. package/dist/commands/shared.js +3 -55
  68. package/dist/commands/status-probe.js +1 -1
  69. package/dist/commands/status.js +1 -1
  70. package/dist/commands/stop.js +2 -7
  71. package/dist/commands/update.js +28 -16
  72. package/dist/doctor.js +4 -1
  73. package/dist/harness.js +134 -43
  74. package/dist/heal-bounds.js +1 -1
  75. package/dist/live-adapter-contract.js +20 -6
  76. package/dist/mm-harness-cli.js +48 -123
  77. package/dist/paths.js +5 -2
  78. package/dist/runner.js +2 -37
  79. package/dist/runtime-context.js +8 -26
  80. package/docs/ADAPTER-SURFACE.md +112 -19
  81. package/docs/CHEATSHEET.md +52 -39
  82. package/docs/CLI-SPEC.md +1098 -0
  83. package/docs/CODE-MAP.md +1 -1
  84. package/docs/DEBUG-HANDOVER.md +36 -0
  85. package/docs/MENTAL-MODEL.md +295 -0
  86. package/docs/UX-PRINCIPLES.md +1 -3
  87. package/docs/architecture.md +375 -65
  88. package/docs/live-adapter-contract.md +2 -4
  89. package/docs/recipe-libraries.md +275 -45
  90. package/library/README.md +0 -4
  91. package/library/actions/core/perps/_controller.mjs +55 -10
  92. package/library/actions/mobile/platform/bridge.mjs +1 -5
  93. package/library/manifests/core.action-manifest.json +7 -7
  94. package/library/manifests/mobile.action-manifest.json +3 -28
  95. package/package.json +1 -4
  96. package/adapters/extension/launch-webpack.cjs +0 -83
  97. package/adapters/extension/stamp-runtime-title.cjs +0 -49
  98. package/adapters/extension/sync-webpack-dist.cjs +0 -105
  99. package/adapters/mobile/launch-console-forwarder.cjs +0 -39
  100. package/adapters/mobile/start-console-forwarder.sh +0 -70
  101. package/adapters/shared/update-check-worker.mjs +0 -35
  102. package/dist/adapters/extension/console-capture.js +0 -93
  103. package/dist/command-contract.js +0 -441
  104. package/dist/command-journal.js +0 -225
  105. package/dist/commands/last.js +0 -52
  106. package/dist/json-stream.js +0 -57
  107. package/dist/run-diagnostics.js +0 -261
  108. package/docs/CLI-ERGONOMICS-AUDIT.md +0 -32
  109. package/docs/CLI-ERGONOMICS-HUMAN-QA.md +0 -64
  110. package/docs/VIDEO-DEMO-VALIDATION.md +0 -74
  111. package/library/recipes/runner/smoke.core.recipe.json +0 -27
  112. package/library/recipes/wallet/smoke.extension.recipe.json +0 -42
  113. package/library/recipes/wallet/smoke.mobile.recipe.json +0 -42
  114. package/scripts/validate-human-outcomes.mjs +0 -169
@@ -3,10 +3,11 @@
3
3
  * open-debug.mjs — open human-facing debugger UIs for recipe runtimes.
4
4
  *
5
5
  * Mobile: Metro /open-debugger or /open-dev-menu HTTP endpoints.
6
- * Extension: DevTools in the slot-owned Chrome process, attached to the exact target.
6
+ * Extension: Chrome DevTools frontend for an extension page or service worker.
7
7
  */
8
8
  import { spawnSync } from 'node:child_process';
9
- import WebSocket from 'ws';
9
+ import http from 'node:http';
10
+ import { createRequire } from 'node:module';
10
11
 
11
12
  function usage() {
12
13
  console.error(`Usage:
@@ -97,8 +98,8 @@ async function fetchOk(url, init) {
97
98
  return response.ok;
98
99
  }
99
100
 
100
- async function fetchJson(url, init) {
101
- const response = await fetch(url, init);
101
+ async function fetchJson(url) {
102
+ const response = await fetch(url);
102
103
  if (!response.ok) {
103
104
  throw new Error(`HTTP ${response.status} for ${url}`);
104
105
  }
@@ -106,52 +107,109 @@ async function fetchJson(url, init) {
106
107
  return body;
107
108
  }
108
109
 
109
- function sendBrowserCommand(webSocketDebuggerUrl, method, params) {
110
+ // Probe whether the debug server will accept a browser DevTools frontend
111
+ // WebSocket for `targetId` under `origin`. Chrome 403s every browser Origin
112
+ // unless the slot launched with --remote-allow-origins for it; a slot launched
113
+ // before that flag existed fails here, so we can tell the user to relaunch
114
+ // instead of opening a window that shows "Debugging connection was closed".
115
+ // Transient: the socket is destroyed the instant the handshake resolves.
116
+ function probeAttach(port, targetId, origin) {
110
117
  return new Promise((resolve) => {
111
- const socket = new WebSocket(webSocketDebuggerUrl);
112
- const timer = setTimeout(() => finish({ error: { message: `${method} timed out` } }), 5000);
113
- const finish = (result) => {
114
- clearTimeout(timer);
115
- socket.close();
116
- resolve(result);
117
- };
118
- socket.once('open', () => {
119
- socket.send(JSON.stringify({
120
- id: 1,
121
- method,
122
- params,
123
- }));
118
+ const req = http.request({
119
+ host: '127.0.0.1',
120
+ port,
121
+ path: `/devtools/page/${targetId}`,
122
+ headers: {
123
+ Connection: 'Upgrade',
124
+ Upgrade: 'websocket',
125
+ 'Sec-WebSocket-Version': '13',
126
+ 'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
127
+ Origin: origin,
128
+ },
124
129
  });
125
- socket.on('message', (raw) => {
126
- let message;
130
+ let settled = false;
131
+ const finish = (value) => {
132
+ if (settled) return;
133
+ settled = true;
127
134
  try {
128
- message = JSON.parse(String(raw));
135
+ req.destroy();
129
136
  } catch {
130
- finish({ error: { message: `${method} returned a non-JSON WebSocket frame` } });
131
- return;
137
+ // Best-effort teardown of the probe socket.
132
138
  }
133
- if (message.id !== 1) return;
134
- finish(message);
135
- });
136
- socket.once('error', () => finish({ error: { message: `${method} WebSocket failed` } }));
139
+ resolve(value);
140
+ };
141
+ req.on('upgrade', () => finish({ ok: true }));
142
+ req.on('response', (res) => finish({ ok: false, status: res.statusCode }));
143
+ req.on('error', (err) => finish({ ok: false, error: err.message }));
144
+ req.setTimeout(3000, () => finish({ ok: false, error: 'timeout' }));
145
+ req.end();
137
146
  });
138
147
  }
139
148
 
140
- async function openNativeDevtools(cdpPort, browserWebSocketUrl, target) {
141
- const response = await sendBrowserCommand(browserWebSocketUrl, 'Target.openDevTools', {
142
- targetId: target.id,
143
- panelId: 'console',
144
- });
145
- if (response.error) throw new Error(response.error.message);
146
- const devtoolsId = response.result?.targetId;
147
- if (!devtoolsId) throw new Error('Chrome did not return a native DevTools target');
148
- for (let attempt = 0; attempt < 20; attempt += 1) {
149
- const targets = await fetchJson(`http://127.0.0.1:${cdpPort}/json/list`);
150
- const devtools = targets.find((candidate) => candidate.id === devtoolsId);
151
- if (devtools && String(devtools.url || '').startsWith('devtools://devtools/')) return devtools;
152
- await new Promise((resolve) => setTimeout(resolve, 100));
149
+ function resolveWebSocketImpl() {
150
+ if (typeof WebSocket === 'function') return WebSocket;
151
+ try {
152
+ const require = createRequire(import.meta.url);
153
+ return require(require.resolve('ws', { paths: [process.cwd()] }));
154
+ } catch {
155
+ return null;
153
156
  }
154
- throw new Error('Chrome opened DevTools but did not expose its native target');
157
+ }
158
+
159
+ // Open the locally-served bundled DevTools frontend as its own window inside the
160
+ // slot's own Chromium, via a browser-level CDP call (the Node client sends no
161
+ // Origin header, so it is never origin-rejected). Returns the new target id.
162
+ async function openDevtoolsWindow(browserWsUrl, url) {
163
+ const WebSocketImpl = resolveWebSocketImpl();
164
+ if (!WebSocketImpl) throw new Error('WebSocket unavailable in this Node runtime to open the DevTools window');
165
+ return new Promise((resolve, reject) => {
166
+ const ws = new WebSocketImpl(browserWsUrl);
167
+ const timer = setTimeout(() => {
168
+ try {
169
+ ws.close();
170
+ } catch {
171
+ // Best-effort cleanup on timeout.
172
+ }
173
+ reject(new Error('timeout creating the DevTools window over CDP'));
174
+ }, 8000);
175
+ const onOpen = () => {
176
+ ws.send(JSON.stringify({
177
+ id: 1,
178
+ method: 'Target.createTarget',
179
+ params: { url, newWindow: true, width: 1200, height: 800 },
180
+ }));
181
+ };
182
+ const onMessage = (event) => {
183
+ const raw = event?.data ?? event;
184
+ let msg;
185
+ try {
186
+ msg = JSON.parse(Buffer.isBuffer(raw) ? raw.toString('utf8') : String(raw));
187
+ } catch {
188
+ return;
189
+ }
190
+ if (msg.id !== 1) return;
191
+ clearTimeout(timer);
192
+ ws.close();
193
+ if (msg.error) {
194
+ reject(new Error(msg.error.message || 'Target.createTarget failed'));
195
+ return;
196
+ }
197
+ resolve(msg.result?.targetId || null);
198
+ };
199
+ const onError = (err) => {
200
+ clearTimeout(timer);
201
+ reject(new Error(`browser CDP websocket error while opening DevTools: ${err?.message || err || 'unknown'}`));
202
+ };
203
+ if (typeof ws.on === 'function') {
204
+ ws.on('open', onOpen);
205
+ ws.on('message', onMessage);
206
+ ws.on('error', onError);
207
+ } else {
208
+ ws.addEventListener('open', onOpen);
209
+ ws.addEventListener('message', onMessage);
210
+ ws.addEventListener('error', onError);
211
+ }
212
+ });
155
213
  }
156
214
 
157
215
  // CDP-eval fallback: invoke DevSettings native module directly via the bridge
@@ -256,8 +314,7 @@ function pickExtensionTarget(targets, targetKind) {
256
314
 
257
315
  const pages = extensionTargets.filter((target) => target.type === 'page');
258
316
  return (
259
- pages.find((target) => String(target.url || '').includes('/sidepanel.html'))
260
- || pages.find((target) => String(target.url || '').includes('/home.html'))
317
+ pages.find((target) => String(target.url || '').includes('/home.html'))
261
318
  || pages.find((target) => !String(target.url || '').includes('/popup-init.html'))
262
319
  || pages[0]
263
320
  || null
@@ -270,7 +327,6 @@ async function openExtensionDevtools(cdpPort, targetKind, shouldOpen) {
270
327
  return { ok: false, adapter: 'extension', error: 'invalid-cdp-port', cdpPort };
271
328
  }
272
329
 
273
- const inspectUrl = 'chrome://inspect/#devices';
274
330
  const configureHint = `127.0.0.1:${numericPort}`;
275
331
  const versionEndpoint = `http://127.0.0.1:${numericPort}/json/version`;
276
332
  const listEndpoint = `http://127.0.0.1:${numericPort}/json/list`;
@@ -286,9 +342,8 @@ async function openExtensionDevtools(cdpPort, targetKind, shouldOpen) {
286
342
  cdpPort: numericPort,
287
343
  error: 'cdp-unreachable',
288
344
  endpoint: versionEndpoint,
289
- inspectUrl,
290
345
  configureHint,
291
- hint: `Start the extension runtime first (mm-harness launch). Then open chrome://inspect and add ${configureHint}.`,
346
+ hint: 'Start the extension runtime first (mm-harness launch).',
292
347
  detail: error instanceof Error ? error.message : String(error),
293
348
  };
294
349
  }
@@ -304,9 +359,8 @@ async function openExtensionDevtools(cdpPort, targetKind, shouldOpen) {
304
359
  cdpPort: numericPort,
305
360
  error: 'cdp-list-failed',
306
361
  endpoint: listEndpoint,
307
- inspectUrl,
308
362
  configureHint,
309
- hint: `CDP is up but /json/list failed. Open chrome://inspect and add ${configureHint}.`,
363
+ hint: 'CDP is up but /json/list failed. Relaunch the runtime (mm-harness launch).',
310
364
  detail: error instanceof Error ? error.message : String(error),
311
365
  };
312
366
  }
@@ -319,80 +373,107 @@ async function openExtensionDevtools(cdpPort, targetKind, shouldOpen) {
319
373
  cdpPort: numericPort,
320
374
  error: 'cdp-list-invalid',
321
375
  endpoint: listEndpoint,
322
- inspectUrl,
323
376
  configureHint,
324
- hint: `Open chrome://inspect and add ${configureHint}.`,
377
+ hint: 'Relaunch the runtime (mm-harness launch).',
325
378
  };
326
379
  }
327
380
 
328
381
  const picked = pickExtensionTarget(targets, targetKind);
329
- if (picked?.id && shouldOpen) {
330
- let devtoolsTarget;
331
- try {
332
- if (!version.webSocketDebuggerUrl) throw new Error('Chrome did not expose its browser debugger endpoint');
333
- devtoolsTarget = await openNativeDevtools(numericPort, version.webSocketDebuggerUrl, picked);
334
- } catch (error) {
335
- return {
336
- ok: false,
337
- adapter: 'extension',
338
- targetKind,
339
- cdpPort: numericPort,
340
- error: 'debugger-open-failed',
341
- endpoint: listEndpoint,
342
- target: { type: picked.type, url: picked.url, title: picked.title || null },
343
- hint: 'The extension target is healthy, but its debugger window could not be opened. Run mm-harness debug --no-open --json to inspect the resolved target.',
344
- detail: error instanceof Error ? error.message : String(error),
345
- };
346
- }
382
+ if (!picked) {
383
+ return {
384
+ ok: false,
385
+ adapter: 'extension',
386
+ targetKind,
387
+ cdpPort: numericPort,
388
+ error: 'no-target',
389
+ endpoint: listEndpoint,
390
+ configureHint,
391
+ hint: 'No extension targets are registered yet. Start the runtime (mm-harness launch).',
392
+ };
393
+ }
394
+
395
+ // Locally-served bundled DevTools frontend (offline, same host:port as the ws
396
+ // target so its Origin is allowed). Built from the target id Chrome omits
397
+ // webSocketDebuggerUrl for already-attached targets, but the id is always present.
398
+ const wsRef = `127.0.0.1:${numericPort}/devtools/page/${picked.id}`;
399
+ const devtoolsUrl = `http://127.0.0.1:${numericPort}/devtools/inspector.html?ws=${wsRef}`;
400
+ const targetInfo = { type: picked.type, url: picked.url, title: picked.title || null };
401
+
402
+ if (!shouldOpen) {
347
403
  return {
348
404
  ok: true,
349
405
  adapter: 'extension',
350
406
  targetKind,
351
- method: 'native-devtools',
352
- endpoint: devtoolsTarget.url,
407
+ method: 'devtools-window',
408
+ endpoint: devtoolsUrl,
353
409
  cdpPort: numericPort,
354
- target: {
355
- type: picked.type,
356
- url: picked.url,
357
- title: picked.title || null,
358
- },
359
- debuggerTargetId: devtoolsTarget.id,
360
- attached: true,
361
- opened: true,
410
+ target: targetInfo,
411
+ devtoolsUrl,
412
+ opened: false,
413
+ detail: 'Open action not invoked (--no-open).',
362
414
  };
363
415
  }
364
416
 
365
- if (shouldOpen) return {
366
- ok: false,
367
- adapter: 'extension',
368
- targetKind,
369
- error: picked ? 'devtools-url-unavailable' : 'extension-target-unavailable',
370
- endpoint: listEndpoint,
371
- cdpPort: numericPort,
372
- target: picked
373
- ? { type: picked.type, url: picked.url, title: picked.title || null }
374
- : null,
375
- inspectUrl,
376
- configureHint,
377
- opened: false,
378
- hint: picked
379
- ? 'The selected extension target did not expose a DevTools URL. Relaunch it with mm-harness launch.'
380
- : 'No extension target is ready. Launch it with mm-harness launch, then retry mm-harness debug.',
381
- };
417
+ // Honest gate: confirm the debug server will accept a browser DevTools Origin
418
+ // before opening a window. A slot launched before --remote-allow-origins support
419
+ // 403s here; tell the user to relaunch rather than open a dead console.
420
+ const probe = await probeAttach(numericPort, picked.id, `http://127.0.0.1:${numericPort}`);
421
+ if (!probe.ok) {
422
+ return {
423
+ ok: false,
424
+ adapter: 'extension',
425
+ targetKind,
426
+ cdpPort: numericPort,
427
+ error: 'origin-rejected',
428
+ endpoint: devtoolsUrl,
429
+ target: targetInfo,
430
+ hint: 'This slot was launched before debugger support (Chrome rejects the DevTools WebSocket origin). Relaunch it: mm-harness launch',
431
+ detail: probe.status ? `HTTP ${probe.status}` : probe.error || 'handshake rejected',
432
+ };
433
+ }
434
+
435
+ const browserWsUrl = version?.webSocketDebuggerUrl;
436
+ if (!browserWsUrl) {
437
+ return {
438
+ ok: false,
439
+ adapter: 'extension',
440
+ targetKind,
441
+ cdpPort: numericPort,
442
+ error: 'no-browser-ws',
443
+ endpoint: devtoolsUrl,
444
+ target: targetInfo,
445
+ hint: 'CDP /json/version did not expose a browser websocket. Relaunch the runtime (mm-harness launch).',
446
+ };
447
+ }
448
+
449
+ let createdTargetId;
450
+ try {
451
+ createdTargetId = await openDevtoolsWindow(browserWsUrl, devtoolsUrl);
452
+ } catch (error) {
453
+ return {
454
+ ok: false,
455
+ adapter: 'extension',
456
+ targetKind,
457
+ cdpPort: numericPort,
458
+ error: 'devtools-window-failed',
459
+ endpoint: devtoolsUrl,
460
+ target: targetInfo,
461
+ hint: 'Could not open the DevTools window over CDP. Relaunch the runtime (mm-harness launch).',
462
+ detail: error instanceof Error ? error.message : String(error),
463
+ };
464
+ }
382
465
 
383
466
  return {
384
467
  ok: true,
385
468
  adapter: 'extension',
386
469
  targetKind,
387
- method: picked?.devtoolsFrontendUrl ? 'devtools-frontend' : 'target-list',
388
- endpoint: picked?.devtoolsFrontendUrl || listEndpoint,
470
+ method: 'devtools-window',
471
+ endpoint: devtoolsUrl,
389
472
  cdpPort: numericPort,
390
- target: picked
391
- ? { type: picked.type, url: picked.url, title: picked.title || null }
392
- : null,
393
- devtoolsUrl: picked?.devtoolsFrontendUrl || null,
394
- opened: false,
395
- detail: 'Open action not invoked (--no-open).',
473
+ target: targetInfo,
474
+ devtoolsUrl,
475
+ createdTargetId,
476
+ opened: true,
396
477
  };
397
478
  }
398
479
 
@@ -0,0 +1,3 @@
1
+ // Back-compat shim: the port-resolution core is now resolve-slot-ports-core.mjs.
2
+ // Re-exports it verbatim for one release so importers of the old path keep resolving.
3
+ export * from './resolve-slot-ports-core.mjs';
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ // Back-compat shim: this leaf is now resolve-slot-ports.mjs. Delegates to it for one
3
+ // release so `node resolve-farmslot-ports.mjs <fn> <repo>` keeps working (the new CLI
4
+ // reads the same argv positions on import).
5
+ import './resolve-slot-ports.mjs';
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env bash
2
+ # Back-compat shim: this wrapper is now resolve-slot-ports.sh. Sources it for one
3
+ # release so old callers keep the same sourced function names (including the
4
+ # resolve_farmslot_ports_by_repo alias defined there).
5
+ _SHIM_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6
+ # shellcheck disable=SC1091
7
+ . "$_SHIM_DIR/resolve-slot-ports.sh"
@@ -276,8 +276,14 @@ export function resolveMobileRuntimePorts(repo) {
276
276
  );
277
277
  }
278
278
 
279
+ // Back-compat: the previous name for resolveSlotPortsByRepo, kept as an alias for
280
+ // one release so existing importers keep resolving.
281
+ export const resolveFarmslotPortsByRepo = resolveSlotPortsByRepo;
282
+
279
283
  export const cliFns = {
280
284
  resolve_slot_ports_by_repo: resolveSlotPortsByRepo,
285
+ // Back-compat cli key for one release.
286
+ resolve_farmslot_ports_by_repo: resolveSlotPortsByRepo,
281
287
  resolve_default_extension_ports: resolveDefaultExtensionPorts,
282
288
  resolve_extension_runtime_context: resolveExtensionRuntimeContext,
283
289
  resolve_extension_slot_ports: (repo) => formatExtensionSlotPorts(repo) || null,
@@ -20,6 +20,11 @@ resolve_slot_ports_by_repo() {
20
20
  _resolve_ports_cli resolve_slot_ports_by_repo "$1"
21
21
  }
22
22
 
23
+ # Back-compat alias for the previous function name (one release).
24
+ resolve_farmslot_ports_by_repo() {
25
+ resolve_slot_ports_by_repo "$1"
26
+ }
27
+
23
28
  infer_extension_slot_suffix() {
24
29
  local repo="$1" base
25
30
  base="$(basename "$repo")"
@@ -48,7 +53,7 @@ apply_extension_slot_ports() {
48
53
  repo="$(cd "$repo" && pwd)"
49
54
  ctx_file="$repo/$runtime_rel/agentic-runtime.json"
50
55
  if [ -f "$ctx_file" ]; then
51
- export RECIPE_RUNTIME_CONTEXT="$ctx_file"
56
+ RECIPE_RUNTIME_CONTEXT="$ctx_file"
52
57
  else
53
58
  unset RECIPE_RUNTIME_CONTEXT
54
59
  fi
@@ -106,4 +111,4 @@ apply_resolved_mobile_ports() {
106
111
  ;;
107
112
  esac
108
113
  done <<< "$resolved"
109
- }
114
+ }
package/bin/mm-harness CHANGED
@@ -50,13 +50,6 @@ if [ -n "${MM_HARNESS_BIN:-}" ]; then
50
50
  fi
51
51
  fi
52
52
 
53
- # Internal executable identity (not part of the public env surface): the CLI
54
- # embeds this in emitted recovery commands so they run verbatim even when
55
- # mm-harness is not on PATH (task-local installs). Set AFTER the MM_HARNESS_BIN
56
- # delegation above so the final executable entered always overwrites any
57
- # inherited/stale value with its own resolved self path.
58
- export MM_HARNESS_EXECUTABLE="$SCRIPT_DIR/$(basename "$SOURCE")"
59
-
60
53
  ENTRY_TS="$RUNNER_DIR/src/mm-harness-cli.ts"
61
54
  ENTRY_DIST="$RUNNER_DIR/dist/mm-harness-cli.js"
62
55
 
@@ -0,0 +1,39 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ import { recipeHarnessPath, runnerDir } from "../../paths.js";
6
+ async function ensureExtensionHarnessFresh(target) {
7
+ if (!fs.existsSync(path.join(target, "package.json"))) return;
8
+ let current;
9
+ try {
10
+ const mod = await import(pathToFileURL(path.join(runnerDir, "adapters/shared/harness-source-fingerprint.mjs")).href);
11
+ current = mod.harnessSourceFingerprint(runnerDir);
12
+ } catch {
13
+ return;
14
+ }
15
+ const manifestPath = recipeHarnessPath(target, "extension", "manifest.json");
16
+ const installed = readInstalledFingerprint(manifestPath);
17
+ if (installed === current) return;
18
+ const reason = installed === null ? "harness not installed" : "runner changed";
19
+ try {
20
+ execFileSync(process.execPath, [path.join(runnerDir, "adapters/extension/inject.mjs"), "--target", target], {
21
+ stdio: ["ignore", "ignore", "inherit"]
22
+ });
23
+ console.error(`\u2192 refreshed extension harness overlay (${reason})`);
24
+ } catch (error) {
25
+ const detail = error instanceof Error ? `: ${error.message}` : "";
26
+ throw new Error(`could not refresh the extension harness overlay (${reason})${detail}`);
27
+ }
28
+ }
29
+ function readInstalledFingerprint(manifestPath) {
30
+ try {
31
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
32
+ return typeof manifest.sourceFingerprint === "string" ? manifest.sourceFingerprint : null;
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+ export {
38
+ ensureExtensionHarnessFresh
39
+ };
@@ -163,80 +163,18 @@ function runtimeDistCheck(target) {
163
163
  let output;
164
164
  try {
165
165
  output = execFileSync("rsync", [
166
- "-rni",
166
+ "-rnic",
167
167
  "--exclude",
168
168
  "_metadata",
169
169
  "--out-format=%n",
170
170
  `${dist}${path.sep}`,
171
171
  `${runtimeDist}${path.sep}`
172
172
  ], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
173
- } catch (error) {
174
- if (error.code === "ENOENT") {
175
- try {
176
- const modified2 = filesystemSnapshotDiff(dist, runtimeDist).filter((relative) => !slotTitleOnlyDifference(dist, runtimeDist, relative)).slice(0, 10);
177
- return modified2.length > 0 ? { status: "stale", comparison: "filesystem-quick", modified: modified2 } : { status: "fresh", comparison: "filesystem-quick" };
178
- } catch (fallbackError) {
179
- return {
180
- status: "unknown",
181
- error: `snapshot comparison failed: ${fallbackError instanceof Error ? fallbackError.message : String(fallbackError)}`
182
- };
183
- }
184
- }
185
- const status = error.status;
186
- return {
187
- status: "unknown",
188
- error: `rsync snapshot comparison failed${typeof status === "number" ? ` with exit ${status}` : ""}`
189
- };
190
- }
191
- const modified = output.split("\n").map((line) => line.trim()).filter(Boolean).filter((relative) => !slotTitleOnlyDifference(dist, runtimeDist, relative)).slice(0, 10);
192
- return modified.length > 0 ? { status: "stale", comparison: "rsync-quick", modified } : { status: "fresh", comparison: "rsync-quick" };
193
- }
194
- function slotTitleOnlyDifference(dist, runtimeDist, relative) {
195
- if (relative !== "home.html" && relative !== "sidepanel.html") return false;
196
- try {
197
- const normalize = (source) => source.replace(
198
- /<title>([^<]*)<\/title>/iu,
199
- (_match, title) => `<title>${String(title || "MetaMask").replace(/^.+?\s+[—–-]\s+/u, "") || "MetaMask"}</title>`
200
- );
201
- return normalize(fs.readFileSync(path.join(dist, relative), "utf8")) === normalize(fs.readFileSync(path.join(runtimeDist, relative), "utf8"));
202
173
  } catch {
203
- return false;
174
+ return { status: "stale", modified: ["<snapshot comparison failed>"] };
204
175
  }
205
- }
206
- function filesystemSnapshotDiff(source, snapshot) {
207
- const sourceEntries = snapshotEntries(source);
208
- const snapshotEntriesByPath = snapshotEntries(snapshot);
209
- const paths = [.../* @__PURE__ */ new Set([...sourceEntries.keys(), ...snapshotEntriesByPath.keys()])].sort();
210
- return paths.filter((relative) => {
211
- const left = sourceEntries.get(relative);
212
- const right = snapshotEntriesByPath.get(relative);
213
- return left === void 0 || right === void 0 || JSON.stringify(left) !== JSON.stringify(right);
214
- });
215
- }
216
- function snapshotEntries(root) {
217
- const entries = /* @__PURE__ */ new Map();
218
- const walk2 = (directory) => {
219
- for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
220
- if (entry.name === "_metadata") continue;
221
- const absolute = path.join(directory, entry.name);
222
- const relative = path.relative(root, absolute);
223
- const stat = fs.lstatSync(absolute);
224
- if (entry.isDirectory()) {
225
- entries.set(relative, { type: "directory" });
226
- walk2(absolute);
227
- } else if (entry.isSymbolicLink()) {
228
- entries.set(relative, { type: "symlink", link: fs.readlinkSync(absolute) });
229
- } else if (entry.isFile()) {
230
- entries.set(relative, {
231
- type: "file",
232
- size: stat.size,
233
- mtimeSeconds: Math.floor(stat.mtimeMs / 1e3)
234
- });
235
- }
236
- }
237
- };
238
- walk2(root);
239
- return entries;
176
+ const modified = output.split("\n").map((line) => line.trim()).filter(Boolean).slice(0, 10);
177
+ return modified.length > 0 ? { status: "stale", modified } : { status: "fresh" };
240
178
  }
241
179
  function distCheck(target) {
242
180
  const manifestPath = path.join(target, "dist/chrome/manifest.json");
@@ -346,13 +284,6 @@ async function decideExtensionReadiness(target, options = {}) {
346
284
  reasons: [dist.reason === "uncommitted-source" ? `Uncommitted source since the build (${dist.modified?.length ?? 0} file(s)); rebuild.` : `dist git id ${dist.distGitId} != HEAD ${dist.head}; rebuild.`],
347
285
  actions: buildAction(resolved, false)
348
286
  },
349
- {
350
- when: runtimeDist.status === "unknown",
351
- decision: "blocked",
352
- reasonCode: "runtime-dist-check-failed",
353
- reasons: [runtimeDist.error ?? "The loaded runtime snapshot could not be compared with dist/chrome."],
354
- actions: []
355
- },
356
287
  {
357
288
  when: runtimeDist.status !== "fresh",
358
289
  decision: "relaunch",
@@ -65,12 +65,11 @@ const extensionSurface = {
65
65
  return [
66
66
  { label: "webpack", path: recipeRuntimePath(target, "webpack.log") },
67
67
  { label: "watcher", path: recipeRuntimePath(target, "recipe-harness-webpack.log") },
68
- { label: "rebuild", path: recipeRuntimePath(target, "rebuild.log") },
69
- { label: "dapp", path: recipeRuntimePath(target, "dapp-console.log") }
68
+ { label: "rebuild", path: recipeRuntimePath(target, "rebuild.log") }
70
69
  ];
71
70
  },
72
71
  appLogSource(target) {
73
- return { label: "extension", path: recipeRuntimePath(target, "extension-console.log") };
72
+ return { label: "app", path: recipeRuntimePath(target, "extension-console.log") };
74
73
  },
75
74
  hints: {
76
75
  launch: "mm-harness launch",