@deeeed/metamask-harness 0.17.4 → 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.
- package/CHANGELOG.md +6 -103
- package/README.md +136 -96
- package/adapters/extension/console-tail.mjs +24 -55
- package/adapters/extension/ensure-browser.sh +7 -1
- package/adapters/extension/inject.mjs +15 -5
- package/adapters/extension/launch-browser.cjs +4 -3
- package/adapters/extension/launch.sh +11 -8
- package/adapters/extension/lib/chrome-args.cjs +24 -0
- package/adapters/extension/live.sh +1 -2
- package/adapters/extension/reattach.sh +0 -1
- package/adapters/extension/seed-fixture.sh +12 -4
- package/adapters/extension/sidepanel-toggle.sh +1 -4
- package/adapters/extension/start-watch.sh +4 -17
- package/adapters/extension/stop-viewers.sh +1 -1
- package/adapters/extension/verify.sh +1 -1
- package/adapters/manifest.json +45 -53
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +0 -16
- package/adapters/mobile/bridge-runtime/console-forwarder.cjs +1 -1
- package/adapters/mobile/bridge-runtime/lib/match-bridge-target.cjs +1 -3
- package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +3 -2
- package/adapters/mobile/cleanup.sh +176 -16
- package/adapters/mobile/inject.sh +671 -90
- package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +185 -0
- package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +1662 -0
- package/adapters/mobile/overlay/compat/README.md +28 -0
- package/adapters/mobile/overlay/compat/rn81-message-event-source.patch +42 -0
- package/adapters/mobile/start-metro.sh +41 -0
- package/adapters/mobile/verify.sh +61 -4
- package/adapters/mobile/wait-for-bridge.sh +8 -22
- package/adapters/shared/harness-source-fingerprint.mjs +49 -0
- package/adapters/shared/install-repo-deps.sh +1 -5
- package/adapters/shared/open-debug.mjs +184 -103
- package/adapters/shared/resolve-farmslot-ports-core.mjs +3 -0
- package/adapters/shared/resolve-farmslot-ports.mjs +5 -0
- package/adapters/shared/resolve-farmslot-ports.sh +7 -0
- package/adapters/shared/resolve-slot-ports-core.mjs +6 -0
- package/adapters/shared/resolve-slot-ports.sh +7 -2
- package/bin/mm-harness +0 -7
- package/dist/adapters/extension/harness-freshness.js +39 -0
- package/dist/adapters/extension/runtime-decision.js +4 -73
- package/dist/adapters/extension/surface.js +2 -3
- package/dist/adapters/mobile/perps-env.js +3 -43
- package/dist/adapters/mobile/prepare.js +7 -22
- package/dist/adapters/mobile/source-freshness.js +26 -41
- package/dist/adapters/mobile/surface.js +1 -1
- package/dist/adapters/resolve-slot-ports.js +2 -0
- package/dist/adapters/slot-ports.js +0 -16
- package/dist/adapters.js +5 -12
- package/dist/cli-commands.js +1 -1
- package/dist/cli.js +12 -6
- package/dist/commands/call.js +22 -56
- package/dist/commands/check.js +3 -9
- package/dist/commands/checklist.js +4 -1
- package/dist/commands/debug.js +31 -38
- package/dist/commands/device-target.js +12 -27
- package/dist/commands/doctor.js +23 -77
- package/dist/commands/fixtures.js +17 -27
- package/dist/commands/launch/extension.js +2 -0
- package/dist/commands/launch/index.js +59 -167
- package/dist/commands/logs.js +6 -8
- package/dist/commands/manifest.js +25 -151
- package/dist/commands/parse-args.js +0 -2
- package/dist/commands/provision.js +3 -10
- package/dist/commands/run-engine.js +69 -71
- package/dist/commands/run-report.js +3 -12
- package/dist/commands/run.js +35 -245
- package/dist/commands/shared.js +3 -55
- package/dist/commands/status-probe.js +1 -1
- package/dist/commands/status.js +1 -1
- package/dist/commands/stop.js +2 -7
- package/dist/commands/update.js +28 -16
- package/dist/doctor.js +4 -1
- package/dist/harness.js +134 -43
- package/dist/heal-bounds.js +1 -1
- package/dist/live-adapter-contract.js +20 -6
- package/dist/mm-harness-cli.js +48 -123
- package/dist/paths.js +5 -2
- package/dist/runner.js +2 -37
- package/dist/runtime-context.js +8 -26
- package/docs/ADAPTER-SURFACE.md +112 -19
- package/docs/CHEATSHEET.md +52 -39
- package/docs/CLI-SPEC.md +1098 -0
- package/docs/CODE-MAP.md +1 -1
- package/docs/DEBUG-HANDOVER.md +36 -0
- package/docs/MENTAL-MODEL.md +295 -0
- package/docs/UX-PRINCIPLES.md +1 -3
- package/docs/architecture.md +375 -65
- package/docs/live-adapter-contract.md +2 -4
- package/docs/recipe-libraries.md +275 -45
- package/library/README.md +0 -4
- package/library/actions/core/perps/_controller.mjs +55 -10
- package/library/actions/mobile/platform/bridge.mjs +1 -5
- package/library/manifests/core.action-manifest.json +7 -7
- package/library/manifests/mobile.action-manifest.json +3 -28
- package/package.json +1 -4
- package/adapters/extension/launch-webpack.cjs +0 -83
- package/adapters/extension/stamp-runtime-title.cjs +0 -58
- package/adapters/extension/sync-webpack-dist.cjs +0 -107
- package/adapters/mobile/launch-console-forwarder.cjs +0 -39
- package/adapters/mobile/start-console-forwarder.sh +0 -70
- package/adapters/shared/update-check-worker.mjs +0 -43
- package/dist/adapters/extension/console-capture.js +0 -93
- package/dist/command-contract.js +0 -441
- package/dist/command-journal.js +0 -225
- package/dist/commands/last.js +0 -52
- package/dist/json-stream.js +0 -57
- package/dist/run-diagnostics.js +0 -261
- package/docs/CLI-ERGONOMICS-AUDIT.md +0 -32
- package/docs/CLI-ERGONOMICS-HUMAN-QA.md +0 -64
- package/docs/VIDEO-DEMO-VALIDATION.md +0 -74
- package/library/recipes/runner/smoke.core.recipe.json +0 -27
- package/library/recipes/wallet/smoke.extension.recipe.json +0 -42
- package/library/recipes/wallet/smoke.mobile.recipe.json +0 -42
- 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
|
|
6
|
+
* Extension: Chrome DevTools frontend for an extension page or service worker.
|
|
7
7
|
*/
|
|
8
8
|
import { spawnSync } from 'node:child_process';
|
|
9
|
-
import
|
|
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
|
|
101
|
-
const response = await fetch(url
|
|
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
|
-
|
|
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
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
126
|
-
|
|
130
|
+
let settled = false;
|
|
131
|
+
const finish = (value) => {
|
|
132
|
+
if (settled) return;
|
|
133
|
+
settled = true;
|
|
127
134
|
try {
|
|
128
|
-
|
|
135
|
+
req.destroy();
|
|
129
136
|
} catch {
|
|
130
|
-
|
|
131
|
-
return;
|
|
137
|
+
// Best-effort teardown of the probe socket.
|
|
132
138
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
});
|
|
136
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
|
|
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('/
|
|
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:
|
|
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:
|
|
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:
|
|
377
|
+
hint: 'Relaunch the runtime (mm-harness launch).',
|
|
325
378
|
};
|
|
326
379
|
}
|
|
327
380
|
|
|
328
381
|
const picked = pickExtensionTarget(targets, targetKind);
|
|
329
|
-
if (picked
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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: '
|
|
352
|
-
endpoint:
|
|
407
|
+
method: 'devtools-window',
|
|
408
|
+
endpoint: devtoolsUrl,
|
|
353
409
|
cdpPort: numericPort,
|
|
354
|
-
target:
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
:
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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:
|
|
388
|
-
endpoint:
|
|
470
|
+
method: 'devtools-window',
|
|
471
|
+
endpoint: devtoolsUrl,
|
|
389
472
|
cdpPort: numericPort,
|
|
390
|
-
target:
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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,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
|
-
|
|
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
|
-
"-
|
|
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
|
|
174
|
+
return { status: "stale", modified: ["<snapshot comparison failed>"] };
|
|
204
175
|
}
|
|
205
|
-
|
|
206
|
-
|
|
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: "
|
|
72
|
+
return { label: "app", path: recipeRuntimePath(target, "extension-console.log") };
|
|
74
73
|
},
|
|
75
74
|
hints: {
|
|
76
75
|
launch: "mm-harness launch",
|