@deeeed/metamask-harness 0.4.0 → 0.5.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 (52) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/adapters/core/surface.js +53 -0
  3. package/dist/adapters/extension/ensure-ready.js +109 -0
  4. package/dist/adapters/extension/extension-id.js +62 -0
  5. package/dist/adapters/extension/runtime-decision.js +305 -0
  6. package/dist/adapters/extension/runtime.js +324 -0
  7. package/dist/adapters/extension/surface.js +69 -0
  8. package/dist/adapters/mobile/deps-markers.js +22 -0
  9. package/dist/adapters/mobile/prepare.js +146 -0
  10. package/dist/adapters/mobile/provision.js +465 -0
  11. package/dist/adapters/mobile/runtime-decision.js +315 -0
  12. package/dist/adapters/mobile/surface.js +54 -0
  13. package/dist/adapters/slot-ports.js +146 -0
  14. package/dist/adapters/surface.js +14 -0
  15. package/dist/adapters.js +485 -0
  16. package/dist/cli-color.js +79 -0
  17. package/dist/cli-commands.js +224 -0
  18. package/dist/cli-version.js +111 -0
  19. package/dist/cli.js +1571 -0
  20. package/dist/commands/debug.js +56 -0
  21. package/dist/commands/fixtures.js +153 -0
  22. package/dist/commands/launch.js +325 -0
  23. package/dist/commands/logs.js +73 -0
  24. package/dist/commands/shared.js +157 -0
  25. package/dist/commands/update.js +243 -0
  26. package/dist/completions-cache.js +53 -0
  27. package/dist/doctor.js +169 -0
  28. package/dist/harness.js +627 -0
  29. package/dist/heal-bounds.js +120 -0
  30. package/dist/index.js +25 -0
  31. package/dist/leaf-invoke.js +19 -0
  32. package/dist/live-adapter-contract.js +240 -0
  33. package/dist/manifest.js +37 -0
  34. package/dist/mm-harness-cli.js +521 -0
  35. package/dist/paths.js +179 -0
  36. package/dist/progress.js +94 -0
  37. package/dist/recording-target.js +133 -0
  38. package/dist/run-recording.js +271 -0
  39. package/dist/runner.js +88 -0
  40. package/dist/types.js +0 -0
  41. package/docs/CLI-SPEC.md +26 -3
  42. package/package.json +5 -1
  43. package/src/adapters/core/surface.ts +15 -0
  44. package/src/adapters/extension/surface.ts +20 -3
  45. package/src/adapters/mobile/provision.ts +594 -0
  46. package/src/adapters/mobile/surface.ts +16 -4
  47. package/src/adapters/slot-ports.ts +1 -1
  48. package/src/adapters/surface.ts +35 -0
  49. package/src/cli-commands.ts +1 -1
  50. package/src/cli.ts +149 -6
  51. package/src/harness.ts +140 -3
  52. package/src/mm-harness-cli.ts +52 -5
@@ -0,0 +1,485 @@
1
+ import http from "node:http";
2
+ import { compatibilityMode, fixtureSummary, repoShape } from "./doctor.js";
3
+ import { runLiveAdapterScript } from "./live-adapter-contract.js";
4
+ import { withExtensionPage } from "../library/actions/extension/platform/cdp.mjs";
5
+ import { bridgeCommand, evalSync, simulatorScreenshot } from "../library/actions/mobile/platform/bridge.mjs";
6
+ function sleep(ms) {
7
+ return new Promise((resolve) => setTimeout(resolve, ms));
8
+ }
9
+ function simpleAdapter(action, executor) {
10
+ return {
11
+ action,
12
+ async execute(node, context) {
13
+ return executor(node, context);
14
+ }
15
+ };
16
+ }
17
+ const LIVE_ONLY_PERPS_ACTIONS = /* @__PURE__ */ new Set([
18
+ "metamask.perps.read_positions",
19
+ "metamask.perps.ensure_positions",
20
+ "metamask.perps.assert_positions",
21
+ "metamask.perps.place_order",
22
+ "metamask.perps.close_positions",
23
+ "metamask.perps.read_orders",
24
+ "metamask.perps.close_orders",
25
+ "metamask.perps.ensure_orders",
26
+ "metamask.perps.assert_orders",
27
+ "metamask.perps.start_state",
28
+ "metamask.perps.teardown_state"
29
+ ]);
30
+ const CORE_ONLY_PERPS_ACTIONS = /* @__PURE__ */ new Set(["metamask.perps.read_account"]);
31
+ const LIVE_ONLY_WALLET_ACTIONS = /* @__PURE__ */ new Set([
32
+ "metamask.wallet.setup",
33
+ "metamask.wallet.ensure_unlocked",
34
+ "metamask.wallet.select_account",
35
+ "metamask.wallet.read_state"
36
+ ]);
37
+ const LIVE_ONLY_APP_ACTIONS = /* @__PURE__ */ new Set(["ui.navigate"]);
38
+ function requiresLiveAdapter(platform, action) {
39
+ return LIVE_ONLY_PERPS_ACTIONS.has(action) || platform === "core" && CORE_ONLY_PERPS_ACTIONS.has(action) || LIVE_ONLY_WALLET_ACTIONS.has(action) || LIVE_ONLY_APP_ACTIONS.has(action);
40
+ }
41
+ function liveRuntimeConfigured(platform, node) {
42
+ if (platform === "mobile" || platform === "core") return true;
43
+ return Boolean(node.cdp_port ?? process.env.CDP_PORT ?? process.env.RECIPE_CDP_PORT);
44
+ }
45
+ async function runLiveFirst(platform, action, node, context) {
46
+ if (node.allow_static_placeholder === true) {
47
+ throw new Error(
48
+ `${action} refused allow_static_placeholder. MetaMask runner proof runs must use live adapters and real artifacts.`
49
+ );
50
+ }
51
+ if (!requiresLiveAdapter(platform, action) && !liveRuntimeConfigured(platform, node)) return null;
52
+ const live = await runLiveAdapterScript({ platform, action, node, context });
53
+ if (!live) return null;
54
+ const liveResult = isRecord(live.result) ? live.result : { result: live.result };
55
+ const output = { ...liveResult, liveAdapter: live.script };
56
+ return {
57
+ output,
58
+ artifacts: isRecord(live.result) && Array.isArray(live.result.artifacts) ? live.result.artifacts : void 0
59
+ };
60
+ }
61
+ function liveAdapterPathHint(platform, action) {
62
+ if (action.startsWith("metamask.")) {
63
+ return `library/actions/${platform}/${action.replace(/^metamask[.]/u, "").replaceAll(".", "/")}.mjs`;
64
+ }
65
+ return `library/actions/${platform}/${action.replaceAll(".", "/")}.mjs`;
66
+ }
67
+ async function semanticResult(platform, action, node, context) {
68
+ const live = await runLiveFirst(platform, action, node, context);
69
+ if (live) return live;
70
+ if (requiresLiveAdapter(platform, action)) {
71
+ const expected = liveAdapterPathHint(platform, action);
72
+ throw new Error(
73
+ `${action} requires a live ${platform} adapter that drives a real supported app/API path; no adapter script was found or no live runtime is configured (for example ${expected}). Static placeholders are refused to avoid fabricated proof. Set METAMASK_RECIPE_LIVE_ADAPTER_DIR or add a runner library/actions script.`
74
+ );
75
+ }
76
+ const output = { platform, action, redacted: true };
77
+ if (action === "metamask.wallet.fixture_status") return { output: fixtureSummary(context.projectRoot) };
78
+ return {
79
+ output: {
80
+ ...output,
81
+ requested: Object.fromEntries(Object.entries(node).filter(([key]) => key !== "action")),
82
+ note: "static semantic adapter; live proof must use a real supported app/API path"
83
+ }
84
+ };
85
+ }
86
+ function isRecord(value) {
87
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
88
+ }
89
+ function optionalScalarText(value, label) {
90
+ if (value === void 0 || value === null) return void 0;
91
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
92
+ return String(value);
93
+ }
94
+ throw new Error(`${label} must be a string, number, or boolean.`);
95
+ }
96
+ function scalarText(value, label, fallback) {
97
+ return optionalScalarText(value, label) ?? fallback;
98
+ }
99
+ function firstScalarText(record, keys, label, fallback) {
100
+ for (const key of keys) {
101
+ const value = optionalScalarText(record[key], `${label}.${key}`);
102
+ if (value !== void 0) return value;
103
+ }
104
+ if (fallback !== void 0) return fallback;
105
+ throw new Error(`${label} requires one of: ${keys.join(", ")}.`);
106
+ }
107
+ function traceText(value) {
108
+ if (value === void 0 || value === null) return "";
109
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
110
+ return String(value);
111
+ }
112
+ return JSON.stringify(value);
113
+ }
114
+ function probeHttpJson(url, timeoutMs = 1e3) {
115
+ return new Promise((resolve) => {
116
+ const request = http.get(url, { timeout: timeoutMs }, (response) => {
117
+ let body = "";
118
+ response.setEncoding("utf8");
119
+ response.on("data", (chunk) => {
120
+ body += chunk;
121
+ });
122
+ response.on("end", () => {
123
+ if (!response.statusCode || response.statusCode < 200 || response.statusCode >= 300) {
124
+ resolve({ reachable: false, statusCode: response.statusCode ?? null, error: `HTTP ${response.statusCode ?? "unknown"}` });
125
+ return;
126
+ }
127
+ try {
128
+ resolve({ reachable: true, statusCode: response.statusCode, json: JSON.parse(body) });
129
+ } catch (error) {
130
+ resolve({
131
+ reachable: true,
132
+ statusCode: response.statusCode,
133
+ json: null,
134
+ parseError: error instanceof Error ? error.message : String(error)
135
+ });
136
+ }
137
+ });
138
+ });
139
+ request.on("timeout", () => {
140
+ request.destroy(new Error(`Timed out after ${timeoutMs}ms`));
141
+ });
142
+ request.on("error", (error) => {
143
+ resolve({ reachable: false, statusCode: null, error: error.message });
144
+ });
145
+ });
146
+ }
147
+ function createMetaMaskSemanticAdapters(platform) {
148
+ const walletActions = [
149
+ "metamask.wallet.fixture_status",
150
+ "metamask.wallet.setup",
151
+ "metamask.wallet.ensure_unlocked",
152
+ "metamask.wallet.select_account",
153
+ "metamask.wallet.read_state"
154
+ ];
155
+ const actions = [
156
+ ...walletActions,
157
+ "metamask.perps.read_positions",
158
+ "metamask.perps.ensure_positions",
159
+ "metamask.perps.assert_positions",
160
+ "metamask.perps.place_order",
161
+ "metamask.perps.close_positions",
162
+ "metamask.perps.read_orders",
163
+ "metamask.perps.close_orders",
164
+ "metamask.perps.ensure_orders",
165
+ "metamask.perps.assert_orders",
166
+ "metamask.perps.start_state",
167
+ "metamask.perps.teardown_state",
168
+ // read_account is core-only: only the headless core adapter implements it.
169
+ ...platform === "core" ? ["metamask.perps.read_account"] : []
170
+ ];
171
+ return actions.map(
172
+ (action) => simpleAdapter(
173
+ action,
174
+ async (node, context) => semanticResult(platform, action, node, context)
175
+ )
176
+ );
177
+ }
178
+ function uiInputFor(context, input) {
179
+ return {
180
+ action: input.action,
181
+ node: input.node,
182
+ context: {
183
+ nodeId: context.nodeId,
184
+ projectRoot: context.projectRoot,
185
+ artifactsDir: context.artifactsDir,
186
+ env: context.env
187
+ }
188
+ };
189
+ }
190
+ function mobileProbeOutput(status, input, projectRoot) {
191
+ const entries = Array.isArray(status) ? status : [status];
192
+ const preferredDevices = [
193
+ input.node?.ios_simulator,
194
+ input.node?.simulator,
195
+ input.node?.android_device,
196
+ input.node?.adb_serial,
197
+ process.env.IOS_SIMULATOR,
198
+ process.env.ANDROID_DEVICE,
199
+ process.env.ADB_SERIAL
200
+ ].filter((value) => typeof value === "string" && value.length > 0);
201
+ const selected = entries.find((entry) => {
202
+ if (!isRecord(entry)) return false;
203
+ return typeof entry.deviceName === "string" && preferredDevices.includes(entry.deviceName);
204
+ }) ?? entries.find((entry) => isRecord(entry) && isRecord(entry.route)) ?? null;
205
+ const route = isRecord(selected) && isRecord(selected.route) ? selected.route : null;
206
+ return {
207
+ reachable: Boolean(selected),
208
+ bridge: mobileBridgePath(projectRoot),
209
+ targetCount: entries.filter((entry) => isRecord(entry)).length,
210
+ deviceName: isRecord(selected) && typeof selected.deviceName === "string" ? selected.deviceName : null,
211
+ routeName: route && typeof route.name === "string" ? route.name : null,
212
+ accountPresent: isRecord(selected) && isRecord(selected.account)
213
+ };
214
+ }
215
+ function mobileBridgePath(_projectRoot) {
216
+ return process.env.METAMASK_RECIPE_MOBILE_BRIDGE_SCRIPT || "runner:adapters/mobile/bridge-runtime/cdp-bridge.cjs";
217
+ }
218
+ async function waitForMobileTarget(input, payload) {
219
+ const timeoutMs = Number(payload.timeout_ms ?? payload.timeoutMs ?? 1e4);
220
+ const deadline = Date.now() + timeoutMs;
221
+ const testId = firstScalarText(payload, ["test_id", "testID"], "Mobile ui.wait_for");
222
+ const expected = scalarText(payload.expected, "ui.wait_for.expected", "present").toLowerCase();
223
+ const expectedText = optionalScalarText(payload.text, "ui.wait_for.text");
224
+ const textMatch = scalarText(payload.text_match ?? payload.textMatch, "ui.wait_for.text_match", "contains").toLowerCase();
225
+ const textExpression = `(function(){
226
+ const api = globalThis.__AGENTIC__;
227
+ if (!api?.getTextByTestId) return null;
228
+ return api.getTextByTestId(${JSON.stringify(testId)});
229
+ })()`;
230
+ const expression = `Boolean(globalThis.__AGENTIC__?.findFiberByTestId?.(${JSON.stringify(testId)}))`;
231
+ let lastValue = null;
232
+ let lastText = null;
233
+ const expectsAbsent = expected === "absent" || expected === "hidden" || expected === "not_present";
234
+ while (Date.now() <= deadline) {
235
+ lastValue = await evalSync(input, expression);
236
+ const present = Boolean(lastValue);
237
+ if (expectsAbsent && !present) {
238
+ return { matched: true, testId, expected, present };
239
+ }
240
+ if (!expectsAbsent && present) {
241
+ if (!expectedText) {
242
+ return { matched: true, testId, expected, present };
243
+ }
244
+ lastText = await evalSync(input, textExpression);
245
+ const text = traceText(lastText);
246
+ const textMatched = textMatch === "exact" ? text === expectedText : text.includes(expectedText);
247
+ if (textMatched) {
248
+ return { matched: true, testId, expected, present, text, textMatch };
249
+ }
250
+ }
251
+ await sleep(250);
252
+ }
253
+ const textReason = expectedText ? ` and text ${textMatch} ${JSON.stringify(expectedText)}; last text=${JSON.stringify(lastText)}` : "";
254
+ throw new Error(`Timed out waiting for mobile testID ${testId} to be ${expected}${textReason}; last present=${Boolean(lastValue)}.`);
255
+ }
256
+ const MOBILE_BRIDGE_HANDLERS = {
257
+ screenshot: handleMobileScreenshot,
258
+ status: handleMobileStatus,
259
+ navigate: handleMobileNavigate,
260
+ press: handleMobilePress,
261
+ setInput: handleMobileSetInput,
262
+ scroll: handleMobileScroll,
263
+ waitFor: handleMobileWaitFor,
264
+ hud: handleMobileHud
265
+ };
266
+ function createMetaMaskMobileBridge() {
267
+ return {
268
+ async send(command, context) {
269
+ const handler = MOBILE_BRIDGE_HANDLERS[command.command];
270
+ if (!handler) {
271
+ throw new Error(`React Native bridge command ${command.command} is not implemented by the MetaMask runner transport.`);
272
+ }
273
+ return handler(command.payload, context);
274
+ }
275
+ };
276
+ }
277
+ function mobileUiInput(context, command, payload) {
278
+ return uiInputFor(context, { action: `ui.${command}`, node: payload });
279
+ }
280
+ async function handleMobileScreenshot(payload, context) {
281
+ const input = mobileUiInput(context, "screenshot", payload);
282
+ const relPath = scalarText(payload.path, "ui.screenshot.path", `screenshots/${context.nodeId}.png`);
283
+ const artifact = await simulatorScreenshot(input, relPath);
284
+ context.registerArtifact(artifact);
285
+ return { captured: true, path: artifact.path, artifact };
286
+ }
287
+ async function handleMobileStatus(payload, context) {
288
+ return bridgeCommand(mobileUiInput(context, "status", payload), ["status"]);
289
+ }
290
+ async function handleMobileNavigate(payload, context) {
291
+ const live = await runLiveAdapterScript({
292
+ platform: "mobile",
293
+ action: "ui.navigate",
294
+ node: payload,
295
+ context
296
+ });
297
+ if (!live) throw new Error("ui.navigate requires library/actions/mobile/ui/navigate.mjs.");
298
+ return isRecord(live.result) ? { ...live.result, liveAdapter: live.script } : live.result;
299
+ }
300
+ async function handleMobilePress(payload, context) {
301
+ const target = firstScalarText(payload, ["test_id", "testID", "selector", "text"], "ui.press");
302
+ return bridgeCommand(mobileUiInput(context, "press", payload), ["press-test-id", target]);
303
+ }
304
+ async function handleMobileSetInput(payload, context) {
305
+ const input = mobileUiInput(context, "set_input", payload);
306
+ const testId = firstScalarText(payload, ["test_id", "testID"], "ui.set_input");
307
+ const value = scalarText(payload.value ?? payload.text, "ui.set_input.value", "");
308
+ const result = await bridgeCommand(input, ["set-input", testId, value]);
309
+ if (isRecord(result) && result.ok === false) {
310
+ throw new Error(`ui.set_input failed for mobile testID ${testId}: ${traceText(result.error ?? result)}`);
311
+ }
312
+ return isRecord(result) ? result : { result, testId, value };
313
+ }
314
+ async function handleMobileScroll(payload, context) {
315
+ const input = mobileUiInput(context, "scroll", payload);
316
+ const testId = optionalScalarText(payload.test_id ?? payload.testID, "ui.scroll.test_id");
317
+ const offset = scalarText(payload.offset ?? payload.delta_y ?? payload.deltaY, "ui.scroll.offset", "600");
318
+ const args = testId ? ["scroll-view", "--test-id", testId, "--offset", offset, animatedFlag(payload)] : ["scroll-view", "--offset", offset, animatedFlag(payload)];
319
+ const result = await bridgeCommand(input, args);
320
+ return {
321
+ ...isRecord(result) ? result : { result },
322
+ intoView: payload.scroll_into_view === true || payload.into_view === true
323
+ };
324
+ }
325
+ async function handleMobileWaitFor(payload, context) {
326
+ return waitForMobileTarget(mobileUiInput(context, "waitFor", payload), payload);
327
+ }
328
+ async function handleMobileHud(payload, context) {
329
+ const input = mobileUiInput(context, "hud", payload);
330
+ if (payload.clear === true) return bridgeCommand(input, ["hide-step"]);
331
+ const hud = mobileHudPayload(payload, context);
332
+ const result = await bridgeCommand(input, ["show-step-json", JSON.stringify(hud.step)]);
333
+ return { hud: true, nodeId: hud.nodeId, status: hud.status, result };
334
+ }
335
+ function animatedFlag(payload) {
336
+ return payload.animated === true ? "--animated" : "--no-animated";
337
+ }
338
+ function mobileHudPayload(payload, context) {
339
+ const nodeId = scalarText(payload.node_id ?? payload.nodeId, "app.hud.node_id", context.nodeId);
340
+ const status = scalarText(payload.status, "app.hud.status", "running");
341
+ const text = optionalScalarText(payload.intent ?? payload.text ?? payload.detail, "app.hud.intent");
342
+ if (!text || !text.trim()) {
343
+ throw new Error("app.hud requires intent/text/detail; automatic recipe progress should provide node intent.");
344
+ }
345
+ const detail = scalarText(payload.detail, "app.hud.detail", "");
346
+ const error = scalarText(payload.error, "app.hud.error", "");
347
+ const progress = mobileHudProgress(payload.progress);
348
+ const display = isRecord(payload.display) ? payload.display : {};
349
+ const showDebug = display.showDebug === true;
350
+ const showDetail = display.showDetail === true;
351
+ const proofTarget = payload.proofTarget ?? payload.proof_target;
352
+ const displayId = [mobileHudStatusLabel(status), progress.text].filter(Boolean).join(" ");
353
+ return {
354
+ nodeId,
355
+ status,
356
+ step: {
357
+ id: displayId || nodeId,
358
+ nodeId,
359
+ status,
360
+ intent: text,
361
+ progress: progress.value,
362
+ detail: showDetail && detail !== text ? detail : void 0,
363
+ error: error || void 0,
364
+ debug: showDebug ? {
365
+ nodeId,
366
+ proofTarget
367
+ } : void 0
368
+ }
369
+ };
370
+ }
371
+ function mobileHudProgress(progress) {
372
+ if (!isRecord(progress)) return { text: "" };
373
+ if (typeof progress.current !== "number" || typeof progress.total !== "number") return { text: "" };
374
+ return {
375
+ text: `${progress.current}/${progress.total}`,
376
+ value: { current: progress.current, total: progress.total }
377
+ };
378
+ }
379
+ function mobileHudStatusLabel(status) {
380
+ if (status === "fail") return "fail";
381
+ if (status === "pass") return "pass";
382
+ return "run";
383
+ }
384
+ function createMetaMaskUiTransport(platform, harness) {
385
+ if (platform === "core") {
386
+ return {
387
+ async execute(action) {
388
+ throw new Error(
389
+ `core adapter is headless and cannot execute UI action ${action}. Use a perps read action or run the recipe against the mobile/extension adapter.`
390
+ );
391
+ }
392
+ };
393
+ }
394
+ const base = platform === "mobile" ? harness.createReactNativeBridgeUiTransport({ bridge: createMetaMaskMobileBridge() }) : harness.createCdpWebUiTransport({
395
+ async withPage(input, callback) {
396
+ return withExtensionPage(
397
+ {
398
+ action: input.action,
399
+ node: input.node,
400
+ context: {
401
+ nodeId: input.context.nodeId,
402
+ projectRoot: input.context.projectRoot,
403
+ artifactsDir: input.context.artifactsDir
404
+ }
405
+ },
406
+ callback
407
+ );
408
+ }
409
+ });
410
+ return {
411
+ async execute(action, node, context) {
412
+ if (action === "ui.navigate") {
413
+ const live = await runLiveFirst(platform, action, node, context);
414
+ if (live) return live.output;
415
+ throw new Error(`ui.navigate requires library/actions/${platform}/ui/navigate.mjs.`);
416
+ }
417
+ return base.execute(action, node, context);
418
+ }
419
+ };
420
+ }
421
+ function createMetaMaskAdapters(adapter) {
422
+ const platform = adapter;
423
+ const adapters = [
424
+ simpleAdapter("app.status", async (_node, context) => ({
425
+ output: {
426
+ platform,
427
+ projectRoot: context.projectRoot,
428
+ compatibilityMode: compatibilityMode(adapter, context.projectRoot),
429
+ shape: repoShape(context.projectRoot)
430
+ }
431
+ })),
432
+ simpleAdapter("cdp.target", async (node, context) => {
433
+ const cdpPort = node.cdp_port ?? process.env.CDP_PORT ?? process.env.RECIPE_CDP_PORT ?? null;
434
+ const metroPort = node.metro_port ?? node.watcher_port ?? process.env.WATCHER_PORT ?? null;
435
+ const timeoutMs = Number(node.probe_timeout_ms ?? node.cdp_timeout_ms ?? node.timeout_ms ?? process.env.CDP_TIMEOUT ?? 1e4);
436
+ const required = node.require_reachable === true || node.required === true;
437
+ if (platform === "mobile" && required) {
438
+ const uiInput = { action: "cdp.target", nodeId: context.nodeId, node };
439
+ const status = await bridgeCommand(uiInputFor(context, uiInput), ["status"]);
440
+ const probe2 = mobileProbeOutput(status, uiInput, context.projectRoot);
441
+ if (!probe2.reachable) {
442
+ throw new Error("cdp.target required a reachable mobile React Native bridge, but bridge status did not expose a selected app route.");
443
+ }
444
+ return {
445
+ output: {
446
+ platform,
447
+ transport: "react-native-debug-bridge",
448
+ cdpPort,
449
+ metroPort,
450
+ timeoutMs,
451
+ ...probe2
452
+ }
453
+ };
454
+ }
455
+ const targetPort = platform === "mobile" ? metroPort ?? cdpPort : cdpPort;
456
+ const url = targetProbeUrl(platform, targetPort);
457
+ const probe = url ? await probeHttpJson(url, timeoutMs) : { reachable: null, statusCode: null, error: "no port declared" };
458
+ if (required && !probe.reachable) {
459
+ throw new Error(`cdp.target required a reachable ${platform} runtime at ${url ?? "<no port>"}: ${traceText(probe.error) || "not reachable"}`);
460
+ }
461
+ return {
462
+ output: {
463
+ platform,
464
+ transport: platform === "mobile" ? "react-native-debug-bridge" : "chrome-extension-cdp",
465
+ cdpPort,
466
+ metroPort,
467
+ probeUrl: url,
468
+ timeoutMs,
469
+ ...probe
470
+ }
471
+ };
472
+ }),
473
+ ...createMetaMaskSemanticAdapters(platform)
474
+ ];
475
+ return adapters;
476
+ }
477
+ function targetProbeUrl(platform, targetPort) {
478
+ if (!targetPort) return null;
479
+ const pathSuffix = platform === "mobile" ? "json/list" : "json/version";
480
+ return `http://127.0.0.1:${traceText(targetPort)}/${pathSuffix}`;
481
+ }
482
+ export {
483
+ createMetaMaskAdapters,
484
+ createMetaMaskUiTransport
485
+ };
@@ -0,0 +1,79 @@
1
+ const STYLES = {
2
+ reset: "\x1B[0m",
3
+ bold: "\x1B[1m",
4
+ dim: "\x1B[2m",
5
+ label: "\x1B[1;36m",
6
+ cmd: "\x1B[1m",
7
+ ok: "\x1B[1;32m",
8
+ warn: "\x1B[1;33m",
9
+ err: "\x1B[1;31m",
10
+ info: "\x1B[0;37m",
11
+ accent: "\x1B[1;35m",
12
+ path: "\x1B[2m",
13
+ comment: "\x1B[2m",
14
+ active: "\x1B[1m"
15
+ };
16
+ function colorEnabled(stream = process.stderr) {
17
+ if (process.env.NO_COLOR != null && process.env.NO_COLOR !== "" && process.env.NO_COLOR !== "0") {
18
+ return false;
19
+ }
20
+ if (process.env.RECIPE_NO_COLOR === "1") return false;
21
+ if (process.env.RECIPE_COLOR === "1" || process.env.FORCE_COLOR === "1") return true;
22
+ return Boolean(stream?.isTTY);
23
+ }
24
+ function stripAnsi(text) {
25
+ return String(text).replace(/\x1b\[[0-9;]*m/gu, "");
26
+ }
27
+ function color(style, text, { stream = process.stderr } = {}) {
28
+ const value = String(text);
29
+ if (!colorEnabled(stream)) return value;
30
+ const code = STYLES[style];
31
+ if (!code) return value;
32
+ return `${code}${value}${STYLES.reset}`;
33
+ }
34
+ function colorKv(label, value, valueStyle = "info", { stream = process.stderr } = {}) {
35
+ return `${color("label", `${label}:`, { stream })} ${color(valueStyle, value, { stream })}`;
36
+ }
37
+ function colorStatusWord(word, { stream = process.stderr } = {}) {
38
+ const normalized = String(word || "").toLowerCase();
39
+ if (["up", "ready", "yes", "pass", "running", "present", "healthy"].includes(normalized)) {
40
+ return color("ok", word, { stream });
41
+ }
42
+ if (["down", "stopped", "no", "fail", "failed", "missing", "blocked", "unknown"].includes(normalized)) {
43
+ return color("err", word, { stream });
44
+ }
45
+ if (["compiling", "bundling", "starting", "waiting", "warn"].includes(normalized)) {
46
+ return color("warn", word, { stream });
47
+ }
48
+ return color("info", word, { stream });
49
+ }
50
+ function classifyLogEvent(event) {
51
+ const text = String(event || "");
52
+ if (/Module build failed|^ERROR in |BUILD FAILED|compiled with [1-9][0-9]* error/iu.test(text)) {
53
+ return "err";
54
+ }
55
+ if (/compiled successfully|compiled with [0-9]+ warning|PASS runtime-launch|launch pass|verify pass|build complete/iu.test(text)) {
56
+ return "ok";
57
+ }
58
+ if (/phase [0-9]\/[0-9]|prepare pipeline|snapshot|fixture:|dist-freshness|build-health/iu.test(text)) {
59
+ return "label";
60
+ }
61
+ if (/webpack [0-9]+%|Bundl(ed|ing)|(iOS|Android).*%/iu.test(text)) {
62
+ return "warn";
63
+ }
64
+ return "info";
65
+ }
66
+ function colorLogEvent(event, { latest = false, stream = process.stderr } = {}) {
67
+ const style = classifyLogEvent(event);
68
+ const painted = color(style === "label" ? "label" : style, event, { stream });
69
+ return latest ? color("active", painted, { stream }) : painted;
70
+ }
71
+ export {
72
+ classifyLogEvent,
73
+ color,
74
+ colorEnabled,
75
+ colorKv,
76
+ colorLogEvent,
77
+ colorStatusWord,
78
+ stripAnsi
79
+ };