@deeeed/metamask-harness 0.17.5 → 0.18.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 (132) hide show
  1. package/CHANGELOG.md +872 -0
  2. package/README.md +94 -140
  3. package/adapters/extension/console-tail.mjs +55 -24
  4. package/adapters/extension/ensure-browser.sh +6 -1
  5. package/adapters/extension/inject.mjs +6 -9
  6. package/adapters/extension/launch-browser.cjs +7 -1
  7. package/adapters/extension/launch-webpack.cjs +83 -0
  8. package/adapters/extension/launch.sh +8 -11
  9. package/adapters/extension/lib/chrome-args.cjs +8 -1
  10. package/adapters/extension/lib/macos-focus.cjs +32 -0
  11. package/adapters/extension/live.sh +12 -21
  12. package/adapters/extension/reattach.sh +1 -0
  13. package/adapters/extension/seed-fixture.sh +4 -12
  14. package/adapters/extension/sidepanel-toggle.sh +4 -1
  15. package/adapters/extension/stamp-runtime-title.cjs +58 -0
  16. package/adapters/extension/start-watch.sh +17 -4
  17. package/adapters/extension/stop-viewers.sh +1 -1
  18. package/adapters/extension/sync-webpack-dist.cjs +107 -0
  19. package/adapters/extension/verify.sh +1 -1
  20. package/adapters/manifest.json +53 -37
  21. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +16 -0
  22. package/adapters/mobile/bridge-runtime/console-forwarder.cjs +1 -1
  23. package/adapters/mobile/bridge-runtime/lib/match-bridge-target.cjs +3 -1
  24. package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +2 -3
  25. package/adapters/mobile/cleanup.sh +16 -176
  26. package/adapters/mobile/inject.sh +90 -671
  27. package/adapters/mobile/launch-console-forwarder.cjs +39 -0
  28. package/adapters/mobile/open-device.sh +45 -7
  29. package/adapters/mobile/start-console-forwarder.sh +70 -0
  30. package/adapters/mobile/start-metro.sh +0 -41
  31. package/adapters/mobile/verify.sh +19 -64
  32. package/adapters/mobile/wait-for-bridge.sh +22 -8
  33. package/adapters/shared/resolve-slot-ports-core.mjs +17 -6
  34. package/adapters/shared/resolve-slot-ports.sh +17 -5
  35. package/adapters/shared/update-check-worker.mjs +43 -0
  36. package/bin/mm-harness +21 -2
  37. package/dist/adapters/core/surface.js +4 -1
  38. package/dist/adapters/extension/console-capture.js +95 -0
  39. package/dist/adapters/extension/product-config.js +110 -0
  40. package/dist/adapters/extension/runtime-decision.js +20 -2
  41. package/dist/adapters/extension/surface.js +22 -3
  42. package/dist/adapters/mobile/perps-env.js +43 -3
  43. package/dist/adapters/mobile/prepare.js +39 -7
  44. package/dist/adapters/mobile/surface.js +5 -2
  45. package/dist/adapters/resolve-slot-ports.js +2 -2
  46. package/dist/adapters/slot-ports.js +13 -16
  47. package/dist/adapters.js +56 -16
  48. package/dist/checkout-lock.js +27 -2
  49. package/dist/cli-color.js +19 -0
  50. package/dist/cli-commands.js +1 -1
  51. package/dist/cli.js +7 -14
  52. package/dist/command-contract.js +451 -0
  53. package/dist/command-journal.js +225 -0
  54. package/dist/commands/call.js +170 -50
  55. package/dist/commands/check.js +9 -3
  56. package/dist/commands/completion-candidates.js +20 -13
  57. package/dist/commands/device-target.js +27 -12
  58. package/dist/commands/doctor.js +106 -25
  59. package/dist/commands/fixtures.js +92 -34
  60. package/dist/commands/flows.js +39 -10
  61. package/dist/commands/last.js +52 -0
  62. package/dist/commands/launch/extension.js +38 -15
  63. package/dist/commands/launch/index.js +207 -63
  64. package/dist/commands/list-executables.js +151 -29
  65. package/dist/commands/logs.js +8 -6
  66. package/dist/commands/manifest.js +270 -35
  67. package/dist/commands/parse-args.js +13 -1
  68. package/dist/commands/provision.js +10 -3
  69. package/dist/commands/run-engine.js +435 -105
  70. package/dist/commands/run-report.js +12 -3
  71. package/dist/commands/run.js +355 -50
  72. package/dist/commands/shared.js +75 -3
  73. package/dist/commands/status-probe.js +4 -1
  74. package/dist/commands/status.js +2 -1
  75. package/dist/commands/stop.js +7 -2
  76. package/dist/commands/update.js +16 -28
  77. package/dist/completions-cache.js +1 -1
  78. package/dist/doctor.js +57 -10
  79. package/dist/harness.js +47 -137
  80. package/dist/heal-bounds.js +2 -2
  81. package/dist/json-stream.js +57 -0
  82. package/dist/live-adapter-contract.js +138 -32
  83. package/dist/manifest.js +161 -1
  84. package/dist/mm-harness-cli.js +135 -54
  85. package/dist/paths.js +2 -5
  86. package/dist/recipe-security.js +178 -0
  87. package/dist/run-diagnostics.js +261 -0
  88. package/dist/runner.js +117 -8
  89. package/docs/CONTRIBUTING.md +137 -0
  90. package/docs/QA.md +185 -0
  91. package/docs/RECIPES.md +161 -0
  92. package/docs/SECURITY.md +88 -0
  93. package/library/README.md +4 -0
  94. package/library/actions/core/perps/_controller.mjs +10 -55
  95. package/library/actions/core/perps/read_account.mjs +2 -2
  96. package/library/actions/core/perps/read_orders.mjs +2 -1
  97. package/library/actions/core/perps/read_positions.mjs +2 -1
  98. package/library/actions/core/wallet/list_accounts.mjs +95 -0
  99. package/library/actions/extension/platform/cdp.mjs +1 -0
  100. package/library/actions/extension/wallet/list_accounts.mjs +41 -0
  101. package/library/actions/mobile/wallet/list_accounts.mjs +37 -0
  102. package/library/manifests/core.action-manifest.json +68 -7
  103. package/library/manifests/extension.action-manifest.json +53 -0
  104. package/library/manifests/mobile.action-manifest.json +81 -3
  105. package/library/recipes/runner/action-validation.extension.recipe.json +8 -1
  106. package/library/recipes/runner/action-validation.mobile.recipe.json +8 -1
  107. package/library/recipes/runner/smoke.core.recipe.json +27 -0
  108. package/library/recipes/wallet/smoke.extension.recipe.json +42 -0
  109. package/library/recipes/wallet/smoke.mobile.recipe.json +42 -0
  110. package/package.json +12 -5
  111. package/scripts/completions.sh +7 -7
  112. package/scripts/validate-human-outcomes.mjs +169 -0
  113. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +0 -185
  114. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +0 -1662
  115. package/adapters/mobile/overlay/compat/README.md +0 -28
  116. package/adapters/mobile/overlay/compat/rn81-message-event-source.patch +0 -42
  117. package/adapters/shared/resolve-farmslot-ports-core.mjs +0 -3
  118. package/adapters/shared/resolve-farmslot-ports.mjs +0 -5
  119. package/adapters/shared/resolve-farmslot-ports.sh +0 -7
  120. package/docs/ADAPTER-SURFACE.md +0 -119
  121. package/docs/CHEATSHEET.md +0 -61
  122. package/docs/CLI-SPEC.md +0 -1098
  123. package/docs/CODE-MAP.md +0 -62
  124. package/docs/DEBUG-HANDOVER.md +0 -36
  125. package/docs/MENTAL-MODEL.md +0 -295
  126. package/docs/UX-PRINCIPLES.md +0 -64
  127. package/docs/architecture.md +0 -398
  128. package/docs/live-adapter-contract.md +0 -188
  129. package/docs/package-boundaries.md +0 -47
  130. package/docs/perps-flow-catalog.md +0 -235
  131. package/docs/recipe-libraries.md +0 -301
  132. package/docs/runtime-file-conventions.md +0 -36
@@ -4,9 +4,10 @@ import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import { color } from "../cli-color.js";
6
6
  import { createDoctorReport, fixtureSummary, renderRuntimeContext, requiredDoctorCheckSummary } from "../doctor.js";
7
- import { detectAdapter } from "../harness.js";
7
+ import { detectAdapter, resolveRuntimeContextPath } from "../harness.js";
8
8
  import { assertAdapter, runnerDir } from "../paths.js";
9
9
  import { getAdapterSurface } from "../adapters/surface.js";
10
+ import { extensionProductConfigBlock } from "../adapters/extension/product-config.js";
10
11
  import { ensureMobilePerpsEnvironment, mobilePerpsEnvironment } from "../adapters/mobile/perps-env.js";
11
12
  import { loadActionManifest, validateManifest } from "../manifest.js";
12
13
  import { ensureOverlay, newHealState, recipeRunning } from "../heal-bounds.js";
@@ -21,12 +22,13 @@ import {
21
22
  import { farmslotReadyIndicator } from "./farmslot-ready.js";
22
23
  import { ensureRuntimeContext } from "../runtime-context.js";
23
24
  import { acquireCheckoutLock } from "../checkout-lock.js";
24
- import { ADAPTER_DETECT_NEXT, EXIT, usageOut } from "./shared.js";
25
+ import { ADAPTER_DETECT_NEXT, checkoutBusyOut, EXIT, usageOut } from "./shared.js";
25
26
  import {
26
27
  actionManifestPathOption,
27
28
  applyRuntimeDirOption,
28
29
  optionFlag,
29
30
  optionString,
31
+ shellQuote,
30
32
  targetPath
31
33
  } from "./parse-args.js";
32
34
  function applyDoctorRuntimePorts(options) {
@@ -56,6 +58,14 @@ async function handleDoctor({ options }) {
56
58
  return usageOut(json, "doctor", `could not detect the MetaMask repo type for ${target}`, ADAPTER_DETECT_NEXT);
57
59
  }
58
60
  assertAdapter(adapter);
61
+ if (!fs.existsSync(target)) {
62
+ return usageOut(
63
+ json,
64
+ "doctor",
65
+ `target does not exist: ${target}`,
66
+ "pass --target <metamask-checkout> pointing to an existing checkout"
67
+ );
68
+ }
59
69
  if (printReady && json) {
60
70
  return usageOut(
61
71
  json,
@@ -72,12 +82,12 @@ async function handleDoctor({ options }) {
72
82
  surface.resolveSlotPorts(target);
73
83
  const dtResult = applyDeviceTargeting("doctor", adapter, options, { gate: false, rerun: "" });
74
84
  if ("code" in dtResult) {
75
- return usageOut(json, "doctor", dtResult.message, "mm-harness doctor --fix --adapter mobile --device <adb-serial|simulator-udid>");
85
+ return usageOut(json, "doctor", dtResult.message, dtResult.userAction);
76
86
  }
77
87
  applyDoctorRuntimePorts(options);
78
88
  const lock = acquireCheckoutLock(target, "doctor-fix");
79
89
  if ("message" in lock) {
80
- return usageOut(json, "doctor", lock.message, `wait for the current owner, or inspect ${lock.path} if its process has exited`);
90
+ return checkoutBusyOut(json, "doctor", lock.message, lock.path);
81
91
  }
82
92
  let fixed;
83
93
  let failed;
@@ -87,11 +97,36 @@ async function handleDoctor({ options }) {
87
97
  lock.release();
88
98
  }
89
99
  const result2 = createDoctorReport(adapter, target, manifestValidation, actionManifestPath);
90
- const nextActions = doctorFixNextActions(failed, result2.fixture);
100
+ let runtime2;
101
+ let runtimeProbeError2;
102
+ try {
103
+ runtime2 = await surface.runtimeStatus(target);
104
+ } catch (error2) {
105
+ runtimeProbeError2 = error2 instanceof Error ? error2.message : String(error2);
106
+ }
107
+ const ready = runtime2?.decision === "ready";
108
+ if (adapter === "extension" && runtime2?.reasonCode === "product-config-required") {
109
+ failed.push("extension-product-config");
110
+ }
111
+ const nextActions = doctorFixNextActions(failed, result2.fixture, adapter, target);
112
+ if (failed.length === 0 && !ready && runtime2?.nextAction) nextActions.push(runtime2.nextAction);
91
113
  const status = result2.status === "pass" && failed.length === 0 ? "pass" : "fail";
92
- if (json) console.log(JSON.stringify({ ...result2, status, fixed, failed, nextActions }, null, 2));
114
+ const retryDoctor = `mm-harness doctor --fix --adapter ${adapter} --target ${shellQuote(target)} --json`;
115
+ const fixUserAction = failed.includes("wallet-fixture") ? `choose one wallet fixture source from nextActions, run it, then retry: ${retryDoctor}` : nextActions[0] ?? retryDoctor;
116
+ const error = status === "fail" ? {
117
+ code: "DOCTOR_FIX_INCOMPLETE",
118
+ message: "doctor --fix could not make every required check pass",
119
+ userAction: fixUserAction
120
+ } : void 0;
121
+ if (json) console.log(JSON.stringify({ ...result2, status, fixed, failed, ready, runtime: runtime2, ...runtimeProbeError2 ? { runtimeProbeError: runtimeProbeError2 } : {}, nextActions, ...error ? { error } : {} }, null, 2));
93
122
  else {
94
- console.log(`${status} ${adapter} ${result2.compatibilityMode} manifest=${actionManifestPath} fixed=[${fixed.join(",")}] failed=[${failed.join(",")}]`);
123
+ console.log(`${status} ${adapter} ${result2.compatibilityMode} ready=${ready} manifest=${actionManifestPath} fixed=[${fixed.join(",")}] failed=[${failed.join(",")}]`);
124
+ if (runtime2) {
125
+ console.log(`runtime: decision=${runtime2.decision}${runtime2.reasonCode ? ` (${runtime2.reasonCode})` : ""}`);
126
+ for (const reason of runtime2.reasons) console.log(` ${reason}`);
127
+ } else if (runtimeProbeError2) {
128
+ console.log(`runtime probe failed: ${runtimeProbeError2}`);
129
+ }
95
130
  for (const action of nextActions) console.error(` Next: ${action}`);
96
131
  }
97
132
  return status === "pass" ? 0 : 1;
@@ -104,8 +139,9 @@ async function handleDoctor({ options }) {
104
139
  surface.resolveSlotPorts(target);
105
140
  const dtResult = applyDeviceTargeting("doctor", adapter, options, { gate: false, rerun: "" });
106
141
  if ("code" in dtResult) {
107
- if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "doctor", adapter, target, error: { code: dtResult.code, message: dtResult.message } }, null, 2));
108
- else console.error(`\u2717 doctor: ${dtResult.message}`);
142
+ if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "doctor", adapter, target, error: { code: dtResult.code, message: dtResult.message, userAction: dtResult.userAction } }, null, 2));
143
+ else console.error(`\u2717 doctor: ${dtResult.message}
144
+ Next: ${dtResult.userAction}`);
109
145
  return EXIT.usage;
110
146
  }
111
147
  applyDoctorRuntimePorts(options);
@@ -123,7 +159,7 @@ async function handleDoctor({ options }) {
123
159
  requiredChecks
124
160
  };
125
161
  const orphanMetros = adapter === "mobile" ? detectOrphanMetros(target, process.env.WATCHER_PORT) : [];
126
- const capture = captureHelperHealth();
162
+ const capture = captureHealth(adapter);
127
163
  const deviceView = adapter === "mobile" ? mobileDeviceView(allDevices) : null;
128
164
  const liveView = deviceView ? await mobileDeviceLiveView(target, deviceView) : null;
129
165
  const devices = liveView?.devicesWithLive ?? deviceView?.devices ?? [];
@@ -142,11 +178,17 @@ async function handleDoctor({ options }) {
142
178
  printReady
143
179
  );
144
180
  }
145
- if (json) console.log(JSON.stringify({ ...doctorResult, ready: runtime?.decision === "ready", runtime, orphanMetros, capture, devices, additionalReachableDevices }, null, 2));
181
+ const doctorUserAction = `mm-harness doctor --fix --adapter ${adapter} --target ${shellQuote(target)} --json`;
182
+ const doctorError = doctorResult.status === "fail" ? { code: "DOCTOR_CHECKS_FAILED", message: "one or more required doctor checks failed", userAction: doctorUserAction } : void 0;
183
+ const next = doctorError?.userAction ?? (runtime?.decision === "ready" ? void 0 : runtime?.nextAction);
184
+ if (json) console.log(JSON.stringify({ ...doctorResult, ready: runtime?.decision === "ready", runtime, orphanMetros, capture, devices, additionalReachableDevices, ...next ? { next } : {}, ...doctorError ? { error: doctorError } : {} }, null, 2));
146
185
  else {
147
186
  const out = (style, text) => color(style, text, { stream: process.stdout });
148
187
  const stateStyle = (value, good) => value === good ? "ok" : "warn";
149
188
  console.log(`${out(doctorResult.status === "pass" ? "ok" : "err", doctorResult.status)} ${out("bold", adapter)} ${doctorResult.compatibilityMode} ${out("dim", `manifest=${actionManifestPath}`)}`);
189
+ console.log(
190
+ `${out("label", "harness:")} ${doctorResult.runner.packageName}@${doctorResult.runner.version} ${out("accent", doctorResult.runner.installKind)} ${out("dim", `executable=${doctorResult.runner.executablePath}`)}`
191
+ );
150
192
  if (runtime) {
151
193
  const provisionedDepsPending = runtime.reasonCode === "app-installed-deps-pending";
152
194
  const decisionStyle = provisionedDepsPending ? "info" : runtime.decision === "ready" ? "ok" : runtime.decision === "blocked" ? "err" : "warn";
@@ -173,18 +215,13 @@ async function handleDoctor({ options }) {
173
215
  );
174
216
  console.log(` ${out("dim", "Next: mm-harness stop # reaps every bundler this checkout leaked")}`);
175
217
  }
176
- if (capture) {
177
- console.log(`${out("label", "capture:")} ${out(capture.status === "pass" ? "ok" : "warn", capture.status)} ${out("dim", "(capture-helper: screenshots + --record video)")}`);
178
- if (capture.status !== "pass") {
179
- if (capture.failing.length > 0) console.log(` ${out("dim", `failing: ${capture.failing.join(", ")}`)}`);
180
- console.log(` ${out("dim", "Next: grant Screen Recording (System Settings \u2192 Privacy & Security \u2192 Screen Recording), or run: capture-helper doctor --open-permissions")}`);
181
- }
182
- }
218
+ if (capture) renderCaptureHealth(capture, out);
183
219
  if (adapter === "mobile") {
184
220
  if (deviceView) renderMobileDeviceList(deviceView, out);
185
221
  if (additionalReachableDevices.length > 0) renderAdditionalReachableDevices(additionalReachableDevices, out);
186
222
  if (liveView) renderMobileLiveBlock(deviceView?.devices ?? [], liveView.liveMap, out);
187
223
  }
224
+ if (next) console.log(` ${out("dim", `Next: ${next}`)}`);
188
225
  }
189
226
  return doctorResult.status === "pass" ? 0 : 1;
190
227
  }
@@ -207,15 +244,25 @@ function doctorRuntimeCheck(runtime, error, expectLive) {
207
244
  detail: runtime?.reasons.join(" | ") || "Runtime readiness probe returned no result."
208
245
  };
209
246
  }
210
- function doctorFixNextActions(failed, fixture) {
247
+ function doctorFixNextActions(failed, fixture, adapter, target) {
211
248
  const actions = [];
249
+ const executable = process.env.MM_HARNESS_EXECUTABLE ? shellQuote(process.env.MM_HARNESS_EXECUTABLE) : "mm-harness";
250
+ const scope = `--adapter ${adapter} --target ${shellQuote(target)}`;
212
251
  if (failed.includes("wallet-fixture")) {
213
252
  const force = fixture.status === "invalid" || fixture.status === "incomplete" ? " --force" : "";
214
- actions.push(`1) mm-harness fixtures init --from <path>${force} # existing team/test fixture`);
215
- actions.push(`2) mm-harness fixtures init --dev${force} # disposable public test wallet; never real funds`);
253
+ actions.push(`1) ${executable} fixtures init --from <path>${force} ${scope} # existing team/test fixture`);
254
+ actions.push(`2) ${executable} fixtures init --dev${force} ${scope} # disposable public test wallet; never real funds`);
255
+ }
256
+ if (failed.includes("runtime-context")) {
257
+ actions.push(
258
+ `inspect ${shellQuote(resolveRuntimeContextPath(target))}, then retry ${executable} doctor --fix ${scope}`
259
+ );
260
+ }
261
+ if (failed.includes("overlay")) actions.push(`${executable} install ${scope}`);
262
+ if (adapter === "extension" && failed.includes("extension-product-config")) {
263
+ const block = extensionProductConfigBlock(target);
264
+ if (block) actions.push(block.userAction);
216
265
  }
217
- if (failed.includes("runtime-context")) actions.push("inspect temp/recipe/runtime/agentic-runtime.json, then retry mm-harness doctor --fix");
218
- if (failed.includes("overlay")) actions.push("mm-harness install --target .");
219
266
  return actions;
220
267
  }
221
268
  function emitExpectLiveVerdict(adapter, target, runtime, verdict) {
@@ -223,7 +270,7 @@ function emitExpectLiveVerdict(adapter, target, runtime, verdict) {
223
270
  const detail = runtime ? `${runtime.decision}${runtime.reasonCode ? ` (${runtime.reasonCode})` : ""}` : "runtime probe unavailable";
224
271
  console.error(`${out("err", verdict)} ${out("bold", adapter)} ${detail} ${out("dim", target)}`);
225
272
  for (const reason of runtime?.reasons ?? []) console.error(` ${out("dim", reason)}`);
226
- console.error(` Next: ${getAdapterSurface(adapter).hints.relaunch}`);
273
+ console.error(` Next: ${runtime?.nextAction ?? getAdapterSurface(adapter).hints.runtimeProbeRecovery(target)}`);
227
274
  }
228
275
  function emitExpectLive(adapter, target, runtime, result, orphanMetros, capture, devices, additionalReachableDevices, json, printReady) {
229
276
  const live = runtime?.decision === "ready";
@@ -241,7 +288,9 @@ function emitExpectLive(adapter, target, runtime, result, orphanMetros, capture,
241
288
  return EXIT.runtime;
242
289
  }
243
290
  if (json) {
244
- console.log(JSON.stringify({ ...result, ready: live, runtime, orphanMetros, capture, devices, additionalReachableDevices }, null, 2));
291
+ const userAction = runtime?.nextAction ?? getAdapterSurface(adapter).hints.runtimeProbeRecovery(target);
292
+ const error = live ? void 0 : { code: "RUNTIME_NOT_LIVE", message: `${adapter} runtime is not live`, userAction };
293
+ console.log(JSON.stringify({ ...result, ready: live, runtime, orphanMetros, capture, devices, additionalReachableDevices, ...error ? { error } : {} }, null, 2));
245
294
  return live ? EXIT.ok : EXIT.runtime;
246
295
  }
247
296
  const out = (style, text) => color(style, text, { stream: process.stdout });
@@ -252,6 +301,25 @@ function emitExpectLive(adapter, target, runtime, result, orphanMetros, capture,
252
301
  emitExpectLiveVerdict(adapter, target, runtime, "not-live");
253
302
  return EXIT.runtime;
254
303
  }
304
+ function captureHealth(adapter) {
305
+ if (adapter === "core") return null;
306
+ const fallback = adapter === "extension" ? ["cdp"] : ["simctl", "adb"];
307
+ const helper = captureHelperHealth();
308
+ if (!helper) {
309
+ return {
310
+ status: "fallback",
311
+ failing: [],
312
+ screenshots: { available: true, providers: fallback },
313
+ recording: { available: false, provider: null }
314
+ };
315
+ }
316
+ return {
317
+ status: helper.status,
318
+ failing: helper.failing,
319
+ screenshots: { available: true, providers: ["capture-helper", ...fallback] },
320
+ recording: { available: helper.status === "pass", provider: helper.status === "pass" ? "capture-helper" : null }
321
+ };
322
+ }
255
323
  function captureHelperHealth() {
256
324
  if (process.platform !== "darwin") return null;
257
325
  const bin = process.env.CAPTURE_HELPER_PATH || "capture-helper";
@@ -264,6 +332,18 @@ function captureHelperHealth() {
264
332
  return null;
265
333
  }
266
334
  }
335
+ function renderCaptureHealth(capture, out) {
336
+ const video = capture.recording.available ? "capture-helper" : "unavailable";
337
+ console.log(
338
+ `${out("label", "capture:")} ${out(capture.status === "pass" ? "ok" : "warn", capture.status)} ${out("dim", `(screenshots: ${capture.screenshots.providers.join(" \u2192 ")}; video: ${video})`)}`
339
+ );
340
+ if (capture.status === "warn") {
341
+ if (capture.failing.length > 0) console.log(` ${out("dim", `failing: ${capture.failing.join(", ")}`)}`);
342
+ console.log(` ${out("dim", "Next: grant Screen Recording (System Settings \u2192 Privacy & Security \u2192 Screen Recording), or run: capture-helper doctor --open-permissions")}`);
343
+ } else if (capture.status === "fallback") {
344
+ console.log(` ${out("dim", "Next: screenshots work without capture-helper; install/configure capture-helper only when recipe video is required")}`);
345
+ }
346
+ }
267
347
  function detectOrphanMetros(target, livePort) {
268
348
  try {
269
349
  const script = path.join(runnerDir, "adapters/shared/reap-checkout-metros.sh");
@@ -381,6 +461,7 @@ function readTextSnapshot(file) {
381
461
  }
382
462
  }
383
463
  export {
464
+ doctorFixNextActions,
384
465
  ensureExtensionPerpsBuildFlag,
385
466
  handleDoctor
386
467
  };
@@ -5,21 +5,22 @@ import { runnerDir, walletFixturePath } from "../paths.js";
5
5
  import { getAdapterSurface } from "../adapters/surface.js";
6
6
  import { fixtureFileSummary, fixtureSummary } from "../doctor.js";
7
7
  import { acquireCheckoutLock } from "../checkout-lock.js";
8
+ import { colorHumanMessage } from "../cli-color.js";
8
9
  import { applyDeviceTargeting } from "./device-target.js";
9
- import { ADAPTER_DETECT_NEXT, EXIT, flag, parseFlags, resolveAdapter, scriptOverride, spawnScript, spawnScriptStreaming, str, targetOf, usageOut } from "./shared.js";
10
+ import { ADAPTER_DETECT_NEXT, checkoutBusyOut, EXIT, flag, parseFlags, resolveAdapter, scriptOverride, spawnScript, spawnScriptStreaming, str, targetOf, usageOut } from "./shared.js";
10
11
  const FIXTURES_BOOLEANS = /* @__PURE__ */ new Set(["dev", "force", "json"]);
11
- const MOBILE_FIXTURE_TIMEOUT_MS = 12e4;
12
- async function handleFixtures(argv, deps) {
12
+ const FIXTURE_STREAM_TIMEOUT_MS = 12e4;
13
+ async function handleFixtures(argv) {
13
14
  const { options } = parseFlags(argv, FIXTURES_BOOLEANS);
14
15
  const json = flag(options, "json");
15
16
  const target = targetOf(options);
16
- if (!fs.existsSync(target)) return handleFixturesLocked(argv, deps);
17
+ if (!fs.existsSync(target)) return handleFixturesLocked(argv);
17
18
  const lock = acquireCheckoutLock(target, "fixtures");
18
19
  if ("message" in lock) {
19
- return usageOut(json, "fixtures", lock.message, `wait for the current owner, or inspect ${lock.path} if its process has exited`);
20
+ return checkoutBusyOut(json, "fixtures", lock.message, lock.path);
20
21
  }
21
22
  try {
22
- return await handleFixturesLocked(argv, deps);
23
+ return await handleFixturesLocked(argv);
23
24
  } finally {
24
25
  lock.release();
25
26
  }
@@ -49,13 +50,21 @@ function ensureFixturePublicationParent(target) {
49
50
  }
50
51
  return fs.realpathSync(parent) === parent ? null : `wallet fixture runtime path escapes the checkout: ${parent}`;
51
52
  }
52
- async function handleFixturesLocked(argv, deps) {
53
+ async function handleFixturesLocked(argv) {
53
54
  const { positional, options } = parseFlags(argv, FIXTURES_BOOLEANS);
54
55
  const json = flag(options, "json");
55
56
  const sub = positional[0];
56
57
  if (sub !== "init" && sub !== "sync" && sub !== "set" && sub !== "generate" && sub !== "finalize") {
57
58
  return usageOut(json, "fixtures", "fixtures requires a subcommand: mm-harness fixtures <init|sync|set|generate|finalize>", "mm-harness fixtures init --from <path> or mm-harness fixtures init --dev");
58
59
  }
60
+ if (sub !== "init" && (flag(options, "dev") || flag(options, "force") || str(options, "from"))) {
61
+ return usageOut(
62
+ json,
63
+ "fixtures",
64
+ "--dev, --from, and --force belong to fixtures init; they do not apply a fixture.",
65
+ "mm-harness fixtures init --dev && mm-harness fixtures set"
66
+ );
67
+ }
59
68
  const target = targetOf(options);
60
69
  const adapter = resolveAdapter(options, target);
61
70
  if (!adapter) {
@@ -65,7 +74,7 @@ async function handleFixturesLocked(argv, deps) {
65
74
  surface.resolveSlotPorts(target);
66
75
  const dtResult = applyDeviceTargeting("fixtures", adapter, options, { gate: false, rerun: "" });
67
76
  if ("code" in dtResult) {
68
- return usageOut(json, "fixtures", dtResult.message, "mm-harness fixtures set --adapter mobile --device <adb-serial|simulator-udid>");
77
+ return usageOut(json, "fixtures", dtResult.message, dtResult.userAction);
69
78
  }
70
79
  if (sub === "generate") return fixturesGenerate(adapter, target, options, json);
71
80
  if (sub === "finalize") return fixturesFinalize(adapter, target, options, json);
@@ -96,6 +105,17 @@ async function handleFixturesLocked(argv, deps) {
96
105
  return exitCode;
97
106
  }
98
107
  const fixturePath = path.resolve(str(options, "fixture") ?? process.env.RECIPE_WALLET_FIXTURE ?? canonicalFixture);
108
+ if (!fs.existsSync(fixturePath)) {
109
+ return usageOut(
110
+ json,
111
+ "fixtures",
112
+ `no wallet fixture at ${fixturePath}.`,
113
+ "mm-harness fixtures init --dev && mm-harness fixtures set"
114
+ );
115
+ }
116
+ if (adapter === "extension") {
117
+ return applyExtensionFixture(target, fixturePath, canonicalFixture, json);
118
+ }
99
119
  process.stderr.write(`\u2192 fixtures set ${adapter} \u2014 connecting bridge + applying wallet fixture (can take ~30s)\u2026
100
120
  `);
101
121
  let status;
@@ -105,8 +125,8 @@ async function handleFixturesLocked(argv, deps) {
105
125
  const previousAppRoot = process.env.APP_ROOT;
106
126
  process.env.APP_ROOT = target;
107
127
  try {
108
- const configuredTimeout = Number(process.env.MM_HARNESS_FIXTURES_SET_TIMEOUT_MS ?? MOBILE_FIXTURE_TIMEOUT_MS);
109
- const timeoutMs = Number.isFinite(configuredTimeout) && configuredTimeout > 0 ? configuredTimeout : MOBILE_FIXTURE_TIMEOUT_MS;
128
+ const configuredTimeout = Number(process.env.MM_HARNESS_FIXTURES_SET_TIMEOUT_MS ?? FIXTURE_STREAM_TIMEOUT_MS);
129
+ const timeoutMs = Number.isFinite(configuredTimeout) && configuredTimeout > 0 ? configuredTimeout : FIXTURE_STREAM_TIMEOUT_MS;
110
130
  mobileSetupResult = await spawnScriptStreaming(
111
131
  setupWalletSh,
112
132
  ["--fixture", fixturePath],
@@ -128,29 +148,8 @@ async function handleFixturesLocked(argv, deps) {
128
148
  if (!json && status === "fail") {
129
149
  console.error(` Next: ${retryHint}`);
130
150
  }
131
- } else {
132
- if (!fs.existsSync(fixturePath)) {
133
- return usageOut(
134
- json,
135
- "fixtures",
136
- `no wallet fixture at ${fixturePath}.`,
137
- "create it (or pass --fixture <path>), then re-run: mm-harness fixtures set"
138
- );
139
- }
140
- const previousFixtureEnv = process.env.RECIPE_WALLET_FIXTURE;
141
- process.env.RECIPE_WALLET_FIXTURE = fixturePath;
142
- try {
143
- const result = await deps.runOneNode("extension", "metamask.wallet.setup", {}, target, str(options, "actionManifest"));
144
- status = result.status;
145
- } finally {
146
- if (previousFixtureEnv === void 0) delete process.env.RECIPE_WALLET_FIXTURE;
147
- else process.env.RECIPE_WALLET_FIXTURE = previousFixtureEnv;
148
- }
149
- if (!json && status === "fail") {
150
- console.error(` Next: ${retryHint}`);
151
- }
152
151
  }
153
- const teaching = `Wallet fixture applied. Want different accounts? Edit: ${canonicalFixture}`;
152
+ const teaching = status === "pass" ? `Wallet fixture applied. Want different accounts? Edit: ${canonicalFixture}` : "Wallet fixture was not applied.";
154
153
  if (json) {
155
154
  console.log(
156
155
  JSON.stringify(
@@ -174,11 +173,68 @@ async function handleFixturesLocked(argv, deps) {
174
173
  2
175
174
  )
176
175
  );
177
- } else {
176
+ } else if (status === "pass") {
178
177
  console.error(teaching);
179
178
  }
180
179
  return status === "pass" ? EXIT.ok : EXIT.runtime;
181
180
  }
181
+ async function applyExtensionFixture(target, fixturePath, canonicalFixture, json) {
182
+ const distManifest = path.join(target, "dist/chrome/manifest.json");
183
+ const userAction = `mm-harness launch --build --verify --target ${JSON.stringify(target)}`;
184
+ if (!fs.existsSync(distManifest)) {
185
+ const message2 = `Extension fixture needs an existing dist/chrome build; no manifest was found at ${distManifest}.`;
186
+ if (json) {
187
+ console.log(JSON.stringify({
188
+ schemaVersion: 1,
189
+ command: "fixtures",
190
+ action: "set",
191
+ adapter: "extension",
192
+ fixture: fixturePath,
193
+ canonicalFixture,
194
+ status: "fail",
195
+ exitCode: EXIT.runtime,
196
+ error: { code: "EXTENSION_FIXTURE_BUILD_REQUIRED", message: message2, userAction }
197
+ }, null, 2));
198
+ } else {
199
+ console.error(`\u2717 mm-harness fixtures set: ${message2}
200
+ Next: ${userAction}`);
201
+ }
202
+ return EXIT.runtime;
203
+ }
204
+ process.stderr.write(`${colorHumanMessage("\u2192 fixtures set extension \u2014 resetting and seeding the slot-owned profile from the existing build\u2026")}
205
+ `);
206
+ const liveScript = path.join(runnerDir, "adapters/extension/live.sh");
207
+ const args = ["--target", target, "--launch-existing-dist"];
208
+ if (process.env.CDP_PORT) args.push("--cdp-port", process.env.CDP_PORT);
209
+ const result = await spawnScriptStreaming(liveScript, args, target, {
210
+ env: { RECIPE_WALLET_FIXTURE: fixturePath },
211
+ timeoutMs: FIXTURE_STREAM_TIMEOUT_MS
212
+ });
213
+ const status = result.status === 0 ? "pass" : "fail";
214
+ const exitCode = status === "pass" ? EXIT.ok : EXIT.runtime;
215
+ const message = status === "pass" ? `Wallet fixture applied and validated. Want different accounts? Edit: ${canonicalFixture}` : "Extension wallet fixture could not be applied and validated.";
216
+ if (json) {
217
+ console.log(JSON.stringify({
218
+ schemaVersion: 1,
219
+ command: "fixtures",
220
+ action: "set",
221
+ adapter: "extension",
222
+ fixture: fixturePath,
223
+ canonicalFixture,
224
+ status,
225
+ exitCode,
226
+ profileReset: status === "pass",
227
+ message,
228
+ error: status === "fail" ? { code: "EXTENSION_FIXTURE_APPLY_FAILED", message, userAction } : null
229
+ }, null, 2));
230
+ } else if (status === "pass") {
231
+ console.error(message);
232
+ } else {
233
+ console.error(`\u2717 mm-harness fixtures set: ${message}
234
+ Next: ${userAction}`);
235
+ }
236
+ return exitCode;
237
+ }
182
238
  const EXT_FIXTURE_LEAF = "adapters/extension/wallet-fixture-state.cjs";
183
239
  function emitLeafMissing(json, action, fields, leaf) {
184
240
  const message = `leaf could not start: ${path.basename(leaf)} (ENOENT).`;
@@ -298,7 +354,9 @@ async function fixturesFinalize(adapter, target, options, json) {
298
354
  if (outPath) leafArgs.push("--out", outPath);
299
355
  process.stderr.write(`\u2192 fixtures finalize extension \u2014 seeding account labels via CDP on port ${cdpPort} (can take ~30s)\u2026
300
356
  `);
301
- const result = await spawnScriptStreaming(process.execPath, leafArgs, target);
357
+ const result = await spawnScriptStreaming(process.execPath, leafArgs, target, {
358
+ timeoutMs: FIXTURE_STREAM_TIMEOUT_MS
359
+ });
302
360
  const status = result.status === 0 ? "pass" : "fail";
303
361
  const exitCode = result.status === 0 ? EXIT.ok : EXIT.runtime;
304
362
  const retry = `mm-harness fixtures finalize --fixture ${fixturePath} --state ${statePath} --cdp-port ${cdpPort} --extension-dir ${extensionDirPath}`;
@@ -2,18 +2,18 @@ import path from "node:path";
2
2
  import { color } from "../cli-color.js";
3
3
  import { importRecipeHarnessCli } from "../paths.js";
4
4
  import { EXIT } from "./shared.js";
5
- import { usageError } from "./parse-args.js";
5
+ import { parseArgs, usageError } from "./parse-args.js";
6
6
  import { resolveMetaMaskLibrarySources } from "./run-engine.js";
7
- const FLOWS_SUBCOMMANDS = ["list", "promote"];
8
- function stripTargetFlag(argv) {
7
+ const FLOWS_SUBCOMMANDS = ["list", "describe", "promote"];
8
+ function stripFrontOnlyFlags(argv) {
9
9
  const out = [];
10
10
  for (let i = 0; i < argv.length; i += 1) {
11
11
  const arg = argv[i];
12
- if (arg === "--target") {
12
+ if (arg === "--target" || arg === "--library") {
13
13
  i += 1;
14
14
  continue;
15
15
  }
16
- if (arg.startsWith("--target=")) continue;
16
+ if (arg.startsWith("--target=") || arg.startsWith("--library=")) continue;
17
17
  out.push(arg);
18
18
  }
19
19
  return out;
@@ -30,14 +30,37 @@ function restoreEnv(key, value) {
30
30
  if (value === void 0) delete process.env[key];
31
31
  else process.env[key] = value;
32
32
  }
33
+ function libraryEntries(argv) {
34
+ const entries = [];
35
+ for (let index = 0; index < argv.length; index += 1) {
36
+ const argument = argv[index];
37
+ if (argument === "--library") {
38
+ const value = argv[index + 1];
39
+ if (value !== void 0) entries.push(value);
40
+ index += 1;
41
+ } else if (argument.startsWith("--library=")) {
42
+ entries.push(argument.slice("--library=".length));
43
+ }
44
+ }
45
+ return entries;
46
+ }
33
47
  async function handleFlows(argv) {
34
- const sources = await resolveMetaMaskLibrarySources(void 0);
48
+ const { positional } = parseArgs(argv);
49
+ const subcommand = positional[0];
50
+ if (subcommand === "describe" && !positional[1]) {
51
+ throw usageError("flows describe requires <ref>. Next: mm-harness flows list");
52
+ }
53
+ if (subcommand !== "describe" && positional[1]) {
54
+ throw usageError(`flows ${subcommand ?? "list"} does not accept positional ${positional[1]}.`);
55
+ }
56
+ const configuredLibraries = libraryEntries(argv);
57
+ const sources = await resolveMetaMaskLibrarySources(configuredLibraries);
35
58
  if (!sources) {
36
59
  throw usageError(
37
60
  "flows requires @farmslot/recipe-harness >= 0.3.3 (recipe-library support; flows resolve personal > team > canonical). Next: update the dependency and run yarn install."
38
61
  );
39
62
  }
40
- const forwarded = stripTargetFlag(argv);
63
+ const forwarded = stripFrontOnlyFlags(argv);
41
64
  const jsonMode = forwarded.includes("--json");
42
65
  if (!jsonMode) {
43
66
  console.error(color("label", "flows resolve by precedence (highest wins; local shadows canonical):"));
@@ -46,16 +69,22 @@ async function handleFlows(argv) {
46
69
  console.error(` ${index + 1}. ${color("cmd", name)} ${color("dim", `[${flowSourceTier(source, index, sources.length)}]`)}`);
47
70
  });
48
71
  }
49
- const withSubcommand = FLOWS_SUBCOMMANDS.includes(forwarded[0]) ? forwarded : ["list", ...forwarded];
72
+ const resolvedSubcommand = subcommand && FLOWS_SUBCOMMANDS.includes(subcommand) ? subcommand : "list";
73
+ const forwardedSubcommandIndex = forwarded.indexOf(resolvedSubcommand);
74
+ const delegatedArgs = forwardedSubcommandIndex === -1 ? forwarded : forwarded.filter((_, index) => index !== forwardedSubcommandIndex);
75
+ const withSubcommand = [resolvedSubcommand, ...delegatedArgs];
50
76
  const { runRecipeHarnessCli } = await importRecipeHarnessCli();
51
77
  const previousLibraryPath = process.env.RECIPE_LIBRARY_PATH;
78
+ const previousExitCode = process.exitCode;
52
79
  process.env.RECIPE_LIBRARY_PATH = serializeLibrarySources(sources);
80
+ process.exitCode = void 0;
53
81
  try {
54
- await runRecipeHarnessCli(["flows", ...withSubcommand]);
82
+ await runRecipeHarnessCli(["flows", ...withSubcommand], { commandName: "mm-harness" });
83
+ return typeof process.exitCode === "number" ? process.exitCode : EXIT.ok;
55
84
  } finally {
56
85
  restoreEnv("RECIPE_LIBRARY_PATH", previousLibraryPath);
86
+ process.exitCode = previousExitCode;
57
87
  }
58
- return EXIT.ok;
59
88
  }
60
89
  export {
61
90
  handleFlows
@@ -0,0 +1,52 @@
1
+ import { readCommandJournal } from "../command-journal.js";
2
+ import { EXIT } from "./shared.js";
3
+ import {
4
+ optionFlag,
5
+ optionString,
6
+ targetPath
7
+ } from "./parse-args.js";
8
+ async function handleLast({ options }) {
9
+ const target = targetPath(options);
10
+ const { file, record } = readCommandJournal(target, optionString(options, "runtimeDir"));
11
+ const json = optionFlag(options, "json");
12
+ if (!record) {
13
+ const message = `no resumability journal exists for ${target}`;
14
+ const userAction = "run a proof or runtime command in this checkout, then re-run mm-harness last --json";
15
+ if (json) {
16
+ console.log(JSON.stringify({
17
+ schemaVersion: 1,
18
+ command: "last",
19
+ target,
20
+ journalPath: file,
21
+ status: "fail",
22
+ exitCode: EXIT.runtime,
23
+ error: { code: "LAST_NOT_FOUND", message, userAction }
24
+ }, null, 2));
25
+ } else {
26
+ console.error(`\u2717 mm-harness last: ${message}
27
+ Next: ${userAction}`);
28
+ }
29
+ return EXIT.runtime;
30
+ }
31
+ if (json) {
32
+ console.log(JSON.stringify({
33
+ schemaVersion: 1,
34
+ command: "last",
35
+ target,
36
+ journalPath: file,
37
+ status: "pass",
38
+ exitCode: EXIT.ok,
39
+ last: record
40
+ }, null, 2));
41
+ } else {
42
+ const end = record.finishedAt ?? "still running or interrupted";
43
+ console.log(`${record.verdict.toUpperCase()} ${record.command} (exit ${record.exitCode ?? "pending"})`);
44
+ console.log(`started: ${record.startedAt}`);
45
+ console.log(`finished: ${end}`);
46
+ if (record.evidencePaths.length > 0) console.log(`evidence: ${record.evidencePaths.join(", ")}`);
47
+ }
48
+ return EXIT.ok;
49
+ }
50
+ export {
51
+ handleLast
52
+ };