@deeeed/metamask-harness 0.74.0 → 0.76.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 (96) hide show
  1. package/CHANGELOG.md +39 -1
  2. package/README.md +1 -1
  3. package/adapters/shared/ensure-runner-deps.sh +15 -3
  4. package/adapters/shared/reap-checkout-metros.sh +9 -0
  5. package/dist/action-values.js +15 -0
  6. package/dist/adapters/core/actions.js +20 -0
  7. package/dist/adapters/core/live-script-env.js +36 -0
  8. package/dist/adapters/core/overlay.js +34 -0
  9. package/dist/adapters/core/readiness.js +8 -0
  10. package/dist/adapters/core/run.js +54 -0
  11. package/dist/adapters/core/surface.js +12 -1
  12. package/dist/adapters/extension/action-inputs.js +206 -0
  13. package/dist/adapters/extension/actions.js +51 -0
  14. package/dist/adapters/extension/console-capture.js +13 -1
  15. package/dist/adapters/extension/doctor-checks.js +135 -0
  16. package/dist/adapters/extension/fixtures.js +103 -0
  17. package/dist/adapters/extension/launch.js +315 -0
  18. package/dist/adapters/extension/launched-browser.js +51 -0
  19. package/dist/adapters/extension/overlay.js +55 -0
  20. package/dist/adapters/extension/readiness.js +32 -0
  21. package/dist/adapters/extension/reload.js +40 -0
  22. package/dist/adapters/extension/run.js +105 -0
  23. package/dist/adapters/extension/surface.js +31 -3
  24. package/dist/adapters/harness-freshness.js +4 -2
  25. package/dist/adapters/mobile/actions.js +54 -0
  26. package/dist/adapters/mobile/devices.js +21 -0
  27. package/dist/adapters/mobile/doctor-checks.js +40 -0
  28. package/dist/adapters/mobile/fixtures.js +141 -0
  29. package/dist/adapters/mobile/launch.js +192 -0
  30. package/dist/adapters/mobile/lifecycle.js +300 -0
  31. package/dist/adapters/mobile/overlay.js +27 -0
  32. package/dist/adapters/mobile/prepare-target.js +38 -0
  33. package/dist/adapters/mobile/readiness.js +39 -0
  34. package/dist/adapters/mobile/reload.js +15 -0
  35. package/dist/adapters/mobile/run.js +156 -0
  36. package/dist/adapters/mobile/runtime-identity.js +34 -0
  37. package/dist/adapters/mobile/surface.js +39 -3
  38. package/dist/adapters/mobile/ui.js +701 -0
  39. package/dist/adapters/performance/cdp-trace.js +24 -6
  40. package/dist/adapters/performance/device-platform.js +10 -0
  41. package/dist/adapters/registry.js +1 -1
  42. package/dist/adapters/terminal/actions.js +43 -0
  43. package/dist/adapters/terminal/console-capture.js +14 -1
  44. package/dist/adapters/terminal/doctor-checks.js +22 -0
  45. package/dist/adapters/terminal/overlay.js +18 -0
  46. package/dist/adapters/terminal/readiness.js +11 -0
  47. package/dist/adapters/terminal/run.js +30 -0
  48. package/dist/adapters/terminal/surface.js +21 -3
  49. package/dist/adapters.js +32 -824
  50. package/dist/cli-commands.js +1 -0
  51. package/dist/cli.js +3 -1
  52. package/dist/command-contract.js +3 -0
  53. package/dist/commands/call.js +2 -5
  54. package/dist/commands/debug.js +6 -25
  55. package/dist/commands/device-target.js +2 -1
  56. package/dist/commands/doctor.js +26 -137
  57. package/dist/commands/fixtures.js +13 -217
  58. package/dist/commands/launch/device.js +2 -6
  59. package/dist/commands/launch/extension.js +3 -303
  60. package/dist/commands/launch/index.js +1 -1
  61. package/dist/commands/launch/mobile.js +2 -190
  62. package/dist/commands/launch/quote.js +7 -0
  63. package/dist/commands/logs.js +1 -1
  64. package/dist/commands/parse-args.js +9 -4
  65. package/dist/commands/pin.js +226 -0
  66. package/dist/commands/prepare.js +23 -53
  67. package/dist/commands/provision.js +3 -7
  68. package/dist/commands/reload.js +8 -47
  69. package/dist/commands/run-engine.js +38 -138
  70. package/dist/commands/run-report.js +4 -48
  71. package/dist/commands/run.js +6 -48
  72. package/dist/commands/runtime-decision.js +5 -27
  73. package/dist/commands/self-test.js +1 -3
  74. package/dist/commands/status.js +20 -28
  75. package/dist/commands/stop.js +1 -1
  76. package/dist/doctor.js +12 -120
  77. package/dist/execution-provenance.js +3 -2
  78. package/dist/harness-pin.js +10 -0
  79. package/dist/harness.js +31 -100
  80. package/dist/heal-bounds.js +3 -2
  81. package/dist/live-adapter-contract.js +8 -34
  82. package/dist/manifest.js +3 -5
  83. package/dist/metamask-action-validation.js +5 -196
  84. package/dist/mm-harness-cli.js +30 -4
  85. package/dist/network-observation.js +10 -3
  86. package/dist/paths.js +8 -2
  87. package/dist/performance-observation.js +15 -22
  88. package/dist/pr-body/render.js +3 -1
  89. package/dist/recording-target.js +4 -39
  90. package/dist/run-diagnostics.js +21 -60
  91. package/dist/run-recording.js +5 -4
  92. package/dist/runner.js +20 -368
  93. package/dist/runtime-context.js +12 -17
  94. package/docs/CONTRIBUTING.md +29 -7
  95. package/package.json +2 -1
  96. package/scripts/completions.sh +2 -1
@@ -1,8 +1,7 @@
1
1
  import { detectAdapter } from "../harness.js";
2
2
  import { assertAdapter } from "../paths.js";
3
3
  import { getAdapterSurface } from "../adapters/surface.js";
4
- import { clearMobileReleaseArtifactState } from "../adapters/mobile/release-artifact-state.js";
5
- import { ADAPTER_DETECT_NEXT, usageOut, writeInteractiveProgress } from "./shared.js";
4
+ import { ADAPTER_DETECT_NEXT, usageOut } from "./shared.js";
6
5
  import {
7
6
  applyWatcherPortOption,
8
7
  optionFlag,
@@ -27,10 +26,7 @@ async function handleProvision({ positional, options, rawArgv }) {
27
26
  const surface = getAdapterSurface(adapter);
28
27
  const rerunCommand = provisionRerunCommand(rawArgv, options, adapter, target);
29
28
  const platform = optionString(options, "platform") ?? optionString(options, "devicePlatform") ?? (positional[0] === "runway" ? positional[1] : positional[0]) ?? "ios";
30
- if (adapter === "mobile") {
31
- writeInteractiveProgress(json, `\u2192 provision mobile ${platform} \u2014 resolving simulator and Runway artifact`);
32
- if (!optionFlag(options, "resolveOnly")) clearMobileReleaseArtifactState(target);
33
- }
29
+ surface.runwayProvision.before?.(target, { json, platform, resolveOnly: optionFlag(options, "resolveOnly") });
34
30
  const result = await surface.runwayProvision.run(target, {
35
31
  json,
36
32
  platform,
@@ -50,7 +46,7 @@ async function handleProvision({ positional, options, rawArgv }) {
50
46
  });
51
47
  const provisionedSimulator = typeof result.simulator === "object" && result.simulator ? result.simulator : void 0;
52
48
  const provisionedDevice = typeof provisionedSimulator?.udid === "string" ? provisionedSimulator.udid : typeof provisionedSimulator?.name === "string" ? provisionedSimulator.name : void 0;
53
- const next = result.status === "pass" && adapter === "mobile" && result.resolveOnly !== true ? `mm-harness launch ${String(result.platform ?? platform)} --adapter mobile --target ${shellQuote(target)}` + (provisionedDevice ? ` --device ${shellQuoteArg(provisionedDevice)}` : "") : void 0;
49
+ const next = result.status === "pass" && surface.runwayProvision.launchNext && result.resolveOnly !== true ? `mm-harness launch ${String(result.platform ?? platform)} --adapter ${adapter} --target ${shellQuote(target)}` + (provisionedDevice ? ` --device ${shellQuoteArg(provisionedDevice)}` : "") : void 0;
54
50
  const output = next ? { ...result, next } : result;
55
51
  if (json) {
56
52
  console.log(JSON.stringify(output, null, 2));
@@ -1,13 +1,9 @@
1
- import path from "node:path";
2
- import { runnerDir } from "../paths.js";
3
1
  import { getAdapterSurface } from "../adapters/surface.js";
4
2
  import {
5
3
  ADAPTER_DETECT_NEXT,
6
- EXIT,
7
4
  flag,
8
5
  parseFlags,
9
6
  resolveAdapter,
10
- spawnScript,
11
7
  targetOf,
12
8
  usageOut
13
9
  } from "./shared.js";
@@ -25,55 +21,20 @@ async function handleReload(argv) {
25
21
  ADAPTER_DETECT_NEXT
26
22
  );
27
23
  }
28
- if (adapter === "core") {
24
+ const surface = getAdapterSurface(adapter);
25
+ if (surface.headless) {
29
26
  return usageOut(
30
27
  json,
31
28
  "reload",
32
- "core is headless; there is no app runtime to reload.",
33
- "mm-harness run <core-recipe>"
29
+ `${adapter} is headless; there is no app runtime to reload.`,
30
+ `mm-harness run <${adapter}-recipe>`
34
31
  );
35
32
  }
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;
33
+ if (!surface.reload) {
34
+ return usageOut(json, "reload", `reload does not support the ${adapter} adapter.`, surface.hints.launch);
69
35
  }
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;
36
+ surface.resolveSlotPorts(target);
37
+ return surface.reload(target, json);
77
38
  }
78
39
  export {
79
40
  handleReload
@@ -14,9 +14,6 @@ import {
14
14
  import { resolveActionManifest, validateManifest } from "../manifest.js";
15
15
  import { getAdapterSurface } from "../adapters/surface.js";
16
16
  import { extensionProductConfigBlock } from "../adapters/extension/product-config.js";
17
- import {
18
- requireCurrentMobileReleaseArtifactState
19
- } from "../adapters/mobile/release-artifact-state.js";
20
17
  import {
21
18
  importRecipeHarness,
22
19
  importRecipeProtocol,
@@ -121,13 +118,8 @@ async function runRecipe(adapter, recipe, artifactsDir, projectRoot, actionManif
121
118
  );
122
119
  }
123
120
  } finally {
124
- if (adapter === "mobile") {
125
- const { hideMobileHudOnTeardown } = await import("../adapters.js");
126
- await hideMobileHudOnTeardown(
127
- projectRoot,
128
- recipeRunEnv(adapter, runtimeOptions)
129
- );
130
- }
121
+ const teardown = getAdapterSurface(adapter).run?.teardown;
122
+ if (teardown) await teardown(projectRoot, recipeRunEnv(adapter, runtimeOptions));
131
123
  }
132
124
  let recordingError;
133
125
  if (executionError === void 0 && result) {
@@ -240,11 +232,12 @@ async function resolveRecipeExecution(adapter, recipe, artifactsDir, projectRoot
240
232
  projectRoot,
241
233
  rootRecipe: recipeDocument
242
234
  });
243
- const suppressHudForOpaqueMobile = adapter === "mobile" && process.env.METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME === "1";
235
+ const surface = getAdapterSurface(adapter);
236
+ const suppressAutoHud = surface.run?.autoHud?.() === false;
244
237
  const runnerOptions = {
245
238
  quietStdout: runtimeOptions.stdoutIsMachineContract === true,
246
239
  suppressLibraryResolutionLogs: runtimeOptions.suppressLibraryResolutionLogs,
247
- autoHud: suppressHudForOpaqueMobile ? false : trust.source && trust.source.trust !== "trusted" ? false : runtimeOptions.autoHud,
240
+ autoHud: suppressAutoHud ? false : trust.source && trust.source.trust !== "trusted" ? false : runtimeOptions.autoHud,
248
241
  onActionEvent: runtimeOptions.onActionEvent,
249
242
  actionSources,
250
243
  // A direct engineer invocation is the explicit trust boundary for a
@@ -256,7 +249,7 @@ async function resolveRecipeExecution(adapter, recipe, artifactsDir, projectRoot
256
249
  ...runnerOptions,
257
250
  trustedMutation
258
251
  });
259
- const useFramedExtensionRecording = adapter === "extension" && recordVideo === "full-run" && trust.source?.trust !== "untrusted" && trust.source?.trust !== "unknown" && captureHelperSupportsRecordSessionSnapshots(projectRoot);
252
+ const useFramedExtensionRecording = surface.recording?.framed !== void 0 && recordVideo === "full-run" && trust.source?.trust !== "untrusted" && trust.source?.trust !== "unknown" && captureHelperSupportsRecordSessionSnapshots(projectRoot);
260
253
  const runRequest = {
261
254
  ...recipeDocument !== void 0 ? { recipeDocument } : absoluteRecipePath ? { recipePath: absoluteRecipePath } : { recipeDocument: recipe },
262
255
  artifactsDir: absoluteArtifactsDir,
@@ -447,31 +440,22 @@ function executionLibrarySources(sources, invocationTrust) {
447
440
  };
448
441
  });
449
442
  }
443
+ const RUN_ENV_KEYS = [
444
+ "CDP_PORT",
445
+ "RECIPE_CDP_PORT",
446
+ "WATCHER_PORT",
447
+ "METRO_PORT",
448
+ "METAMASK_RECIPE_EXTENSION_AUTOLAUNCH"
449
+ ];
450
450
  function activateRecipeRuntimeEnvironment(adapter, projectRoot, runtimeOptions) {
451
- const previousCdpPort = process.env.CDP_PORT;
452
- const previousRecipeCdpPort = process.env.RECIPE_CDP_PORT;
453
- const previousWatcherPort = process.env.WATCHER_PORT;
454
- const previousMetroPort = process.env.METRO_PORT;
455
- const previousExtensionAutolaunch = process.env.METAMASK_RECIPE_EXTENSION_AUTOLAUNCH;
456
- const previousMobileOpaqueRuntime = process.env.METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME;
457
- const previousAndroidPackageId = process.env.ANDROID_PACKAGE_ID;
458
- const previousAdbSerial = process.env.ADB_SERIAL;
459
- const previousAndroidSerial = process.env.ANDROID_SERIAL;
460
- const explicitMobileDeviceEnv = adapter === "mobile" && process.env.MM_HARNESS_EXPLICIT_PLATFORM ? Object.fromEntries(
461
- [
462
- "PLATFORM",
463
- "MM_HARNESS_EXPLICIT_PLATFORM",
464
- "IOS_SIMULATOR",
465
- "SIM_UDID",
466
- "ADB_SERIAL",
467
- "ANDROID_SERIAL",
468
- "ANDROID_DEVICE",
469
- "ANDROID_TARGET_DEVICE_NAME"
470
- ].map((key) => [key, process.env[key]])
471
- ) : void 0;
472
- getAdapterSurface(adapter).resolveSlotPorts(projectRoot);
473
- if (explicitMobileDeviceEnv) {
474
- for (const [key, value] of Object.entries(explicitMobileDeviceEnv)) {
451
+ const surface = getAdapterSurface(adapter);
452
+ const previousEnv = Object.fromEntries(
453
+ [...RUN_ENV_KEYS, ...surface.run?.envKeys ?? []].map((key) => [key, process.env[key]])
454
+ );
455
+ const pinnedEnv = surface.run?.pinnedEnv?.();
456
+ surface.resolveSlotPorts(projectRoot);
457
+ if (pinnedEnv) {
458
+ for (const [key, value] of Object.entries(pinnedEnv)) {
475
459
  restoreEnv(key, value);
476
460
  }
477
461
  }
@@ -486,37 +470,9 @@ function activateRecipeRuntimeEnvironment(adapter, projectRoot, runtimeOptions)
486
470
  if (runtimeOptions.launchExistingDist) {
487
471
  process.env.METAMASK_RECIPE_EXTENSION_AUTOLAUNCH = "1";
488
472
  }
489
- if (adapter === "mobile") {
490
- const state = requireCurrentMobileReleaseArtifactState(projectRoot);
491
- if (state) {
492
- const selected = process.env.ADB_SERIAL ?? process.env.ANDROID_SERIAL;
493
- if (selected && selected !== state.deviceSerial) {
494
- throw new Error(
495
- `The Mobile release artifact is installed on ${state.deviceSerial}, not ${selected}. Next: rerun runtime-launch for ${selected}.`
496
- );
497
- }
498
- process.env.METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME = "1";
499
- process.env.ANDROID_PACKAGE_ID = state.packageId;
500
- process.env.ADB_SERIAL = state.deviceSerial;
501
- process.env.ANDROID_SERIAL = state.deviceSerial;
502
- }
503
- }
473
+ surface.run?.activateEnv?.(projectRoot);
504
474
  return () => {
505
- restoreEnv("CDP_PORT", previousCdpPort);
506
- restoreEnv("RECIPE_CDP_PORT", previousRecipeCdpPort);
507
- restoreEnv("WATCHER_PORT", previousWatcherPort);
508
- restoreEnv("METRO_PORT", previousMetroPort);
509
- restoreEnv(
510
- "METAMASK_RECIPE_EXTENSION_AUTOLAUNCH",
511
- previousExtensionAutolaunch
512
- );
513
- restoreEnv(
514
- "METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME",
515
- previousMobileOpaqueRuntime
516
- );
517
- restoreEnv("ANDROID_PACKAGE_ID", previousAndroidPackageId);
518
- restoreEnv("ADB_SERIAL", previousAdbSerial);
519
- restoreEnv("ANDROID_SERIAL", previousAndroidSerial);
475
+ for (const [key, value] of Object.entries(previousEnv)) restoreEnv(key, value);
520
476
  };
521
477
  }
522
478
  function actionSourceMap(sources) {
@@ -539,42 +495,11 @@ function recipeRunEnv(adapter, runtimeOptions = {}) {
539
495
  PLATFORM: process.env.PLATFORM,
540
496
  MM_HARNESS_EXPLICIT_PLATFORM: process.env.MM_HARNESS_EXPLICIT_PLATFORM
541
497
  };
542
- if (adapter !== "mobile") return base;
543
- return {
544
- ...base,
545
- WATCHER_PORT: process.env.WATCHER_PORT ?? base.CDP_PORT ?? base.RECIPE_CDP_PORT,
546
- METRO_PORT: process.env.METRO_PORT ?? process.env.WATCHER_PORT ?? base.CDP_PORT ?? base.RECIPE_CDP_PORT,
547
- IOS_SIMULATOR: process.env.IOS_SIMULATOR,
548
- SIM_UDID: process.env.SIM_UDID,
549
- ANDROID_DEVICE: process.env.ANDROID_DEVICE,
550
- ANDROID_TARGET_DEVICE_NAME: process.env.ANDROID_TARGET_DEVICE_NAME,
551
- ADB_SERIAL: process.env.ADB_SERIAL,
552
- ANDROID_SERIAL: process.env.ANDROID_SERIAL,
553
- ANDROID_PACKAGE_ID: process.env.ANDROID_PACKAGE_ID,
554
- METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME: process.env.METAMASK_RECIPE_MOBILE_OPAQUE_RUNTIME
555
- };
498
+ const childEnv = getAdapterSurface(adapter).run?.childEnv;
499
+ return childEnv ? childEnv(base) : base;
556
500
  }
557
501
  async function prepareRuntimeIfNeeded(adapter, projectRoot, runtimeOptions) {
558
- if (adapter !== "extension" || runtimeOptions.skipExtensionRuntimePrepare === true)
559
- return;
560
- const { releaseArtifactState, runtimeDistCheck } = await import("../adapters/extension/runtime-decision.js");
561
- const artifact = releaseArtifactState(projectRoot);
562
- const runtimeSnapshot = runtimeDistCheck(projectRoot, artifact);
563
- if (artifact.status !== "none") {
564
- if (artifact.status === "valid" && runtimeSnapshot.status === "fresh") return;
565
- throw new Error(
566
- "The loaded release artifact identity is invalid or stale. Next: rerun runtime-launch with the same artifact source."
567
- );
568
- }
569
- const { prepareExtensionRuntime } = await import("../adapters/extension/runtime.js");
570
- await prepareExtensionRuntime({
571
- projectRoot,
572
- cdpPort: runtimeOptions.cdpPort,
573
- slot: runtimeOptions.slot,
574
- launchExistingDist: runtimeOptions.launchExistingDist === true,
575
- validationRuntimeDir: runtimeOptions.validationRuntimeDir,
576
- pageMode: "home-or-sidepanel"
577
- });
502
+ await getAdapterSurface(adapter).run?.prepareRuntime?.(projectRoot, runtimeOptions);
578
503
  }
579
504
  function restoreEnv(key, value) {
580
505
  if (value === void 0) delete process.env[key];
@@ -1051,7 +976,15 @@ async function prepareHeal(adapter, target, options, json, prepareOptions = {})
1051
976
  return EXIT.usage;
1052
977
  }
1053
978
  const state = newHealState();
1054
- const mobileArtifact = adapter === "mobile" ? requireCurrentMobileReleaseArtifactState(target) : null;
979
+ const runtimeCheck = getAdapterSurface(adapter).run?.runtimeCheck?.({
980
+ target,
981
+ options,
982
+ heal,
983
+ state,
984
+ json,
985
+ appRestartAuthored: prepareOptions.appRestartAuthored,
986
+ onRecovery: (code) => prepareOptions.onPhase?.("recover", { code })
987
+ });
1055
988
  prepareOptions.onPhase?.("install");
1056
989
  const ensured = await ensureOverlay(
1057
990
  adapter,
@@ -1089,41 +1022,8 @@ async function prepareHeal(adapter, target, options, json, prepareOptions = {})
1089
1022
  return EXIT.infra;
1090
1023
  }
1091
1024
  prepareOptions.onPhase?.("healthcheck");
1092
- if (adapter === "extension") {
1093
- const previousCdpPort = process.env.CDP_PORT;
1094
- const previousRecipeCdpPort = process.env.RECIPE_CDP_PORT;
1095
- const previousWatcherPort = process.env.WATCHER_PORT;
1096
- const explicitCdpPort = optionString(options, "cdpPort");
1097
- const explicitWatcherPort = optionString(options, "watcherPort") ?? optionString(options, "metroPort");
1098
- if (explicitCdpPort) {
1099
- process.env.CDP_PORT = explicitCdpPort;
1100
- process.env.RECIPE_CDP_PORT = explicitCdpPort;
1101
- }
1102
- if (explicitWatcherPort) process.env.WATCHER_PORT = explicitWatcherPort;
1103
- try {
1104
- const current = await ensureExtensionProofRuntime(
1105
- target,
1106
- heal,
1107
- state,
1108
- json,
1109
- {
1110
- onRecovery: (code) => prepareOptions.onPhase?.("recover", { code })
1111
- }
1112
- );
1113
- if (current !== null) return current;
1114
- } finally {
1115
- restoreEnv("CDP_PORT", previousCdpPort);
1116
- restoreEnv("RECIPE_CDP_PORT", previousRecipeCdpPort);
1117
- restoreEnv("WATCHER_PORT", previousWatcherPort);
1118
- }
1119
- }
1120
- if (adapter === "mobile" && mobileArtifact === null && prepareOptions.skipMobileSourceFreshness !== true) {
1121
- const current = await ensureMobileProofRuntime(
1122
- target,
1123
- heal,
1124
- state,
1125
- json
1126
- );
1025
+ if (runtimeCheck) {
1026
+ const current = await runtimeCheck();
1127
1027
  if (current !== null) return current;
1128
1028
  }
1129
1029
  return { state, heal };
@@ -1372,7 +1272,7 @@ async function executeWithHealBounds(exec, target, state) {
1372
1272
  };
1373
1273
  }
1374
1274
  function emitHealViolation(json, command, result, violation, state, adapter) {
1375
- const userAction = adapter === "core" && violation.code === "WALLET_STATE_REQUIRED" ? "for signing, configure a reserved wallet-fixture.json account and pass its non-empty name; an explicit 0x address is supported only by read-only actions" : violation.userAction ?? `inspect ${shellQuoteArg(result.summaryPath)} and ${shellQuoteArg(result.tracePath)}; fix the application or recipe failure before retrying`;
1275
+ const userAction = (adapter ? getAdapterSurface(adapter).run?.violationUserAction?.(violation) : void 0) ?? violation.userAction ?? `inspect ${shellQuoteArg(result.summaryPath)} and ${shellQuoteArg(result.tracePath)}; fix the application or recipe failure before retrying`;
1376
1276
  if (json) {
1377
1277
  console.log(
1378
1278
  JSON.stringify(
@@ -1,8 +1,7 @@
1
1
  import fs from "node:fs";
2
- import { createRequire } from "node:module";
3
2
  import path from "node:path";
4
3
  import { execFileSync } from "node:child_process";
5
- import { recipeRuntimeDir } from "../paths.js";
4
+ import { getAdapterSurface } from "../adapters/surface.js";
6
5
  import { isRecord } from "./parse-args.js";
7
6
  function writeRunReport(result) {
8
7
  const summaryPath = String(result.summaryPath);
@@ -95,33 +94,6 @@ function indexProductProvenanceArtifact(artifactManifestPath, target, adapter, b
95
94
  `
96
95
  );
97
96
  }
98
- function readRecord(file) {
99
- try {
100
- const recorded = JSON.parse(fs.readFileSync(file, "utf8"));
101
- return isRecord(recorded) ? recorded : null;
102
- } catch {
103
- return null;
104
- }
105
- }
106
- const browserCdp = createRequire(import.meta.url)("../../adapters/shared/browser-cdp.cjs");
107
- function verifyLaunch(record, cdpPort) {
108
- const launch = isRecord(record.launch) ? record.launch : {};
109
- const pid = Number(launch.pid);
110
- const port = Number(launch.cdpPort);
111
- if (!Number.isInteger(pid) || !Number.isInteger(port) || typeof launch.profile !== "string" || typeof launch.processStartedAt !== "string") {
112
- return "the browser record has no process identity (pid, start time, profile, CDP port)";
113
- }
114
- if (cdpPort && port !== Number(cdpPort)) return `the browser record is for CDP port ${port}, not ${cdpPort}`;
115
- try {
116
- const owner = browserCdp.cdpOwner(port, launch.profile);
117
- if (owner.pid !== pid || owner.startedAt !== launch.processStartedAt) {
118
- return `CDP port ${port} is now held by another browser process than the recorded pid ${pid}`;
119
- }
120
- } catch (error) {
121
- return error instanceof Error ? error.message : String(error);
122
- }
123
- return null;
124
- }
125
97
  function resolveTemplatedPort(value, params, recipe) {
126
98
  let resolved = value;
127
99
  const template = /^\{\{\s*params\.([\w-]+)\s*\}\}$/u.exec(String(value).trim());
@@ -155,26 +127,11 @@ function recipeCdpPorts(request) {
155
127
  return [...ports];
156
128
  }
157
129
  function executedBrowser(adapter, target, artifactManifestPath, ports) {
158
- if (adapter !== "extension") return null;
130
+ const launched = getAdapterSurface(adapter).run?.launchedBrowser;
131
+ if (!launched) return null;
159
132
  if (ports.length === 0) return { boundTo: "none", reason: "the run had no known CDP port" };
160
133
  if (ports.length > 1) return { boundTo: "none", reason: `the run drove several CDP ports (${ports.join(", ")})` };
161
- return launchedBrowser(target, path.dirname(artifactManifestPath), ports[0]);
162
- }
163
- function launchedBrowser(target, artifactsDir, cdpPort) {
164
- const runtime = readRecord(path.join(artifactsDir, "extension-runtime", "runtime.json"));
165
- const autolaunched = runtime && isRecord(runtime.browser) ? runtime.browser : null;
166
- const candidates = [];
167
- if (autolaunched) candidates.push([autolaunched, "run-autolaunch"]);
168
- const slot = readRecord(path.join(path.resolve(target), recipeRuntimeDir(), "browser-resolution.json"));
169
- if (slot) candidates.push([slot, "slot-runtime"]);
170
- if (candidates.length === 0) return null;
171
- const reasons = [];
172
- for (const [record, boundTo] of candidates) {
173
- const problem = verifyLaunch(record, cdpPort);
174
- if (problem === null) return { ...record, boundTo };
175
- reasons.push(`${boundTo}: ${problem}`);
176
- }
177
- return { boundTo: "none", reason: reasons.join("; ") };
134
+ return launched(target, path.dirname(artifactManifestPath), ports[0]);
178
135
  }
179
136
  function renderRunReport(summary, entries) {
180
137
  const lines = [
@@ -279,7 +236,6 @@ function indexRunReportArtifact(artifactManifestPath) {
279
236
  export {
280
237
  executedBrowser,
281
238
  indexProductProvenanceArtifact,
282
- launchedBrowser,
283
239
  recipeCdpPorts,
284
240
  writeRunReport
285
241
  };
@@ -28,8 +28,6 @@ import {
28
28
  import { handleDescribeRecipe, handleListExecutables } from "./list-executables.js";
29
29
  import { applyDeviceTargeting } from "./device-target.js";
30
30
  import { getAdapterSurface } from "../adapters/surface.js";
31
- import { coreDependencyBlock } from "./core-readiness.js";
32
- import { readMobileReleaseArtifactState } from "../adapters/mobile/release-artifact-state.js";
33
31
  import {
34
32
  indexProductProvenanceArtifact,
35
33
  writeRunReport
@@ -244,7 +242,10 @@ async function handleRunInner({ positional, options }, stream) {
244
242
  actionLibraryContextArgs(validated.librarySources)
245
243
  );
246
244
  }
247
- const depsBlock = adapter === "core" && await recipeUsesCoreController(validated.recipe, validated.librarySources) ? coreDependencyBlock(target) : null;
245
+ const depsBlock = await getAdapterSurface(adapter).run?.dependencyBlock?.(target, {
246
+ recipe: validated.recipe,
247
+ librarySources: validated.librarySources
248
+ }) ?? null;
248
249
  if (depsBlock) {
249
250
  return emitRunUsageError(jsonOutput, stream, adapter, validated.recipeFile, depsBlock.code, depsBlock.message, depsBlock.userAction);
250
251
  }
@@ -295,8 +296,7 @@ async function handleRunInner({ positional, options }, stream) {
295
296
  }
296
297
  try {
297
298
  const prepared = await prepareHeal(adapter, target, options, machine, {
298
- onPhase: (phase, fields) => stream.phase(phase, fields),
299
- skipMobileSourceFreshness: adapter === "mobile" && readMobileReleaseArtifactState(target) !== null
299
+ onPhase: (phase, fields) => stream.phase(phase, fields)
300
300
  });
301
301
  if (typeof prepared === "number") {
302
302
  stream.error({
@@ -511,48 +511,6 @@ function formatPreviewLine(line, out) {
511
511
  const rest = match[2];
512
512
  return `${out(status === "PASS" ? "ok" : "err", status)} ${rest}`;
513
513
  }
514
- async function recipeUsesCoreController(recipe, librarySources) {
515
- if (objectUsesCoreController(recipe)) return true;
516
- const refs = collectCallRefs(recipe);
517
- if (refs.length === 0) return false;
518
- const sources = librarySources ?? [];
519
- if (sources.length === 0) return false;
520
- const { loadRecipeLibraries } = await import("@farmslot/recipe-harness");
521
- const recipes = (await loadRecipeLibraries(sources, { adapter: "core" })).recipes;
522
- const visited = /* @__PURE__ */ new Set();
523
- const visit = (ref) => {
524
- if (visited.has(ref)) return false;
525
- visited.add(ref);
526
- const dependency = recipes.get(ref);
527
- if (!dependency) return false;
528
- if (objectUsesCoreController(dependency.document)) return true;
529
- return collectCallRefs(dependency.document).some(visit);
530
- };
531
- return refs.some(visit);
532
- }
533
- function objectUsesCoreController(value) {
534
- if (Array.isArray(value)) return value.some(objectUsesCoreController);
535
- if (!isRecord(value)) return false;
536
- if (actionUsesCoreController(value.action)) return true;
537
- return Object.values(value).some(objectUsesCoreController);
538
- }
539
- function collectCallRefs(value) {
540
- const refs = [];
541
- const visit = (node) => {
542
- if (Array.isArray(node)) {
543
- for (const item of node) visit(item);
544
- return;
545
- }
546
- if (!isRecord(node)) return;
547
- if (node.action === "call" && typeof node.ref === "string" && node.ref) refs.push(node.ref);
548
- for (const child of Object.values(node)) visit(child);
549
- };
550
- visit(value);
551
- return [...new Set(refs)];
552
- }
553
- function actionUsesCoreController(action) {
554
- return typeof action === "string" && action.startsWith("metamask.perps.");
555
- }
556
514
  async function handleRunPlan(recipeArg, params, options, stream) {
557
515
  const json = optionFlag(options, "json");
558
516
  const jsonOutput = json && !stream.enabled;
@@ -635,7 +593,7 @@ async function handleRunPlan(recipeArg, params, options, stream) {
635
593
  status: "planned",
636
594
  detail: "would auto-ensure the runtime overlay if missing (install phase)"
637
595
  },
638
- ...adapter === "core" ? [] : [
596
+ ...getAdapterSurface(adapter).headless ? [] : [
639
597
  {
640
598
  step: "launch.app",
641
599
  confidence: "conditional",
@@ -1,32 +1,16 @@
1
- import { decideExtensionReadiness } from "../adapters/extension/runtime-decision.js";
2
- import { decideMobileReadiness } from "../adapters/mobile/runtime-decision.js";
1
+ import { getAdapterSurface } from "../adapters/surface.js";
3
2
  import {
4
3
  adapterOption,
5
4
  applyRuntimeDirOption,
6
5
  optionFlag,
7
- optionString,
8
- parsePort,
9
6
  targetPath
10
7
  } from "./parse-args.js";
11
8
  async function handleRuntimeDecision({ options }) {
12
9
  applyRuntimeDirOption(options);
13
10
  const adapter = adapterOption(options);
14
11
  const target = targetPath(options);
15
- if (adapter === "mobile") {
16
- const watcherPortRaw = optionString(options, "watcherPort") ?? process.env.WATCHER_PORT ?? process.env.METRO_PORT;
17
- const watcherPort = watcherPortRaw === void 0 ? void 0 : parsePort(watcherPortRaw, "runtime-decision --watcher-port must be a port.");
18
- const report2 = await decideMobileReadiness(target, {
19
- watcherPort,
20
- metroLog: optionString(options, "metroLog"),
21
- platform: optionString(options, "platform") ?? process.env.PLATFORM ?? process.env.RECIPE_HARNESS_PLATFORM,
22
- record: optionFlag(options, "record"),
23
- preflightMode: optionString(options, "preflightMode")
24
- });
25
- if (optionFlag(options, "json")) console.log(JSON.stringify(report2, null, 2));
26
- else console.log(`${report2.decision} ${report2.reasonCode} \u2014 ${report2.reasons[0] ?? ""}`);
27
- return 0;
28
- }
29
- if (adapter !== "extension") {
12
+ const decide = getAdapterSurface(adapter).runtimeDecision;
13
+ if (!decide) {
30
14
  const report2 = {
31
15
  schemaVersion: 1,
32
16
  adapter,
@@ -42,15 +26,9 @@ async function handleRuntimeDecision({ options }) {
42
26
  else console.log(`unknown adapter-unsupported \u2014 ${report2.reasons[0]}`);
43
27
  return 0;
44
28
  }
45
- const cdpPortRaw = optionString(options, "cdpPort") ?? process.env.CDP_PORT ?? process.env.RECIPE_CDP_PORT;
46
- const cdpPort = cdpPortRaw === void 0 ? void 0 : parsePort(cdpPortRaw, "runtime-decision --cdp-port must be a port.");
47
- const report = await decideExtensionReadiness(target, {
48
- cdpPort,
49
- watchLog: optionString(options, "watchLog"),
50
- record: optionFlag(options, "record")
51
- });
29
+ const { report, line } = await decide(target, options);
52
30
  if (optionFlag(options, "json")) console.log(JSON.stringify(report, null, 2));
53
- else console.log(`${report.decision}${report.clean ? " (clean)" : ""} ${report.reasonCode} \u2014 ${report.reasons[0] ?? ""}`);
31
+ else console.log(line);
54
32
  return 0;
55
33
  }
56
34
  export {
@@ -25,9 +25,7 @@ async function runSelfTest(options) {
25
25
  for (const adapter of ["mobile", "extension"]) {
26
26
  const manifest = loadActionManifest(adapter);
27
27
  const manifestValidation = await validateManifest(manifest);
28
- const smokeRecipe = recipePath(
29
- adapter === "mobile" ? "mobile/runner/smoke.recipe.json" : "extension/runner/smoke.recipe.json"
30
- );
28
+ const smokeRecipe = recipePath(`${adapter}/runner/smoke.recipe.json`);
31
29
  const artifactsDir = path.join(root, adapter);
32
30
  const result = await runRecipe(adapter, smokeRecipe, artifactsDir, runnerDir, void 0, {
33
31
  skipExtensionRuntimePrepare: true