@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
@@ -19,6 +19,7 @@ const SPEC = {
19
19
  { name: "ports", desc: "Slot ports and runtime paths", flags: ["--json"] },
20
20
  { name: "up", desc: "Decide + run minimum work to reach ready", flags: ["--json", "--dry-run"] },
21
21
  { name: "sync", desc: "Refresh harness + canonicalize wallet fixture", flags: ["--json"] },
22
+ { name: "pin", desc: "Copy this install into an immutable store for a task to run", flags: ["--json"] },
22
23
  { name: "logs", aliases: ["tail"], desc: "Compact build events or full log", flags: ["--full", "-f", "--window", "--events", "--source", "--json"] },
23
24
  { name: "debug", aliases: ["devtools", "inspect"], desc: "Open DevTools UI", flags: ["--json", "--no-open"] },
24
25
  { name: "reload", desc: "Reload the connected app runtime", flags: ["--json"] },
package/dist/cli.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { adapterRegistry } from "./adapters/registry.js";
1
2
  import { detectAdapter, handleHarness } from "./harness.js";
2
3
  import { handleActions } from "./commands/manifest.js";
3
4
  import { handleDoctor } from "./commands/doctor.js";
@@ -108,7 +109,8 @@ async function main(argv) {
108
109
  const { options } = parseArgs(argv.slice(1), command);
109
110
  const target = targetPath(options);
110
111
  const adapterRaw = (typeof options["adapter"] === "string" ? options["adapter"] : void 0) ?? (typeof options["platform"] === "string" ? options["platform"] : void 0) ?? detectAdapter(target);
111
- if (adapterRaw === "mobile" || adapterRaw === "extension" || adapterRaw === "core" || adapterRaw === "terminal") {
112
+ await import("./adapters/surface.js");
113
+ if (adapterRaw && adapterRegistry.has(adapterRaw)) {
112
114
  await handleHarness(["install", "--platform", adapterRaw, "--target", target, "--quiet"]);
113
115
  }
114
116
  } catch {
@@ -398,6 +398,9 @@ const PUBLIC_COMMAND_CONTRACTS = {
398
398
  update: {
399
399
  options: options(HELP, JSON, { "--check": bool() })
400
400
  },
401
+ pin: {
402
+ options: options(HELP, JSON)
403
+ },
401
404
  fixtures: {
402
405
  options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, DEVICE, {
403
406
  "--from": value(),
@@ -28,7 +28,6 @@ import {
28
28
  import { getAdapterSurface } from "../adapters/surface.js";
29
29
  import { handleListExecutables } from "./list-executables.js";
30
30
  import { applyDeviceTargeting } from "./device-target.js";
31
- import { coreDependencyBlock } from "./core-readiness.js";
32
31
  import {
33
32
  emitHealViolation,
34
33
  executeWithHealBounds,
@@ -40,7 +39,6 @@ import {
40
39
  synthesizeOneNodeRecipe,
41
40
  validateRecipeAdapterAware
42
41
  } from "./run-engine.js";
43
- import { readMobileReleaseArtifactState } from "../adapters/mobile/release-artifact-state.js";
44
42
  import { acquireCheckoutLock } from "../checkout-lock.js";
45
43
  import { formatRunDiagnosticsForHuman, readRunDiagnosticsDocument } from "../run-diagnostics.js";
46
44
  import {
@@ -178,7 +176,7 @@ async function handleCall(argv) {
178
176
  const resolvedAction = resolution.resolved;
179
177
  const describedAction = describeManifestActions(manifest, actionSources).find((entry) => entry.name === resolvedAction);
180
178
  const defaultsUsed = actionDefaultsUsed(describedAction, args);
181
- const depsBlock = adapter === "core" && resolvedAction.startsWith("metamask.perps.") ? coreDependencyBlock(target) : null;
179
+ const depsBlock = await getAdapterSurface(adapter).run?.dependencyBlock?.(target, { action: resolvedAction }) ?? null;
182
180
  if (depsBlock) {
183
181
  if (json) {
184
182
  console.log(JSON.stringify({ schemaVersion: 1, command: "call", adapter, status: "fail", exitCode: EXIT.usage, error: depsBlock }, null, 2));
@@ -286,9 +284,8 @@ async function handleCall(argv) {
286
284
  return checkoutBusyOut(json, "call", lock.message, lock.path);
287
285
  }
288
286
  try {
289
- const authoredMobileRestart = adapter === "mobile" && resolvedAction === "app.lifecycle" && args.command === "restart";
290
287
  const prepared = await prepareHeal(adapter, target, options, json, {
291
- skipMobileSourceFreshness: authoredMobileRestart || adapter === "mobile" && readMobileReleaseArtifactState(target) !== null
288
+ appRestartAuthored: resolvedAction === "app.lifecycle" && args.command === "restart"
292
289
  });
293
290
  if (typeof prepared === "number") return prepared;
294
291
  const { state } = prepared;
@@ -10,36 +10,17 @@ async function handleDebug(argv) {
10
10
  const adapter = resolveAdapter(options, target);
11
11
  if (!adapter) return usageOut(json, "debug", `could not detect the MetaMask repo type for ${target}`, ADAPTER_DETECT_NEXT);
12
12
  const surface = getAdapterSurface(adapter);
13
- if (surface.headless) return usageOut(json, "debug", "core is headless; there is no debug console.", surface.hints.relaunch);
13
+ if (surface.headless) return usageOut(json, "debug", `${adapter} is headless; there is no debug console.`, surface.hints.relaunch);
14
14
  surface.resolveSlotPorts(target);
15
15
  const worker = flag(options, "worker");
16
16
  const devMenu = flag(options, "devMenu");
17
17
  const noOpen = flag(options, "noOpen");
18
- if (adapter === "mobile" && worker) {
19
- return usageOut(
20
- json,
21
- "debug",
22
- "--worker is extension-only (service-worker DevTools). Use --dev-menu on mobile.",
23
- "mm-harness debug --dev-menu"
24
- );
25
- }
26
- if (adapter === "extension" && devMenu) {
27
- return usageOut(
28
- json,
29
- "debug",
30
- "--dev-menu is mobile-only (RN developer menu). Use --worker on the extension.",
31
- "mm-harness debug --worker"
32
- );
33
- }
18
+ const debug = surface.debug;
19
+ if (!debug) return usageOut(json, "debug", `${adapter} has no debug console.`, surface.hints.relaunch);
20
+ const refused = debug.refuse?.({ worker, devMenu });
21
+ if (refused) return usageOut(json, "debug", refused.message, refused.userAction);
34
22
  const openDebug = path.join(runnerDir, "adapters/shared/open-debug.mjs");
35
- const debugArgs = ["--adapter", adapter];
36
- if (adapter === "extension") {
37
- if (process.env.CDP_PORT) debugArgs.push("--cdp-port", process.env.CDP_PORT);
38
- debugArgs.push("--target", worker ? "worker" : "page");
39
- } else {
40
- if (process.env.WATCHER_PORT) debugArgs.push("--port", process.env.WATCHER_PORT);
41
- debugArgs.push("--action", devMenu ? "dev-menu" : "debug");
42
- }
23
+ const debugArgs = ["--adapter", adapter, ...debug.args({ worker, devMenu })];
43
24
  if (json) debugArgs.push("--json");
44
25
  if (noOpen) debugArgs.push("--no-open");
45
26
  const result = spawnScript(process.execPath, [openDebug, ...debugArgs], target, json);
@@ -4,6 +4,7 @@ import {
4
4
  findAvailableIosSimulatorById,
5
5
  listConnectedDevices
6
6
  } from "../devices.js";
7
+ import { getAdapterSurface } from "../adapters/surface.js";
7
8
  import { persistExplicitDevicePin } from "../runtime-context.js";
8
9
  import { optionString, shellQuoteArg, targetPath } from "./parse-args.js";
9
10
  const DEVICE_SELECT_HINT = "Pass --device <name|udid|serial> to select one target. Do not shut down other connected devices to force selection.";
@@ -97,7 +98,7 @@ function deviceRecovery(options, rerun) {
97
98
  function applyDeviceTargeting(command, adapter, options, opts) {
98
99
  const device = optionString(options, "device");
99
100
  const platformPreference = mobilePlatformPreference(options);
100
- if (adapter !== "mobile") {
101
+ if (!getAdapterSurface(adapter).readiness.devices) {
101
102
  if (device !== void 0) {
102
103
  return {
103
104
  ok: false,
@@ -1,28 +1,16 @@
1
1
  import { execFileSync } from "node:child_process";
2
- import { randomUUID } from "node:crypto";
3
2
  import fs from "node:fs";
4
3
  import path from "node:path";
5
- import { readExtensionPinnedFlags } from "../adapters/extension/feature-flags.js";
6
4
  import { featureFlagReport } from "../feature-flags.js";
7
5
  import { color } from "../cli-color.js";
8
6
  import { collectCheckoutView, renderCheckoutView } from "../checkout-view.js";
9
7
  import { createDoctorReport, fixtureSummary, requiredDoctorCheckSummary } from "../doctor.js";
10
- import { terminalAppPort, terminalCdpPort } from "../adapters/terminal/surface.js";
11
8
  import { detectAdapter, resolveRuntimeContextPath } from "../harness.js";
12
9
  import { assertAdapter, runnerDir } from "../paths.js";
13
10
  import { getAdapterSurface } from "../adapters/surface.js";
14
- import { extensionProductConfigBlock } from "../adapters/extension/product-config.js";
15
- import { ensureMobilePerpsEnvironment, mobilePerpsEnvironment } from "../adapters/mobile/perps-env.js";
16
11
  import { loadActionManifest, validateManifest } from "../manifest.js";
17
12
  import { ensureOverlay, newHealState, recipeRunning } from "../heal-bounds.js";
18
13
  import { applyDeviceTargeting } from "./device-target.js";
19
- import {
20
- mobileDeviceView,
21
- mobileDeviceLiveView,
22
- renderAdditionalReachableDevices,
23
- renderMobileDeviceList,
24
- renderMobileLiveBlock
25
- } from "./mobile-device-view.js";
26
14
  import { farmslotReadyIndicator } from "./farmslot-ready.js";
27
15
  import { updateStatus } from "./update.js";
28
16
  import { ensureRuntimeContext } from "../runtime-context.js";
@@ -63,6 +51,7 @@ async function handleDoctor({ options }) {
63
51
  return usageOut(json, "doctor", `could not detect the MetaMask repo type for ${target}`, ADAPTER_DETECT_NEXT);
64
52
  }
65
53
  assertAdapter(adapter);
54
+ const { readiness } = getAdapterSurface(adapter);
66
55
  if (!fs.existsSync(target)) {
67
56
  return usageOut(
68
57
  json,
@@ -110,10 +99,9 @@ async function handleDoctor({ options }) {
110
99
  runtimeProbeError2 = error2 instanceof Error ? error2.message : String(error2);
111
100
  }
112
101
  const ready = runtime2?.decision === "ready";
113
- if (adapter === "extension" && runtime2?.reasonCode === "product-config-required") {
114
- failed.push("extension-product-config");
115
- }
116
- const nextActions = doctorFixNextActions(failed, result2.fixture, adapter, target);
102
+ const block = readiness.runtimeBlock?.(target, runtime2);
103
+ if (block) failed.push(block.id);
104
+ const nextActions = doctorFixNextActions(failed, result2.fixture, adapter, target, block);
117
105
  if (failed.length === 0 && !ready && runtime2?.nextAction) nextActions.push(runtime2.nextAction);
118
106
  const status = result2.status === "pass" && failed.length === 0 ? "pass" : "fail";
119
107
  const retryDoctor = `mm-harness doctor --fix --adapter ${adapter} --target ${shellQuote(target)} --json`;
@@ -136,13 +124,12 @@ async function handleDoctor({ options }) {
136
124
  }
137
125
  return status === "pass" ? 0 : 1;
138
126
  }
139
- const mobilePlatform = adapter === "mobile" && (platformOption === "ios" || platformOption === "android") ? platformOption : void 0;
140
127
  const result = createDoctorReport(
141
128
  adapter,
142
129
  target,
143
130
  manifestValidation,
144
131
  actionManifestPath,
145
- mobilePlatform
132
+ platformOption
146
133
  );
147
134
  const view = collectCheckoutView(target, adapter);
148
135
  const missingRequiredTool = result.checks.find(
@@ -183,7 +170,7 @@ async function handleDoctor({ options }) {
183
170
  const checks = [
184
171
  ...result.checks,
185
172
  runtimeCheck,
186
- ...adapter === "terminal" ? await terminalDoctorChecks(target) : []
173
+ ...await readiness.liveChecks?.(target) ?? []
187
174
  ];
188
175
  const requiredChecks = requiredDoctorCheckSummary(checks);
189
176
  const doctorResult = {
@@ -192,10 +179,11 @@ async function handleDoctor({ options }) {
192
179
  checks,
193
180
  requiredChecks
194
181
  };
195
- const orphanMetros = adapter === "mobile" ? detectOrphanMetros(target, process.env.WATCHER_PORT) : [];
196
- const capture = captureHealth(adapter);
197
- const deviceView = adapter === "mobile" ? mobileDeviceView(allDevices) : null;
198
- const liveView = deviceView ? await mobileDeviceLiveView(target, deviceView) : null;
182
+ const orphanMetros = readiness.orphanDevServers?.(target, process.env.WATCHER_PORT) ?? [];
183
+ const capture = captureHealth(readiness.captureProviders);
184
+ const deviceSource = await readiness.devices?.();
185
+ const deviceView = deviceSource ? deviceSource.view(allDevices) : null;
186
+ const liveView = deviceSource && deviceView ? await deviceSource.live(target, deviceView) : null;
199
187
  const devices = liveView?.devicesWithLive ?? deviceView?.devices ?? [];
200
188
  const additionalReachableDevices = liveView?.additionalReachableDevices ?? [];
201
189
  const discovery = deviceView?.deviceDiscoveryErrors.length ? { deviceDiscoveryErrors: deviceView.deviceDiscoveryErrors } : {};
@@ -216,8 +204,8 @@ async function handleDoctor({ options }) {
216
204
  }
217
205
  const skills = skillsView(target);
218
206
  const update = updateStatus();
219
- const extensionFlags = adapter === "extension" ? readExtensionPinnedFlags(target) : null;
220
- const featureFlags = extensionFlags ? featureFlagReport(extensionFlags.overrides, extensionFlags) : featureFlagReport(liveView?.featureFlagOverrides ?? null);
207
+ const pinnedFlags = readiness.pinnedFlags?.(target) ?? null;
208
+ const featureFlags = pinnedFlags ? featureFlagReport(pinnedFlags.overrides, pinnedFlags) : featureFlagReport(liveView?.featureFlagOverrides ?? null);
221
209
  const doctorUserAction = doctorResult.checks.find(
222
210
  (check) => check.required && check.status === "fail" && check.userAction
223
211
  )?.userAction ?? `mm-harness doctor --fix --adapter ${adapter} --target ${shellQuote(target)} --json`;
@@ -249,12 +237,7 @@ async function handleDoctor({ options }) {
249
237
  featureFlags.error || (featureFlags.overrideCount ?? 0) > 0 ? "warn" : "dim",
250
238
  featureFlags.summary.replace(/^feature flags: /u, "")
251
239
  )}`);
252
- if (adapter === "mobile") {
253
- const perps = mobilePerpsEnvironment(target);
254
- console.log(
255
- `mobile-perps: ${perps.status} (${perps.file}; MM_PERPS_ENABLED=${perps.perpsEnabled.value ?? "missing"}; OVERRIDE_REMOTE_FEATURE_FLAGS=${perps.remoteFeatureFlagOverride.value ?? "missing"})`
256
- );
257
- }
240
+ for (const line of readiness.lines?.(target) ?? []) console.log(line);
258
241
  if (orphanMetros.length > 0) {
259
242
  console.log(
260
243
  `${out("warn", "orphan Metro:")} ${orphanMetros.length} leaked bundler(s) for this checkout (pids ${orphanMetros.join(", ")})`
@@ -264,33 +247,15 @@ async function handleDoctor({ options }) {
264
247
  if (capture) renderCaptureHealth(capture, out);
265
248
  renderSkillsView(skills, out);
266
249
  renderUpdateStatus(update, out);
267
- if (adapter === "mobile") {
268
- if (deviceView) renderMobileDeviceList(deviceView, out);
269
- if (additionalReachableDevices.length > 0) renderAdditionalReachableDevices(additionalReachableDevices, out);
270
- if (liveView) renderMobileLiveBlock(deviceView?.devices ?? [], liveView.liveMap, out);
250
+ if (deviceSource) {
251
+ if (deviceView) deviceSource.renderList(deviceView, out);
252
+ if (additionalReachableDevices.length > 0) deviceSource.renderAdditional(additionalReachableDevices, out);
253
+ if (liveView) deviceSource.renderLive(deviceView?.devices ?? [], liveView.liveMap, out);
271
254
  }
272
255
  if (next) console.log(` ${out("dim", `Next: ${next}`)}`);
273
256
  }
274
257
  return doctorResult.status === "pass" ? 0 : 1;
275
258
  }
276
- async function terminalDoctorChecks(target) {
277
- const { terminalReadiness } = await import("../../adapters/terminal/lib/readiness.mjs");
278
- const { resolveBrowser } = await import("../../adapters/terminal/launch.mjs");
279
- const report = await terminalReadiness({
280
- target,
281
- appPort: terminalAppPort(),
282
- cdpPort: terminalCdpPort(),
283
- signer: process.env.TERMINAL_SIGNER || "extension",
284
- account: process.env.TERMINAL_ACCOUNT || "dev1",
285
- probeBrowser: resolveBrowser
286
- });
287
- return report.checks.map((check) => ({
288
- id: `terminal-${check.id}`,
289
- status: check.status === "pass" ? "pass" : "fail",
290
- required: check.required,
291
- message: check.detail
292
- }));
293
- }
294
259
  function doctorRuntimeCheck(runtime, error, expectLive) {
295
260
  if (error) {
296
261
  return {
@@ -310,7 +275,7 @@ function doctorRuntimeCheck(runtime, error, expectLive) {
310
275
  detail: runtime?.reasons.join(" | ") || "Runtime readiness probe returned no result."
311
276
  };
312
277
  }
313
- function doctorFixNextActions(failed, fixture, adapter, target) {
278
+ function doctorFixNextActions(failed, fixture, adapter, target, block) {
314
279
  const actions = [];
315
280
  const executable = process.env.MM_HARNESS_EXECUTABLE ? shellQuote(process.env.MM_HARNESS_EXECUTABLE) : "mm-harness";
316
281
  const scope = `--adapter ${adapter} --target ${shellQuote(target)}`;
@@ -325,10 +290,7 @@ function doctorFixNextActions(failed, fixture, adapter, target) {
325
290
  );
326
291
  }
327
292
  if (failed.includes("overlay")) actions.push(`${executable} install ${scope}`);
328
- if (adapter === "extension" && failed.includes("extension-product-config")) {
329
- const block = extensionProductConfigBlock(target);
330
- if (block) actions.push(block.userAction);
331
- }
293
+ if (block?.userAction && failed.includes(block.id)) actions.push(block.userAction);
332
294
  return actions;
333
295
  }
334
296
  function emitExpectLiveVerdict(adapter, target, runtime, verdict) {
@@ -367,9 +329,9 @@ function emitExpectLive(adapter, target, runtime, result, orphanMetros, capture,
367
329
  emitExpectLiveVerdict(adapter, target, runtime, "not-live");
368
330
  return EXIT.runtime;
369
331
  }
370
- function captureHealth(adapter) {
371
- if (adapter === "core") return null;
372
- const fallback = adapter === "extension" ? ["cdp"] : ["simctl", "adb"];
332
+ function captureHealth(providers) {
333
+ if (!providers) return null;
334
+ const fallback = [...providers];
373
335
  const helper = captureHelperHealth();
374
336
  if (!helper) {
375
337
  return {
@@ -445,18 +407,6 @@ function renderUpdateStatus(status, out) {
445
407
  status.updateAvailable ? `${out("label", "update:")} ${out("warn", `${status.current} \u2192 ${status.latest} available`)} ${out("dim", "(mm-harness update)")}` : status.latest && status.latest !== status.current ? `${out("label", "update:")} ${out("ok", status.current)} ${out("dim", `ahead of registry ${status.latest}`)}` : `${out("label", "update:")} ${out("ok", `${status.current} is latest`)}`
446
408
  );
447
409
  }
448
- function detectOrphanMetros(target, livePort) {
449
- try {
450
- const script = path.join(runnerDir, "adapters/shared/reap-checkout-metros.sh");
451
- const out = execFileSync("bash", ["-c", `. "$1"; detect_checkout_metros "$2" "$3"`, "mm-harness-doctor", script, target, livePort ?? ""], {
452
- encoding: "utf8",
453
- timeout: 5e3
454
- });
455
- return out.split("\n").map((line) => line.trim()).filter((line) => /^\d+$/u.test(line));
456
- } catch {
457
- return [];
458
- }
459
- }
460
410
  async function runDoctorFix(adapter, target, manifestValidation, json) {
461
411
  const fixed = [];
462
412
  const failed = [];
@@ -473,18 +423,11 @@ async function runDoctorFix(adapter, target, manifestValidation, json) {
473
423
  } catch {
474
424
  failed.push("runtime-context");
475
425
  }
476
- if (adapter === "extension") {
477
- try {
478
- if (ensureExtensionPerpsBuildFlag(target)) fixed.push("extension-perps-build");
479
- } catch {
480
- failed.push("extension-perps-build");
481
- }
482
- }
483
- if (adapter === "mobile") {
426
+ for (const fix of getAdapterSurface(adapter).readiness.fixes ?? []) {
484
427
  try {
485
- if (ensureMobilePerpsEnvironment(target).length > 0) fixed.push("mobile-perps-env");
428
+ if (fix.apply(target)) fixed.push(fix.id);
486
429
  } catch {
487
- failed.push("mobile-perps-env");
430
+ failed.push(fix.id);
488
431
  }
489
432
  }
490
433
  const state = newHealState();
@@ -508,62 +451,8 @@ async function runDoctorFix(adapter, target, manifestValidation, json) {
508
451
  }
509
452
  return { fixed, failed };
510
453
  }
511
- function ensureExtensionPerpsBuildFlag(target) {
512
- const configPath = path.join(target, ".metamaskrc");
513
- const templatePath = path.join(target, ".metamaskrc.dist");
514
- const config = readTextSnapshot(configPath);
515
- const template = config.kind === "file" ? null : readTextSnapshot(templatePath);
516
- const source = config.kind === "file" ? config : template?.kind === "file" ? template : config.kind === "symlink" ? { kind: "file", text: "", mode: 438 } : null;
517
- if (!source) return false;
518
- const before = source.text;
519
- if (/^\s*PERPS_ENABLED\s*=\s*(['"]?)true\1\s*$/mu.test(before)) return false;
520
- const line = "PERPS_ENABLED='true'";
521
- const after = /^\s*PERPS_ENABLED\s*=.*$/mu.test(before) ? before.replace(/^\s*PERPS_ENABLED\s*=.*$/mu, line) : `${before.replace(/\s*$/u, "")}
522
- ${line}
523
- `;
524
- const temporary = `${configPath}.${process.pid}.${randomUUID()}.tmp`;
525
- let descriptor = null;
526
- try {
527
- const mode = config.kind === "file" ? config.mode : 438;
528
- descriptor = fs.openSync(
529
- temporary,
530
- fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL,
531
- mode
532
- );
533
- fs.writeFileSync(descriptor, after);
534
- if (config.kind === "file") fs.fchmodSync(descriptor, config.mode);
535
- fs.closeSync(descriptor);
536
- descriptor = null;
537
- fs.renameSync(temporary, configPath);
538
- } finally {
539
- if (descriptor !== null) fs.closeSync(descriptor);
540
- fs.rmSync(temporary, { force: true });
541
- }
542
- return true;
543
- }
544
- function readTextSnapshot(file) {
545
- let descriptor;
546
- try {
547
- descriptor = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
548
- } catch (error) {
549
- const code = error.code;
550
- if (code === "ENOENT") return { kind: "missing" };
551
- if (code === "ELOOP") return { kind: "symlink" };
552
- throw error;
553
- }
554
- try {
555
- return {
556
- kind: "file",
557
- text: fs.readFileSync(descriptor, "utf8"),
558
- mode: fs.fstatSync(descriptor).mode & 511
559
- };
560
- } finally {
561
- fs.closeSync(descriptor);
562
- }
563
- }
564
454
  export {
565
455
  doctorFixNextActions,
566
- ensureExtensionPerpsBuildFlag,
567
456
  handleDoctor,
568
457
  skillsView
569
458
  };