@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.
- package/CHANGELOG.md +34 -0
- package/README.md +90 -140
- package/adapters/core/cleanup.sh +0 -0
- package/adapters/core/inject.sh +0 -0
- package/adapters/extension/cleanup.mjs +0 -0
- package/adapters/extension/console-tail.mjs +51 -24
- package/adapters/extension/ensure-browser.sh +0 -0
- package/adapters/extension/inject.mjs +5 -9
- package/adapters/extension/launch-browser.cjs +0 -0
- package/adapters/extension/launch-webpack.cjs +83 -0
- package/adapters/extension/launch.sh +8 -11
- package/adapters/extension/live.sh +2 -0
- package/adapters/extension/readiness.mjs +0 -0
- package/adapters/extension/reattach.sh +1 -0
- package/adapters/extension/refresh-build.sh +0 -0
- package/adapters/extension/seed-fixture.sh +4 -12
- package/adapters/extension/sidepanel-toggle.sh +4 -1
- package/adapters/extension/snapshot-dist.sh +0 -0
- package/adapters/extension/stamp-runtime-title.cjs +49 -0
- package/adapters/extension/start-watch.sh +16 -4
- package/adapters/extension/stop-viewers.sh +1 -1
- package/adapters/extension/sync-webpack-dist.cjs +105 -0
- package/adapters/extension/verify.sh +1 -1
- package/adapters/extension/wallet-fixture-state.cjs +0 -0
- package/adapters/manifest.json +46 -38
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +0 -0
- package/adapters/mobile/bridge-runtime/console-forwarder.cjs +1 -1
- package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +2 -3
- package/adapters/mobile/bridge-runtime/setup-wallet.sh +0 -0
- package/adapters/mobile/cleanup.sh +16 -176
- package/adapters/mobile/inject.sh +88 -671
- package/adapters/mobile/launch-console-forwarder.cjs +39 -0
- package/adapters/mobile/lib/metro-listener.sh +0 -0
- package/adapters/mobile/lib/tmux-viewer.sh +0 -0
- package/adapters/mobile/open-device.sh +0 -0
- package/adapters/mobile/prewarm-bundle.sh +0 -0
- package/adapters/mobile/start-console-forwarder.sh +70 -0
- package/adapters/mobile/start-metro.sh +0 -41
- package/adapters/mobile/stop-metro.sh +0 -0
- package/adapters/mobile/verify.sh +4 -61
- package/adapters/mobile/wait-for-bridge.sh +0 -0
- package/adapters/mobile/yarn-setup.sh +0 -0
- package/adapters/shared/activate-repo-node.sh +0 -0
- package/adapters/shared/activate-repo-ruby.sh +0 -0
- package/adapters/shared/cli-ux.sh +0 -0
- package/adapters/shared/ensure-runner-deps.sh +0 -0
- package/adapters/shared/harness-path.sh +0 -0
- package/adapters/shared/hash-helpers.sh +0 -0
- package/adapters/shared/install-repo-deps.sh +0 -0
- package/adapters/shared/json-field.sh +0 -0
- package/adapters/shared/open-debug.mjs +90 -36
- package/adapters/shared/open-log-window.sh +0 -0
- package/adapters/shared/reap-checkout-metros.sh +0 -0
- package/adapters/shared/resolve-slot-ports-core.mjs +0 -6
- package/adapters/shared/resolve-slot-ports.mjs +0 -0
- package/adapters/shared/resolve-slot-ports.sh +0 -5
- package/adapters/shared/sync-wallet-fixture.sh +0 -0
- package/adapters/shared/tmux-session.sh +0 -0
- package/adapters/shared/tmux-viewer.sh +0 -0
- package/adapters/shared/update-check-worker.mjs +37 -0
- package/dist/adapters/extension/console-capture.js +84 -0
- package/dist/adapters/extension/runtime-decision.js +14 -2
- package/dist/adapters/extension/surface.js +3 -2
- package/dist/adapters/mobile/prepare.js +22 -7
- package/dist/adapters/mobile/surface.js +1 -1
- package/dist/adapters/resolve-slot-ports.js +0 -2
- package/dist/adapters/slot-ports.js +8 -0
- package/dist/cli.js +3 -11
- package/dist/commands/call.js +9 -4
- package/dist/commands/debug.js +38 -31
- package/dist/commands/fixtures.js +18 -10
- package/dist/commands/launch/index.js +10 -0
- package/dist/commands/logs.js +8 -6
- package/dist/commands/manifest.js +5 -17
- package/dist/commands/run-engine.js +1 -32
- package/dist/commands/run-report.js +1 -1
- package/dist/commands/run.js +50 -3
- package/dist/commands/status-probe.js +1 -1
- package/dist/commands/update.js +15 -28
- package/dist/doctor.js +1 -4
- package/dist/harness.js +5 -123
- package/dist/live-adapter-contract.js +6 -20
- package/dist/mm-harness-cli.js +6 -43
- package/dist/paths.js +2 -5
- package/dist/run-diagnostics.js +35 -40
- package/dist/runtime-context.js +26 -8
- package/docs/CHEATSHEET.md +39 -52
- package/docs/CLI-ERGONOMICS-HUMAN-QA.md +48 -104
- package/docs/CODE-MAP.md +1 -1
- package/docs/VIDEO-DEMO-VALIDATION.md +62 -0
- package/docs/architecture.md +65 -375
- package/docs/live-adapter-contract.md +3 -1
- package/library/actions/core/perps/_controller.mjs +10 -55
- package/library/manifests/mobile.action-manifest.json +1 -1
- package/package.json +2 -2
- package/scripts/completions.sh +0 -0
- package/scripts/install-completions.sh +0 -0
- package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +0 -185
- package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +0 -1662
- package/adapters/mobile/overlay/compat/README.md +0 -28
- package/adapters/mobile/overlay/compat/rn81-message-event-source.patch +0 -42
- package/adapters/shared/resolve-farmslot-ports-core.mjs +0 -3
- package/adapters/shared/resolve-farmslot-ports.mjs +0 -5
- package/adapters/shared/resolve-farmslot-ports.sh +0 -7
- package/docs/CLI-SPEC.md +0 -1142
- 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
|
|
264
|
+
checks+=("{\"name\":\"NavigationService agentic bridge\",\"status\":\"fail\"}")
|
|
265
265
|
status="fail"
|
|
266
266
|
else
|
|
267
|
-
checks+=("{\"name\":\"NavigationService
|
|
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
|
|
271
|
+
checks+=("{\"name\":\"App AgentStepHud\",\"status\":\"fail\"}")
|
|
272
272
|
status="fail"
|
|
273
273
|
else
|
|
274
|
-
checks+=("{\"name\":\"App AgentStepHud
|
|
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:
|
|
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
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
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('/
|
|
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?.
|
|
295
|
-
|
|
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
|
|
301
|
-
endpoint:
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
configureHint,
|
|
353
|
+
debuggerTargetId: devtoolsTarget.id,
|
|
354
|
+
attached: true,
|
|
311
355
|
opened: true,
|
|
312
356
|
};
|
|
313
357
|
}
|
|
314
358
|
|
|
315
|
-
if (shouldOpen) {
|
|
316
|
-
|
|
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' : '
|
|
324
|
-
endpoint: picked?.devtoolsFrontendUrl ||
|
|
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
|
-
|
|
331
|
-
|
|
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
|
+
};
|