@deeeed/metamask-harness 0.41.0 → 0.42.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 (44) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +7 -0
  3. package/adapters/manifest.json +25 -1
  4. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +70 -10
  5. package/adapters/mobile/bridge-runtime/console-forwarder.cjs +115 -15
  6. package/adapters/mobile/bridge-runtime/lib/cdp-broker.cjs +752 -0
  7. package/adapters/mobile/bridge-runtime/lib/devtools-proxy.cjs +177 -0
  8. package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +13 -3
  9. package/adapters/mobile/coalesce-metro-log.cjs +24 -0
  10. package/adapters/mobile/launch-metro.cjs +9 -8
  11. package/adapters/mobile/metro-log-generation.cjs +106 -0
  12. package/adapters/mobile/reload-app.mjs +67 -0
  13. package/adapters/mobile/start-console-forwarder.sh +17 -2
  14. package/adapters/mobile/start-metro.sh +23 -18
  15. package/adapters/mobile/stop-metro.sh +15 -7
  16. package/adapters/shared/open-debug.mjs +172 -2
  17. package/adapters/shared/reap-checkout-metros.sh +17 -0
  18. package/dist/adapters/extension/network-observer.js +300 -0
  19. package/dist/adapters/mobile/metro-env.js +0 -5
  20. package/dist/adapters/mobile/prepare.js +1 -3
  21. package/dist/adapters/mobile/runtime-decision.js +6 -30
  22. package/dist/adapters.js +14 -1
  23. package/dist/cli-commands.js +6 -3
  24. package/dist/cli.js +4 -0
  25. package/dist/command-contract.js +3 -0
  26. package/dist/commands/call.js +45 -20
  27. package/dist/commands/launch/index.js +25 -5
  28. package/dist/commands/reload.js +80 -0
  29. package/dist/commands/run.js +49 -22
  30. package/dist/mm-harness-cli.js +17 -1
  31. package/dist/network-observation.js +271 -0
  32. package/docs/NETWORK-CAPTURE.md +98 -0
  33. package/docs/QA.md +2 -0
  34. package/docs/RECIPES.md +10 -0
  35. package/library/actions/mobile/app/network_assert.mjs +14 -0
  36. package/library/actions/mobile/app/network_capture.mjs +72 -0
  37. package/library/actions/mobile/platform/bridge.mjs +7 -2
  38. package/library/actions/shared/app/network-artifact.mjs +10 -0
  39. package/library/actions/shared/app/network-assert.mjs +154 -0
  40. package/library/manifests/extension.action-manifest.json +88 -0
  41. package/library/manifests/mobile.action-manifest.json +107 -0
  42. package/library/recipes/mobile/perps/performance.recipe.json +11 -11
  43. package/package.json +1 -1
  44. package/scripts/completions.sh +2 -1
@@ -2,12 +2,23 @@
2
2
  /**
3
3
  * open-debug.mjs — open human-facing debugger UIs for recipe runtimes.
4
4
  *
5
- * Mobile: Metro /open-debugger or /open-dev-menu HTTP endpoints.
5
+ * Mobile: broker-backed React Native DevTools or Metro /open-dev-menu.
6
6
  * Extension: Chrome DevTools frontend for an extension page or service worker.
7
7
  */
8
- import { spawnSync } from 'node:child_process';
8
+ import { spawn, spawnSync } from 'node:child_process';
9
+ import { createHash } from 'node:crypto';
10
+ import fs from 'node:fs';
9
11
  import http from 'node:http';
10
12
  import { createRequire } from 'node:module';
13
+ import path from 'node:path';
14
+
15
+ const require = createRequire(import.meta.url);
16
+ const {
17
+ discoverTarget,
18
+ } = require('../mobile/bridge-runtime/lib/target-discovery.cjs');
19
+ const {
20
+ deviceIdFromUrl,
21
+ } = require('../mobile/bridge-runtime/lib/cdp-broker.cjs');
11
22
 
12
23
  function usage() {
13
24
  console.error(`Usage:
@@ -236,6 +247,9 @@ async function tryDevSettingsCdpFallback(numericPort, normalizedAction) {
236
247
 
237
248
  async function openMobileDebugger(port, action, shouldOpen) {
238
249
  const normalizedAction = action === 'dev-menu' ? 'dev-menu' : 'debug';
250
+ if (normalizedAction === 'debug') {
251
+ return openBrokeredMobileDebugger(port, shouldOpen);
252
+ }
239
253
  const urlPath = normalizedAction === 'dev-menu' ? '/open-dev-menu' : '/open-debugger';
240
254
  const numericPort = Number.parseInt(String(port), 10);
241
255
  if (!Number.isInteger(numericPort) || numericPort <= 0) {
@@ -302,6 +316,162 @@ async function openMobileDebugger(port, action, shouldOpen) {
302
316
  };
303
317
  }
304
318
 
319
+ function processIsAlive(pid) {
320
+ if (!Number.isInteger(pid) || pid < 1) return false;
321
+ try {
322
+ process.kill(pid, 0);
323
+ return true;
324
+ } catch {
325
+ return false;
326
+ }
327
+ }
328
+
329
+ async function openBrokeredMobileDebugger(port, shouldOpen) {
330
+ const numericPort = Number.parseInt(String(port), 10);
331
+ if (!Number.isInteger(numericPort) || numericPort <= 0) {
332
+ return { ok: false, adapter: 'mobile', action: 'debug', error: 'invalid-port', port };
333
+ }
334
+ const runtimeDir = path.resolve(
335
+ process.cwd(),
336
+ process.env.RECIPE_RUNTIME_DIR || path.join('temp', 'recipe', 'runtime'),
337
+ );
338
+ const descriptorPath = path.join(runtimeDir, 'devtools-proxy.json');
339
+ let descriptor;
340
+ try {
341
+ descriptor = JSON.parse(fs.readFileSync(descriptorPath, 'utf8'));
342
+ } catch {
343
+ descriptor = null;
344
+ }
345
+ if (
346
+ descriptor?.schemaVersion !== 1 ||
347
+ !Number.isInteger(descriptor.port) ||
348
+ descriptor.port <= 0 ||
349
+ !processIsAlive(descriptor.pid)
350
+ ) {
351
+ return {
352
+ ok: false,
353
+ adapter: 'mobile',
354
+ action: 'debug',
355
+ error: 'broker-unavailable',
356
+ port: numericPort,
357
+ hint: 'Start the Mobile runtime first: mm-harness launch ios|android',
358
+ };
359
+ }
360
+
361
+ let target;
362
+ try {
363
+ const selected = await discoverTarget(numericPort, { probe: false });
364
+ const targets = await fetchJson(`http://127.0.0.1:${numericPort}/json/list`);
365
+ const selectedUrl = new URL(selected.wsUrl);
366
+ target = targets.find(
367
+ (candidate) => {
368
+ try {
369
+ const candidateUrl = new URL(candidate.webSocketDebuggerUrl);
370
+ return (
371
+ candidateUrl.pathname === selectedUrl.pathname &&
372
+ candidateUrl.search === selectedUrl.search
373
+ );
374
+ } catch {
375
+ return false;
376
+ }
377
+ },
378
+ );
379
+ } catch (error) {
380
+ return {
381
+ ok: false,
382
+ adapter: 'mobile',
383
+ action: 'debug',
384
+ error: 'target-unavailable',
385
+ port: numericPort,
386
+ hint: 'Wait for the app runtime, then retry: mm-harness debug',
387
+ detail: error instanceof Error ? error.message : String(error),
388
+ };
389
+ }
390
+ if (!target?.webSocketDebuggerUrl) {
391
+ return {
392
+ ok: false,
393
+ adapter: 'mobile',
394
+ action: 'debug',
395
+ error: 'target-unavailable',
396
+ port: numericPort,
397
+ hint: 'Wait for the app runtime, then retry: mm-harness debug',
398
+ };
399
+ }
400
+
401
+ const deviceId = deviceIdFromUrl(target.webSocketDebuggerUrl);
402
+ const proxyPath = `/devtools?device=${encodeURIComponent(deviceId)}`;
403
+ const publicProxyEndpoint = `ws://127.0.0.1:${descriptor.port}${proxyPath}`;
404
+ const params = new URLSearchParams([
405
+ ['ws', `127.0.0.1:${descriptor.port}${proxyPath}`],
406
+ ['sources.hide_add_folder', 'true'],
407
+ ['unstable_enableNetworkPanel', 'true'],
408
+ ]);
409
+ if (target.appId) params.set('appId', target.appId);
410
+ const frontendUrl = `http://127.0.0.1:${numericPort}/debugger-frontend/rn_fusebox.html?${params}`;
411
+ if (!shouldOpen) {
412
+ return {
413
+ ok: true,
414
+ adapter: 'mobile',
415
+ action: 'debug',
416
+ method: 'broker-proxy',
417
+ endpoint: publicProxyEndpoint,
418
+ port: numericPort,
419
+ opened: false,
420
+ };
421
+ }
422
+
423
+ try {
424
+ const mobileRequire = createRequire(path.join(process.cwd(), 'package.json'));
425
+ const dotslash = mobileRequire('fb-dotslash');
426
+ const shellPackage = mobileRequire.resolve(
427
+ '@react-native/debugger-shell/package.json',
428
+ );
429
+ const shellDescriptor = path.join(
430
+ path.dirname(shellPackage),
431
+ 'bin',
432
+ 'react-native-devtools',
433
+ );
434
+ const windowKey = createHash('sha256')
435
+ .update([frontendUrl, target.appId || '', deviceId].join('-'))
436
+ .digest('hex');
437
+ await new Promise((resolve, reject) => {
438
+ const child = spawn(
439
+ dotslash,
440
+ [
441
+ shellDescriptor,
442
+ `--frontendUrl=${frontendUrl}`,
443
+ `--windowKey=${windowKey}`,
444
+ ],
445
+ { detached: true, stdio: 'ignore' },
446
+ );
447
+ child.once('spawn', resolve);
448
+ child.once('error', reject);
449
+ child.unref();
450
+ });
451
+ } catch (error) {
452
+ return {
453
+ ok: false,
454
+ adapter: 'mobile',
455
+ action: 'debug',
456
+ error: 'devtools-launch-failed',
457
+ endpoint: publicProxyEndpoint,
458
+ port: numericPort,
459
+ hint: 'Install the checkout dependencies, then retry: mm-harness debug',
460
+ detail: error instanceof Error ? error.message : String(error),
461
+ };
462
+ }
463
+
464
+ return {
465
+ ok: true,
466
+ adapter: 'mobile',
467
+ action: 'debug',
468
+ method: 'broker-proxy',
469
+ endpoint: publicProxyEndpoint,
470
+ port: numericPort,
471
+ opened: true,
472
+ };
473
+ }
474
+
305
475
  function pickExtensionTarget(targets, targetKind) {
306
476
  const extensionTargets = targets.filter((target) => String(target.url || '').startsWith('chrome-extension://'));
307
477
  if (targetKind === 'worker') {
@@ -40,6 +40,23 @@ LIST
40
40
  return 0
41
41
  }
42
42
 
43
+ reap_checkout_metros_on_port() {
44
+ local repo="$1" port="$2" reaped="" pid args
45
+ [ -n "$repo" ] && [ -n "$port" ] || return 0
46
+ while IFS= read -r pid; do
47
+ [ -n "$pid" ] || continue
48
+ args="$(ps -o args= -p "$pid" 2>/dev/null || true)"
49
+ case " $args " in *" --port $port "*|*" --port=$port "*) ;; *) continue ;; esac
50
+ kill "$pid" 2>/dev/null && reaped="$reaped $pid"
51
+ done <<LIST
52
+ $(_checkout_metro_pids "$repo")
53
+ LIST
54
+ if [ -n "$reaped" ]; then
55
+ printf 'Reaped leaked Metro bundler(s) for checkout port %s:%s\n' "$port" "$reaped" >&2
56
+ fi
57
+ return 0
58
+ }
59
+
43
60
  detect_checkout_metros() {
44
61
  local repo="$1" live_port="${2:-}" pid args
45
62
  [ -n "$repo" ] || return 0
@@ -0,0 +1,300 @@
1
+ import path from "node:path";
2
+ import { WebSocket } from "ws";
3
+ import brokerModule from "../../../adapters/mobile/bridge-runtime/lib/cdp-broker.cjs";
4
+ const DEVICE_ID = "extension";
5
+ const COMMAND_TIMEOUT_MS = 1e4;
6
+ const EXTENSION_TARGET_TYPES = /* @__PURE__ */ new Set([
7
+ "background_page",
8
+ "other",
9
+ "page",
10
+ "service_worker"
11
+ ]);
12
+ const { brokerSocketPath, createBrokerClient, createCdpBroker } = brokerModule;
13
+ async function createExtensionNetworkObserver(cdpPort, runtimeDir) {
14
+ if (!Number.isInteger(cdpPort) || cdpPort <= 0) {
15
+ throw new Error("Extension network observation requires a valid CDP port.");
16
+ }
17
+ const [version, targets] = await Promise.all([
18
+ fetchJson(`http://127.0.0.1:${cdpPort}/json/version`),
19
+ fetchJson(`http://127.0.0.1:${cdpPort}/json`)
20
+ ]);
21
+ const browserWebSocketUrl = String(
22
+ asRecord(version).webSocketDebuggerUrl ?? ""
23
+ );
24
+ const extensionId = extensionIdFromTargets(targets);
25
+ if (!browserWebSocketUrl || !extensionId) {
26
+ throw new Error(
27
+ "Extension CDP browser or loaded extension target is unavailable."
28
+ );
29
+ }
30
+ const socket = new WebSocket(browserWebSocketUrl);
31
+ await waitForOpen(socket);
32
+ let nextId = 0;
33
+ let closed = false;
34
+ const brokerRef = {};
35
+ const pending = /* @__PURE__ */ new Map();
36
+ const targetSessions = /* @__PURE__ */ new Map();
37
+ const attaching = /* @__PURE__ */ new Set();
38
+ const send = (method, params = {}, sessionId, timeoutMs = COMMAND_TIMEOUT_MS) => new Promise((resolve, reject) => {
39
+ const id = ++nextId;
40
+ const timer = setTimeout(() => {
41
+ pending.delete(id);
42
+ reject(new Error(`Extension CDP command timed out: ${method}`));
43
+ }, timeoutMs);
44
+ pending.set(id, { resolve, reject, timer });
45
+ socket.send(
46
+ JSON.stringify({
47
+ id,
48
+ method,
49
+ params,
50
+ ...sessionId ? { sessionId } : {}
51
+ })
52
+ );
53
+ });
54
+ const relevantTarget = (target) => {
55
+ if (!EXTENSION_TARGET_TYPES.has(String(target.type))) return false;
56
+ try {
57
+ return new URL(String(target.url)).hostname === extensionId;
58
+ } catch {
59
+ return false;
60
+ }
61
+ };
62
+ const refreshBrokerSession = () => {
63
+ if (!brokerRef.current) return;
64
+ brokerRef.current.onSessionClose(DEVICE_ID);
65
+ brokerRef.current.onSessionOpen(DEVICE_ID);
66
+ };
67
+ const attachTarget = async (target) => {
68
+ if (closed || !relevantTarget(target) || targetSessions.has(target.targetId) || attaching.has(target.targetId)) {
69
+ return;
70
+ }
71
+ attaching.add(target.targetId);
72
+ try {
73
+ const result = asRecord(
74
+ await send("Target.attachToTarget", {
75
+ targetId: target.targetId,
76
+ flatten: true
77
+ })
78
+ );
79
+ const sessionId = String(result.sessionId ?? "");
80
+ if (sessionId) {
81
+ targetSessions.set(target.targetId, sessionId);
82
+ refreshBrokerSession();
83
+ }
84
+ } catch (error) {
85
+ if (!/already attached/iu.test(String(error))) {
86
+ brokerRef.current?.onSessionClose(DEVICE_ID);
87
+ }
88
+ } finally {
89
+ attaching.delete(target.targetId);
90
+ }
91
+ };
92
+ socket.on("message", (data) => {
93
+ let message;
94
+ try {
95
+ message = JSON.parse(String(data));
96
+ } catch {
97
+ return;
98
+ }
99
+ if (message.id && pending.has(message.id)) {
100
+ const command = pending.get(message.id);
101
+ pending.delete(message.id);
102
+ if (!command) return;
103
+ clearTimeout(command.timer);
104
+ if (message.error) {
105
+ command.reject(
106
+ new Error(
107
+ String(message.error.message ?? "Extension CDP command failed")
108
+ )
109
+ );
110
+ } else {
111
+ command.resolve(message.result ?? {});
112
+ }
113
+ return;
114
+ }
115
+ if (message.method === "Target.targetCreated") {
116
+ const target = asTargetInfo(message.params?.targetInfo);
117
+ if (target) void attachTarget(target);
118
+ return;
119
+ }
120
+ if (message.method === "Target.attachedToTarget") {
121
+ const target = asTargetInfo(message.params?.targetInfo);
122
+ const sessionId = String(message.params?.sessionId ?? "");
123
+ if (target && sessionId && relevantTarget(target)) {
124
+ targetSessions.set(target.targetId, sessionId);
125
+ refreshBrokerSession();
126
+ }
127
+ return;
128
+ }
129
+ if (message.method === "Target.detachedFromTarget") {
130
+ const sessionId = String(message.params?.sessionId ?? "");
131
+ for (const [targetId, activeSessionId] of targetSessions) {
132
+ if (activeSessionId !== sessionId) continue;
133
+ targetSessions.delete(targetId);
134
+ refreshBrokerSession();
135
+ }
136
+ return;
137
+ }
138
+ if (message.sessionId && message.method?.startsWith("Network.")) {
139
+ brokerRef.current?.onCdpEvent(
140
+ DEVICE_ID,
141
+ message.method,
142
+ message.params ?? {}
143
+ );
144
+ }
145
+ });
146
+ socket.on("close", () => {
147
+ if (!closed) brokerRef.current?.onSessionClose(DEVICE_ID);
148
+ for (const command of pending.values()) {
149
+ clearTimeout(command.timer);
150
+ command.reject(new Error("Extension browser CDP connection closed"));
151
+ }
152
+ pending.clear();
153
+ });
154
+ await send("Target.setDiscoverTargets", { discover: true });
155
+ const targetResult = asRecord(await send("Target.getTargets"));
156
+ const targetInfos = Array.isArray(targetResult.targetInfos) ? targetResult.targetInfos : [];
157
+ await Promise.all(
158
+ targetInfos.map(asTargetInfo).filter((target) => Boolean(target)).map(attachTarget)
159
+ );
160
+ if (targetSessions.size === 0) {
161
+ socket.close();
162
+ throw new Error("Extension CDP exposes no attachable extension targets.");
163
+ }
164
+ const sessions = /* @__PURE__ */ new Map([[DEVICE_ID, { brokerReady: true }]]);
165
+ const socketPath = brokerSocketPath(
166
+ path.join(runtimeDir, "extension-network")
167
+ );
168
+ brokerRef.current = createCdpBroker({
169
+ socketPath,
170
+ sessions,
171
+ async sendCommand(_session, method, params, timeoutMs) {
172
+ const sessionIds = [...new Set(targetSessions.values())];
173
+ if (sessionIds.length === 0) {
174
+ throw new Error(
175
+ "Extension CDP has no active extension target sessions."
176
+ );
177
+ }
178
+ const results = await Promise.allSettled(
179
+ sessionIds.map(
180
+ (sessionId) => send(method, params, sessionId, timeoutMs)
181
+ )
182
+ );
183
+ const successes = results.filter(
184
+ (result) => result.status === "fulfilled"
185
+ );
186
+ if (successes.length === 0) {
187
+ const firstFailure = results.find(
188
+ (result) => result.status === "rejected"
189
+ );
190
+ throw new Error(
191
+ `Extension CDP command failed on every target: ${String(firstFailure?.reason ?? method)}`
192
+ );
193
+ }
194
+ if (successes.length !== results.length) {
195
+ results.forEach((result, index) => {
196
+ if (result.status === "fulfilled") return;
197
+ const failedSession = sessionIds[index];
198
+ for (const [targetId, sessionId] of targetSessions) {
199
+ if (sessionId === failedSession) targetSessions.delete(targetId);
200
+ }
201
+ });
202
+ brokerRef.current?.onSessionClose(DEVICE_ID);
203
+ }
204
+ return successes[0].value;
205
+ },
206
+ requestDiscovery() {
207
+ void send("Target.getTargets").then((result) => {
208
+ const infos = asRecord(result).targetInfos;
209
+ if (!Array.isArray(infos)) return;
210
+ for (const value of infos) {
211
+ const target = asTargetInfo(value);
212
+ if (target) void attachTarget(target);
213
+ }
214
+ }).catch(() => brokerRef.current?.onSessionClose(DEVICE_ID));
215
+ }
216
+ });
217
+ brokerRef.current.onSessionOpen(DEVICE_ID);
218
+ await waitForSocket(socketPath);
219
+ const client = await createBrokerClient(
220
+ socketPath,
221
+ DEVICE_ID,
222
+ COMMAND_TIMEOUT_MS
223
+ );
224
+ return {
225
+ start(params) {
226
+ return client.control("capture-start", params, COMMAND_TIMEOUT_MS);
227
+ },
228
+ async end(id) {
229
+ return asRecord(
230
+ await client.control("capture-end", { id }, COMMAND_TIMEOUT_MS)
231
+ );
232
+ },
233
+ async close() {
234
+ closed = true;
235
+ client.close();
236
+ brokerRef.current?.close();
237
+ socket.close();
238
+ }
239
+ };
240
+ }
241
+ async function fetchJson(url) {
242
+ const response = await fetch(url);
243
+ if (!response.ok)
244
+ throw new Error(`Extension CDP returned HTTP ${response.status}.`);
245
+ return response.json();
246
+ }
247
+ function extensionIdFromTargets(targets) {
248
+ const values = Array.isArray(targets) ? targets : [];
249
+ for (const target of values) {
250
+ const info = asTargetInfo(target);
251
+ if (!info) continue;
252
+ try {
253
+ const url = new URL(info.url);
254
+ if (url.protocol !== "chrome-extension:") continue;
255
+ const extensionId = url.hostname;
256
+ if (extensionId) return extensionId;
257
+ } catch {
258
+ continue;
259
+ }
260
+ }
261
+ return null;
262
+ }
263
+ function asRecord(value) {
264
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
265
+ }
266
+ function asTargetInfo(value) {
267
+ const target = asRecord(value);
268
+ const targetId = String(target.targetId ?? target.id ?? "");
269
+ const type = String(target.type ?? "");
270
+ const url = String(target.url ?? "");
271
+ return targetId && type && url ? { targetId, type, url } : null;
272
+ }
273
+ function waitForOpen(socket) {
274
+ return new Promise((resolve, reject) => {
275
+ const timer = setTimeout(() => {
276
+ socket.close();
277
+ reject(new Error("Extension browser CDP connection timed out."));
278
+ }, COMMAND_TIMEOUT_MS);
279
+ socket.once("open", () => {
280
+ clearTimeout(timer);
281
+ resolve();
282
+ });
283
+ socket.once("error", (error) => {
284
+ clearTimeout(timer);
285
+ reject(error);
286
+ });
287
+ });
288
+ }
289
+ async function waitForSocket(socketPath) {
290
+ const { existsSync } = await import("node:fs");
291
+ const deadline = Date.now() + COMMAND_TIMEOUT_MS;
292
+ while (Date.now() < deadline) {
293
+ if (existsSync(socketPath)) return;
294
+ await new Promise((resolve) => setTimeout(resolve, 25));
295
+ }
296
+ throw new Error("Extension network observer broker did not start.");
297
+ }
298
+ export {
299
+ createExtensionNetworkObserver
300
+ };
@@ -32,10 +32,6 @@ function mobileMetroEnvCheck(target) {
32
32
  status: marker === null ? "missing" : marker.fingerprint === fingerprint ? "current" : "changed"
33
33
  };
34
34
  }
35
- function applyMobileMetroEnvPolicy(target, actions) {
36
- if (mobileMetroEnvCheck(target).status === "current") return actions;
37
- return actions.map((action) => action.id === "start-metro" && !action.argv?.includes("--clear") ? { ...action, argv: [...action.argv ?? [], "--clear"] } : action);
38
- }
39
35
  function recordMobileMetroEnvBaseline(target, expectedFingerprint) {
40
36
  if (mobileMetroEnvFingerprint(target) !== expectedFingerprint) return false;
41
37
  const marker = recipeRuntimePath(target, BASELINE_FILE);
@@ -69,7 +65,6 @@ function readBaseline(target) {
69
65
  }
70
66
  }
71
67
  export {
72
- applyMobileMetroEnvPolicy,
73
68
  mobileMetroEnvCheck,
74
69
  mobileMetroEnvFingerprint,
75
70
  recordMobileMetroEnvBaseline
@@ -9,7 +9,6 @@ import {
9
9
  launchActions
10
10
  } from "./runtime-decision.js";
11
11
  import {
12
- applyMobileMetroEnvPolicy,
13
12
  mobileMetroEnvCheck,
14
13
  recordMobileMetroEnvBaseline
15
14
  } from "./metro-env.js";
@@ -241,9 +240,8 @@ function withAppRestart(actions, restartApp) {
241
240
  }
242
241
  async function dispatchActionSequence(requestedActions, target, platform, json, preflightMode, watcherPort) {
243
242
  const resolved = path.resolve(target);
244
- const actions = applyMobileMetroEnvPolicy(resolved, requestedActions);
245
243
  let pendingMetroEnvFingerprint;
246
- for (const action of actions) {
244
+ for (const action of requestedActions) {
247
245
  if (action.id === "start-metro") {
248
246
  pendingMetroEnvFingerprint = mobileMetroEnvCheck(resolved).fingerprint;
249
247
  }
@@ -2,20 +2,18 @@ import { execFileSync } from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import {
5
- clearDecisionState,
6
5
  depsCheck,
7
6
  recordDepsBaseline
8
7
  } from "@farmslot/recipe-harness/runtime/deps-readiness";
9
8
  import {
10
9
  analyzeBundleLog,
11
- evaluatePersistentBundleError,
12
10
  supersededErrorCapture
13
11
  } from "@farmslot/recipe-harness/runtime/log-analysis";
14
12
  import { probeMetroPackager } from "@farmslot/recipe-harness/runtime/metro-probe";
15
13
  import { recipeRuntimePath } from "../../paths.js";
16
14
  import { resolveMobileToolPath } from "../../../library/actions/mobile/platform/tool-paths.mjs";
17
15
  import { mobileProductMarkers } from "./deps-markers.js";
18
- import { applyMobileMetroEnvPolicy, mobileMetroEnvCheck } from "./metro-env.js";
16
+ import { mobileMetroEnvCheck } from "./metro-env.js";
19
17
  const BUNDLE_ERR = /Bundling failed|Unable to resolve /u;
20
18
  const BUNDLE_OK = /Bundled \d+ms|iOS Bundled|Android Bundled|Finished bundling/u;
21
19
  const NATIVE_MODULE_STALE = /\[runtime not ready\].*HybridObject "([^"]+)" - It has not yet been registered in the Nitro Modules HybridObjectRegistry/u;
@@ -132,13 +130,10 @@ async function decideMobileReadiness(target, options = {}) {
132
130
  decision: "launch",
133
131
  reasonCode: report.decision === "ready" ? env.status === "missing" ? "metro-env-baseline-missing" : "metro-env-changed" : report.reasonCode,
134
132
  reasons: [
135
- env.status === "missing" ? "Metro environment has no successful bundle baseline; restart once with a cleared transform cache." : "Metro environment inputs changed since the successful bundle baseline; restart once with a cleared transform cache.",
133
+ env.status === "missing" ? "Metro environment has no successful bundle baseline; restart Metro to load the current environment without clearing its transform cache." : "Metro environment inputs changed since the successful bundle baseline; restart Metro without clearing its transform cache. Pass --clear-metro only if stale transforms are observed.",
136
134
  ...report.reasons
137
135
  ],
138
- actions: applyMobileMetroEnvPolicy(
139
- resolved,
140
- report.decision === "ready" ? launchActions(resolved) : report.actions
141
- )
136
+ actions: report.decision === "ready" ? launchActions(resolved) : report.actions
142
137
  };
143
138
  }
144
139
  async function computeMobileReadiness(resolved, options, fast) {
@@ -160,9 +155,6 @@ async function computeMobileReadiness(resolved, options, fast) {
160
155
  }
161
156
  const metroLog = metroLogCheck(resolved, options.metroLog);
162
157
  const metro = options.watcherPort ? await probeMetroPackager(options.watcherPort) : { status: "skipped" };
163
- if (metroLog.status === "ok") {
164
- clearDecisionState(resolved, "bundle-error-state.json");
165
- }
166
158
  const checks = { deps, metroLog, metro };
167
159
  if (deps.status === "missing") {
168
160
  return {
@@ -221,7 +213,7 @@ async function computeMobileReadiness(resolved, options, fast) {
221
213
  checks,
222
214
  actions: [
223
215
  { id: "rebuild-native-dev-client" },
224
- ...launchActions(resolved, true)
216
+ ...launchActions(resolved)
225
217
  ]
226
218
  };
227
219
  }
@@ -229,22 +221,6 @@ async function computeMobileReadiness(resolved, options, fast) {
229
221
  const depsSatisfied = deps.status === "current";
230
222
  if (depsSatisfied && (metroLog.reason === "stale-bundle-error" || metroLog.reason === "bundle-error")) {
231
223
  const excerpt = metroLog.excerpt ?? "";
232
- const persistent = evaluatePersistentBundleError(resolved, excerpt);
233
- if (persistent.blocked) {
234
- return {
235
- schemaVersion: 1,
236
- adapter: "mobile",
237
- target: resolved,
238
- decision: "blocked",
239
- reasonCode: "bundle-error-persistent",
240
- reasons: [
241
- "Metro bundle keeps failing with the same error after a cache-cleared relaunch was already suggested; fix the bundle error in app code before retrying recipe up.",
242
- ...excerpt ? [excerpt] : []
243
- ],
244
- checks,
245
- actions: []
246
- };
247
- }
248
224
  return {
249
225
  schemaVersion: 1,
250
226
  adapter: "mobile",
@@ -252,11 +228,11 @@ async function computeMobileReadiness(resolved, options, fast) {
252
228
  decision: "launch",
253
229
  reasonCode: metroLog.reason === "stale-bundle-error" ? "metro-stale-log" : "bundle-errors-recoverable",
254
230
  reasons: [
255
- metroLog.reason === "stale-bundle-error" ? "Metro log shows prior bundle failures but cited modules are installed; restart Metro with a cleared cache." : "Metro bundle is failing with current deps; restart Metro (clear cache) before relaunching the dev client.",
231
+ metroLog.reason === "stale-bundle-error" ? "Metro log shows prior bundle failures but cited modules are installed; restart Metro without clearing the cache. Pass --clear-metro if the failure persists." : "Metro bundle is failing with current deps; restart Metro without clearing the cache. Pass --clear-metro if the failure persists.",
256
232
  ...excerpt ? [excerpt] : []
257
233
  ],
258
234
  checks,
259
- actions: launchActions(resolved, true)
235
+ actions: launchActions(resolved)
260
236
  };
261
237
  }
262
238
  return {