@deeeed/metamask-harness 0.17.0 → 0.17.2

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 (106) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +90 -140
  3. package/adapters/core/cleanup.sh +0 -0
  4. package/adapters/core/inject.sh +0 -0
  5. package/adapters/extension/cleanup.mjs +0 -0
  6. package/adapters/extension/console-tail.mjs +51 -24
  7. package/adapters/extension/ensure-browser.sh +0 -0
  8. package/adapters/extension/inject.mjs +5 -9
  9. package/adapters/extension/launch-browser.cjs +0 -0
  10. package/adapters/extension/launch-webpack.cjs +83 -0
  11. package/adapters/extension/launch.sh +8 -11
  12. package/adapters/extension/live.sh +2 -0
  13. package/adapters/extension/readiness.mjs +0 -0
  14. package/adapters/extension/reattach.sh +1 -0
  15. package/adapters/extension/refresh-build.sh +0 -0
  16. package/adapters/extension/seed-fixture.sh +4 -12
  17. package/adapters/extension/sidepanel-toggle.sh +4 -1
  18. package/adapters/extension/snapshot-dist.sh +0 -0
  19. package/adapters/extension/stamp-runtime-title.cjs +49 -0
  20. package/adapters/extension/start-watch.sh +16 -4
  21. package/adapters/extension/stop-viewers.sh +1 -1
  22. package/adapters/extension/sync-webpack-dist.cjs +105 -0
  23. package/adapters/extension/verify.sh +1 -1
  24. package/adapters/extension/wallet-fixture-state.cjs +0 -0
  25. package/adapters/manifest.json +46 -38
  26. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +0 -0
  27. package/adapters/mobile/bridge-runtime/console-forwarder.cjs +1 -1
  28. package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +2 -3
  29. package/adapters/mobile/bridge-runtime/setup-wallet.sh +0 -0
  30. package/adapters/mobile/cleanup.sh +16 -176
  31. package/adapters/mobile/inject.sh +88 -671
  32. package/adapters/mobile/launch-console-forwarder.cjs +39 -0
  33. package/adapters/mobile/lib/metro-listener.sh +0 -0
  34. package/adapters/mobile/lib/tmux-viewer.sh +0 -0
  35. package/adapters/mobile/open-device.sh +0 -0
  36. package/adapters/mobile/prewarm-bundle.sh +0 -0
  37. package/adapters/mobile/start-console-forwarder.sh +70 -0
  38. package/adapters/mobile/start-metro.sh +0 -41
  39. package/adapters/mobile/stop-metro.sh +0 -0
  40. package/adapters/mobile/verify.sh +4 -61
  41. package/adapters/mobile/wait-for-bridge.sh +0 -0
  42. package/adapters/mobile/yarn-setup.sh +0 -0
  43. package/adapters/shared/activate-repo-node.sh +0 -0
  44. package/adapters/shared/activate-repo-ruby.sh +0 -0
  45. package/adapters/shared/cli-ux.sh +0 -0
  46. package/adapters/shared/ensure-runner-deps.sh +0 -0
  47. package/adapters/shared/harness-path.sh +0 -0
  48. package/adapters/shared/hash-helpers.sh +0 -0
  49. package/adapters/shared/install-repo-deps.sh +0 -0
  50. package/adapters/shared/json-field.sh +0 -0
  51. package/adapters/shared/open-debug.mjs +90 -36
  52. package/adapters/shared/open-log-window.sh +0 -0
  53. package/adapters/shared/reap-checkout-metros.sh +0 -0
  54. package/adapters/shared/resolve-slot-ports-core.mjs +0 -6
  55. package/adapters/shared/resolve-slot-ports.mjs +0 -0
  56. package/adapters/shared/resolve-slot-ports.sh +0 -5
  57. package/adapters/shared/sync-wallet-fixture.sh +0 -0
  58. package/adapters/shared/tmux-session.sh +0 -0
  59. package/adapters/shared/tmux-viewer.sh +0 -0
  60. package/adapters/shared/update-check-worker.mjs +37 -0
  61. package/dist/adapters/extension/console-capture.js +84 -0
  62. package/dist/adapters/extension/runtime-decision.js +14 -2
  63. package/dist/adapters/extension/surface.js +3 -2
  64. package/dist/adapters/mobile/prepare.js +22 -7
  65. package/dist/adapters/mobile/surface.js +1 -1
  66. package/dist/adapters/resolve-slot-ports.js +0 -2
  67. package/dist/adapters/slot-ports.js +8 -0
  68. package/dist/cli.js +3 -11
  69. package/dist/commands/call.js +9 -4
  70. package/dist/commands/debug.js +38 -31
  71. package/dist/commands/fixtures.js +18 -10
  72. package/dist/commands/launch/index.js +10 -0
  73. package/dist/commands/logs.js +8 -6
  74. package/dist/commands/manifest.js +5 -17
  75. package/dist/commands/run-engine.js +1 -32
  76. package/dist/commands/run-report.js +1 -1
  77. package/dist/commands/run.js +50 -3
  78. package/dist/commands/status-probe.js +1 -1
  79. package/dist/commands/update.js +15 -28
  80. package/dist/doctor.js +1 -4
  81. package/dist/harness.js +5 -123
  82. package/dist/live-adapter-contract.js +6 -20
  83. package/dist/mm-harness-cli.js +6 -43
  84. package/dist/paths.js +2 -5
  85. package/dist/run-diagnostics.js +35 -40
  86. package/dist/runtime-context.js +26 -8
  87. package/docs/CHEATSHEET.md +39 -52
  88. package/docs/CLI-ERGONOMICS-HUMAN-QA.md +48 -104
  89. package/docs/CODE-MAP.md +1 -1
  90. package/docs/VIDEO-DEMO-VALIDATION.md +62 -0
  91. package/docs/architecture.md +65 -375
  92. package/docs/live-adapter-contract.md +3 -1
  93. package/library/actions/core/perps/_controller.mjs +10 -55
  94. package/library/manifests/mobile.action-manifest.json +1 -1
  95. package/package.json +2 -2
  96. package/scripts/completions.sh +0 -0
  97. package/scripts/install-completions.sh +0 -0
  98. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +0 -185
  99. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +0 -1662
  100. package/adapters/mobile/overlay/compat/README.md +0 -28
  101. package/adapters/mobile/overlay/compat/rn81-message-event-source.patch +0 -42
  102. package/adapters/shared/resolve-farmslot-ports-core.mjs +0 -3
  103. package/adapters/shared/resolve-farmslot-ports.mjs +0 -5
  104. package/adapters/shared/resolve-farmslot-ports.sh +0 -7
  105. package/docs/CLI-SPEC.md +0 -1142
  106. package/docs/MENTAL-MODEL.md +0 -295
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+ const { spawn } = require('node:child_process');
7
+
8
+ const args = { port: '', out: '', err: '' };
9
+ for (let i = 2; i < process.argv.length; i += 1) {
10
+ const key = process.argv[i];
11
+ if (key === '--port') args.port = process.argv[++i] || '';
12
+ else if (key === '--out') args.out = process.argv[++i] || '';
13
+ else if (key === '--err') args.err = process.argv[++i] || '';
14
+ else if (key === '--help' || key === '-h') {
15
+ process.stdout.write('Usage: launch-console-forwarder.cjs --port <port> --out <app-log> --err <error-log>\n');
16
+ process.exit(0);
17
+ } else {
18
+ process.stderr.write(`launch-console-forwarder: unknown arg: ${key}\n`);
19
+ process.exit(2);
20
+ }
21
+ }
22
+
23
+ if (!args.port || !args.out || !args.err) {
24
+ process.stderr.write('launch-console-forwarder: --port, --out, and --err are required\n');
25
+ process.exit(2);
26
+ }
27
+
28
+ const forwarder = path.join(__dirname, 'bridge-runtime', 'console-forwarder.cjs');
29
+ const errFd = fs.openSync(args.err, 'a');
30
+ try {
31
+ const child = spawn(process.execPath, [forwarder, '--port', args.port, '--out', args.out], {
32
+ detached: true,
33
+ stdio: ['ignore', errFd, errFd],
34
+ });
35
+ child.unref();
36
+ process.stdout.write(`${child.pid}\n`);
37
+ } finally {
38
+ fs.closeSync(errFd);
39
+ }
File without changes
File without changes
File without changes
File without changes
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env bash
2
+ # Ensure one runner-owned CDP console forwarder is alive for this Mobile slot.
3
+ set -uo pipefail
4
+
5
+ TARGET="$PWD"
6
+ PORT="${WATCHER_PORT:-${METRO_PORT:-8081}}"
7
+ QUIET=false
8
+
9
+ while [ "$#" -gt 0 ]; do
10
+ case "$1" in
11
+ --target) TARGET="$2"; shift 2 ;;
12
+ --port) PORT="$2"; shift 2 ;;
13
+ --quiet) QUIET=true; shift ;;
14
+ -h|--help)
15
+ printf 'Usage: start-console-forwarder.sh [--target <dir>] [--port <port>] [--quiet]\n'
16
+ exit 0
17
+ ;;
18
+ *) printf 'start-console-forwarder: unknown arg: %s\n' "$1" >&2; exit 2 ;;
19
+ esac
20
+ done
21
+
22
+ [ "${METAMASK_RECIPE_CONSOLE_FORWARD:-1}" != "0" ] || exit 0
23
+ TARGET="$(cd "$TARGET" && pwd)"
24
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
25
+ # shellcheck disable=SC1091
26
+ . "$SCRIPT_DIR/../shared/harness-path.sh"
27
+ LOG_DIR="$TARGET/$(recipe_runtime_dir)" || exit 1
28
+ FORWARDER="$SCRIPT_DIR/bridge-runtime/console-forwarder.cjs"
29
+ PID_FILE="$LOG_DIR/console-forwarder.pid"
30
+ APP_LOG_FILE="$LOG_DIR/app-console.log"
31
+
32
+ { [ -f "$FORWARDER" ] && command -v node >/dev/null 2>&1; } || exit 0
33
+ mkdir -p "$LOG_DIR"
34
+
35
+ # One debugger owns a React Native page. Reap any collector for this Metro port,
36
+ # including one left by an older globally installed harness.
37
+ while read -r pid cmd; do
38
+ case "$cmd" in
39
+ *console-forwarder.cjs*" --port $PORT "*|*console-forwarder.cjs*" --port=$PORT "*)
40
+ kill "$pid" 2>/dev/null || true
41
+ ;;
42
+ esac
43
+ done < <(ps -axo pid=,command= 2>/dev/null || true)
44
+
45
+ if [ -f "$PID_FILE" ]; then
46
+ old_pid="$(cat "$PID_FILE" 2>/dev/null || true)"
47
+ if [ -n "$old_pid" ]; then
48
+ case "$(ps -p "$old_pid" -o command= 2>/dev/null)" in
49
+ *console-forwarder.cjs*) kill "$old_pid" 2>/dev/null || true ;;
50
+ esac
51
+ fi
52
+ rm -f "$PID_FILE"
53
+ fi
54
+
55
+ : > "$APP_LOG_FILE"
56
+ pid="$(node "$SCRIPT_DIR/launch-console-forwarder.cjs" \
57
+ --port "$PORT" \
58
+ --out "$APP_LOG_FILE" \
59
+ --err "$LOG_DIR/console-forwarder.err")" || exit 1
60
+ printf '%s\n' "$pid" > "$PID_FILE"
61
+
62
+ # A successful launch must never claim capture when the collector died at boot.
63
+ sleep 0.1
64
+ if ! kill -0 "$pid" 2>/dev/null; then
65
+ rm -f "$PID_FILE"
66
+ printf 'start-console-forwarder: collector exited during startup; inspect %s\n' \
67
+ "$LOG_DIR/console-forwarder.err" >&2
68
+ exit 1
69
+ fi
70
+ [ "$QUIET" = true ] || printf 'Device console → app-console.log (CDP forwarder pid %s)\n' "$pid" >&2
@@ -77,45 +77,6 @@ default_metro_workers() {
77
77
  # shellcheck disable=SC1091
78
78
  . "$SCRIPT_DIR/lib/tmux-viewer.sh"
79
79
 
80
- # Bridgeless RN gates console→Metro forwarding on console._isPolyfilled, which is
81
- # false with the Hermes native console — device logs (incl. DevLogger) never reach
82
- # metro.log. The forwarder attaches over CDP and appends them back. Kill switch:
83
- # METAMASK_RECIPE_CONSOLE_FORWARD=0.
84
- start_console_forwarder() {
85
- local forwarder="$SCRIPT_DIR/bridge-runtime/console-forwarder.cjs"
86
- local fwd_pid_file="$LOG_DIR/console-forwarder.pid"
87
- [ "${METAMASK_RECIPE_CONSOLE_FORWARD:-1}" != "0" ] || return 0
88
- { [ -f "$forwarder" ] && command -v node >/dev/null 2>&1; } || return 0
89
- # Only one debugger can own a React Native page. Reap any forwarder already
90
- # attached to this Metro port, even if it was launched from another checkout or
91
- # package install path and therefore does not own this runtime dir's pid file.
92
- local pid cmd
93
- while read -r pid cmd; do
94
- case "$cmd" in
95
- *console-forwarder.cjs*" --port $PORT "*|*console-forwarder.cjs*" --port=$PORT "*)
96
- kill "$pid" 2>/dev/null || true
97
- ;;
98
- esac
99
- done < <(ps -axo pid=,command= 2>/dev/null || true)
100
- if [ -f "$fwd_pid_file" ]; then
101
- local old_fwd
102
- old_fwd="$(cat "$fwd_pid_file" 2>/dev/null || true)"
103
- # A crash can leave a stale pid file and the OS can recycle the pid for an
104
- # unrelated process — only kill when the command line is really ours.
105
- if [ -n "$old_fwd" ]; then
106
- case "$(ps -p "$old_fwd" -o command= 2>/dev/null)" in
107
- *console-forwarder.cjs*) kill "$old_fwd" 2>/dev/null ;;
108
- esac
109
- fi
110
- rm -f "$fwd_pid_file"
111
- fi
112
- nohup node "$forwarder" --port "$PORT" --out "$LOG_FILE" \
113
- </dev/null >> "$LOG_DIR/console-forwarder.err" 2>&1 &
114
- echo "$!" > "$fwd_pid_file"
115
- disown 2>/dev/null || true
116
- printf 'Device console → metro.log (CDP forwarder pid %s)\n' "$(cat "$fwd_pid_file")" >&2
117
- }
118
-
119
80
  write_metro_runner() {
120
81
  local runner="$1"
121
82
  local clear_flag=""
@@ -179,7 +140,6 @@ if metro_ready; then
179
140
  rm -f "$PID_FILE"
180
141
  elif metro_listener_valid; then
181
142
  printf 'Metro already running and valid on port %s\n' "$PORT" >&2
182
- start_console_forwarder
183
143
  exit 0
184
144
  else
185
145
  stop_metro_listener || exit 1
@@ -269,7 +229,6 @@ if [ "$READY" = true ]; then
269
229
  first_ready_pid="${ready_pids%% *}"
270
230
  [ -z "$first_ready_pid" ] || printf '%s\n' "$first_ready_pid" > "$PID_FILE"
271
231
  printf 'Metro ready on port %s\n' "$PORT" >&2
272
- start_console_forwarder
273
232
  printf ' Next: mm-harness logs (tail Metro)\n' >&2
274
233
  exit 0
275
234
  fi
File without changes
@@ -261,76 +261,19 @@ check_file "package.json"
261
261
  check_file "app/dev-tools/AgenticService/AgenticService.ts"
262
262
 
263
263
  if ! grep -q "AgenticService.install" "$TARGET/app/core/NavigationService/NavigationService.ts" 2>/dev/null; then
264
- checks+=("{\"name\":\"NavigationService patch\",\"status\":\"fail\"}")
264
+ checks+=("{\"name\":\"NavigationService agentic bridge\",\"status\":\"fail\"}")
265
265
  status="fail"
266
266
  else
267
- checks+=("{\"name\":\"NavigationService patch\",\"status\":\"pass\"}")
267
+ checks+=("{\"name\":\"NavigationService agentic bridge\",\"status\":\"pass\"}")
268
268
  fi
269
269
 
270
270
  if ! grep -q "AgentStepHud" "$TARGET/app/components/Nav/App/App.tsx" 2>/dev/null; then
271
- checks+=("{\"name\":\"App AgentStepHud patch\",\"status\":\"fail\"}")
271
+ checks+=("{\"name\":\"App AgentStepHud\",\"status\":\"fail\"}")
272
272
  status="fail"
273
273
  else
274
- checks+=("{\"name\":\"App AgentStepHud patch\",\"status\":\"pass\"}")
274
+ checks+=("{\"name\":\"App AgentStepHud\",\"status\":\"pass\"}")
275
275
  fi
276
276
 
277
- # Drift detection (read-only): compare the in-repo AgenticService/HUD against
278
- # the resolved runner overlay, not a runner-owned copy. The runner is the only
279
- # MetaMask-aware source of truth for Mobile injection.
280
- overlay_drift_check="$(
281
- HARNESS_DIR_FOR_DRIFT="$HARNESS_DIR" TARGET_FOR_DRIFT="$TARGET" node <<'NODE'
282
- const fs = require('fs');
283
- const path = require('path');
284
- const name = 'agentic overlay matches runner (HUD freshness)';
285
- const harnessDir = process.env.HARNESS_DIR_FOR_DRIFT;
286
- const targetDir = path.join(process.env.TARGET_FOR_DRIFT, 'app', 'dev-tools', 'AgenticService');
287
- function readTrimmed(file) {
288
- try { return fs.readFileSync(file, 'utf8').trim(); } catch { return ''; }
289
- }
290
- let runnerDir = readTrimmed(path.join(harnessDir, 'runner', '.runner-source'));
291
- if (!runnerDir) {
292
- try {
293
- const manifest = JSON.parse(fs.readFileSync(path.join(harnessDir, 'manifest.json'), 'utf8'));
294
- runnerDir = manifest && manifest.source && manifest.source.runnerDir || '';
295
- } catch {}
296
- }
297
- if (!runnerDir) {
298
- process.stdout.write(JSON.stringify({ name, status: 'warn', detail: 'runner source unavailable; overlay freshness compare skipped' }));
299
- process.exit(0);
300
- }
301
- const overlayDir = path.join(runnerDir, 'adapters', 'mobile', 'overlay', 'app', 'dev-tools', 'AgenticService');
302
- let checked = 0;
303
- const drifted = [];
304
- const missing = [];
305
- try {
306
- for (const entry of fs.readdirSync(overlayDir)) {
307
- if (!entry.endsWith('.patch')) continue;
308
- if (/\.test\./u.test(entry)) continue;
309
- const base = entry.slice(0, -'.patch'.length);
310
- const targetFile = path.join(targetDir, base);
311
- if (!fs.existsSync(targetFile)) { missing.push(base); continue; }
312
- checked += 1;
313
- if (fs.readFileSync(path.join(overlayDir, entry), 'utf8') !== fs.readFileSync(targetFile, 'utf8')) {
314
- drifted.push(base);
315
- }
316
- }
317
- } catch (error) {
318
- process.stdout.write(JSON.stringify({ name, status: 'warn', detail: 'runner overlay compare skipped: ' + String((error && error.message) || error) }));
319
- process.exit(0);
320
- }
321
- if (drifted.length || missing.length) {
322
- const parts = [];
323
- if (drifted.length) parts.push('behind runner: ' + drifted.join(', '));
324
- if (missing.length) parts.push('absent in repo: ' + missing.join(', '));
325
- process.stdout.write(JSON.stringify({ name, status: 'warn', detail: parts.join('; ') + ' — rerun mm-harness install --force-overlay or mm-harness fixtures sync to refresh the in-repo HUD/AgenticService from the runner' }));
326
- } else if (checked === 0) {
327
- process.stdout.write(JSON.stringify({ name, status: 'pass' }));
328
- } else {
329
- process.stdout.write(JSON.stringify({ name, status: 'pass' }));
330
- }
331
- NODE
332
- )"
333
- checks+=("$overlay_drift_check")
334
277
  run_with_timeout() {
335
278
  local log_path="$1"
336
279
  local timeout_s="$2"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -3,9 +3,10 @@
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: Chrome DevTools frontend for an extension page or service worker.
6
+ * Extension: DevTools in the slot-owned Chrome process, attached to the exact target.
7
7
  */
8
8
  import { spawnSync } from 'node:child_process';
9
+ import WebSocket from 'ws';
9
10
 
10
11
  function usage() {
11
12
  console.error(`Usage:
@@ -96,8 +97,8 @@ async function fetchOk(url, init) {
96
97
  return response.ok;
97
98
  }
98
99
 
99
- async function fetchJson(url) {
100
- const response = await fetch(url);
100
+ async function fetchJson(url, init) {
101
+ const response = await fetch(url, init);
101
102
  if (!response.ok) {
102
103
  throw new Error(`HTTP ${response.status} for ${url}`);
103
104
  }
@@ -105,20 +106,46 @@ async function fetchJson(url) {
105
106
  return body;
106
107
  }
107
108
 
108
- function openUrl(url) {
109
- if (process.platform === 'darwin') {
110
- if (url.startsWith('devtools://') || url.startsWith('chrome://')) {
111
- const chrome = spawnSync('open', ['-a', 'Google Chrome', url], { stdio: 'ignore' });
112
- if (chrome.status === 0) return 'chrome';
113
- }
114
- spawnSync('open', [url], { stdio: 'ignore' });
115
- return 'open';
116
- }
117
- if (process.platform === 'linux') {
118
- spawnSync('xdg-open', [url], { stdio: 'ignore' });
119
- return 'xdg-open';
109
+ function sendBrowserCommand(webSocketDebuggerUrl, method, params) {
110
+ 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
+ }));
124
+ });
125
+ socket.on('message', (raw) => {
126
+ const message = JSON.parse(String(raw));
127
+ if (message.id !== 1) return;
128
+ finish(message);
129
+ });
130
+ socket.once('error', () => finish({ error: { message: `${method} WebSocket failed` } }));
131
+ });
132
+ }
133
+
134
+ async function openNativeDevtools(cdpPort, browserWebSocketUrl, target) {
135
+ const response = await sendBrowserCommand(browserWebSocketUrl, 'Target.openDevTools', {
136
+ targetId: target.id,
137
+ panelId: 'console',
138
+ });
139
+ if (response.error) throw new Error(response.error.message);
140
+ const devtoolsId = response.result?.targetId;
141
+ if (!devtoolsId) throw new Error('Chrome did not return a native DevTools target');
142
+ for (let attempt = 0; attempt < 20; attempt += 1) {
143
+ const targets = await fetchJson(`http://127.0.0.1:${cdpPort}/json/list`);
144
+ const devtools = targets.find((candidate) => candidate.id === devtoolsId);
145
+ if (devtools && String(devtools.url || '').startsWith('devtools://devtools/')) return devtools;
146
+ await new Promise((resolve) => setTimeout(resolve, 100));
120
147
  }
121
- return null;
148
+ throw new Error('Chrome opened DevTools but did not expose its native target');
122
149
  }
123
150
 
124
151
  // CDP-eval fallback: invoke DevSettings native module directly via the bridge
@@ -223,7 +250,8 @@ function pickExtensionTarget(targets, targetKind) {
223
250
 
224
251
  const pages = extensionTargets.filter((target) => target.type === 'page');
225
252
  return (
226
- pages.find((target) => String(target.url || '').includes('/home.html'))
253
+ pages.find((target) => String(target.url || '').includes('/sidepanel.html'))
254
+ || pages.find((target) => String(target.url || '').includes('/home.html'))
227
255
  || pages.find((target) => !String(target.url || '').includes('/popup-init.html'))
228
256
  || pages[0]
229
257
  || null
@@ -241,8 +269,9 @@ async function openExtensionDevtools(cdpPort, targetKind, shouldOpen) {
241
269
  const versionEndpoint = `http://127.0.0.1:${numericPort}/json/version`;
242
270
  const listEndpoint = `http://127.0.0.1:${numericPort}/json/list`;
243
271
 
272
+ let version;
244
273
  try {
245
- await fetchJson(versionEndpoint);
274
+ version = await fetchJson(versionEndpoint);
246
275
  } catch (error) {
247
276
  return {
248
277
  ok: false,
@@ -291,48 +320,73 @@ async function openExtensionDevtools(cdpPort, targetKind, shouldOpen) {
291
320
  }
292
321
 
293
322
  const picked = pickExtensionTarget(targets, targetKind);
294
- if (picked?.devtoolsFrontendUrl && shouldOpen) {
295
- openUrl(picked.devtoolsFrontendUrl);
323
+ if (picked?.id && shouldOpen) {
324
+ let devtoolsTarget;
325
+ try {
326
+ if (!version.webSocketDebuggerUrl) throw new Error('Chrome did not expose its browser debugger endpoint');
327
+ devtoolsTarget = await openNativeDevtools(numericPort, version.webSocketDebuggerUrl, picked);
328
+ } catch (error) {
329
+ return {
330
+ ok: false,
331
+ adapter: 'extension',
332
+ targetKind,
333
+ cdpPort: numericPort,
334
+ error: 'debugger-open-failed',
335
+ endpoint: listEndpoint,
336
+ target: { type: picked.type, url: picked.url, title: picked.title || null },
337
+ 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.',
338
+ detail: error instanceof Error ? error.message : String(error),
339
+ };
340
+ }
296
341
  return {
297
342
  ok: true,
298
343
  adapter: 'extension',
299
344
  targetKind,
300
- method: 'devtools-frontend',
301
- endpoint: picked.devtoolsFrontendUrl,
345
+ method: 'native-devtools',
346
+ endpoint: devtoolsTarget.url,
302
347
  cdpPort: numericPort,
303
348
  target: {
304
349
  type: picked.type,
305
350
  url: picked.url,
306
351
  title: picked.title || null,
307
352
  },
308
- devtoolsUrl: picked.devtoolsFrontendUrl,
309
- inspectUrl,
310
- configureHint,
353
+ debuggerTargetId: devtoolsTarget.id,
354
+ attached: true,
311
355
  opened: true,
312
356
  };
313
357
  }
314
358
 
315
- if (shouldOpen) {
316
- openUrl(inspectUrl);
317
- }
359
+ if (shouldOpen) return {
360
+ ok: false,
361
+ adapter: 'extension',
362
+ targetKind,
363
+ error: picked ? 'devtools-url-unavailable' : 'extension-target-unavailable',
364
+ endpoint: listEndpoint,
365
+ cdpPort: numericPort,
366
+ target: picked
367
+ ? { type: picked.type, url: picked.url, title: picked.title || null }
368
+ : null,
369
+ inspectUrl,
370
+ configureHint,
371
+ opened: false,
372
+ hint: picked
373
+ ? 'The selected extension target did not expose a DevTools URL. Relaunch it with mm-harness launch.'
374
+ : 'No extension target is ready. Launch it with mm-harness launch, then retry mm-harness debug.',
375
+ };
318
376
 
319
377
  return {
320
378
  ok: true,
321
379
  adapter: 'extension',
322
380
  targetKind,
323
- method: picked?.devtoolsFrontendUrl ? 'devtools-frontend' : 'chrome-inspect',
324
- endpoint: picked?.devtoolsFrontendUrl || inspectUrl,
381
+ method: picked?.devtoolsFrontendUrl ? 'devtools-frontend' : 'target-list',
382
+ endpoint: picked?.devtoolsFrontendUrl || listEndpoint,
325
383
  cdpPort: numericPort,
326
384
  target: picked
327
385
  ? { type: picked.type, url: picked.url, title: picked.title || null }
328
386
  : null,
329
387
  devtoolsUrl: picked?.devtoolsFrontendUrl || null,
330
- inspectUrl,
331
- configureHint,
332
- opened: shouldOpen,
333
- hint: picked
334
- ? 'Opened chrome://inspect because no devtools frontend URL was available for the selected target.'
335
- : `No extension targets are registered yet. Open chrome://inspect and add ${configureHint}.`,
388
+ opened: false,
389
+ detail: 'Open action not invoked (--no-open).',
336
390
  };
337
391
  }
338
392
 
File without changes
File without changes
@@ -276,14 +276,8 @@ 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
-
283
279
  export const cliFns = {
284
280
  resolve_slot_ports_by_repo: resolveSlotPortsByRepo,
285
- // Back-compat cli key for one release.
286
- resolve_farmslot_ports_by_repo: resolveSlotPortsByRepo,
287
281
  resolve_default_extension_ports: resolveDefaultExtensionPorts,
288
282
  resolve_extension_runtime_context: resolveExtensionRuntimeContext,
289
283
  resolve_extension_slot_ports: (repo) => formatExtensionSlotPorts(repo) || null,
File without changes
@@ -20,11 +20,6 @@ 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
-
28
23
  infer_extension_slot_suffix() {
29
24
  local repo="$1" base
30
25
  base="$(basename "$repo")"
File without changes
File without changes
File without changes
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env node
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+
5
+ const [cacheFile, timeoutText, endpointOverride] = process.argv.slice(2);
6
+ const timeoutMs = Number.parseInt(timeoutText ?? '', 10);
7
+ const endpoint = endpointOverride
8
+ ?? 'https://registry.npmjs.org/-/package/%40deeeed%2Fmetamask-harness/dist-tags';
9
+
10
+ async function main() {
11
+ if (!cacheFile || !Number.isInteger(timeoutMs) || timeoutMs < 1) return;
12
+ const controller = new AbortController();
13
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
14
+ try {
15
+ const response = await fetch(endpoint, { signal: controller.signal });
16
+ if (!response.ok) return;
17
+ const data = await response.json();
18
+ if (!data || typeof data.latest !== 'string' || data.latest.length === 0) return;
19
+ let lastCheck = Date.now();
20
+ try {
21
+ const cached = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
22
+ if (typeof cached.lastCheck === 'number') lastCheck = cached.lastCheck;
23
+ } catch {
24
+ // The parent may have exited before creating the cache; recreate it below.
25
+ }
26
+ fs.mkdirSync(path.dirname(cacheFile), { recursive: true });
27
+ const temporary = `${cacheFile}.${process.pid}.tmp`;
28
+ fs.writeFileSync(temporary, JSON.stringify({ lastCheck, latest: data.latest }));
29
+ fs.renameSync(temporary, cacheFile);
30
+ } catch {
31
+ // Passive checks are intentionally silent; the explicit update command teaches failures.
32
+ } finally {
33
+ clearTimeout(timer);
34
+ }
35
+ }
36
+
37
+ await main();
@@ -0,0 +1,84 @@
1
+ import { spawn, spawnSync } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { recipeRuntimePath, runnerDir } from "../../paths.js";
5
+ async function ensureExtensionConsoleCapture(target) {
6
+ const cdpPort = process.env.CDP_PORT ?? process.env.RECIPE_CDP_PORT;
7
+ if (!cdpPort) throw new Error("Extension console capture requires a resolved CDP port.");
8
+ const response = await fetch(`http://127.0.0.1:${cdpPort}/json/version`);
9
+ if (!response.ok) throw new Error(`Extension CDP ${cdpPort} is not ready for console capture.`);
10
+ const extensionLog = recipeRuntimePath(target, "extension-console.log");
11
+ const dappLog = recipeRuntimePath(target, "dapp-console.log");
12
+ const collectorLog = recipeRuntimePath(target, "console-tail.log");
13
+ const pidFile = recipeRuntimePath(target, "console-tail.pid");
14
+ fs.mkdirSync(path.dirname(extensionLog), { recursive: true });
15
+ const existingPid = readPid(pidFile);
16
+ if (existingPid && collectorOwnsLog(existingPid, extensionLog)) return;
17
+ if (existingPid) {
18
+ try {
19
+ process.kill(existingPid, "SIGTERM");
20
+ } catch {
21
+ }
22
+ }
23
+ fs.rmSync(pidFile, { force: true });
24
+ fs.writeFileSync(extensionLog, "");
25
+ fs.writeFileSync(dappLog, "");
26
+ const script = path.join(runnerDir, "adapters", "extension", "console-tail.mjs");
27
+ const output = fs.openSync(collectorLog, "a");
28
+ let child;
29
+ try {
30
+ child = spawn(process.execPath, [
31
+ script,
32
+ "--cdp-port",
33
+ cdpPort,
34
+ "--extension-log",
35
+ extensionLog,
36
+ "--dapp-log",
37
+ dappLog
38
+ ], {
39
+ cwd: target,
40
+ detached: true,
41
+ stdio: ["ignore", output, output]
42
+ });
43
+ } finally {
44
+ fs.closeSync(output);
45
+ }
46
+ if (!child.pid) throw new Error("Extension console collector did not start.");
47
+ child.unref();
48
+ fs.writeFileSync(pidFile, `${child.pid}
49
+ `);
50
+ for (let attempt = 0; attempt < 30; attempt += 1) {
51
+ if (collectorOwnsLog(child.pid, extensionLog) && containsAttachment(extensionLog, dappLog)) return;
52
+ await new Promise((resolve) => setTimeout(resolve, 100));
53
+ }
54
+ throw new Error(`Extension console collector ${child.pid} did not attach to CDP ${cdpPort}.`);
55
+ }
56
+ function readPid(pidFile) {
57
+ try {
58
+ const pid = Number.parseInt(fs.readFileSync(pidFile, "utf8").trim(), 10);
59
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
60
+ } catch {
61
+ return null;
62
+ }
63
+ }
64
+ function collectorOwnsLog(pid, extensionLog) {
65
+ try {
66
+ process.kill(pid, 0);
67
+ } catch {
68
+ return false;
69
+ }
70
+ const result = spawnSync("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf8" });
71
+ return result.status === 0 && result.stdout.includes("console-tail.mjs") && result.stdout.includes(extensionLog);
72
+ }
73
+ function containsAttachment(...logs) {
74
+ return logs.some((log) => {
75
+ try {
76
+ return fs.readFileSync(log, "utf8").includes("[attached]");
77
+ } catch {
78
+ return false;
79
+ }
80
+ });
81
+ }
82
+ export {
83
+ ensureExtensionConsoleCapture
84
+ };