@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
package/dist/adapters.js CHANGED
@@ -12,6 +12,7 @@ import { observeNativeUi } from "../library/actions/mobile/platform/observe-ui.m
12
12
  import { nativeAgentDeviceStateDir } from "../library/actions/mobile/platform/native-session-name.mjs";
13
13
  import { resolveMobileToolPath } from "../library/actions/mobile/platform/tool-paths.mjs";
14
14
  import { resolveWalletImportCredentials, validateWalletImportOptions } from "../library/actions/shared/wallet/import-source.mjs";
15
+ import { handleRunNetworkAction } from "./network-observation.js";
15
16
  const execFileAsync = promisify(execFile);
16
17
  const NATIVE_PROVIDER_UI_ACTIONS = /* @__PURE__ */ new Set([
17
18
  "ui.press",
@@ -103,7 +104,11 @@ const LIVE_ONLY_WALLET_ACTIONS = /* @__PURE__ */ new Set([
103
104
  "metamask.wallet.list_accounts",
104
105
  "metamask.wallet.read_state"
105
106
  ]);
106
- const LIVE_ONLY_APP_ACTIONS = /* @__PURE__ */ new Set(["ui.navigate"]);
107
+ const LIVE_ONLY_APP_ACTIONS = /* @__PURE__ */ new Set([
108
+ "ui.navigate",
109
+ "app.network_capture",
110
+ "app.network_assert"
111
+ ]);
107
112
  function requiresLiveAdapter(platform, action) {
108
113
  return LIVE_ONLY_ACTIONS.has(action) || platform === "core" && CORE_ONLY_PERPS_ACTIONS.has(action) || LIVE_ONLY_WALLET_ACTIONS.has(action) || LIVE_ONLY_APP_ACTIONS.has(action);
109
114
  }
@@ -140,6 +145,13 @@ function liveAdapterPathHint(platform, action) {
140
145
  return `library/actions/${platform}/${action.replaceAll(".", "/")}.mjs`;
141
146
  }
142
147
  async function semanticResult(platform, action, node, context, forceLive = false, preparedLiveAdapters) {
148
+ const networkAction = await handleRunNetworkAction(
149
+ platform,
150
+ action,
151
+ node,
152
+ context
153
+ );
154
+ if (networkAction) return networkAction;
143
155
  const live = await runLiveFirst(
144
156
  platform,
145
157
  action,
@@ -253,6 +265,7 @@ function createMetaMaskSemanticAdapters(platform, declaredCustomActions = [], pr
253
265
  ];
254
266
  const bundledActions = [
255
267
  ...walletActions,
268
+ ...platform !== "core" ? ["app.network_capture", "app.network_assert"] : [],
256
269
  "metamask.assets.read_visible_state",
257
270
  "metamask.assets.open_details",
258
271
  "metamask.assets.read_details",
@@ -15,6 +15,7 @@ const SPEC = {
15
15
  { name: "sync", desc: "Refresh harness + canonicalize wallet fixture", flags: ["--json"] },
16
16
  { name: "logs", aliases: ["tail"], desc: "Compact build events or full log", flags: ["--full", "-f", "--window", "--events", "--source", "--json"] },
17
17
  { name: "debug", aliases: ["devtools", "inspect"], desc: "Open DevTools UI", flags: ["--json", "--no-open"] },
18
+ { name: "reload", desc: "Reload the connected app runtime", flags: ["--json"] },
18
19
  { name: "fixtures", desc: "Manage the canonical wallet fixture (sync/set/reset/generate)", args: ["sync", "set", "reset", "generate"], flags: ["--fixture", "--out", "--adapter", "--target", "--device", "--json"] },
19
20
  { name: "actions", desc: "List runnable recipe actions", flags: ["--json", "--matrix", "--categories", "--category", "--action", "--library"] },
20
21
  { name: "doctor", desc: "Check harness/orchestration health", flags: ["--json", "--target", "--adapter", "--runtime-dir", "--expect-live", "--print-ready", "--cdp-port", "--device"] },
@@ -31,10 +32,11 @@ const SPEC = {
31
32
  { name: "ready", aliases: ["ensure-ready"], desc: "Ensure one healthy extension home tab" },
32
33
  { name: "watch", aliases: ["start-watch", "watcher"], desc: "Start/reuse webpack watcher", flags: ["--full", "-f"] },
33
34
  { name: "stop", aliases: ["stop-watch", "stop-watcher"], desc: "Stop this checkout watcher" },
34
- { name: "rebuild", aliases: ["reset", "reopen", "reload", "browser", "launch", "full-launch", "runtime-launch", "build", "build:once", "build-once", "refresh-once"], desc: "Clean webpack + browser launch", flags: ["--json", "--dry-run", "--full"] },
35
+ { name: "rebuild", aliases: ["reset", "reopen", "browser", "launch", "full-launch", "runtime-launch", "build", "build:once", "build-once", "refresh-once"], desc: "Clean webpack + browser launch", flags: ["--json", "--dry-run", "--full"] },
35
36
  { name: "sidepanel", desc: "Side panel helper", args: ["cycle", "open", "close", "toggle", "status"] },
36
37
  { name: "prepare", flags: ["--target", "--cdp-port", "--runtime-dir", "--validate"] },
37
- { name: "debug", args: ["page", "worker"], flags: ["--json", "--no-open"] }
38
+ { name: "debug", args: ["page", "worker"], flags: ["--json", "--no-open"] },
39
+ { name: "reload", desc: "Reload the live Extension UI over CDP", flags: ["--json"] }
38
40
  ],
39
41
  mobile: [
40
42
  { name: "ios", aliases: ["start"], desc: "Start Metro + launch iOS dev client" },
@@ -53,7 +55,8 @@ const SPEC = {
53
55
  { name: "screenshot", desc: "Capture simulator/device screenshot", args: ["path"] },
54
56
  { name: "dev-menu", aliases: ["devmenu"], desc: "Open RN developer menu", flags: ["--json", "--no-open"] },
55
57
  { name: "prepare", flags: ["--target", "--platform", "--preflight-mode", "--port", "--simulator", "--adb-serial", "--runtime-dir", "--wallet-setup", "--wallet-fixture"] },
56
- { name: "debug", flags: ["--json", "--no-open", "--action"] }
58
+ { name: "debug", flags: ["--json", "--no-open", "--action"] },
59
+ { name: "reload", desc: "Reload the app without restarting Metro", flags: ["--json"] }
57
60
  ]
58
61
  };
59
62
  const GLOBAL_FLAGS = ["--json", "--dry-run", "--full", "-f", "--help", "-h", "--no-open", "--no-color"];
package/dist/cli.js CHANGED
@@ -15,6 +15,7 @@ import { handleCompletionCandidates, invalidateCompletionCache } from "./command
15
15
  import { handleLaunch } from "./commands/launch/index.js";
16
16
  import { handleLogs } from "./commands/logs.js";
17
17
  import { handleDebug } from "./commands/debug.js";
18
+ import { handleReload } from "./commands/reload.js";
18
19
  import { handleFixtures } from "./commands/fixtures.js";
19
20
  import { handleRecipeQuality } from "./commands/recipe-quality.js";
20
21
  import { handleStatus } from "./commands/status.js";
@@ -47,6 +48,8 @@ DAILY LOOP \u2014 what a teammate runs many times a day:
47
48
  mm-harness logs
48
49
  debug Open the debug console (extension DevTools / mobile RN).
49
50
  mm-harness debug
51
+ reload Reload the connected Mobile or Extension runtime.
52
+ mm-harness reload
50
53
  fixtures Sync files, set/reset the wallet, generate fixture-state, or finalize labels over CDP.
51
54
  mm-harness fixtures sync # or: set | reset | generate --fixture <f> --out <o> | finalize \u2026
52
55
 
@@ -118,6 +121,7 @@ async function main(argv) {
118
121
  if (command === "stop") return handleStop(argv.slice(1));
119
122
  if (command === "logs") return handleLogs(argv.slice(1));
120
123
  if (command === "debug") return handleDebug(argv.slice(1));
124
+ if (command === "reload") return handleReload(argv.slice(1));
121
125
  if (command === "fixtures") return handleFixtures(argv.slice(1));
122
126
  if (command === "recipe-quality") return handleRecipeQuality(argv.slice(1));
123
127
  if (command === "check") return handleCheck(argv.slice(1));
@@ -278,6 +278,9 @@ const PUBLIC_COMMAND_CONTRACTS = {
278
278
  "--no-open": bool()
279
279
  })
280
280
  },
281
+ reload: {
282
+ options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM)
283
+ },
281
284
  update: {
282
285
  options: options(HELP, JSON, { "--check": bool() })
283
286
  },
@@ -43,6 +43,9 @@ import {
43
43
  import { readMobileReleaseArtifactState } from "../adapters/mobile/release-artifact-state.js";
44
44
  import { acquireCheckoutLock } from "../checkout-lock.js";
45
45
  import { formatRunDiagnosticsForHuman, readRunDiagnosticsDocument } from "../run-diagnostics.js";
46
+ import {
47
+ startRunNetworkObservation
48
+ } from "../network-observation.js";
46
49
  import { recipeTrustFailure } from "../recipe-security.js";
47
50
  import { isSensitiveKey, recordCommandEvidence, redactStructuredValue } from "../command-journal.js";
48
51
  import { closest } from "../command-contract.js";
@@ -209,12 +212,14 @@ async function handleCall(argv) {
209
212
  recordCommandEvidence(artifactsDir);
210
213
  const requestedRuntimeOptions = runtimeOptionsFromCli(options);
211
214
  const inheritedSource = process.env.FARMSLOT_RECIPE_SOURCE_TRUST || process.env.FARMSLOT_RECIPE_SOURCE_KIND || process.env.FARMSLOT_RECIPE_SOURCE_NAME || process.env.FARMSLOT_RECIPE_SOURCE_DIGEST;
215
+ let networkObservation;
212
216
  const callRuntimeOptions = {
213
217
  ...requestedRuntimeOptions,
214
218
  ...librarySources ? { librarySources } : {},
215
219
  autoHud: false,
216
220
  suppressLibraryResolutionLogs: true,
217
221
  stdoutIsMachineContract: json,
222
+ onActionEvent: ({ nodeId, action, status }) => networkObservation?.onActionEvent({ nodeId, action, status }),
218
223
  ...requestedRuntimeOptions.source ? { source: requestedRuntimeOptions.source } : inheritedSource ? {} : {
219
224
  source: {
220
225
  kind: "operator",
@@ -275,29 +280,49 @@ async function handleCall(argv) {
275
280
  });
276
281
  if (typeof prepared === "number") return prepared;
277
282
  const { state, heal } = prepared;
278
- const { result, violation } = await executeWithHealBounds(
279
- () => {
280
- const execution = preflightedExecution;
281
- preflightedExecution = void 0;
282
- return runRecipe(
283
- adapter,
284
- recipe,
285
- artifactsDir,
286
- target,
287
- actionManifestOverride,
288
- callRuntimeOptions,
289
- execution
290
- );
291
- },
283
+ networkObservation = await startRunNetworkObservation(
292
284
  adapter,
293
285
  target,
294
- heal,
295
- state,
296
- () => recoverRunInfra(adapter, target, json, {
297
- cdpPort: optionString(options, "cdpPort"),
298
- watcherPort: optionString(options, "watcherPort") ?? optionString(options, "metroPort")
299
- })
286
+ artifactsDir,
287
+ process.env,
288
+ {
289
+ cdpPort: callRuntimeOptions.cdpPort,
290
+ watcherPort: callRuntimeOptions.watcherPort
291
+ }
300
292
  );
293
+ let executionResult;
294
+ try {
295
+ executionResult = await executeWithHealBounds(
296
+ () => {
297
+ const execution = preflightedExecution;
298
+ preflightedExecution = void 0;
299
+ return runRecipe(
300
+ adapter,
301
+ recipe,
302
+ artifactsDir,
303
+ target,
304
+ actionManifestOverride,
305
+ callRuntimeOptions,
306
+ execution
307
+ );
308
+ },
309
+ adapter,
310
+ target,
311
+ heal,
312
+ state,
313
+ () => recoverRunInfra(adapter, target, json, {
314
+ cdpPort: optionString(options, "cdpPort"),
315
+ watcherPort: optionString(options, "watcherPort") ?? optionString(options, "metroPort")
316
+ })
317
+ );
318
+ } catch (error) {
319
+ await networkObservation?.finalize();
320
+ networkObservation = void 0;
321
+ throw error;
322
+ }
323
+ const { result, violation } = executionResult;
324
+ await networkObservation?.finalize(result.artifactManifestPath);
325
+ networkObservation = void 0;
301
326
  if (violation !== null) {
302
327
  const conciseFailure = violation.originalError ? conciseFailureForHuman(violation.originalError) : "";
303
328
  const example = describedAction ? actionExampleCommand(
@@ -53,6 +53,21 @@ const LAUNCH_BOOLEANS = /* @__PURE__ */ new Set([
53
53
  "jsonStream"
54
54
  ]);
55
55
  const DEFAULT_EXTENSION_DAPP_URL = "https://metamask.github.io/test-dapp/";
56
+ function metroLaunchEvidence(adapter, target) {
57
+ if (adapter !== "mobile") return void 0;
58
+ const evidencePath = recipeRuntimePath(target, "metro-generation.json");
59
+ let descriptor;
60
+ try {
61
+ descriptor = fs.openSync(evidencePath, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
62
+ const stat = fs.fstatSync(descriptor);
63
+ if (!stat.isFile() || stat.size > 256 * 1024) return void 0;
64
+ return JSON.parse(fs.readFileSync(descriptor, "utf8"));
65
+ } catch {
66
+ return void 0;
67
+ } finally {
68
+ if (descriptor !== void 0) fs.closeSync(descriptor);
69
+ }
70
+ }
56
71
  async function handleLaunch(argv) {
57
72
  const { options } = parseFlags(argv, LAUNCH_BOOLEANS);
58
73
  const json = flag(options, "json");
@@ -627,14 +642,15 @@ async function finishLaunch(jsonOutput, machine, stream, adapter, mobileTarget,
627
642
  platform: mobileTarget ?? (adapter === "extension" ? displayMode : null),
628
643
  tier,
629
644
  recovered: state.recovered,
630
- mutations: state.mutations
645
+ mutations: state.mutations,
646
+ metro: metroLaunchEvidence(adapter, target)
631
647
  });
632
648
  }
633
649
  return exitCode;
634
650
  }
635
- return launchPass(jsonOutput, stream, adapter, mobileTarget, tier, displayMode, state);
651
+ return launchPass(jsonOutput, stream, adapter, mobileTarget, tier, displayMode, target, state);
636
652
  }
637
- function launchPass(json, stream, adapter, mobileTarget, tier, displayMode, state) {
653
+ function launchPass(json, stream, adapter, mobileTarget, tier, displayMode, target, state) {
638
654
  if (stream.enabled) {
639
655
  for (const mutation of state.mutations) stream.mutation(mutation);
640
656
  for (const recovery of state.recovered) stream.recovery(recovery);
@@ -643,7 +659,8 @@ function launchPass(json, stream, adapter, mobileTarget, tier, displayMode, stat
643
659
  platform: mobileTarget ?? (adapter === "extension" ? displayMode : null),
644
660
  tier,
645
661
  recovered: state.recovered,
646
- mutations: state.mutations
662
+ mutations: state.mutations,
663
+ metro: metroLaunchEvidence(adapter, target)
647
664
  });
648
665
  } else if (json) {
649
666
  console.log(
@@ -658,6 +675,7 @@ function launchPass(json, stream, adapter, mobileTarget, tier, displayMode, stat
658
675
  phase: "launch",
659
676
  recovered: state.recovered,
660
677
  mutations: state.mutations,
678
+ metro: metroLaunchEvidence(adapter, target),
661
679
  exitCode: EXIT.ok
662
680
  },
663
681
  null,
@@ -706,7 +724,8 @@ function launchFail(json, stream, adapter, mobileTarget, tier, state, target, fa
706
724
  recovered: state.recovered,
707
725
  mutations: state.mutations,
708
726
  recoverable: failure.recoverable,
709
- attemptedRecoveries: state.attemptedRecoveries
727
+ attemptedRecoveries: state.attemptedRecoveries,
728
+ metro: metroLaunchEvidence(adapter, target)
710
729
  });
711
730
  } else if (json) {
712
731
  console.log(
@@ -723,6 +742,7 @@ function launchFail(json, stream, adapter, mobileTarget, tier, state, target, fa
723
742
  mutations: state.mutations,
724
743
  recoverable: failure.recoverable,
725
744
  attemptedRecoveries: state.attemptedRecoveries,
745
+ metro: metroLaunchEvidence(adapter, target),
726
746
  exitCode: failure.exitCode,
727
747
  error: {
728
748
  code: failure.code,
@@ -0,0 +1,80 @@
1
+ import path from "node:path";
2
+ import { runnerDir } from "../paths.js";
3
+ import { getAdapterSurface } from "../adapters/surface.js";
4
+ import {
5
+ ADAPTER_DETECT_NEXT,
6
+ EXIT,
7
+ flag,
8
+ parseFlags,
9
+ resolveAdapter,
10
+ spawnScript,
11
+ targetOf,
12
+ usageOut
13
+ } from "./shared.js";
14
+ const RELOAD_BOOLEANS = /* @__PURE__ */ new Set(["json"]);
15
+ async function handleReload(argv) {
16
+ const { options } = parseFlags(argv, RELOAD_BOOLEANS);
17
+ const json = flag(options, "json");
18
+ const target = targetOf(options);
19
+ const adapter = resolveAdapter(options, target);
20
+ if (!adapter) {
21
+ return usageOut(
22
+ json,
23
+ "reload",
24
+ `could not detect the MetaMask repo type for ${target}`,
25
+ ADAPTER_DETECT_NEXT
26
+ );
27
+ }
28
+ if (adapter === "core") {
29
+ return usageOut(
30
+ json,
31
+ "reload",
32
+ "core is headless; there is no app runtime to reload.",
33
+ "mm-harness run <core-recipe>"
34
+ );
35
+ }
36
+ getAdapterSurface(adapter).resolveSlotPorts(target);
37
+ if (adapter === "extension") {
38
+ if (!process.env.CDP_PORT) {
39
+ return usageOut(
40
+ json,
41
+ "reload",
42
+ "the Extension CDP port could not be resolved.",
43
+ "mm-harness launch"
44
+ );
45
+ }
46
+ const script2 = path.join(runnerDir, "adapters/extension/reattach.sh");
47
+ const args2 = ["--target", target, "--cdp-port", process.env.CDP_PORT];
48
+ if (process.env.WATCHER_PORT) {
49
+ args2.push("--watcher-port", process.env.WATCHER_PORT);
50
+ }
51
+ const result2 = spawnScript(script2, args2, target, json);
52
+ if (json) {
53
+ console.log(
54
+ JSON.stringify(
55
+ {
56
+ ok: result2.status === 0,
57
+ adapter,
58
+ method: "cdp-reattach",
59
+ command: "reload",
60
+ cdpPort: Number(process.env.CDP_PORT),
61
+ ...result2.status === 0 ? {} : { error: result2.output.slice(-4e3) || "Extension reload failed." }
62
+ },
63
+ null,
64
+ 2
65
+ )
66
+ );
67
+ }
68
+ return result2.status === 0 ? EXIT.ok : EXIT.runtime;
69
+ }
70
+ const script = path.join(runnerDir, "adapters/mobile/reload-app.mjs");
71
+ const args = [];
72
+ if (process.env.WATCHER_PORT) args.push("--port", process.env.WATCHER_PORT);
73
+ if (json) args.push("--json");
74
+ const result = spawnScript(process.execPath, [script, ...args], target, json);
75
+ if (json) process.stdout.write(result.output);
76
+ return result.status === 0 ? EXIT.ok : EXIT.runtime;
77
+ }
78
+ export {
79
+ handleReload
80
+ };
@@ -41,6 +41,9 @@ import {
41
41
  missingActionCapabilities,
42
42
  resolveActionCapabilityMatrix
43
43
  } from "./manifest.js";
44
+ import {
45
+ startRunNetworkObservation
46
+ } from "../network-observation.js";
44
47
  async function validationCapabilityRefusals(adapter, findings, librarySources) {
45
48
  const actionNames = findings.flatMap((finding) => {
46
49
  if (finding.code !== "recipe.action_not_declared_by_manifest") return [];
@@ -227,12 +230,16 @@ async function handleRunInner({ positional, options }, stream) {
227
230
  }
228
231
  recordCommandEvidence(artifactsDir);
229
232
  const librarySources = validated.librarySources;
233
+ let networkObservation;
230
234
  const runtimeOptions = {
231
235
  ...runtimeOptionsFromCli(options),
232
236
  params,
233
237
  ...librarySources ? { librarySources } : {},
234
238
  stdoutIsMachineContract: machine,
235
- onActionEvent: ({ nodeId, action, status }) => stream.node(nodeId, action, status)
239
+ onActionEvent: ({ nodeId, action, status }) => {
240
+ stream.node(nodeId, action, status);
241
+ networkObservation?.onActionEvent({ nodeId, action, status });
242
+ }
236
243
  };
237
244
  stream.phase("authorize");
238
245
  let preflightedExecution = await preflightRecipe(
@@ -263,30 +270,50 @@ async function handleRunInner({ positional, options }, stream) {
263
270
  return prepared;
264
271
  }
265
272
  const { state, heal } = prepared;
266
- stream.phase("execute");
267
- const { result, violation } = await executeWithHealBounds(
268
- // validated.recipeFile, not the raw arg: the arg may be a library recipe NAME
269
- // that only the resolver knows how to turn into a file.
270
- () => {
271
- const execution = preflightedExecution;
272
- preflightedExecution = void 0;
273
- return runRecipe(
274
- adapter,
275
- validated.recipeFile,
276
- artifactsDir,
277
- target,
278
- optionString(options, "actionManifest"),
279
- runtimeOptions,
280
- execution
281
- );
282
- },
273
+ networkObservation = await startRunNetworkObservation(
283
274
  adapter,
284
275
  target,
285
- heal,
286
- state,
287
- () => recoverRunInfra(adapter, target, machine, runtimeOptions),
288
- (code) => stream.phase("recover", { code })
276
+ artifactsDir,
277
+ process.env,
278
+ {
279
+ cdpPort: runtimeOptions.cdpPort,
280
+ watcherPort: runtimeOptions.watcherPort
281
+ }
289
282
  );
283
+ stream.phase("execute");
284
+ let executionResult;
285
+ try {
286
+ executionResult = await executeWithHealBounds(
287
+ // validated.recipeFile, not the raw arg: the arg may be a library recipe NAME
288
+ // that only the resolver knows how to turn into a file.
289
+ () => {
290
+ const execution = preflightedExecution;
291
+ preflightedExecution = void 0;
292
+ return runRecipe(
293
+ adapter,
294
+ validated.recipeFile,
295
+ artifactsDir,
296
+ target,
297
+ optionString(options, "actionManifest"),
298
+ runtimeOptions,
299
+ execution
300
+ );
301
+ },
302
+ adapter,
303
+ target,
304
+ heal,
305
+ state,
306
+ () => recoverRunInfra(adapter, target, machine, runtimeOptions),
307
+ (code) => stream.phase("recover", { code })
308
+ );
309
+ } catch (error) {
310
+ await networkObservation?.finalize();
311
+ networkObservation = void 0;
312
+ throw error;
313
+ }
314
+ const { result, violation } = executionResult;
315
+ await networkObservation?.finalize(result.artifactManifestPath);
316
+ networkObservation = void 0;
290
317
  for (const mutation of state.mutations) stream.mutation(mutation);
291
318
  for (const recovery of state.recovered) stream.recovery(recovery);
292
319
  if (violation !== null) {
@@ -524,6 +524,22 @@ Example:
524
524
  mm-harness debug
525
525
  mm-harness debug --worker`
526
526
  },
527
+ {
528
+ name: "reload",
529
+ summary: "Reload the connected Mobile or Extension runtime without restarting its dev server.",
530
+ example: "mm-harness reload",
531
+ helpText: `mm-harness reload [flags]
532
+
533
+ Mobile broadcasts the same reload command as Metro's interactive r key.
534
+ Extension refreshes its loaded UI in place over CDP. Core is headless.
535
+
536
+ --adapter <mobile|extension> Target adapter (auto-detected inside a checkout)
537
+ --target <path> Checkout path (default: cwd)
538
+ --json Machine-readable output
539
+
540
+ Example:
541
+ mm-harness reload`
542
+ },
527
543
  {
528
544
  name: "update",
529
545
  summary: "Update the installed mm-harness to the published latest (--check reports only; --json = {current, latest, updateAvailable}).",
@@ -648,7 +664,7 @@ const HELP_GROUPS = [
648
664
  {
649
665
  title: "DAILY LOOP",
650
666
  blurb: "what a teammate runs many times a day (auto-ensures the overlay; --heal owns recovery)",
651
- commands: ["status", "launch", "stop", "logs", "debug", "fixtures"]
667
+ commands: ["status", "launch", "stop", "logs", "debug", "reload", "fixtures"]
652
668
  },
653
669
  {
654
670
  title: "DISCOVER",