@deeeed/metamask-harness 0.17.5 → 0.19.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 (169) hide show
  1. package/CHANGELOG.md +1059 -0
  2. package/README.md +91 -140
  3. package/adapters/core/inject.sh +1 -6
  4. package/adapters/extension/console-tail.mjs +55 -24
  5. package/adapters/extension/ensure-browser.sh +6 -1
  6. package/adapters/extension/inject.mjs +7 -13
  7. package/adapters/extension/launch-browser.cjs +7 -1
  8. package/adapters/extension/launch-webpack.cjs +83 -0
  9. package/adapters/extension/launch.sh +8 -11
  10. package/adapters/extension/lib/chrome-args.cjs +8 -1
  11. package/adapters/extension/lib/macos-focus.cjs +32 -0
  12. package/adapters/extension/live.sh +12 -21
  13. package/adapters/extension/reattach.sh +1 -0
  14. package/adapters/extension/seed-fixture.sh +4 -12
  15. package/adapters/extension/sidepanel-toggle.sh +4 -1
  16. package/adapters/extension/stamp-runtime-title.cjs +58 -0
  17. package/adapters/extension/start-watch.sh +17 -4
  18. package/adapters/extension/stop-viewers.sh +1 -1
  19. package/adapters/extension/sync-webpack-dist.cjs +107 -0
  20. package/adapters/extension/verify.sh +2 -2
  21. package/adapters/manifest.json +53 -37
  22. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +16 -0
  23. package/adapters/mobile/bridge-runtime/console-forwarder.cjs +1 -1
  24. package/adapters/mobile/bridge-runtime/lib/match-bridge-target.cjs +3 -1
  25. package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +2 -3
  26. package/adapters/mobile/cleanup.sh +16 -176
  27. package/adapters/mobile/inject.sh +91 -671
  28. package/adapters/mobile/launch-console-forwarder.cjs +39 -0
  29. package/adapters/mobile/open-device.sh +45 -7
  30. package/adapters/mobile/start-console-forwarder.sh +70 -0
  31. package/adapters/mobile/start-metro.sh +0 -41
  32. package/adapters/mobile/verify.sh +34 -83
  33. package/adapters/mobile/wait-for-bridge.sh +22 -8
  34. package/adapters/shared/harness-source-fingerprint.mjs +14 -12
  35. package/adapters/shared/resolve-slot-ports-core.mjs +17 -6
  36. package/adapters/shared/resolve-slot-ports.sh +17 -5
  37. package/adapters/shared/update-check-worker.mjs +43 -0
  38. package/bin/mm-harness +21 -2
  39. package/dist/adapters/core/surface.js +4 -1
  40. package/dist/adapters/extension/console-capture.js +95 -0
  41. package/dist/adapters/extension/product-config.js +110 -0
  42. package/dist/adapters/extension/runtime-decision.js +20 -2
  43. package/dist/adapters/extension/surface.js +22 -3
  44. package/dist/adapters/harness-freshness.js +49 -0
  45. package/dist/adapters/mobile/perps-env.js +43 -3
  46. package/dist/adapters/mobile/prepare.js +39 -7
  47. package/dist/adapters/mobile/surface.js +5 -2
  48. package/dist/adapters/resolve-slot-ports.js +2 -2
  49. package/dist/adapters/slot-ports.js +13 -16
  50. package/dist/adapters.js +56 -16
  51. package/dist/checkout-lock.js +27 -2
  52. package/dist/cli-color.js +19 -0
  53. package/dist/cli-commands.js +1 -1
  54. package/dist/cli.js +7 -18
  55. package/dist/command-contract.js +462 -0
  56. package/dist/command-journal.js +263 -0
  57. package/dist/commands/call.js +252 -57
  58. package/dist/commands/check.js +10 -4
  59. package/dist/commands/completion-candidates.js +17 -22
  60. package/dist/commands/device-target.js +27 -12
  61. package/dist/commands/doctor.js +106 -25
  62. package/dist/commands/fixtures.js +142 -35
  63. package/dist/commands/last.js +60 -0
  64. package/dist/commands/launch/extension.js +40 -17
  65. package/dist/commands/launch/index.js +207 -63
  66. package/dist/commands/launch/mobile.js +2 -0
  67. package/dist/commands/list-executables.js +187 -29
  68. package/dist/commands/logs.js +8 -6
  69. package/dist/commands/manifest.js +286 -35
  70. package/dist/commands/parse-args.js +68 -4
  71. package/dist/commands/provision.js +10 -4
  72. package/dist/commands/run-engine.js +772 -264
  73. package/dist/commands/run-report.js +12 -3
  74. package/dist/commands/run.js +378 -85
  75. package/dist/commands/shared.js +76 -4
  76. package/dist/commands/status-probe.js +4 -1
  77. package/dist/commands/status.js +2 -1
  78. package/dist/commands/stop.js +7 -2
  79. package/dist/commands/update.js +16 -28
  80. package/dist/completions-cache.js +1 -1
  81. package/dist/doctor.js +57 -10
  82. package/dist/harness.js +47 -137
  83. package/dist/heal-bounds.js +7 -2
  84. package/dist/json-stream.js +57 -0
  85. package/dist/live-adapter-contract.js +138 -32
  86. package/dist/manifest.js +161 -1
  87. package/dist/mm-harness-cli.js +146 -73
  88. package/dist/paths.js +2 -5
  89. package/dist/recipe-security.js +178 -0
  90. package/dist/run-diagnostics.js +261 -0
  91. package/dist/run-recording.js +1 -1
  92. package/dist/runner.js +140 -9
  93. package/docs/CONTRIBUTING.md +136 -0
  94. package/docs/QA.md +184 -0
  95. package/docs/RECIPES.md +122 -0
  96. package/docs/SECURITY.md +88 -0
  97. package/library/README.md +7 -3
  98. package/library/actions/core/perps/_controller.mjs +43 -56
  99. package/library/actions/core/perps/assert_orders.mjs +6 -7
  100. package/library/actions/core/perps/assert_positions.mjs +6 -7
  101. package/library/actions/core/perps/close_orders.mjs +2 -0
  102. package/library/actions/core/perps/close_positions.mjs +2 -0
  103. package/library/actions/core/perps/ensure_orders.mjs +4 -2
  104. package/library/actions/core/perps/ensure_positions.mjs +4 -2
  105. package/library/actions/core/perps/place_order.mjs +7 -3
  106. package/library/actions/core/perps/read_account.mjs +2 -2
  107. package/library/actions/core/perps/read_orders.mjs +2 -1
  108. package/library/actions/core/perps/read_positions.mjs +2 -1
  109. package/library/actions/core/wallet/list_accounts.mjs +95 -0
  110. package/library/actions/extension/perps/assert_orders.mjs +2 -1
  111. package/library/actions/extension/perps/assert_positions.mjs +2 -1
  112. package/library/actions/extension/perps/perps.mjs +43 -14
  113. package/library/actions/extension/platform/cdp.mjs +1 -0
  114. package/library/actions/extension/wallet/list_accounts.mjs +41 -0
  115. package/library/actions/mobile/perps/assert_orders.mjs +2 -1
  116. package/library/actions/mobile/perps/assert_positions.mjs +2 -1
  117. package/library/actions/mobile/perps/perps.mjs +40 -12
  118. package/library/actions/mobile/wallet/list_accounts.mjs +37 -0
  119. package/library/library.json +1 -1
  120. package/library/manifests/core.action-manifest.json +1214 -396
  121. package/library/manifests/extension.action-manifest.json +1635 -728
  122. package/library/manifests/mobile.action-manifest.json +1740 -727
  123. package/library/recipes/app/lifecycle.android-smoke.mobile.recipe.json +63 -81
  124. package/library/recipes/perps/clean-market-testnet.core.recipe.json +44 -0
  125. package/library/recipes/perps/clean-market-testnet.recipe.json +49 -0
  126. package/library/recipes/perps/lifecycle.recipe.json +136 -180
  127. package/library/recipes/perps/order-lifecycle.core.recipe.json +71 -67
  128. package/library/recipes/perps/performance.background-resume.mobile.recipe.json +51 -67
  129. package/library/recipes/perps/performance.cold-start.mobile.recipe.json +51 -67
  130. package/library/recipes/perps/performance.mobile.recipe.json +37 -51
  131. package/library/recipes/perps/performance.warm-start.mobile.recipe.json +44 -59
  132. package/library/recipes/perps/read-markets.core.recipe.json +29 -31
  133. package/library/recipes/perps/smoke.core.recipe.json +29 -32
  134. package/library/recipes/perps/smoke.extension.recipe.json +41 -44
  135. package/library/recipes/perps/smoke.mobile.recipe.json +42 -44
  136. package/library/recipes/perps/trading-lifecycle.core.recipe.json +69 -65
  137. package/library/recipes/runner/action-validation.extension.recipe.json +312 -398
  138. package/library/recipes/runner/action-validation.mobile.recipe.json +316 -402
  139. package/library/recipes/runner/smoke.core.recipe.json +25 -0
  140. package/library/recipes/runner/smoke.extension.recipe.json +23 -24
  141. package/library/recipes/runner/smoke.mobile.recipe.json +23 -24
  142. package/library/recipes/wallet/smoke.extension.recipe.json +40 -0
  143. package/library/recipes/wallet/smoke.mobile.recipe.json +40 -0
  144. package/package.json +12 -5
  145. package/scripts/completions.sh +8 -11
  146. package/scripts/validate-human-outcomes.mjs +169 -0
  147. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +0 -185
  148. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +0 -1662
  149. package/adapters/mobile/overlay/compat/README.md +0 -28
  150. package/adapters/mobile/overlay/compat/rn81-message-event-source.patch +0 -42
  151. package/adapters/shared/resolve-farmslot-ports-core.mjs +0 -3
  152. package/adapters/shared/resolve-farmslot-ports.mjs +0 -5
  153. package/adapters/shared/resolve-farmslot-ports.sh +0 -7
  154. package/dist/adapters/extension/harness-freshness.js +0 -39
  155. package/dist/commands/flows.js +0 -62
  156. package/docs/ADAPTER-SURFACE.md +0 -119
  157. package/docs/CHEATSHEET.md +0 -61
  158. package/docs/CLI-SPEC.md +0 -1098
  159. package/docs/CODE-MAP.md +0 -62
  160. package/docs/DEBUG-HANDOVER.md +0 -36
  161. package/docs/MENTAL-MODEL.md +0 -295
  162. package/docs/UX-PRINCIPLES.md +0 -64
  163. package/docs/architecture.md +0 -398
  164. package/docs/live-adapter-contract.md +0 -188
  165. package/docs/package-boundaries.md +0 -47
  166. package/docs/perps-flow-catalog.md +0 -235
  167. package/docs/recipe-libraries.md +0 -301
  168. package/docs/runtime-file-conventions.md +0 -36
  169. package/library/flows/perps.flows.json +0 -64
package/dist/harness.js CHANGED
@@ -4,13 +4,9 @@ import path from "node:path";
4
4
  import { resolveLeafInvoke, shellLeafMissing, missingShellLeafMessage } from "./leaf-invoke.js";
5
5
  import { resolveExtensionSlotPorts } from "./adapters/slot-ports.js";
6
6
  import { recipeHarnessPath, recipeRuntimeDir, runnerDir } from "./paths.js";
7
- import { prepareMobile } from "./adapters/mobile/prepare.js";
8
- const HARNESS_ACTIONS = ["install", "verify", "cleanup", "live"];
7
+ import { colorHumanMessage } from "./cli-color.js";
8
+ const HARNESS_ACTIONS = ["install", "verify", "cleanup"];
9
9
  const ADAPTERS = ["mobile", "extension", "core"];
10
- function isValidAdapterAction(adapter, action) {
11
- if (adapter === "core") return action === "install" || action === "verify" || action === "cleanup";
12
- return true;
13
- }
14
10
  function harnessUsage() {
15
11
  console.error(`mm-harness \u2014 install and validate the MetaMask recipe runtime in a checkout.
16
12
 
@@ -50,6 +46,12 @@ function failureHint(adapter, action) {
50
46
  const helper = adapter === "mobile" ? "mm-harness launch ios # or launch android" : "mm-harness launch";
51
47
  return `Read the error above for the specific cause. To (re)start the runtime, run: ${helper}`;
52
48
  }
49
+ function successNext(adapter, action, target) {
50
+ const base = `--adapter ${adapter} --target ${shellQuote(target)}`;
51
+ if (action === "install") return `mm-harness verify ${base}`;
52
+ if (action === "cleanup") return `mm-harness install ${base}`;
53
+ return `mm-harness status --target ${shellQuote(target)} --json`;
54
+ }
53
55
  function hasArg(args, needle) {
54
56
  return args.some((arg) => arg === needle || arg.startsWith(`${needle}=`));
55
57
  }
@@ -70,6 +72,7 @@ function parseHarnessArgs(args) {
70
72
  const forward = [];
71
73
  let adapter;
72
74
  let json = false;
75
+ let quiet = false;
73
76
  let separator = false;
74
77
  for (let i = 0; i < args.length; i += 1) {
75
78
  const arg = args[i];
@@ -85,6 +88,10 @@ function parseHarnessArgs(args) {
85
88
  json = true;
86
89
  continue;
87
90
  }
91
+ if (arg === "--quiet") {
92
+ quiet = true;
93
+ continue;
94
+ }
88
95
  if (arg === "--adapter" || arg === "--platform") {
89
96
  const value = args[i + 1];
90
97
  if (arg === "--adapter") {
@@ -115,7 +122,7 @@ function parseHarnessArgs(args) {
115
122
  }
116
123
  forward.push(arg);
117
124
  }
118
- return { adapter, json, forward };
125
+ return { adapter, json, quiet, forward };
119
126
  }
120
127
  const ADAPTER_DETECT_NEXT = "cd into a MetaMask checkout or pass --target <path>, or force it with --adapter <mobile|extension|core>";
121
128
  function detectAdapter(target) {
@@ -161,7 +168,7 @@ function readRuntimeContextField(contextPath, field) {
161
168
  if (node === void 0 || node === null || node === "") return void 0;
162
169
  return String(node);
163
170
  }
164
- function applyExtensionRuntimeEnv(target, action, args) {
171
+ function applyExtensionRuntimeEnv(target, args) {
165
172
  const contextPath = resolveRuntimeContextPath(target);
166
173
  const contextExists = fs.existsSync(contextPath);
167
174
  if (contextExists) {
@@ -174,19 +181,6 @@ function applyExtensionRuntimeEnv(target, action, args) {
174
181
  const extensionId = readRuntimeContextField(contextPath, "extensionId");
175
182
  if (extensionId) process.env.RECIPE_HARNESS_EXTENSION_ID = extensionId;
176
183
  }
177
- if (process.env.RECIPE_RUNTIME_START_APPROVED === void 0) {
178
- const approved = readRuntimeContextField(contextPath, "runtimeStart.approved");
179
- if (approved === "true" || approved === "True" || approved === "1") process.env.RECIPE_RUNTIME_START_APPROVED = "1";
180
- else if (approved === "false" || approved === "False" || approved === "0") process.env.RECIPE_RUNTIME_START_APPROVED = "0";
181
- }
182
- if (!process.env.RECIPE_RUNTIME_START_CMD) {
183
- const command = readRuntimeContextField(contextPath, "runtimeStart.command");
184
- if (command) process.env.RECIPE_RUNTIME_START_CMD = command;
185
- }
186
- if (!process.env.RECIPE_RUNTIME_READY_URL) {
187
- const readyUrl = readRuntimeContextField(contextPath, "runtimeStart.readyUrl");
188
- if (readyUrl) process.env.RECIPE_RUNTIME_READY_URL = readyUrl;
189
- }
190
184
  } else {
191
185
  delete process.env.RECIPE_RUNTIME_CONTEXT;
192
186
  }
@@ -200,11 +194,6 @@ function applyExtensionRuntimeEnv(target, action, args) {
200
194
  result = [...result, "--cdp-port", cdpPort];
201
195
  }
202
196
  }
203
- if (action === "live" && !hasArg(result, "--prepare-cmd")) {
204
- if (process.env.RECIPE_RUNTIME_START_APPROVED === "1" && process.env.RECIPE_RUNTIME_START_CMD) {
205
- result = [...result, "--prepare-cmd", process.env.RECIPE_RUNTIME_START_CMD];
206
- }
207
- }
208
197
  return result;
209
198
  }
210
199
  function resolveEntry(base, candidates, mode) {
@@ -281,81 +270,6 @@ Next: mm-harness install --platform core --target ${target}`
281
270
  );
282
271
  return { command, prefixArgs: [] };
283
272
  }
284
- async function handleMobileLive(target, forwardArgs, json, autoDetected) {
285
- const platform = argValue(forwardArgs, "--platform") ?? "ios";
286
- const watcherPortStr = argValue(forwardArgs, "--watcher-port") ?? process.env.WATCHER_PORT;
287
- const watcherPort = watcherPortStr ? parseInt(watcherPortStr, 10) : void 0;
288
- const start = Date.now();
289
- if (!json) {
290
- const detected = autoDetected ? ", auto-detected" : "";
291
- console.error(`\u2192 live (mobile${detected}) \u2014 target: ${target}`);
292
- }
293
- const prepResult = await prepareMobile(target, { platform, json, watcherPort });
294
- if (prepResult.status !== 0) {
295
- const elapsed2 = ((Date.now() - start) / 1e3).toFixed(1);
296
- if (json) {
297
- console.log(
298
- harnessSummary("live", "mobile", target, "fail", prepResult.status, autoDetected, {
299
- code: "MOBILE_PREPARE_FAILED",
300
- message: `mobile prepare failed (exit ${prepResult.status})`,
301
- userAction: failureHint("mobile", "live")
302
- })
303
- );
304
- } else {
305
- console.error(
306
- `\u2717 live mobile failed (exit ${prepResult.status}, ${elapsed2}s)
307
- ${failureHint("mobile", "live")}`
308
- );
309
- }
310
- return prepResult.status;
311
- }
312
- const installedVerify = path.join(recipeHarnessPath(target, "mobile"), "scripts", "verify.sh");
313
- const verifySh = fs.existsSync(installedVerify) ? installedVerify : resolveEntry(runnerDir, ["adapters/mobile/verify.sh", "scripts/mobile/verify.sh"], "file");
314
- const verifyArgs = hasArg(forwardArgs, "--no-auto-start") ? [...forwardArgs] : ["--no-auto-start", ...forwardArgs];
315
- if (shellLeafMissing(verifySh)) {
316
- const message = missingShellLeafMessage(verifySh);
317
- if (json) {
318
- console.log(
319
- harnessSummary("live", "mobile", target, "fail", 1, autoDetected, {
320
- code: "HARNESS_SPAWN_FAILED",
321
- message,
322
- userAction: failureHint("mobile", "live")
323
- })
324
- );
325
- } else {
326
- console.error(`\u2717 ${message}`);
327
- }
328
- return 1;
329
- }
330
- const { bin: verifyBin, args: verifySpawnArgs } = resolveLeafInvoke(verifySh, verifyArgs);
331
- const result = spawnSync(verifyBin, verifySpawnArgs, {
332
- stdio: json ? ["inherit", 2, "inherit"] : "inherit",
333
- env: process.env
334
- });
335
- const elapsed = ((Date.now() - start) / 1e3).toFixed(1);
336
- const exitCode = result.status ?? 1;
337
- if (json) {
338
- console.log(
339
- harnessSummary(
340
- "live",
341
- "mobile",
342
- target,
343
- exitCode === 0 ? "pass" : "fail",
344
- exitCode,
345
- autoDetected,
346
- exitCode === 0 ? void 0 : { code: "MOBILE_VERIFY_FAILED", message: `mobile live verify failed (exit ${exitCode})`, userAction: failureHint("mobile", "live") }
347
- )
348
- );
349
- } else if (exitCode === 0) {
350
- console.error(`\u2713 live mobile passed (${elapsed}s)`);
351
- } else {
352
- console.error(
353
- `\u2717 live mobile failed (exit ${exitCode}, ${elapsed}s)
354
- ${failureHint("mobile", "live")}`
355
- );
356
- }
357
- return exitCode;
358
- }
359
273
  async function handleHarness(argv) {
360
274
  const action = argv[0];
361
275
  if (!action || action === "-h" || action === "--help") {
@@ -368,12 +282,14 @@ async function handleHarness(argv) {
368
282
  return 2;
369
283
  }
370
284
  const harnessAction = action;
371
- const { adapter: parsedAdapter, json, forward } = parseHarnessArgs(argv.slice(1));
285
+ const { adapter: parsedAdapter, json, quiet, forward } = parseHarnessArgs(argv.slice(1));
372
286
  const rawTarget = argValue(forward, "--target");
373
287
  const target = path.resolve(rawTarget ?? process.cwd());
374
288
  const adapter = parsedAdapter ?? detectAdapter(target);
375
289
  if (!adapter || !isAdapter(adapter)) {
376
- if (json) {
290
+ if (quiet) {
291
+ return 2;
292
+ } else if (json) {
377
293
  const detectError = adapter ? { code: "UNSUPPORTED_PLATFORM", message: `unsupported platform: ${adapter}`, userAction: "pass --adapter <mobile|extension|core> to specify a supported adapter" } : { code: "ADAPTER_DETECTION_FAILED", message: `could not detect the MetaMask repo type for ${target}`, userAction: ADAPTER_DETECT_NEXT };
378
294
  console.log(harnessSummary(harnessAction, void 0, target, "fail", 2, false, detectError));
379
295
  } else {
@@ -387,36 +303,19 @@ async function handleHarness(argv) {
387
303
  }
388
304
  return 2;
389
305
  }
390
- if (!isValidAdapterAction(adapter, harnessAction)) {
391
- if (json) {
392
- console.log(
393
- harnessSummary(harnessAction, adapter, target, "fail", 2, parsedAdapter === void 0, {
394
- code: "UNSUPPORTED_ACTION",
395
- message: `${harnessAction} is not supported for ${adapter} (core supports install, verify, cleanup).`,
396
- userAction: "run mm-harness doctor to see which actions are supported for this adapter"
397
- })
398
- );
399
- } else {
400
- console.error(
401
- `\u2717 ${harnessAction} is not supported for ${adapter} (core supports install, verify, cleanup).`
402
- );
403
- }
404
- return 2;
405
- }
406
306
  const autoDetected = parsedAdapter === void 0;
407
307
  let forwardArgs = hasArg(forward, "--target") ? [...forward] : ["--target", target, ...forward];
408
308
  if (harnessAction === "install" && hasArg(forward, "--runway")) {
409
309
  return handleRunwayInstall(adapter, target, forward, json);
410
310
  }
411
- if (adapter === "extension" && (harnessAction === "live" || harnessAction === "verify")) {
412
- forwardArgs = applyExtensionRuntimeEnv(target, harnessAction, forwardArgs);
413
- }
414
- if (adapter === "mobile" && harnessAction === "live") {
415
- return handleMobileLive(target, forwardArgs, json, autoDetected);
311
+ if (adapter === "extension" && harnessAction === "verify") {
312
+ forwardArgs = applyExtensionRuntimeEnv(target, forwardArgs);
416
313
  }
417
314
  const dispatch = resolveHarnessDispatch(adapter, harnessAction, target);
418
315
  if ("error" in dispatch) {
419
- if (json) {
316
+ if (quiet) {
317
+ return 1;
318
+ } else if (json) {
420
319
  console.log(
421
320
  harnessSummary(harnessAction, adapter, target, "fail", 1, autoDetected, {
422
321
  code: "DISPATCH_UNAVAILABLE",
@@ -432,14 +331,16 @@ async function handleHarness(argv) {
432
331
  }
433
332
  return 1;
434
333
  }
435
- if (!json) {
334
+ if (!json && !quiet) {
436
335
  const detected = autoDetected ? ", auto-detected" : "";
437
- console.error(`\u2192 ${harnessAction} (${adapter}${detected}) \u2014 target: ${target}`);
336
+ console.error(colorHumanMessage(`\u2192 ${harnessAction} (${adapter}${detected}) \u2014 target: ${target}`));
438
337
  }
439
338
  const start = Date.now();
440
339
  if (shellLeafMissing(dispatch.command)) {
441
340
  const message = missingShellLeafMessage(dispatch.command);
442
- if (json) {
341
+ if (quiet) {
342
+ return 1;
343
+ } else if (json) {
443
344
  console.log(
444
345
  harnessSummary(harnessAction, adapter, target, "fail", 1, autoDetected, {
445
346
  code: "HARNESS_SPAWN_FAILED",
@@ -459,12 +360,14 @@ async function handleHarness(argv) {
459
360
  const result = spawnSync(dispatchBin, dispatchArgs, {
460
361
  // Default: stream child output verbatim (byte-identical to the skill path).
461
362
  // --json: route child output to our stderr so stdout carries only the summary.
462
- stdio: json ? ["inherit", 2, "inherit"] : "inherit",
363
+ stdio: quiet ? "ignore" : json ? ["inherit", 2, "inherit"] : "inherit",
463
364
  env: process.env
464
365
  });
465
366
  const seconds = ((Date.now() - start) / 1e3).toFixed(1);
466
367
  if (result.error) {
467
- if (json) {
368
+ if (quiet) {
369
+ return 1;
370
+ } else if (json) {
468
371
  console.log(
469
372
  harnessSummary(harnessAction, adapter, target, "fail", 1, autoDetected, {
470
373
  code: "HARNESS_SPAWN_FAILED",
@@ -479,7 +382,9 @@ async function handleHarness(argv) {
479
382
  return 1;
480
383
  }
481
384
  const exitCode = result.status ?? 1;
482
- if (json) {
385
+ if (quiet) {
386
+ return exitCode;
387
+ } else if (json) {
483
388
  console.log(
484
389
  harnessSummary(
485
390
  harnessAction,
@@ -492,10 +397,11 @@ async function handleHarness(argv) {
492
397
  )
493
398
  );
494
399
  } else if (exitCode === 0) {
495
- console.error(`\u2713 ${harnessAction} ${adapter} passed (${seconds}s)`);
400
+ console.error(colorHumanMessage(`\u2713 ${harnessAction} ${adapter} passed (${seconds}s)
401
+ Next: ${successNext(adapter, harnessAction, target)}`));
496
402
  } else {
497
- console.error(`\u2717 ${harnessAction} ${adapter} failed (exit ${exitCode}, ${seconds}s)
498
- ${failureHint(adapter, harnessAction)}`);
403
+ console.error(colorHumanMessage(`\u2717 ${harnessAction} ${adapter} failed (exit ${exitCode}, ${seconds}s)
404
+ ${failureHint(adapter, harnessAction)}`));
499
405
  }
500
406
  return exitCode;
501
407
  }
@@ -522,8 +428,10 @@ async function handleRunwayInstall(adapter, target, forward, json) {
522
428
  resolveOnly: hasArg(forward, "--resolve-only"),
523
429
  rerunCommand
524
430
  });
431
+ const next = result.status === "pass" && adapter === "mobile" && result.resolveOnly !== true ? `mm-harness launch ${String(result.platform ?? "ios")} --adapter mobile --target ${shellQuote(target)}` : void 0;
432
+ const output = next ? { ...result, next } : result;
525
433
  if (json) {
526
- console.log(JSON.stringify(result, null, 2));
434
+ console.log(JSON.stringify(output, null, 2));
527
435
  } else if (result.status === "pass") {
528
436
  const cache = typeof result.cache === "object" && result.cache ? result.cache : void 0;
529
437
  const simulator = typeof result.simulator === "object" && result.simulator ? result.simulator : void 0;
@@ -534,6 +442,7 @@ async function handleRunwayInstall(adapter, target, forward, json) {
534
442
  const action = result.skipped ? "already provisioned" : "installed Runway app";
535
443
  console.error(`\u2713 ${action} for ${adapter} ${result.platform ?? ""} simulator=${simulator?.name ?? "unknown"} cache=${cache?.status ?? "skip"}`);
536
444
  }
445
+ if (next) console.error(` Next: ${next}`);
537
446
  } else {
538
447
  console.error(`\u2717 mm-harness install --runway: ${result.error?.message ?? "runway install failed"}
539
448
  Next: ${result.error?.userAction ?? rerunCommand}`);
@@ -576,9 +485,10 @@ function harnessSummary(action, adapter, target, status, exitCode, autoDetected,
576
485
  status,
577
486
  exitCode,
578
487
  // Error contract: every --json failure carries a stable machine code + human
579
- // message (CLI-SPEC.md §5.1). userAction is included when present so callers
488
+ // message. userAction is included when present so callers
580
489
  // can surface the reachable escape without parsing the human message.
581
- ...status === "fail" && error ? { error } : {}
490
+ ...status === "fail" && error ? { error } : {},
491
+ ...status === "pass" && adapter ? { next: successNext(adapter, action, target) } : {}
582
492
  });
583
493
  }
584
494
  export {
@@ -7,6 +7,10 @@ import { EXIT } from "./commands/shared.js";
7
7
  function newHealState() {
8
8
  return { recovered: [], mutations: [], attemptedRecoveries: [] };
9
9
  }
10
+ function conciseFailureForHuman(output) {
11
+ const errors = output.split(/\r?\n/u).map((line) => /^(?:[A-Za-z]*Error):\s*(.+)$/u.exec(line.trim())?.[1]).filter((line) => Boolean(line));
12
+ return errors.at(-1) ?? output.trim();
13
+ }
10
14
  function parseHeal(options, fallback) {
11
15
  const value = options.heal;
12
16
  if (value === void 0) return fallback;
@@ -45,7 +49,7 @@ async function ensureOverlay(adapter, target, heal, state, json) {
45
49
  });
46
50
  code = result.status ?? 1;
47
51
  } else {
48
- code = await handleHarness(["install", "--platform", adapter, "--target", target, ...json ? ["--json"] : []]);
52
+ code = await handleHarness(["install", "--platform", adapter, "--target", target, "--quiet"]);
49
53
  }
50
54
  if (code !== 0 || !overlayPresent(target, adapter)) {
51
55
  return { ok: false, error: `runtime overlay install failed (exit ${code})` };
@@ -103,7 +107,7 @@ function checkHealBounds(target, output, state) {
103
107
  return {
104
108
  code: "SAME_RECOVERY_TWICE",
105
109
  exitCode: EXIT.bounded,
106
- message: "same recovery already failed once this invocation \u2014 refusing to loop.",
110
+ message: state.recovered.length > 0 ? "one recovery already succeeded this invocation \u2014 bounded policy refuses a second recovery." : "one recovery was already attempted this invocation \u2014 refusing to loop.",
107
111
  originalError
108
112
  };
109
113
  }
@@ -113,6 +117,7 @@ export {
113
117
  RECOVERY_CODE,
114
118
  checkHealBounds,
115
119
  classifyFailure,
120
+ conciseFailureForHuman,
116
121
  ensureOverlay,
117
122
  newHealState,
118
123
  parseHeal,
@@ -0,0 +1,57 @@
1
+ class JsonStreamWriter {
2
+ enabled;
3
+ command;
4
+ completed = false;
5
+ writeLine;
6
+ constructor(command, enabled, output = process.stdout) {
7
+ this.command = command;
8
+ this.enabled = enabled;
9
+ const write = output.write.bind(output);
10
+ this.writeLine = (line) => {
11
+ write(`${line}
12
+ `);
13
+ };
14
+ }
15
+ isolateStdout() {
16
+ if (!this.enabled) return () => void 0;
17
+ const original = process.stdout.write;
18
+ const redirect = ((chunk, encoding, callback) => process.stderr.write(chunk, encoding, callback));
19
+ process.stdout.write = redirect;
20
+ return () => {
21
+ if (process.stdout.write === redirect) process.stdout.write = original;
22
+ };
23
+ }
24
+ emit(event, fields = {}) {
25
+ if (!this.enabled || this.completed) return;
26
+ this.writeLine(JSON.stringify({
27
+ schemaVersion: 1,
28
+ command: this.command,
29
+ event,
30
+ ...fields,
31
+ ts: (/* @__PURE__ */ new Date()).toISOString()
32
+ }));
33
+ }
34
+ phase(phase, fields = {}) {
35
+ this.emit("phase", { phase, ...fields });
36
+ }
37
+ node(nodeId, action, status) {
38
+ this.emit("node", { nodeId, action, status });
39
+ }
40
+ mutation(mutation) {
41
+ this.emit("mutation", { mutation });
42
+ }
43
+ recovery(code) {
44
+ this.emit("recovery", { code });
45
+ }
46
+ error(error) {
47
+ this.emit("error", { error });
48
+ }
49
+ complete(status, exitCode, fields = {}) {
50
+ if (!this.enabled || this.completed) return;
51
+ this.emit("complete", { status, exitCode, ...fields });
52
+ this.completed = true;
53
+ }
54
+ }
55
+ export {
56
+ JsonStreamWriter
57
+ };
@@ -1,8 +1,12 @@
1
- import { access, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
1
+ import { createHash } from "node:crypto";
2
+ import { access, mkdtemp, readFile, realpath, rm, writeFile } from "node:fs/promises";
2
3
  import { existsSync, readFileSync } from "node:fs";
3
4
  import os from "node:os";
4
5
  import path from "node:path";
5
6
  import { spawn } from "node:child_process";
7
+ import { pathToFileURL } from "node:url";
8
+ import { build } from "esbuild";
9
+ import { init, parse } from "es-module-lexer";
6
10
  import { resolveRequiredLocalProtocolRoot, runnerDir } from "./paths.js";
7
11
  import { corePnpNodeOptions } from "./commands/core-readiness.js";
8
12
  function actionFileStem(action) {
@@ -31,13 +35,17 @@ function candidateFamilies(action) {
31
35
  const families = [family];
32
36
  return [...new Set(families)];
33
37
  }
34
- function candidatePaths(platform, action) {
38
+ function candidatePaths(platform, action, runtimeEnv = process.env) {
35
39
  const stems = candidateStems(action);
36
40
  const families = candidateFamilies(action);
37
- const roots = [
38
- process.env.METAMASK_RECIPE_LIVE_ADAPTER_DIR,
41
+ const declaredRoot = declaredActionRoot(
42
+ runtimeEnv.METAMASK_RECIPE_ACTION_SOURCE_MAP ?? process.env.METAMASK_RECIPE_ACTION_SOURCE_MAP,
43
+ action
44
+ );
45
+ const roots = [...new Set((declaredRoot ? [declaredRoot] : [
46
+ runtimeEnv.METAMASK_RECIPE_LIVE_ADAPTER_DIR ?? process.env.METAMASK_RECIPE_LIVE_ADAPTER_DIR,
39
47
  path.join(runnerDir, "library/actions")
40
- ].filter(Boolean);
48
+ ]).filter((entry) => Boolean(entry)))];
41
49
  const files = [];
42
50
  for (const root of roots) {
43
51
  for (const family of families) {
@@ -46,29 +54,26 @@ function candidatePaths(platform, action) {
46
54
  }
47
55
  pushDomainDispatcherFiles(files, root, platform, family);
48
56
  }
49
- for (const candidateStem of stems) {
50
- pushFlatCandidateFiles(files, root, platform, candidateStem);
51
- }
52
57
  }
53
58
  return files;
54
59
  }
55
- function pushCandidateFiles(files, root, platform, family, stem) {
56
- for (const extension of ["mjs", "js", "sh"]) {
57
- files.push(path.join(root, platform, family, `${stem}.${extension}`));
58
- files.push(path.join(root, "shared", family, `${stem}.${extension}`));
60
+ function declaredActionRoot(raw, action) {
61
+ if (!raw) return void 0;
62
+ try {
63
+ const parsed = JSON.parse(raw);
64
+ const root = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed[action] : void 0;
65
+ return typeof root === "string" && root.length > 0 ? root : void 0;
66
+ } catch {
67
+ return void 0;
59
68
  }
60
69
  }
61
- function pushDomainDispatcherFiles(files, root, platform, family) {
62
- for (const extension of ["mjs", "js", "sh"]) {
63
- files.push(path.join(root, platform, family, `${family}.${extension}`));
64
- files.push(path.join(root, "shared", family, `${family}.${extension}`));
65
- }
70
+ function pushCandidateFiles(files, root, platform, family, stem) {
71
+ files.push(path.join(root, platform, family, `${stem}.mjs`));
72
+ files.push(path.join(root, "shared", family, `${stem}.mjs`));
66
73
  }
67
- function pushFlatCandidateFiles(files, root, platform, stem) {
68
- for (const extension of ["mjs", "js", "sh"]) {
69
- files.push(path.join(root, platform, `${stem}.${extension}`));
70
- files.push(path.join(root, "shared", `${stem}.${extension}`));
71
- }
74
+ function pushDomainDispatcherFiles(files, root, platform, family) {
75
+ files.push(path.join(root, platform, family, `${family}.mjs`));
76
+ files.push(path.join(root, "shared", family, `${family}.mjs`));
72
77
  }
73
78
  async function firstExecutablePath(paths) {
74
79
  for (const file of paths) {
@@ -87,8 +92,16 @@ function runProcess(command, args, options) {
87
92
  const child = spawn(command, args, {
88
93
  cwd: options.cwd,
89
94
  env: options.env,
90
- stdio: ["ignore", "pipe", "pipe"]
95
+ stdio: [options.stdin == null ? "ignore" : "pipe", "pipe", "pipe"]
91
96
  });
97
+ if (options.stdin != null) {
98
+ child.stdin.on("error", (error) => {
99
+ if (error.code === "EPIPE" || settled) return;
100
+ settled = true;
101
+ reject(error);
102
+ });
103
+ child.stdin.end(options.stdin);
104
+ }
92
105
  let stdout = "";
93
106
  let stderr = "";
94
107
  child.stdout.on("data", (chunk) => {
@@ -119,10 +132,82 @@ function runProcess(command, args, options) {
119
132
  });
120
133
  });
121
134
  }
135
+ async function prepareLiveAdapterScript({
136
+ platform,
137
+ action,
138
+ implementationRoot,
139
+ allowRuntimeImports = false
140
+ }) {
141
+ const entryPath = await firstExecutablePath(
142
+ candidatePaths(platform, action, {
143
+ METAMASK_RECIPE_ACTION_SOURCE_MAP: JSON.stringify({ [action]: implementationRoot })
144
+ })
145
+ );
146
+ if (!entryPath) return null;
147
+ const rootReal = await realpath(implementationRoot);
148
+ const entryReal = await realpath(entryPath);
149
+ if (!isPathWithin(rootReal, entryReal)) {
150
+ throw new Error(`Live adapter ${entryPath} resolves outside ${implementationRoot}.`);
151
+ }
152
+ const result = await build({
153
+ entryPoints: [entryReal],
154
+ bundle: true,
155
+ write: false,
156
+ platform: "node",
157
+ format: "esm",
158
+ target: `node${process.versions.node.split(".")[0]}`,
159
+ legalComments: "none",
160
+ sourcemap: false,
161
+ banner: { js: `process.argv[1] = ${JSON.stringify(entryReal)};` },
162
+ plugins: [bindImportMetaUrls()],
163
+ logLevel: "silent"
164
+ });
165
+ const output = result.outputFiles[0];
166
+ if (!output) throw new Error(`Bundling live adapter ${entryReal} produced no executable output.`);
167
+ const sourceText = output.text;
168
+ await init;
169
+ const [imports] = parse(sourceText);
170
+ if (!allowRuntimeImports && imports.some((entry) => entry.d >= 0)) {
171
+ const error = new Error(
172
+ `Live adapter ${entryReal} contains a runtime dynamic import that cannot be bound to the approved bundle. Replace it with a static import so its implementation bytes are included in the plan digest.`
173
+ );
174
+ Object.assign(error, {
175
+ code: "RECIPE_TRUST_UNBOUND_IMPORT",
176
+ userAction: "replace the computed import with a static import, or promote the adapter into an explicitly trusted engineer-owned library"
177
+ });
178
+ throw error;
179
+ }
180
+ return {
181
+ action,
182
+ platform,
183
+ entryPath: entryReal,
184
+ sourceText,
185
+ sourceDigest: `sha256:${createHash("sha256").update(sourceText).digest("hex")}`
186
+ };
187
+ }
188
+ function bindImportMetaUrls() {
189
+ return {
190
+ name: "bind-import-meta-urls",
191
+ setup(buildContext) {
192
+ buildContext.onLoad({ filter: /\.[cm]?[jt]sx?$/ }, async (args) => {
193
+ const contents = await readFile(args.path, "utf8");
194
+ if (!contents.includes("import.meta.url")) return void 0;
195
+ const extension = path.extname(args.path);
196
+ const loader = extension === ".ts" || extension === ".mts" || extension === ".cts" ? "ts" : extension === ".tsx" ? "tsx" : extension === ".jsx" ? "jsx" : "js";
197
+ return {
198
+ contents: contents.replaceAll(
199
+ "import.meta.url",
200
+ JSON.stringify(pathToFileURL(args.path).href)
201
+ ),
202
+ loader
203
+ };
204
+ });
205
+ }
206
+ };
207
+ }
122
208
  function commandFor(file, projectRoot, platform) {
123
- if (file.endsWith(".sh")) return { command: "bash", args: [file] };
124
209
  const needsTsx = platform === "core" || importsSourceTypescript(file);
125
- if (!needsTsx && (file.endsWith(".mjs") || file.endsWith(".js"))) {
210
+ if (!needsTsx) {
126
211
  return { command: process.execPath, args: [file] };
127
212
  }
128
213
  const tsxBin = resolveTsxBin(projectRoot);
@@ -165,15 +250,15 @@ function importsSourceTypescriptFrom(file, visited) {
165
250
  const specifier = match[1] ?? "";
166
251
  if (specifier.endsWith(".ts")) return true;
167
252
  if (!specifier.startsWith(".")) continue;
168
- if (!specifier.endsWith(".mjs") && !specifier.endsWith(".js")) continue;
253
+ if (!specifier.endsWith(".mjs")) continue;
169
254
  if (importsSourceTypescriptFrom(path.resolve(path.dirname(absolute), specifier), visited)) {
170
255
  return true;
171
256
  }
172
257
  }
173
258
  return false;
174
259
  }
175
- async function resolveLiveAdapter(platform, action) {
176
- return firstExecutablePath(candidatePaths(platform, action));
260
+ async function resolveLiveAdapter(platform, action, runtimeEnv) {
261
+ return firstExecutablePath(candidatePaths(platform, action, runtimeEnv));
177
262
  }
178
263
  const CORE_WORKSPACE_PACKAGES = [
179
264
  "@metamask/base-controller",
@@ -205,8 +290,8 @@ async function platformAdapterEnv(platform, projectRoot, tempDir) {
205
290
  NODE_OPTIONS: corePnpNodeOptions(projectRoot, process.env.NODE_OPTIONS)
206
291
  };
207
292
  }
208
- async function runLiveAdapterScript({ platform, action, node, context }) {
209
- const script = await resolveLiveAdapter(platform, action);
293
+ async function runLiveAdapterScript({ platform, action, node, context, prepared }) {
294
+ const script = prepared?.entryPath ?? await resolveLiveAdapter(platform, action, context?.env);
210
295
  if (!script) return null;
211
296
  const tempDir = await mkdtemp(path.join(os.tmpdir(), "mm-harness-live-adapter-"));
212
297
  const inputPath = path.join(tempDir, "input.json");
@@ -225,7 +310,7 @@ async function runLiveAdapterScript({ platform, action, node, context }) {
225
310
  };
226
311
  await writeFile(inputPath, `${JSON.stringify(input, null, 2)}
227
312
  `);
228
- const command = commandFor(script, context.projectRoot, platform);
313
+ const command = prepared ? commandForPrepared(context.projectRoot, platform) : commandFor(script, context.projectRoot, platform);
229
314
  const platformEnv = await platformAdapterEnv(platform, context.projectRoot, tempDir);
230
315
  const result = await runProcess(command.command, [...command.args, inputPath], {
231
316
  cwd: context.projectRoot,
@@ -236,7 +321,8 @@ async function runLiveAdapterScript({ platform, action, node, context }) {
236
321
  METAMASK_RECIPE_ADAPTER_INPUT: inputPath,
237
322
  METAMASK_RECIPE_ADAPTER_OUTPUT: outputPath
238
323
  },
239
- timeoutMs: Number(node.live_adapter_timeout_ms ?? node.timeout_ms ?? 6e4)
324
+ timeoutMs: Number(node.live_adapter_timeout_ms ?? node.timeout_ms ?? 6e4),
325
+ ...prepared ? { stdin: prepared.sourceText } : {}
240
326
  });
241
327
  try {
242
328
  if (result.timedOut) {
@@ -258,7 +344,27 @@ async function runLiveAdapterScript({ platform, action, node, context }) {
258
344
  await rm(tempDir, { recursive: true, force: true });
259
345
  }
260
346
  }
347
+ function commandForPrepared(projectRoot, platform) {
348
+ if (platform !== "core") {
349
+ return { command: process.execPath, args: ["--input-type=module", "-"] };
350
+ }
351
+ const tsxBin = resolveTsxBin(projectRoot);
352
+ if (!tsxBin) {
353
+ const error = new Error(
354
+ `this action imports TypeScript from the checkout but no tsx runtime was found.
355
+ Next: run 'yarn install' in ${projectRoot}`
356
+ );
357
+ error.exitCode = 2;
358
+ throw error;
359
+ }
360
+ return { command: tsxBin, args: ["--input-type=module", "-"] };
361
+ }
362
+ function isPathWithin(root, candidate) {
363
+ const relative = path.relative(root, candidate);
364
+ return relative === "" || relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
365
+ }
261
366
  export {
367
+ prepareLiveAdapterScript,
262
368
  resolveLiveAdapter,
263
369
  runLiveAdapterScript
264
370
  };