@deeeed/metamask-harness 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/CHANGELOG.md +161 -0
  2. package/README.md +140 -0
  3. package/bin/mm-harness +99 -0
  4. package/docs/CHEATSHEET.md +61 -0
  5. package/docs/CLI-SPEC.md +915 -0
  6. package/docs/MENTAL-MODEL.md +295 -0
  7. package/docs/architecture.md +367 -0
  8. package/docs/extension-runtime-commands.md +60 -0
  9. package/docs/harness-cli.md +43 -0
  10. package/docs/live-adapter-contract.md +188 -0
  11. package/docs/package-boundaries.md +47 -0
  12. package/docs/perps-flow-catalog.md +235 -0
  13. package/docs/recipe-libraries.md +95 -0
  14. package/docs/runtime-file-conventions.md +36 -0
  15. package/library/actions/core/perps/_controller.mjs +727 -0
  16. package/library/actions/core/perps/assert_orders.mjs +53 -0
  17. package/library/actions/core/perps/assert_positions.mjs +52 -0
  18. package/library/actions/core/perps/close_orders.mjs +97 -0
  19. package/library/actions/core/perps/close_positions.mjs +118 -0
  20. package/library/actions/core/perps/ensure_orders.mjs +40 -0
  21. package/library/actions/core/perps/ensure_positions.mjs +37 -0
  22. package/library/actions/core/perps/place_order.mjs +201 -0
  23. package/library/actions/core/perps/read_account.mjs +30 -0
  24. package/library/actions/core/perps/read_orders.mjs +27 -0
  25. package/library/actions/core/perps/read_positions.mjs +27 -0
  26. package/library/actions/core/perps/start_state.mjs +92 -0
  27. package/library/actions/core/perps/teardown_state.mjs +86 -0
  28. package/library/actions/extension/perps/assert_orders.mjs +11 -0
  29. package/library/actions/extension/perps/assert_positions.mjs +11 -0
  30. package/library/actions/extension/perps/close_orders.mjs +8 -0
  31. package/library/actions/extension/perps/close_positions.mjs +8 -0
  32. package/library/actions/extension/perps/ensure_orders.mjs +4 -0
  33. package/library/actions/extension/perps/ensure_positions.mjs +4 -0
  34. package/library/actions/extension/perps/perps.mjs +730 -0
  35. package/library/actions/extension/perps/place_order.mjs +7 -0
  36. package/library/actions/extension/perps/read_orders.mjs +4 -0
  37. package/library/actions/extension/perps/read_positions.mjs +3 -0
  38. package/library/actions/extension/platform/cdp.mjs +541 -0
  39. package/library/actions/extension/ui/navigate.mjs +44 -0
  40. package/library/actions/extension/wallet/ensure_unlocked.mjs +36 -0
  41. package/library/actions/extension/wallet/read_state.mjs +27 -0
  42. package/library/actions/extension/wallet/select_account.mjs +48 -0
  43. package/library/actions/extension/wallet/setup.mjs +35 -0
  44. package/library/actions/mobile/app-overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +185 -0
  45. package/library/actions/mobile/app-overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +1662 -0
  46. package/library/actions/mobile/bridge-runtime/cdp-bridge.cjs +686 -0
  47. package/library/actions/mobile/bridge-runtime/lib/cdp-eval.cjs +110 -0
  48. package/library/actions/mobile/bridge-runtime/lib/config.cjs +39 -0
  49. package/library/actions/mobile/bridge-runtime/lib/issue-capture.cjs +446 -0
  50. package/library/actions/mobile/bridge-runtime/lib/target-discovery.cjs +204 -0
  51. package/library/actions/mobile/bridge-runtime/lib/ws-client.cjs +108 -0
  52. package/library/actions/mobile/bridge-runtime/setup-wallet.sh +442 -0
  53. package/library/actions/mobile/perps/assert_orders.mjs +11 -0
  54. package/library/actions/mobile/perps/assert_positions.mjs +11 -0
  55. package/library/actions/mobile/perps/close_orders.mjs +8 -0
  56. package/library/actions/mobile/perps/close_positions.mjs +8 -0
  57. package/library/actions/mobile/perps/ensure_orders.mjs +4 -0
  58. package/library/actions/mobile/perps/ensure_positions.mjs +4 -0
  59. package/library/actions/mobile/perps/perps.mjs +709 -0
  60. package/library/actions/mobile/perps/place_order.mjs +7 -0
  61. package/library/actions/mobile/perps/read_orders.mjs +4 -0
  62. package/library/actions/mobile/perps/read_positions.mjs +3 -0
  63. package/library/actions/mobile/platform/bridge.mjs +283 -0
  64. package/library/actions/mobile/ui/navigate.mjs +38 -0
  65. package/library/actions/mobile/wallet/ensure_unlocked.mjs +107 -0
  66. package/library/actions/mobile/wallet/home.mjs +35 -0
  67. package/library/actions/mobile/wallet/read_state.mjs +40 -0
  68. package/library/actions/mobile/wallet/select_account.mjs +48 -0
  69. package/library/actions/mobile/wallet/setup.mjs +220 -0
  70. package/library/flows/perps.flows.json +64 -0
  71. package/library/library.json +7 -0
  72. package/library/manifests/core.action-manifest.json +1282 -0
  73. package/library/manifests/extension.action-manifest.json +1749 -0
  74. package/library/manifests/mobile.action-manifest.json +1753 -0
  75. package/library/recipes/action-validation.extension.recipe.json +417 -0
  76. package/library/recipes/action-validation.mobile.recipe.json +422 -0
  77. package/library/recipes/order-lifecycle.core.recipe.json +78 -0
  78. package/library/recipes/perps-lifecycle.recipe.json +194 -0
  79. package/library/recipes/read-markets.core.recipe.json +38 -0
  80. package/library/recipes/smoke.extension.recipe.json +31 -0
  81. package/library/recipes/smoke.mobile.recipe.json +31 -0
  82. package/library/recipes/trading-lifecycle.core.recipe.json +76 -0
  83. package/orchestration/compat-overlays/README.md +19 -0
  84. package/orchestration/compat-overlays/mobile/README.md +13 -0
  85. package/orchestration/compat-overlays/mobile/rn81-message-event-source.patch +42 -0
  86. package/orchestration/core/cleanup.sh +37 -0
  87. package/orchestration/core/inject.sh +154 -0
  88. package/orchestration/doctor.mjs +72 -0
  89. package/orchestration/extension/cleanup.mjs +60 -0
  90. package/orchestration/extension/console-tail.mjs +228 -0
  91. package/orchestration/extension/ensure-browser.sh +416 -0
  92. package/orchestration/extension/ensure-ready.ts +185 -0
  93. package/orchestration/extension/extension-id.ts +107 -0
  94. package/orchestration/extension/inject.mjs +266 -0
  95. package/orchestration/extension/launch-browser.cjs +216 -0
  96. package/orchestration/extension/launch.sh +175 -0
  97. package/orchestration/extension/live.sh +320 -0
  98. package/orchestration/extension/pin-remote-flags.cjs +45 -0
  99. package/orchestration/extension/readiness.mjs +414 -0
  100. package/orchestration/extension/refresh-build.sh +190 -0
  101. package/orchestration/extension/runtime-decision.ts +445 -0
  102. package/orchestration/extension/runtime.ts +407 -0
  103. package/orchestration/extension/seed-fixture.sh +177 -0
  104. package/orchestration/extension/sidepanel-toggle.sh +291 -0
  105. package/orchestration/extension/snapshot-dist.sh +84 -0
  106. package/orchestration/extension/start-watch.sh +339 -0
  107. package/orchestration/extension/wallet-fixture-state.cjs +1086 -0
  108. package/orchestration/lib/activate-repo-node.sh +144 -0
  109. package/orchestration/lib/cli-color.mjs +84 -0
  110. package/orchestration/lib/cli-commands.mjs +243 -0
  111. package/orchestration/lib/cli-home.mjs +354 -0
  112. package/orchestration/lib/cli-ux.sh +252 -0
  113. package/orchestration/lib/cli-version.mjs +123 -0
  114. package/orchestration/lib/ensure-runner-deps.sh +56 -0
  115. package/orchestration/lib/harness-path.sh +55 -0
  116. package/orchestration/lib/hash-helpers.sh +44 -0
  117. package/orchestration/lib/json-field.sh +23 -0
  118. package/orchestration/lib/log-tui.mjs +304 -0
  119. package/orchestration/lib/open-debug.mjs +317 -0
  120. package/orchestration/lib/path-defaults.json +4 -0
  121. package/orchestration/lib/progress.mjs +107 -0
  122. package/orchestration/lib/recipe-paths.mjs +26 -0
  123. package/orchestration/lib/resolve-farmslot-ports.sh +144 -0
  124. package/orchestration/manifest.json +358 -0
  125. package/orchestration/mobile/cleanup.sh +192 -0
  126. package/orchestration/mobile/deps-markers.ts +21 -0
  127. package/orchestration/mobile/inject.sh +681 -0
  128. package/orchestration/mobile/launch.sh +137 -0
  129. package/orchestration/mobile/live.sh +125 -0
  130. package/orchestration/mobile/runtime-decision.ts +292 -0
  131. package/orchestration/porcelain/metamask-recipe +99 -0
  132. package/orchestration/porcelain/mm-recipe +1591 -0
  133. package/orchestration/porcelain/mme-recipe +1181 -0
  134. package/package.json +59 -0
  135. package/runner/extension/verify.sh +511 -0
  136. package/runner/mobile/verify.sh +501 -0
  137. package/runner/src/adapters.ts +601 -0
  138. package/runner/src/cli.ts +1820 -0
  139. package/runner/src/commands/debug.ts +44 -0
  140. package/runner/src/commands/fixtures.ts +99 -0
  141. package/runner/src/commands/launch.ts +397 -0
  142. package/runner/src/commands/logs.ts +60 -0
  143. package/runner/src/commands/shared.ts +138 -0
  144. package/runner/src/completions-cache.ts +86 -0
  145. package/runner/src/doctor.ts +203 -0
  146. package/runner/src/harness.ts +516 -0
  147. package/runner/src/heal-bounds.ts +179 -0
  148. package/runner/src/index.ts +6 -0
  149. package/runner/src/live-adapter-contract.ts +274 -0
  150. package/runner/src/manifest.ts +47 -0
  151. package/runner/src/mm-harness-cli.ts +488 -0
  152. package/runner/src/paths.ts +198 -0
  153. package/runner/src/recording-target.ts +147 -0
  154. package/runner/src/run-recording.ts +329 -0
  155. package/runner/src/runner.ts +108 -0
  156. package/runner/src/types.ts +57 -0
  157. package/scripts/completions.sh +125 -0
  158. package/scripts/install-completions.sh +62 -0
@@ -0,0 +1,414 @@
1
+ #!/usr/bin/env node
2
+ // readiness.mjs — health probe for one extension instance
3
+ // runner/extension/extension-readiness.mjs)
4
+ //
5
+ // Purpose:
6
+ // App-control health probe: verifies the dist build's expected entry
7
+ // files and (with --cdp-port) the live extension target over CDP,
8
+ // repairing <runtime-dir>/extension.id when the runtime disagrees.
9
+ //
10
+ // Inputs (flags / env):
11
+ // --target <metamask-extension> (default $PWD), --cdp-port <port>, --json
12
+ // env RECIPE_RUNTIME_DIR (else lib path-defaults)
13
+ // Outputs: readiness report (JSON with --json) on stdout.
14
+ // Exit 0 — ready; non-zero — not ready / bad args (thrown message).
15
+ // Never touches: the dist build (read-only) other than the
16
+ // <runtime-dir>/extension.id repair.
17
+ import fs from 'node:fs';
18
+ import http from 'node:http';
19
+ import path from 'node:path';
20
+ import { createRequire } from 'node:module';
21
+ import { fileURLToPath } from 'node:url';
22
+
23
+ const require = createRequire(import.meta.url);
24
+ const scriptDir = path.dirname(fileURLToPath(import.meta.url));
25
+
26
+ function pathDefault(key) {
27
+ for (const candidate of [
28
+ path.join(scriptDir, 'lib/path-defaults.json'),
29
+ path.join(scriptDir, '../lib/path-defaults.json'),
30
+ path.join(scriptDir, '../../orchestration/lib/path-defaults.json'),
31
+ ]) {
32
+ if (!fs.existsSync(candidate)) continue;
33
+ const value = JSON.parse(fs.readFileSync(candidate, 'utf8'))[key];
34
+ if (value) return value;
35
+ }
36
+ throw new Error(`Missing path default: ${key}`);
37
+ }
38
+
39
+ function recipeRuntimeDir() {
40
+ return process.env.RECIPE_RUNTIME_DIR || pathDefault('recipeRuntimeDir');
41
+ }
42
+
43
+ function parseArgs(argv) {
44
+ const out = { target: process.cwd(), cdpPort: '', json: false };
45
+ for (let i = 0; i < argv.length; i += 1) {
46
+ const arg = argv[i];
47
+ if (arg === '--target') {
48
+ out.target = argv[++i];
49
+ } else if (arg === '--cdp-port') {
50
+ out.cdpPort = argv[++i];
51
+ } else if (arg === '--json') {
52
+ out.json = true;
53
+ } else if (arg === '-h' || arg === '--help') {
54
+ console.log('Usage: readiness.mjs --target <metamask-extension> [--cdp-port <port>] [--json]');
55
+ process.exit(0);
56
+ } else {
57
+ throw new Error(`Unknown arg: ${arg}`);
58
+ }
59
+ }
60
+ return out;
61
+ }
62
+
63
+ function readManifest(target) {
64
+ const manifestPath = path.join(target, 'dist/chrome/manifest.json');
65
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
66
+ return { manifestPath, manifest };
67
+ }
68
+
69
+ function manifestExpectedFiles(manifest) {
70
+ const entries = new Set(['home.html']);
71
+ if (manifest.background?.service_worker) entries.add(manifest.background.service_worker);
72
+ for (const script of manifest.background?.scripts || []) entries.add(script);
73
+ const popup =
74
+ manifest.action?.default_popup ||
75
+ manifest.browser_action?.default_popup ||
76
+ manifest.page_action?.default_popup;
77
+ if (popup) entries.add(popup);
78
+ if (manifest.side_panel?.default_path) entries.add(manifest.side_panel.default_path);
79
+ return [...entries];
80
+ }
81
+
82
+ function manifestDefaultPage(manifest) {
83
+ return (
84
+ manifest.action?.default_popup ||
85
+ manifest.browser_action?.default_popup ||
86
+ manifest.page_action?.default_popup ||
87
+ manifest.side_panel?.default_path ||
88
+ 'home.html'
89
+ );
90
+ }
91
+
92
+ function extensionIdPath(target) {
93
+ return path.join(target, recipeRuntimeDir(), 'extension.id');
94
+ }
95
+
96
+ function readExpectedExtensionId(target) {
97
+ const idPath = path.join(target, recipeRuntimeDir(), 'extension.id');
98
+ if (!fs.existsSync(idPath)) return '';
99
+ const id = fs.readFileSync(idPath, 'utf8').trim();
100
+ return /^[a-p]{32}$/.test(id) ? id : '';
101
+ }
102
+
103
+ function writeExtensionId(target, extensionId) {
104
+ if (!/^[a-p]{32}$/.test(extensionId)) return false;
105
+ const idPath = extensionIdPath(target);
106
+ fs.mkdirSync(path.dirname(idPath), { recursive: true });
107
+ const existing = fs.existsSync(idPath) ? fs.readFileSync(idPath, 'utf8').trim() : '';
108
+ if (existing === extensionId) return false;
109
+ fs.writeFileSync(idPath, `${extensionId}\n`);
110
+ return true;
111
+ }
112
+
113
+ function httpJson(url, timeoutMs = 3000) {
114
+ return new Promise((resolve, reject) => {
115
+ const req = http.get(url, (res) => {
116
+ let data = '';
117
+ res.setEncoding('utf8');
118
+ res.on('data', (chunk) => {
119
+ data += chunk;
120
+ });
121
+ res.on('end', () => {
122
+ try {
123
+ resolve(JSON.parse(data));
124
+ } catch (err) {
125
+ reject(new Error(`invalid JSON from ${url}: ${err.message}`));
126
+ }
127
+ });
128
+ });
129
+ req.setTimeout(timeoutMs, () => {
130
+ req.destroy(new Error(`timeout from ${url}`));
131
+ });
132
+ req.on('error', reject);
133
+ });
134
+ }
135
+
136
+ function httpJsonRequest(method, url, timeoutMs = 3000) {
137
+ return new Promise((resolve, reject) => {
138
+ const req = http.request(url, { method }, (res) => {
139
+ let data = '';
140
+ res.setEncoding('utf8');
141
+ res.on('data', (chunk) => {
142
+ data += chunk;
143
+ });
144
+ res.on('end', () => {
145
+ try {
146
+ resolve(JSON.parse(data));
147
+ } catch (err) {
148
+ reject(new Error(`invalid JSON from ${method} ${url}: ${err.message}`));
149
+ }
150
+ });
151
+ });
152
+ req.setTimeout(timeoutMs, () => {
153
+ req.destroy(new Error(`timeout from ${method} ${url}`));
154
+ });
155
+ req.on('error', reject);
156
+ req.end();
157
+ });
158
+ }
159
+
160
+ function resolveWebSocket(target) {
161
+ try {
162
+ return require(require.resolve('ws', { paths: [target, process.cwd()] }));
163
+ } catch {
164
+ return typeof WebSocket === 'function' ? WebSocket : null;
165
+ }
166
+ }
167
+
168
+ async function cdpEvaluate(target, webSocketDebuggerUrl, expression, timeoutMs = 5000) {
169
+ const WebSocketImpl = resolveWebSocket(target);
170
+ if (!WebSocketImpl) return { skipped: true, reason: 'WebSocket unavailable in this Node runtime' };
171
+ return new Promise((resolve, reject) => {
172
+ const ws = new WebSocketImpl(webSocketDebuggerUrl);
173
+ const timer = setTimeout(() => {
174
+ try {
175
+ ws.close();
176
+ } catch {
177
+ // Best effort timeout cleanup.
178
+ }
179
+ reject(new Error('timeout evaluating extension page via CDP'));
180
+ }, timeoutMs);
181
+ const onOpen = () => {
182
+ ws.send(JSON.stringify({
183
+ id: 1,
184
+ method: 'Runtime.evaluate',
185
+ params: { expression, awaitPromise: true, returnByValue: true },
186
+ }));
187
+ };
188
+ const onMessage = (event) => {
189
+ const raw = event?.data ?? event;
190
+ const msg = JSON.parse(Buffer.isBuffer(raw) ? raw.toString('utf8') : String(raw));
191
+ if (msg.id !== 1) return;
192
+ clearTimeout(timer);
193
+ ws.close();
194
+ if (msg.error) {
195
+ reject(new Error(msg.error.message || JSON.stringify(msg.error)));
196
+ return;
197
+ }
198
+ resolve(msg.result?.result?.value ?? null);
199
+ };
200
+ const onError = (err) => {
201
+ clearTimeout(timer);
202
+ reject(new Error(`CDP websocket error while inspecting extension page: ${err?.message || err || 'unknown'}`));
203
+ };
204
+ if (typeof ws.on === 'function') {
205
+ ws.on('open', onOpen);
206
+ ws.on('message', onMessage);
207
+ ws.on('error', onError);
208
+ } else {
209
+ ws.addEventListener('open', onOpen);
210
+ ws.addEventListener('message', onMessage);
211
+ ws.addEventListener('error', onError);
212
+ }
213
+ });
214
+ }
215
+
216
+ function extensionIdFromTarget(target) {
217
+ const url = String(target.url || '');
218
+ const match = url.match(/^chrome-extension:\/\/([a-p]{32})\//u);
219
+ return match ? match[1] : '';
220
+ }
221
+
222
+ function chooseExtensionId(targets, expectedExtensionId, expectedServiceWorker) {
223
+ const byId = new Map();
224
+ for (const target of targets) {
225
+ const id = extensionIdFromTarget(target);
226
+ if (!id) continue;
227
+ const entries = byId.get(id) || [];
228
+ entries.push(target);
229
+ byId.set(id, entries);
230
+ }
231
+ const ids = [...byId.keys()];
232
+ if (ids.length === 0) return { extensionIds: [], selectedExtensionId: '' };
233
+
234
+ const hasExpectedWorker = (id) => byId.get(id).some((target) => {
235
+ const url = String(target.url || '');
236
+ return target.type === 'service_worker' && expectedServiceWorker && url.endsWith(`/${expectedServiceWorker}`);
237
+ });
238
+ const hasAnyWorker = (id) => byId.get(id).some((target) => target.type === 'service_worker');
239
+
240
+ const selectedExtensionId =
241
+ ids.find((id) => id === expectedExtensionId && hasExpectedWorker(id)) ||
242
+ ids.find((id) => hasExpectedWorker(id)) ||
243
+ ids.find((id) => id === expectedExtensionId && hasAnyWorker(id)) ||
244
+ ids.find((id) => hasAnyWorker(id)) ||
245
+ (expectedExtensionId && ids.includes(expectedExtensionId) ? expectedExtensionId : ids[0]);
246
+
247
+ return { extensionIds: ids, selectedExtensionId };
248
+ }
249
+
250
+ async function openExtensionPage(cdpPort, extensionId, pagePath) {
251
+ const normalizedPath = String(pagePath || 'home.html').replace(/^\/+/u, '');
252
+ const url = `chrome-extension://${extensionId}/${normalizedPath}`;
253
+ try {
254
+ await httpJsonRequest('PUT', `http://127.0.0.1:${cdpPort}/json/new?${encodeURIComponent(url)}`, 3000);
255
+ return true;
256
+ } catch (err) {
257
+ // Some CDP implementations disable /json/new. Readiness can still pass
258
+ // when the caller already opened an extension page, so report this as a
259
+ // best-effort page-open miss instead of hiding a required check failure.
260
+ return false;
261
+ }
262
+ }
263
+
264
+ function findPageTarget(targets, selectedExtensionId, preferredPagePath = '') {
265
+ const extensionPages = targets.filter((target) => {
266
+ const url = String(target.url || '');
267
+ return (
268
+ target.type === 'page' &&
269
+ url.startsWith(`chrome-extension://${selectedExtensionId}/`) &&
270
+ typeof target.webSocketDebuggerUrl === 'string'
271
+ );
272
+ });
273
+ const normalizedPreferred = String(preferredPagePath || '').replace(/^\/+/u, '');
274
+ return (
275
+ extensionPages.find((target) => normalizedPreferred && String(target.url || '').endsWith(`/${normalizedPreferred}`)) ||
276
+ extensionPages.find((target) => !String(target.url || '').includes('/popup-init.html')) ||
277
+ extensionPages[0]
278
+ );
279
+ }
280
+
281
+ async function inspectCdp(target, cdpPort, expectedExtensionId, expectedServiceWorker, extensionPagePath) {
282
+ const version = await httpJson(`http://127.0.0.1:${cdpPort}/json/version`);
283
+ let targets = await httpJson(`http://127.0.0.1:${cdpPort}/json/list`);
284
+ if (!Array.isArray(targets)) throw new Error('/json/list did not return an array');
285
+ let { extensionIds, selectedExtensionId } = chooseExtensionId(targets, expectedExtensionId, expectedServiceWorker);
286
+ if (!selectedExtensionId) {
287
+ throw new Error('CDP is reachable but no chrome-extension:// targets are present');
288
+ }
289
+ let pageTarget = findPageTarget(targets, selectedExtensionId, extensionPagePath);
290
+ let openedPage = false;
291
+ if (!pageTarget) {
292
+ openedPage = await openExtensionPage(cdpPort, selectedExtensionId, extensionPagePath);
293
+ await new Promise((resolve) => setTimeout(resolve, 500));
294
+ targets = await httpJson(`http://127.0.0.1:${cdpPort}/json/list`);
295
+ ({ extensionIds, selectedExtensionId } = chooseExtensionId(targets, expectedExtensionId, expectedServiceWorker));
296
+ pageTarget = findPageTarget(targets, selectedExtensionId, extensionPagePath);
297
+ }
298
+ let ui = null;
299
+ if (pageTarget) {
300
+ ui = await cdpEvaluate(
301
+ target,
302
+ pageTarget.webSocketDebuggerUrl,
303
+ `(() => {
304
+ const text = document.body?.innerText || '';
305
+ const cell = (sel) => document.querySelector(sel)?.innerText?.trim() || '';
306
+ // React error-boundary screen (ui/pages/error-page); deterministic testids.
307
+ const errorBoundaryMessage = cell('[data-testid="error-page-error-message"]');
308
+ const errorBoundaryName = cell('[data-testid="error-page-error-name"]');
309
+ // Testid-only trigger: ui/pages/error-page reliably renders these testids.
310
+ // Avoid matching the visible phrase in body text (false positives).
311
+ const hasErrorBoundary = Boolean(errorBoundaryMessage) || Boolean(errorBoundaryName);
312
+ return {
313
+ title: document.title,
314
+ url: location.href,
315
+ textSample: text.slice(0, 500),
316
+ hasStartupError: /MetaMask had trouble starting|Background connection unresponsive|Unknown Infura network/i.test(text),
317
+ hasErrorBoundary,
318
+ errorBoundaryName,
319
+ errorBoundaryMessage: errorBoundaryMessage || (hasErrorBoundary ? text.slice(0, 300) : ''),
320
+ };
321
+ })()`,
322
+ );
323
+ if (ui && !ui.skipped && ui.hasStartupError) {
324
+ throw Object.assign(new Error('MetaMask extension page loaded startup error UI'), {
325
+ report: { cdp: { browser: version.Browser || 'unknown', selectedExtensionId, ui } },
326
+ });
327
+ }
328
+ if (ui && !ui.skipped && ui.hasErrorBoundary) {
329
+ const detail = [ui.errorBoundaryName, ui.errorBoundaryMessage].filter(Boolean).join(': ');
330
+ throw Object.assign(new Error(`MetaMask UI crashed (React error boundary)${detail ? `: ${detail}` : ''}`), {
331
+ report: { cdp: { browser: version.Browser || 'unknown', selectedExtensionId, ui } },
332
+ });
333
+ }
334
+ if (ui && !ui.skipped && String(ui.url || '').startsWith('chrome-error://')) {
335
+ throw Object.assign(new Error('MetaMask extension page loaded Chrome error UI'), {
336
+ report: { cdp: { browser: version.Browser || 'unknown', selectedExtensionId, ui } },
337
+ });
338
+ }
339
+ }
340
+ return {
341
+ browser: version.Browser || 'unknown',
342
+ extensionIds,
343
+ selectedExtensionId,
344
+ markerMatched: Boolean(expectedExtensionId && selectedExtensionId === expectedExtensionId),
345
+ targetCount: targets.length,
346
+ openedPage,
347
+ openedPagePath: extensionPagePath,
348
+ ui,
349
+ };
350
+ }
351
+
352
+ async function main() {
353
+ const args = parseArgs(process.argv.slice(2));
354
+ const target = path.resolve(args.target);
355
+ const checks = [];
356
+ const { manifestPath, manifest } = readManifest(target);
357
+ const expectedFiles = manifestExpectedFiles(manifest);
358
+ const missingFiles = [];
359
+ for (const rel of expectedFiles) {
360
+ const exists = fs.existsSync(path.join(target, 'dist/chrome', rel));
361
+ checks.push({ name: `dist/chrome/${rel}`, status: exists ? 'pass' : 'fail' });
362
+ if (!exists) missingFiles.push(rel);
363
+ }
364
+ if (missingFiles.length > 0) {
365
+ throw Object.assign(
366
+ new Error(`extension build incomplete; missing ${missingFiles.join(', ')}`),
367
+ { report: { target, manifestPath, expectedFiles, checks } },
368
+ );
369
+ }
370
+
371
+ const defaultPage = fs.existsSync(path.join(target, 'dist/chrome/home.html'))
372
+ ? 'home.html'
373
+ : manifestDefaultPage(manifest);
374
+ const report = {
375
+ target,
376
+ manifestPath,
377
+ manifestVersion: manifest.manifest_version || null,
378
+ defaultPage,
379
+ expectedFiles,
380
+ checks,
381
+ };
382
+
383
+ if (args.cdpPort) {
384
+ const expectedExtensionId = readExpectedExtensionId(target);
385
+ report.cdp = await inspectCdp(
386
+ target,
387
+ args.cdpPort,
388
+ expectedExtensionId,
389
+ manifest.background?.service_worker || '',
390
+ report.defaultPage,
391
+ );
392
+ report.cdp.markerRepaired = writeExtensionId(target, report.cdp.selectedExtensionId);
393
+ checks.push({ name: `CDP ${args.cdpPort} extension targets`, status: 'pass' });
394
+ }
395
+
396
+ if (args.json) {
397
+ console.log(JSON.stringify(report, null, 2));
398
+ } else {
399
+ console.log(`Extension readiness OK: ${expectedFiles.join(', ')}`);
400
+ if (report.cdp) {
401
+ console.log(`CDP OK: ${report.cdp.browser}; extensions=${report.cdp.extensionIds.join(',')}`);
402
+ }
403
+ }
404
+ }
405
+
406
+ main().catch((err) => {
407
+ const report = err && err.report ? err.report : null;
408
+ if (process.argv.includes('--json')) {
409
+ console.log(JSON.stringify({ status: 'fail', error: err.message, ...(report || {}) }, null, 2));
410
+ } else {
411
+ console.error(`Extension readiness failed: ${err.message}`);
412
+ }
413
+ process.exit(1);
414
+ });
@@ -0,0 +1,190 @@
1
+ #!/usr/bin/env bash
2
+ # refresh-build.sh — one-shot rebuild for a frozen extension slot
3
+ #
4
+ # Usage:
5
+ # bash refresh-build.sh --repo /path/to/metamask-extension [--watcher-port 9013]
6
+ #
7
+ # Context:
8
+ # In sidepanel mode, preflight.sh defaults to `watch=off` and kills the
9
+ # webpack watcher once dist/chrome is stable. That guarantees side-panel
10
+ # close/reopen never races a rebuild, but means code edits do not reach
11
+ # the browser until an explicit rebuild.
12
+ #
13
+ # This script runs `yarn start` once, waits until the critical extension
14
+ # entry points are back on disk, then kills the watcher again — leaving
15
+ # dist/chrome fresh AND frozen. After it returns:
16
+ # - Close any open extension pages (sidepanel / popup / fullscreen)
17
+ # - Reopen them to pick up the new build
18
+ # e.g. `bash sidepanel-toggle.sh cycle --cdp-port $CDP_PORT`
19
+ #
20
+ # Exit codes:
21
+ # 0 — success
22
+ # 1 — bad args / repo missing
23
+ # 2 — rebuild did not complete within the timeout
24
+
25
+ set -euo pipefail
26
+
27
+ REPO=""
28
+ WATCHER_PORT=""
29
+ TIMEOUT_S="180"
30
+ CLEAN_TIMEOUT_S=""
31
+ while [[ $# -gt 0 ]]; do
32
+ case "$1" in
33
+ --repo) REPO="$2"; shift 2 ;;
34
+ --watcher-port) WATCHER_PORT="$2"; shift 2 ;;
35
+ --timeout) TIMEOUT_S="$2"; shift 2 ;;
36
+ --clean-timeout) CLEAN_TIMEOUT_S="$2"; shift 2 ;;
37
+ -h|--help)
38
+ echo "Usage: refresh-build.sh --repo /path/to/metamask-extension [--watcher-port <port>] [--timeout <s>] [--clean-timeout <s>]"
39
+ exit 0
40
+ ;;
41
+ *) echo "unknown arg: $1" >&2; exit 1 ;;
42
+ esac
43
+ done
44
+
45
+ if [ -z "$REPO" ] || [ ! -f "$REPO/package.json" ]; then
46
+ echo "FAIL: --repo must point to a metamask-extension checkout" >&2
47
+ exit 1
48
+ fi
49
+
50
+ PORT_ARG=""
51
+ if [ -n "$WATCHER_PORT" ]; then
52
+ PORT_ARG="PORT=${WATCHER_PORT}"
53
+ fi
54
+
55
+ cd "$REPO"
56
+
57
+ HTML_ENTRIES=()
58
+ while IFS= read -r entry; do
59
+ [ -n "$entry" ] && HTML_ENTRIES+=("dist/chrome/$entry")
60
+ done < <(node <<'NODE'
61
+ const fs = require('fs');
62
+ const manifestPath = 'dist/chrome/manifest.json';
63
+ if (!fs.existsSync(manifestPath)) {
64
+ console.log('home.html');
65
+ console.log('scripts/app-init.js');
66
+ console.log('popup-init.html');
67
+ console.log('sidepanel.html');
68
+ process.exit(0);
69
+ }
70
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
71
+ const entries = new Set(['home.html']);
72
+ if (manifest.background?.service_worker) entries.add(manifest.background.service_worker);
73
+ for (const script of manifest.background?.scripts || []) entries.add(script);
74
+ const popup = manifest.action?.default_popup || manifest.browser_action?.default_popup || manifest.page_action?.default_popup;
75
+ if (popup) entries.add(popup);
76
+ if (manifest.side_panel?.default_path) entries.add(manifest.side_panel.default_path);
77
+ for (const entry of entries) console.log(entry);
78
+ NODE
79
+ )
80
+
81
+ ASDF_SHIMS="${ASDF_DATA_DIR:-$HOME/.asdf}/shims"
82
+ if [ -d "$ASDF_SHIMS" ]; then
83
+ export PATH="$ASDF_SHIMS:$PATH"
84
+ fi
85
+
86
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
87
+ # shellcheck disable=SC1091
88
+ for _arn in "$SCRIPT_DIR/lib/activate-repo-node.sh" "$SCRIPT_DIR/../lib/activate-repo-node.sh"; do
89
+ [ -f "$_arn" ] && { . "$_arn"; break; }
90
+ done
91
+ unset _arn
92
+ if command -v require_repo_node >/dev/null 2>&1; then
93
+ require_repo_node "$REPO"
94
+ elif command -v activate_repo_node >/dev/null 2>&1; then
95
+ activate_repo_node "$REPO" || {
96
+ echo "[refresh] refusing to run yarn start without a pinned Node for $REPO" >&2
97
+ exit 1
98
+ }
99
+ else
100
+ echo "[refresh] activate-repo-node.sh missing from harness; run: recipe sync" >&2
101
+ exit 1
102
+ fi
103
+
104
+ echo "[refresh] Using node $(node -v 2>/dev/null || echo 'not found')"
105
+ echo "[refresh] Starting yarn start (watcher-port=${WATCHER_PORT:-default})..."
106
+ LOG="$(mktemp -t refresh-build-XXXXXX.log)"
107
+ echo "[refresh] Log: $LOG"
108
+ env ${PORT_ARG} yarn start > "$LOG" 2>&1 &
109
+ BUILD_PID=$!
110
+ echo "[refresh] Build PID: $BUILD_PID"
111
+
112
+ cleanup() {
113
+ if kill -0 "$BUILD_PID" 2>/dev/null; then
114
+ kill "$BUILD_PID" 2>/dev/null || true
115
+ fi
116
+ # Also clear any child yarn/gulp workers holding the watcher port
117
+ if [ -n "$WATCHER_PORT" ]; then
118
+ lsof -i ":$WATCHER_PORT" -t 2>/dev/null | xargs kill 2>/dev/null || true
119
+ fi
120
+ }
121
+ trap cleanup EXIT
122
+
123
+ # First wait for the clean pass (confirms this build, not a pre-existing stale
124
+ # dist, owns the next entry files we observe).
125
+ CLEAN_TIMEOUT_S="${CLEAN_TIMEOUT_S:-$TIMEOUT_S}"
126
+ echo "[refresh] Waiting for clean pass (timeout ${CLEAN_TIMEOUT_S}s)..."
127
+ for i in $(seq 1 "$CLEAN_TIMEOUT_S"); do
128
+ if grep -q "Finished 'clean'" "$LOG" 2>/dev/null; then
129
+ echo "[refresh] Clean pass reached (${i}s)"
130
+ break
131
+ fi
132
+ if grep -q "webpack.Progress.*100%" "$LOG" 2>/dev/null; then
133
+ echo "[refresh] Webpack reached 100% (${i}s)"
134
+ break
135
+ fi
136
+ if ! kill -0 "$BUILD_PID" 2>/dev/null; then
137
+ echo "FAIL: yarn start exited before clean pass" >&2
138
+ tail -40 "$LOG" 2>/dev/null || true
139
+ exit 2
140
+ fi
141
+ if [ $((i % 10)) -eq 0 ]; then
142
+ echo "[refresh] still waiting for clean... (${i}s)"
143
+ tail -5 "$LOG" 2>/dev/null | sed 's/^/[refresh:log] /' || true
144
+ fi
145
+ sleep 1
146
+ done
147
+ if ! grep -q "Finished 'clean'" "$LOG" 2>/dev/null && ! grep -q "webpack.Progress.*100%" "$LOG" 2>/dev/null; then
148
+ echo "FAIL: build did not reach clean/100% within ${CLEAN_TIMEOUT_S}s" >&2
149
+ tail -40 "$LOG" 2>/dev/null || true
150
+ exit 2
151
+ fi
152
+
153
+ echo "[refresh] Waiting for entry points (timeout ${TIMEOUT_S}s): ${HTML_ENTRIES[*]}"
154
+ for i in $(seq 1 "$TIMEOUT_S"); do
155
+ ready=true
156
+ for entry in "${HTML_ENTRIES[@]}"; do
157
+ if [ ! -f "$REPO/${entry}" ]; then
158
+ ready=false
159
+ break
160
+ fi
161
+ done
162
+ $ready && break
163
+ if [ $((i % 15)) -eq 0 ]; then
164
+ echo "[refresh] still building entry points... (${i}s)"
165
+ tail -5 "$LOG" 2>/dev/null | sed 's/^/[refresh:log] /' || true
166
+ fi
167
+ sleep 1
168
+ done
169
+
170
+ all_present=true
171
+ for entry in "${HTML_ENTRIES[@]}"; do
172
+ if [ ! -f "$REPO/${entry}" ]; then
173
+ all_present=false
174
+ echo "[refresh] MISSING: ${entry}"
175
+ fi
176
+ done
177
+
178
+ if ! $all_present; then
179
+ echo "FAIL: rebuild did not restore all entry points within ${TIMEOUT_S}s" >&2
180
+ tail -40 "$LOG" 2>/dev/null || true
181
+ exit 2
182
+ fi
183
+
184
+ # Kill the watcher — leave dist fresh and frozen.
185
+ kill "$BUILD_PID" 2>/dev/null || true
186
+ wait "$BUILD_PID" 2>/dev/null || true
187
+ trap - EXIT
188
+
189
+ echo "[refresh] Build refreshed and frozen — run: recipe reopen"
190
+ echo "[refresh] Log kept at: $LOG"