@deeeed/metamask-harness 0.14.3 → 0.14.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 +13 -0
- package/adapters/core/cleanup.sh +0 -0
- package/adapters/core/inject.sh +0 -0
- package/adapters/extension/cleanup.mjs +0 -0
- package/adapters/extension/ensure-browser.sh +20 -4
- package/adapters/extension/inject.mjs +1 -0
- package/adapters/extension/launch-browser.cjs +0 -0
- package/adapters/extension/launch.sh +0 -0
- package/adapters/extension/lib/slot-title.cjs +294 -0
- package/adapters/extension/live.sh +0 -0
- package/adapters/extension/readiness.mjs +7 -28
- package/adapters/extension/reattach.sh +13 -15
- package/adapters/extension/refresh-build.sh +0 -0
- package/adapters/extension/seed-fixture.sh +0 -0
- package/adapters/extension/sidepanel-toggle.sh +0 -0
- package/adapters/extension/snapshot-dist.sh +0 -0
- package/adapters/extension/start-watch.sh +0 -0
- package/adapters/extension/stop-viewers.sh +0 -0
- package/adapters/extension/verify.sh +0 -0
- package/adapters/extension/wallet-fixture-state.cjs +0 -0
- package/adapters/manifest.json +16 -0
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +0 -0
- package/adapters/mobile/bridge-runtime/setup-wallet.sh +0 -0
- package/adapters/mobile/cleanup.sh +0 -0
- package/adapters/mobile/inject.sh +0 -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-metro.sh +0 -0
- package/adapters/mobile/stop-metro.sh +0 -0
- package/adapters/mobile/verify.sh +0 -0
- 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/json-field.sh +0 -0
- package/adapters/shared/open-log-window.sh +0 -0
- package/adapters/shared/reap-checkout-metros.sh +0 -0
- package/adapters/shared/resolve-farmslot-ports.mjs +0 -0
- package/adapters/shared/resolve-farmslot-ports.sh +0 -0
- package/adapters/shared/resolve-slot-ports.mjs +0 -0
- package/adapters/shared/resolve-slot-ports.sh +0 -0
- 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/dist/adapters/extension/ensure-ready.js +19 -0
- package/package.json +4 -3
- package/scripts/completions.sh +0 -0
- package/scripts/install-completions.sh +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.14.5 - 2026-07-10
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- Extension `ensure-browser` closes disposable New Tab / blank / extensions pages after reopen, matching reattach/ensure-ready hygiene.
|
|
9
|
+
- Extension slot browser titles are stamped through one shared helper (`adapters/extension/lib/slot-title.cjs`) with a persistent `MutationObserver`, and re-applied after `ensureExtensionReady` open/prune/reopen so relaunch and recipe paths keep `<slot-id> — MetaMask` instead of a bare MetaMask window.
|
|
10
|
+
- Extension slot-title contract test is hermetic (local CDP/WebSocket stub + fake DOM); Playwright and CDP stamps share one function source so they cannot drift.
|
|
11
|
+
- Extension inject copies `scripts/lib/slot-title.cjs` with the other installed helpers so reattach/readiness/reopen do not `MODULE_NOT_FOUND` on injected checkouts.
|
|
12
|
+
|
|
13
|
+
## 0.14.4 - 2026-07-09
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- Include the new extension viewer cleanup scripts in the adapter manifest so published packages pass adapter-surface validation.
|
|
17
|
+
|
|
5
18
|
## 0.14.3 - 2026-07-09
|
|
6
19
|
|
|
7
20
|
### Fixed
|
package/adapters/core/cleanup.sh
CHANGED
|
File without changes
|
package/adapters/core/inject.sh
CHANGED
|
File without changes
|
|
File without changes
|
|
@@ -358,8 +358,9 @@ const resumeWebpack = () => {
|
|
|
358
358
|
const page = ctx.pages().find(p => p.url().includes('chrome-extension://')) || ctx.pages()[0];
|
|
359
359
|
await page.goto(homeUrl, { waitUntil: 'load', timeout: 15000 });
|
|
360
360
|
|
|
361
|
-
// Set window title to slot ID
|
|
362
|
-
|
|
361
|
+
// Set window title to slot ID (persists across SPA navigations)
|
|
362
|
+
const { applyPersistentSlotTitle } = require(path.join('${SCRIPT_DIR}', 'lib/slot-title.cjs'));
|
|
363
|
+
await page.evaluate(applyPersistentSlotTitle, SLOT_ID);
|
|
363
364
|
|
|
364
365
|
// Wait for runtime state to settle, then unlock if needed.
|
|
365
366
|
const unlockSelector = '[data-testid=\"unlock-password\"]';
|
|
@@ -393,10 +394,25 @@ const resumeWebpack = () => {
|
|
|
393
394
|
console.log('[reopen] Screen: ' + page.url());
|
|
394
395
|
}
|
|
395
396
|
|
|
396
|
-
// Close extra extension tabs
|
|
397
|
+
// Close extra extension tabs and disposable Chrome pages (New Tab / blank).
|
|
397
398
|
for (const p of ctx.pages()) {
|
|
398
|
-
if (p
|
|
399
|
+
if (p === page) continue;
|
|
400
|
+
const url = p.url();
|
|
401
|
+
if (url.includes('chrome-extension://')) {
|
|
402
|
+
await p.close().catch(() => {});
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
if (
|
|
406
|
+
url === 'chrome://newtab/' ||
|
|
407
|
+
url === 'chrome://new-tab-page/' ||
|
|
408
|
+
url.startsWith('chrome://new-tab-page') ||
|
|
409
|
+
url === 'about:blank' ||
|
|
410
|
+
url === 'chrome://extensions/'
|
|
411
|
+
) {
|
|
412
|
+
await p.close().catch(() => {});
|
|
413
|
+
}
|
|
399
414
|
}
|
|
415
|
+
await page.bringToFront().catch(() => {});
|
|
400
416
|
|
|
401
417
|
fs.writeFileSync(path.join(AGENT_DIR, 'extension.id'), extId);
|
|
402
418
|
console.log('[reopen] Ready \u2014 ' + SLOT_ID + (CDP_PORT ? ' CDP:' + CDP_PORT : ''));
|
|
@@ -130,6 +130,7 @@ copyFile(path.join(runnerDir, 'adapters/shared/tmux-session.sh'), path.join(harn
|
|
|
130
130
|
copyFile(path.join(runnerDir, 'adapters/shared/tmux-viewer.sh'), path.join(harnessDir, 'scripts/lib/tmux-viewer.sh'));
|
|
131
131
|
copyFile(path.join(runnerDir, 'adapters/shared/log-tui.mjs'), path.join(harnessDir, 'scripts/lib/log-tui.mjs'));
|
|
132
132
|
copyFile(path.join(runnerDir, 'adapters/extension/lib/extension-id.cjs'), path.join(harnessDir, 'scripts/lib/extension-id.cjs'));
|
|
133
|
+
copyFile(path.join(runnerDir, 'adapters/extension/lib/slot-title.cjs'), path.join(harnessDir, 'scripts/lib/slot-title.cjs'));
|
|
133
134
|
makeExecutableTree(path.join(harnessDir, 'scripts'));
|
|
134
135
|
fs.writeFileSync(path.join(harnessDir, 'installed-scripts.sha256'), `${dirContentHash(path.join(harnessDir, 'scripts'))}\n`);
|
|
135
136
|
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Operator slot title for headed extension Chrome.
|
|
5
|
+
*
|
|
6
|
+
* Chrome's window label is the active tab's document.title. Farmslot stamps
|
|
7
|
+
* home.html to "<slot-id> — MetaMask" so operators can tell which slot owns a
|
|
8
|
+
* window when several farms run side by side.
|
|
9
|
+
*
|
|
10
|
+
* Stamping is best-effort: missing slot context or an uninspectable tab must
|
|
11
|
+
* never fail launch/readiness. A MutationObserver keeps the prefix across
|
|
12
|
+
* MetaMask SPA title resets.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('node:fs');
|
|
16
|
+
const http = require('node:http');
|
|
17
|
+
const path = require('node:path');
|
|
18
|
+
|
|
19
|
+
function sanitizeSlotId(value) {
|
|
20
|
+
const slotId = typeof value === 'string' ? value.trim() : '';
|
|
21
|
+
return /^[A-Za-z0-9._:-]{1,64}$/u.test(slotId) ? slotId : '';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function readSlotId(target, runtimeDir) {
|
|
25
|
+
for (const key of ['RECIPE_SLOT_ID', 'SLOT_ID', 'FARMSLOT_SLOT_ID']) {
|
|
26
|
+
const value = sanitizeSlotId(process.env[key]);
|
|
27
|
+
if (value) return value;
|
|
28
|
+
}
|
|
29
|
+
if (!target || !runtimeDir) return '';
|
|
30
|
+
try {
|
|
31
|
+
const runtimeContext = JSON.parse(
|
|
32
|
+
fs.readFileSync(path.join(target, runtimeDir, 'agentic-runtime.json'), 'utf8'),
|
|
33
|
+
);
|
|
34
|
+
return sanitizeSlotId(runtimeContext.slotId);
|
|
35
|
+
} catch {
|
|
36
|
+
// Standalone harness users do not need a custom browser title.
|
|
37
|
+
return '';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Playwright page.evaluate callback. Must stay a plain function (no closure)
|
|
43
|
+
* so Playwright can serialize it into the page.
|
|
44
|
+
*/
|
|
45
|
+
function applyPersistentSlotTitle(slot) {
|
|
46
|
+
// Inline sanitize — this function is serialized into the page for Playwright
|
|
47
|
+
// and CDP, so it cannot call Node helpers.
|
|
48
|
+
const id =
|
|
49
|
+
typeof slot === 'string' && /^[A-Za-z0-9._:-]{1,64}$/u.test(slot.trim())
|
|
50
|
+
? slot.trim()
|
|
51
|
+
: '';
|
|
52
|
+
if (!id) return document.title;
|
|
53
|
+
window.__farmslotSlotId = id;
|
|
54
|
+
|
|
55
|
+
const stripSlotPrefixes = (title) => {
|
|
56
|
+
let base = String(title || 'MetaMask');
|
|
57
|
+
// Accept em dash, en dash, or hyphen — historical stampers mixed them.
|
|
58
|
+
for (let i = 0; i < 32; i += 1) {
|
|
59
|
+
const stripped = base.replace(/^.+?\s+[—–-]\s+/u, '');
|
|
60
|
+
if (stripped === base) break;
|
|
61
|
+
base = stripped;
|
|
62
|
+
}
|
|
63
|
+
return base || 'MetaMask';
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const desiredTitle = () => {
|
|
67
|
+
const id = window.__farmslotSlotId;
|
|
68
|
+
if (!id) return document.title;
|
|
69
|
+
return `${id} — ${stripSlotPrefixes(document.title)}`;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const setTitle = () => {
|
|
73
|
+
if (window.__farmslotSlotTitleLock) {
|
|
74
|
+
window.__farmslotSlotTitlePending = true;
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const next = desiredTitle();
|
|
78
|
+
if (document.title === next) return;
|
|
79
|
+
window.__farmslotSlotTitleLock = true;
|
|
80
|
+
try {
|
|
81
|
+
document.title = next;
|
|
82
|
+
} finally {
|
|
83
|
+
// Release after the title mutation is delivered so the observer cannot
|
|
84
|
+
// re-enter and stack prefixes while React also mutates the DOM. If a
|
|
85
|
+
// mutation arrived while locked, re-apply once after unlock.
|
|
86
|
+
queueMicrotask(() => {
|
|
87
|
+
window.__farmslotSlotTitleLock = false;
|
|
88
|
+
if (window.__farmslotSlotTitlePending) {
|
|
89
|
+
window.__farmslotSlotTitlePending = false;
|
|
90
|
+
setTitle();
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
setTitle();
|
|
97
|
+
if (!window.__farmslotSlotTitleObserver) {
|
|
98
|
+
let node = document.querySelector('title');
|
|
99
|
+
if (!node) {
|
|
100
|
+
node = document.createElement('title');
|
|
101
|
+
(document.head || document.documentElement).appendChild(node);
|
|
102
|
+
}
|
|
103
|
+
window.__farmslotSlotTitleObserver = new MutationObserver(setTitle);
|
|
104
|
+
// Observe only the <title> element — watching head/documentElement with
|
|
105
|
+
// subtree:true re-fires on every React DOM mutation and can livelock CDP.
|
|
106
|
+
window.__farmslotSlotTitleObserver.observe(node, {
|
|
107
|
+
childList: true,
|
|
108
|
+
characterData: true,
|
|
109
|
+
subtree: true,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
return document.title;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function buildStampExpression(slotId) {
|
|
116
|
+
const slot = sanitizeSlotId(slotId);
|
|
117
|
+
if (!slot) return 'document.title';
|
|
118
|
+
// Single source of truth: serialize the Playwright callback for CDP evaluate.
|
|
119
|
+
return `(${applyPersistentSlotTitle.toString()})(${JSON.stringify(slot)})`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function assertLocalUrl(url) {
|
|
123
|
+
if (!/^http:\/\/127\.0\.0\.1:\d+\//u.test(url)) {
|
|
124
|
+
throw new Error(`slot-title: refusing non-local URL: ${url}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function assertLocalWebSocketUrl(url) {
|
|
129
|
+
if (!/^ws:\/\/(?:127\.0\.0\.1|localhost):\d+\//u.test(String(url))) {
|
|
130
|
+
throw new Error(`slot-title: refusing non-local websocket URL: ${url}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function httpJson(url, timeoutMs = 3000) {
|
|
135
|
+
assertLocalUrl(url);
|
|
136
|
+
return new Promise((resolve, reject) => {
|
|
137
|
+
const req = http.get(url, (res) => {
|
|
138
|
+
let data = '';
|
|
139
|
+
res.setEncoding('utf8');
|
|
140
|
+
res.on('data', (chunk) => {
|
|
141
|
+
data += chunk;
|
|
142
|
+
});
|
|
143
|
+
res.on('end', () => {
|
|
144
|
+
try {
|
|
145
|
+
resolve(JSON.parse(data));
|
|
146
|
+
} catch (err) {
|
|
147
|
+
reject(new Error(`invalid JSON from ${url}: ${err.message}`));
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
req.setTimeout(timeoutMs, () => {
|
|
152
|
+
req.destroy(new Error(`timeout from ${url}`));
|
|
153
|
+
});
|
|
154
|
+
req.on('error', reject);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function resolveWebSocket(target) {
|
|
159
|
+
try {
|
|
160
|
+
// Prefer the target checkout's ws, then the harness install.
|
|
161
|
+
const { createRequire } = require('node:module');
|
|
162
|
+
const req = createRequire(__filename);
|
|
163
|
+
if (target) {
|
|
164
|
+
try {
|
|
165
|
+
return req(req.resolve('ws', { paths: [target] }));
|
|
166
|
+
} catch {
|
|
167
|
+
// Fall through to harness / global.
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return req('ws');
|
|
171
|
+
} catch {
|
|
172
|
+
return typeof WebSocket === 'function' ? WebSocket : null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function cdpEvaluate(target, webSocketDebuggerUrl, expression, timeoutMs = 5000) {
|
|
177
|
+
assertLocalWebSocketUrl(webSocketDebuggerUrl);
|
|
178
|
+
const WebSocketImpl = resolveWebSocket(target);
|
|
179
|
+
if (!WebSocketImpl) return { skipped: true, reason: 'WebSocket unavailable' };
|
|
180
|
+
return new Promise((resolve, reject) => {
|
|
181
|
+
const ws = new WebSocketImpl(webSocketDebuggerUrl);
|
|
182
|
+
const timer = setTimeout(() => {
|
|
183
|
+
try {
|
|
184
|
+
ws.close();
|
|
185
|
+
} catch {
|
|
186
|
+
// Best-effort timeout cleanup.
|
|
187
|
+
}
|
|
188
|
+
reject(new Error('timeout evaluating extension page via CDP'));
|
|
189
|
+
}, timeoutMs);
|
|
190
|
+
const onOpen = () => {
|
|
191
|
+
ws.send(
|
|
192
|
+
JSON.stringify({
|
|
193
|
+
id: 1,
|
|
194
|
+
method: 'Runtime.evaluate',
|
|
195
|
+
params: { expression, awaitPromise: true, returnByValue: true },
|
|
196
|
+
}),
|
|
197
|
+
);
|
|
198
|
+
};
|
|
199
|
+
const onMessage = (event) => {
|
|
200
|
+
const raw = event?.data ?? event;
|
|
201
|
+
const msg = JSON.parse(Buffer.isBuffer(raw) ? raw.toString('utf8') : String(raw));
|
|
202
|
+
if (msg.id !== 1) return;
|
|
203
|
+
clearTimeout(timer);
|
|
204
|
+
ws.close();
|
|
205
|
+
if (msg.error) {
|
|
206
|
+
reject(new Error(msg.error.message || JSON.stringify(msg.error)));
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
resolve(msg.result?.result?.value ?? null);
|
|
210
|
+
};
|
|
211
|
+
const onError = (err) => {
|
|
212
|
+
clearTimeout(timer);
|
|
213
|
+
reject(new Error(`CDP websocket error while stamping slot title: ${err?.message || err || 'unknown'}`));
|
|
214
|
+
};
|
|
215
|
+
if (typeof ws.on === 'function') {
|
|
216
|
+
ws.on('open', onOpen);
|
|
217
|
+
ws.on('message', onMessage);
|
|
218
|
+
ws.on('error', onError);
|
|
219
|
+
} else {
|
|
220
|
+
ws.addEventListener('open', onOpen);
|
|
221
|
+
ws.addEventListener('message', onMessage);
|
|
222
|
+
ws.addEventListener('error', onError);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function isHomePage(target, extensionId) {
|
|
228
|
+
if (!target || target.type !== 'page' || typeof target.url !== 'string') return false;
|
|
229
|
+
if (!target.url.startsWith(`chrome-extension://${extensionId}/`)) return false;
|
|
230
|
+
return target.url.includes('/home.html');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Stamp every inspectable home.html tab for this extension over CDP.
|
|
235
|
+
* Attached tabs (no webSocketDebuggerUrl) are skipped — another client owns them.
|
|
236
|
+
*/
|
|
237
|
+
async function stampHomeTabsViaCdp(options) {
|
|
238
|
+
const {
|
|
239
|
+
target = process.cwd(),
|
|
240
|
+
cdpPort,
|
|
241
|
+
extensionId,
|
|
242
|
+
slotId: slotIdOption,
|
|
243
|
+
runtimeDir,
|
|
244
|
+
} = options || {};
|
|
245
|
+
const slotId = sanitizeSlotId(slotIdOption) || readSlotId(target, runtimeDir);
|
|
246
|
+
const empty = { slotId: slotId || '', stamped: 0, skipped: 0, titles: [] };
|
|
247
|
+
if (!slotId || !cdpPort || !extensionId) return empty;
|
|
248
|
+
|
|
249
|
+
let targets;
|
|
250
|
+
try {
|
|
251
|
+
targets = await httpJson(`http://127.0.0.1:${cdpPort}/json/list`);
|
|
252
|
+
} catch {
|
|
253
|
+
return empty;
|
|
254
|
+
}
|
|
255
|
+
if (!Array.isArray(targets)) return empty;
|
|
256
|
+
|
|
257
|
+
const homes = targets.filter((t) => isHomePage(t, extensionId));
|
|
258
|
+
const titles = [];
|
|
259
|
+
let stamped = 0;
|
|
260
|
+
let skipped = 0;
|
|
261
|
+
const expression = buildStampExpression(slotId);
|
|
262
|
+
|
|
263
|
+
for (const home of homes) {
|
|
264
|
+
if (typeof home.webSocketDebuggerUrl !== 'string') {
|
|
265
|
+
skipped += 1;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
const title = await cdpEvaluate(target, home.webSocketDebuggerUrl, expression);
|
|
270
|
+
if (title && typeof title === 'object' && title.skipped) {
|
|
271
|
+
skipped += 1;
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
if (typeof title === 'string') {
|
|
275
|
+
titles.push(title);
|
|
276
|
+
stamped += 1;
|
|
277
|
+
} else {
|
|
278
|
+
skipped += 1;
|
|
279
|
+
}
|
|
280
|
+
} catch {
|
|
281
|
+
skipped += 1;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return { slotId, stamped, skipped, titles };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
module.exports = {
|
|
289
|
+
sanitizeSlotId,
|
|
290
|
+
readSlotId,
|
|
291
|
+
applyPersistentSlotTitle,
|
|
292
|
+
buildStampExpression,
|
|
293
|
+
stampHomeTabsViaCdp,
|
|
294
|
+
};
|
|
File without changes
|
|
@@ -22,6 +22,10 @@ import { fileURLToPath } from 'node:url';
|
|
|
22
22
|
|
|
23
23
|
const require = createRequire(import.meta.url);
|
|
24
24
|
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
const {
|
|
26
|
+
readSlotId: readSlotIdFromLib,
|
|
27
|
+
buildStampExpression,
|
|
28
|
+
} = require(path.join(scriptDir, 'lib/slot-title.cjs'));
|
|
25
29
|
|
|
26
30
|
function pathDefault(key) {
|
|
27
31
|
for (const candidate of [
|
|
@@ -101,25 +105,7 @@ function readExpectedExtensionId(target) {
|
|
|
101
105
|
}
|
|
102
106
|
|
|
103
107
|
function readSlotId(target) {
|
|
104
|
-
|
|
105
|
-
const value = sanitizeSlotId(process.env[key]);
|
|
106
|
-
if (value) return value;
|
|
107
|
-
}
|
|
108
|
-
try {
|
|
109
|
-
const runtimeContext = JSON.parse(fs.readFileSync(path.join(target, recipeRuntimeDir(), 'agentic-runtime.json'), 'utf8'));
|
|
110
|
-
const value = sanitizeSlotId(runtimeContext.slotId);
|
|
111
|
-
if (value) {
|
|
112
|
-
return value;
|
|
113
|
-
}
|
|
114
|
-
} catch {
|
|
115
|
-
// No slot context: standalone harness users do not need a custom browser title.
|
|
116
|
-
}
|
|
117
|
-
return '';
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function sanitizeSlotId(value) {
|
|
121
|
-
const slotId = typeof value === 'string' ? value.trim() : '';
|
|
122
|
-
return /^[A-Za-z0-9._:-]{1,64}$/u.test(slotId) ? slotId : '';
|
|
108
|
+
return readSlotIdFromLib(target, recipeRuntimeDir());
|
|
123
109
|
}
|
|
124
110
|
|
|
125
111
|
function writeExtensionId(target, extensionId) {
|
|
@@ -407,16 +393,9 @@ async function inspectCdp(target, cdpPort, expectedExtensionId, expectedServiceW
|
|
|
407
393
|
const stampedTitle = await cdpEvaluate(
|
|
408
394
|
target,
|
|
409
395
|
pageTarget.webSocketDebuggerUrl,
|
|
410
|
-
|
|
411
|
-
const slot = ${JSON.stringify(slotId)};
|
|
412
|
-
const current = document.title || 'MetaMask';
|
|
413
|
-
const base = current.replace(/^.+?\\s+—\\s+/, '') || 'MetaMask';
|
|
414
|
-
const next = slot + ' — ' + base;
|
|
415
|
-
if (document.title !== next) document.title = next;
|
|
416
|
-
return document.title;
|
|
417
|
-
})()`,
|
|
396
|
+
buildStampExpression(slotId),
|
|
418
397
|
);
|
|
419
|
-
if (stampedTitle
|
|
398
|
+
if (typeof stampedTitle === 'string') {
|
|
420
399
|
ui.title = stampedTitle;
|
|
421
400
|
ui.slotTitleApplied = true;
|
|
422
401
|
}
|
|
@@ -123,6 +123,7 @@ let chromium; try { chromium = require('@playwright/test').chromium; } catch { c
|
|
|
123
123
|
const fs = require('node:fs');
|
|
124
124
|
const path = require('node:path');
|
|
125
125
|
const { extensionIdFromManifestFile } = require(path.join(process.env.SCRIPT_DIR, 'lib/extension-id.cjs'));
|
|
126
|
+
const { readSlotId, applyPersistentSlotTitle } = require(path.join(process.env.SCRIPT_DIR, 'lib/slot-title.cjs'));
|
|
126
127
|
|
|
127
128
|
const port = process.env.CDP_PORT;
|
|
128
129
|
const target = process.env.TARGET;
|
|
@@ -131,22 +132,13 @@ const runtimeDistDir = process.env.RUNTIME_DIST_DIR;
|
|
|
131
132
|
const startUrl = process.env.START_URL || '';
|
|
132
133
|
const settleMs = Number(process.env.SETTLE_MS || '8000') || 8000;
|
|
133
134
|
const displayMode = process.env.DISPLAY_MODE || 'fullscreen';
|
|
134
|
-
const slotId =
|
|
135
|
+
const slotId = readSlotId(target, runtimeDir);
|
|
135
136
|
|
|
136
137
|
if (!runtimeDir || !runtimeDistDir) {
|
|
137
138
|
console.error('reattach: resolved runtime paths were not provided.');
|
|
138
139
|
process.exit(2);
|
|
139
140
|
}
|
|
140
141
|
|
|
141
|
-
function readRuntimeSlotId() {
|
|
142
|
-
try {
|
|
143
|
-
const runtimeContext = JSON.parse(fs.readFileSync(path.join(target, runtimeDir, 'agentic-runtime.json'), 'utf8'));
|
|
144
|
-
return typeof runtimeContext.slotId === 'string' ? runtimeContext.slotId.trim() : '';
|
|
145
|
-
} catch {
|
|
146
|
-
return '';
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
142
|
function extensionIdFromRuntimeDist() {
|
|
151
143
|
return extensionIdFromManifestFile(path.join(target, runtimeDir, runtimeDistDir, 'manifest.json'));
|
|
152
144
|
}
|
|
@@ -204,11 +196,7 @@ async function connect() {
|
|
|
204
196
|
|
|
205
197
|
async function stampSlotTitle(page) {
|
|
206
198
|
if (!slotId || !page) return;
|
|
207
|
-
await page.evaluate((
|
|
208
|
-
const base = (document.title || 'MetaMask').replace(/^.+?\s+—\s+/, '') || 'MetaMask';
|
|
209
|
-
const next = `${slot} — ${base}`;
|
|
210
|
-
if (document.title !== next) document.title = next;
|
|
211
|
-
}, slotId).catch(() => {
|
|
199
|
+
await page.evaluate(applyPersistentSlotTitle, slotId).catch(() => {
|
|
212
200
|
// Best-effort operator affordance; reload success is verified by CDP state.
|
|
213
201
|
});
|
|
214
202
|
}
|
|
@@ -285,6 +273,16 @@ async function closePages(context, predicate) {
|
|
|
285
273
|
.catch(() => { /* wallet home is best-effort; the reload already applied */ });
|
|
286
274
|
}
|
|
287
275
|
await stampSlotTitle(home);
|
|
276
|
+
for (const page of allPages(context)) {
|
|
277
|
+
const url = page.url();
|
|
278
|
+
if (
|
|
279
|
+
page !== home &&
|
|
280
|
+
url.startsWith(`chrome-extension://${extId}/`) &&
|
|
281
|
+
url.includes('/home.html')
|
|
282
|
+
) {
|
|
283
|
+
await stampSlotTitle(page);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
288
286
|
await closeDisposableChromeTabs(context);
|
|
289
287
|
if (startUrl) {
|
|
290
288
|
const dapp = allPages(context)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/adapters/manifest.json
CHANGED
|
@@ -106,6 +106,14 @@
|
|
|
106
106
|
"inputs": "--target --runtime-dir --runner-bin --summary",
|
|
107
107
|
"outputs": "<runtime-dir>/recipe-harness-webpack.{pid,log}; optional summary; exit 0/1/2"
|
|
108
108
|
},
|
|
109
|
+
{
|
|
110
|
+
"id": "extension/stop-viewers",
|
|
111
|
+
"entry": "adapters/extension/stop-viewers.sh",
|
|
112
|
+
"kind": "bash",
|
|
113
|
+
"purpose": "Close extension webpack/console tmux viewers and the checkout-scoped console tail.",
|
|
114
|
+
"inputs": "--target <metamask-extension>",
|
|
115
|
+
"outputs": "removes viewer markers and closes matching run-owned windows; exit 0/1/2"
|
|
116
|
+
},
|
|
109
117
|
{
|
|
110
118
|
"id": "extension/console-tail",
|
|
111
119
|
"entry": "adapters/extension/console-tail.mjs",
|
|
@@ -306,6 +314,14 @@
|
|
|
306
314
|
"inputs": "resolve_run_tmux_session [runtime-dir]; env RECIPE_TMUX_SESSION, RECIPE_RUNTIME_CONTEXT, TMUX",
|
|
307
315
|
"outputs": "resolved session name on stdout (empty when none)"
|
|
308
316
|
},
|
|
317
|
+
{
|
|
318
|
+
"id": "lib/tmux-viewer",
|
|
319
|
+
"entry": "adapters/shared/tmux-viewer.sh",
|
|
320
|
+
"kind": "lib",
|
|
321
|
+
"purpose": "Run-owned tmux log viewer helpers that open, replace, and clean prefixed viewer windows without slot-name inference.",
|
|
322
|
+
"inputs": "tmux_viewer_open/close_marker/sweep_prefix functions; env RECIPE_TMUX_SESSION, RECIPE_RUNTIME_CONTEXT, TMUX",
|
|
323
|
+
"outputs": "viewer windows and marker files; no-op when no run-owned session is available"
|
|
324
|
+
},
|
|
309
325
|
{
|
|
310
326
|
"id": "lib/activate-repo-node",
|
|
311
327
|
"entry": "adapters/shared/activate-repo-node.sh",
|
|
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
|
|
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
|
|
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
|
|
@@ -1,5 +1,11 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
1
2
|
import path from "node:path";
|
|
3
|
+
import { recipeRuntimeDir, runnerDir } from "../../paths.js";
|
|
2
4
|
import { resolveExtensionId } from "./extension-id.js";
|
|
5
|
+
const requireFromHarness = createRequire(import.meta.url);
|
|
6
|
+
const { stampHomeTabsViaCdp } = requireFromHarness(
|
|
7
|
+
path.join(runnerDir, "adapters/extension/lib/slot-title.cjs")
|
|
8
|
+
);
|
|
3
9
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
4
10
|
async function jsonList(port) {
|
|
5
11
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
@@ -161,12 +167,25 @@ async function ensureExtensionReady(target, options) {
|
|
|
161
167
|
health = await checkHealth();
|
|
162
168
|
}
|
|
163
169
|
}
|
|
170
|
+
let slotTitle;
|
|
171
|
+
if (after >= 1) {
|
|
172
|
+
const stamp = await stampHomeTabsViaCdp({
|
|
173
|
+
target: resolved,
|
|
174
|
+
cdpPort,
|
|
175
|
+
extensionId,
|
|
176
|
+
runtimeDir: recipeRuntimeDir()
|
|
177
|
+
});
|
|
178
|
+
if (stamp.slotId || stamp.stamped > 0 || stamp.skipped > 0) {
|
|
179
|
+
slotTitle = stamp;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
164
182
|
const ready = health.status === "PASS" && after === 1;
|
|
165
183
|
return base({
|
|
166
184
|
extensionId,
|
|
167
185
|
opened,
|
|
168
186
|
action,
|
|
169
187
|
homeTabs: { before, closed, after },
|
|
188
|
+
...slotTitle ? { slotTitle } : {},
|
|
170
189
|
ready,
|
|
171
190
|
reasonCode: ready ? "ready" : after !== 1 ? "tab-count" : "unhealthy",
|
|
172
191
|
health
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deeeed/metamask-harness",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"mm-harness": "bin/mm-harness"
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"@farmslot/protocol": "^0.7.6",
|
|
21
21
|
"@farmslot/recipe-harness": "^0.4.3",
|
|
22
22
|
"commander": "^12.0.0",
|
|
23
|
-
"viem": "^2.54.3"
|
|
23
|
+
"viem": "^2.54.3",
|
|
24
|
+
"ws": "8.21.0"
|
|
24
25
|
},
|
|
25
26
|
"resolutions": {
|
|
26
27
|
"esbuild": "0.28.1",
|
|
@@ -69,4 +70,4 @@
|
|
|
69
70
|
"url": "https://github.com/MetaMask/experimental-metamask-harness/issues"
|
|
70
71
|
},
|
|
71
72
|
"homepage": "https://github.com/MetaMask/experimental-metamask-harness#readme"
|
|
72
|
-
}
|
|
73
|
+
}
|
package/scripts/completions.sh
CHANGED
|
File without changes
|
|
File without changes
|