@deeeed/metamask-harness 0.17.0 → 0.17.2

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 (106) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +90 -140
  3. package/adapters/core/cleanup.sh +0 -0
  4. package/adapters/core/inject.sh +0 -0
  5. package/adapters/extension/cleanup.mjs +0 -0
  6. package/adapters/extension/console-tail.mjs +51 -24
  7. package/adapters/extension/ensure-browser.sh +0 -0
  8. package/adapters/extension/inject.mjs +5 -9
  9. package/adapters/extension/launch-browser.cjs +0 -0
  10. package/adapters/extension/launch-webpack.cjs +83 -0
  11. package/adapters/extension/launch.sh +8 -11
  12. package/adapters/extension/live.sh +2 -0
  13. package/adapters/extension/readiness.mjs +0 -0
  14. package/adapters/extension/reattach.sh +1 -0
  15. package/adapters/extension/refresh-build.sh +0 -0
  16. package/adapters/extension/seed-fixture.sh +4 -12
  17. package/adapters/extension/sidepanel-toggle.sh +4 -1
  18. package/adapters/extension/snapshot-dist.sh +0 -0
  19. package/adapters/extension/stamp-runtime-title.cjs +49 -0
  20. package/adapters/extension/start-watch.sh +16 -4
  21. package/adapters/extension/stop-viewers.sh +1 -1
  22. package/adapters/extension/sync-webpack-dist.cjs +105 -0
  23. package/adapters/extension/verify.sh +1 -1
  24. package/adapters/extension/wallet-fixture-state.cjs +0 -0
  25. package/adapters/manifest.json +46 -38
  26. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +0 -0
  27. package/adapters/mobile/bridge-runtime/console-forwarder.cjs +1 -1
  28. package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +2 -3
  29. package/adapters/mobile/bridge-runtime/setup-wallet.sh +0 -0
  30. package/adapters/mobile/cleanup.sh +16 -176
  31. package/adapters/mobile/inject.sh +88 -671
  32. package/adapters/mobile/launch-console-forwarder.cjs +39 -0
  33. package/adapters/mobile/lib/metro-listener.sh +0 -0
  34. package/adapters/mobile/lib/tmux-viewer.sh +0 -0
  35. package/adapters/mobile/open-device.sh +0 -0
  36. package/adapters/mobile/prewarm-bundle.sh +0 -0
  37. package/adapters/mobile/start-console-forwarder.sh +70 -0
  38. package/adapters/mobile/start-metro.sh +0 -41
  39. package/adapters/mobile/stop-metro.sh +0 -0
  40. package/adapters/mobile/verify.sh +4 -61
  41. package/adapters/mobile/wait-for-bridge.sh +0 -0
  42. package/adapters/mobile/yarn-setup.sh +0 -0
  43. package/adapters/shared/activate-repo-node.sh +0 -0
  44. package/adapters/shared/activate-repo-ruby.sh +0 -0
  45. package/adapters/shared/cli-ux.sh +0 -0
  46. package/adapters/shared/ensure-runner-deps.sh +0 -0
  47. package/adapters/shared/harness-path.sh +0 -0
  48. package/adapters/shared/hash-helpers.sh +0 -0
  49. package/adapters/shared/install-repo-deps.sh +0 -0
  50. package/adapters/shared/json-field.sh +0 -0
  51. package/adapters/shared/open-debug.mjs +90 -36
  52. package/adapters/shared/open-log-window.sh +0 -0
  53. package/adapters/shared/reap-checkout-metros.sh +0 -0
  54. package/adapters/shared/resolve-slot-ports-core.mjs +0 -6
  55. package/adapters/shared/resolve-slot-ports.mjs +0 -0
  56. package/adapters/shared/resolve-slot-ports.sh +0 -5
  57. package/adapters/shared/sync-wallet-fixture.sh +0 -0
  58. package/adapters/shared/tmux-session.sh +0 -0
  59. package/adapters/shared/tmux-viewer.sh +0 -0
  60. package/adapters/shared/update-check-worker.mjs +37 -0
  61. package/dist/adapters/extension/console-capture.js +84 -0
  62. package/dist/adapters/extension/runtime-decision.js +14 -2
  63. package/dist/adapters/extension/surface.js +3 -2
  64. package/dist/adapters/mobile/prepare.js +22 -7
  65. package/dist/adapters/mobile/surface.js +1 -1
  66. package/dist/adapters/resolve-slot-ports.js +0 -2
  67. package/dist/adapters/slot-ports.js +8 -0
  68. package/dist/cli.js +3 -11
  69. package/dist/commands/call.js +9 -4
  70. package/dist/commands/debug.js +38 -31
  71. package/dist/commands/fixtures.js +18 -10
  72. package/dist/commands/launch/index.js +10 -0
  73. package/dist/commands/logs.js +8 -6
  74. package/dist/commands/manifest.js +5 -17
  75. package/dist/commands/run-engine.js +1 -32
  76. package/dist/commands/run-report.js +1 -1
  77. package/dist/commands/run.js +50 -3
  78. package/dist/commands/status-probe.js +1 -1
  79. package/dist/commands/update.js +15 -28
  80. package/dist/doctor.js +1 -4
  81. package/dist/harness.js +5 -123
  82. package/dist/live-adapter-contract.js +6 -20
  83. package/dist/mm-harness-cli.js +6 -43
  84. package/dist/paths.js +2 -5
  85. package/dist/run-diagnostics.js +35 -40
  86. package/dist/runtime-context.js +26 -8
  87. package/docs/CHEATSHEET.md +39 -52
  88. package/docs/CLI-ERGONOMICS-HUMAN-QA.md +48 -104
  89. package/docs/CODE-MAP.md +1 -1
  90. package/docs/VIDEO-DEMO-VALIDATION.md +62 -0
  91. package/docs/architecture.md +65 -375
  92. package/docs/live-adapter-contract.md +3 -1
  93. package/library/actions/core/perps/_controller.mjs +10 -55
  94. package/library/manifests/mobile.action-manifest.json +1 -1
  95. package/package.json +2 -2
  96. package/scripts/completions.sh +0 -0
  97. package/scripts/install-completions.sh +0 -0
  98. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +0 -185
  99. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +0 -1662
  100. package/adapters/mobile/overlay/compat/README.md +0 -28
  101. package/adapters/mobile/overlay/compat/rn81-message-event-source.patch +0 -42
  102. package/adapters/shared/resolve-farmslot-ports-core.mjs +0 -3
  103. package/adapters/shared/resolve-farmslot-ports.mjs +0 -5
  104. package/adapters/shared/resolve-farmslot-ports.sh +0 -7
  105. package/docs/CLI-SPEC.md +0 -1142
  106. package/docs/MENTAL-MODEL.md +0 -295
@@ -173,7 +173,7 @@ function runtimeDistCheck(target) {
173
173
  } catch (error) {
174
174
  if (error.code === "ENOENT") {
175
175
  try {
176
- const modified2 = filesystemSnapshotDiff(dist, runtimeDist).slice(0, 10);
176
+ const modified2 = filesystemSnapshotDiff(dist, runtimeDist).filter((relative) => !slotTitleOnlyDifference(dist, runtimeDist, relative)).slice(0, 10);
177
177
  return modified2.length > 0 ? { status: "stale", comparison: "filesystem-quick", modified: modified2 } : { status: "fresh", comparison: "filesystem-quick" };
178
178
  } catch (fallbackError) {
179
179
  return {
@@ -188,9 +188,21 @@ function runtimeDistCheck(target) {
188
188
  error: `rsync snapshot comparison failed${typeof status === "number" ? ` with exit ${status}` : ""}`
189
189
  };
190
190
  }
191
- const modified = output.split("\n").map((line) => line.trim()).filter(Boolean).slice(0, 10);
191
+ const modified = output.split("\n").map((line) => line.trim()).filter(Boolean).filter((relative) => !slotTitleOnlyDifference(dist, runtimeDist, relative)).slice(0, 10);
192
192
  return modified.length > 0 ? { status: "stale", comparison: "rsync-quick", modified } : { status: "fresh", comparison: "rsync-quick" };
193
193
  }
194
+ function slotTitleOnlyDifference(dist, runtimeDist, relative) {
195
+ if (relative !== "home.html" && relative !== "sidepanel.html") return false;
196
+ try {
197
+ const normalize = (source) => source.replace(
198
+ /<title>([^<]*)<\/title>/iu,
199
+ (_match, title) => `<title>${String(title || "MetaMask").replace(/^.+?\s+[—–-]\s+/u, "") || "MetaMask"}</title>`
200
+ );
201
+ return normalize(fs.readFileSync(path.join(dist, relative), "utf8")) === normalize(fs.readFileSync(path.join(runtimeDist, relative), "utf8"));
202
+ } catch {
203
+ return false;
204
+ }
205
+ }
194
206
  function filesystemSnapshotDiff(source, snapshot) {
195
207
  const sourceEntries = snapshotEntries(source);
196
208
  const snapshotEntriesByPath = snapshotEntries(snapshot);
@@ -65,11 +65,12 @@ const extensionSurface = {
65
65
  return [
66
66
  { label: "webpack", path: recipeRuntimePath(target, "webpack.log") },
67
67
  { label: "watcher", path: recipeRuntimePath(target, "recipe-harness-webpack.log") },
68
- { label: "rebuild", path: recipeRuntimePath(target, "rebuild.log") }
68
+ { label: "rebuild", path: recipeRuntimePath(target, "rebuild.log") },
69
+ { label: "dapp", path: recipeRuntimePath(target, "dapp-console.log") }
69
70
  ];
70
71
  },
71
72
  appLogSource(target) {
72
- return { label: "app", path: recipeRuntimePath(target, "extension-console.log") };
73
+ return { label: "extension", path: recipeRuntimePath(target, "extension-console.log") };
73
74
  },
74
75
  hints: {
75
76
  launch: "mm-harness launch",
@@ -56,14 +56,21 @@ async function prepareMobile(target, opts = {}) {
56
56
  json,
57
57
  preflightMode
58
58
  );
59
- if (confirm.status === 0) return { status: 0, output: "" };
59
+ if (confirm.status === 0) return startMobileConsoleForwarder(target, json);
60
60
  if (!json) {
61
61
  process.stderr.write(
62
62
  `launch: runtime looked ready but no ${platform} bridge target answered; launching the app.
63
63
  `
64
64
  );
65
65
  }
66
- return dispatchActionSequence(launchActions(path.resolve(target)), target, platform, json, preflightMode);
66
+ const launch = await dispatchActionSequence(
67
+ launchActions(path.resolve(target)),
68
+ target,
69
+ platform,
70
+ json,
71
+ preflightMode
72
+ );
73
+ return launch.status === 0 ? startMobileConsoleForwarder(target, json) : launch;
67
74
  }
68
75
  if (report.decision === "unknown") {
69
76
  const msg = "mobile prepare: runtime state unknown\n Next: run mm-harness verify --adapter mobile --target <checkout>";
@@ -111,7 +118,15 @@ async function prepareMobile(target, opts = {}) {
111
118
  }
112
119
  }
113
120
  }
114
- return { status: 0, output: "" };
121
+ return startMobileConsoleForwarder(target, json);
122
+ }
123
+ function startMobileConsoleForwarder(target, json) {
124
+ return dispatchAction(
125
+ { id: "start-console-forwarder", cwd: target, argv: json ? ["--quiet"] : [] },
126
+ target,
127
+ "ios",
128
+ json
129
+ );
115
130
  }
116
131
  async function dispatchActionSequence(requestedActions, target, platform, json, preflightMode) {
117
132
  const resolved = path.resolve(target);
@@ -147,6 +162,10 @@ async function dispatchAction(action, target, platform, json, preflightMode = "f
147
162
  const extra = action.argv ?? [];
148
163
  return spawnScriptStreaming(leaf, ["--target", cwd, ...extra], target);
149
164
  }
165
+ case "start-console-forwarder": {
166
+ const leaf = path.join(runnerDir, "adapters/mobile/start-console-forwarder.sh");
167
+ return spawnScriptStreaming(leaf, ["--target", cwd, ...action.argv ?? []], target);
168
+ }
150
169
  case "prewarm-bundle": {
151
170
  const leaf = path.join(runnerDir, "adapters/mobile/prewarm-bundle.sh");
152
171
  return spawnScriptStreaming(leaf, ["--platform", platform, "--target", cwd], target);
@@ -156,10 +175,6 @@ async function dispatchAction(action, target, platform, json, preflightMode = "f
156
175
  const extra = action.argv ?? [];
157
176
  return spawnScriptStreaming(leaf, ["--target", cwd, "--platform", platform, ...extra], target);
158
177
  }
159
- case "clear-metro-cache": {
160
- const leaf = path.join(runnerDir, "adapters/mobile/start-metro.sh");
161
- return spawnScriptStreaming(leaf, ["--target", cwd, "--clear"], target);
162
- }
163
178
  case "launch-mobile-runtime": {
164
179
  const leaf = path.join(runnerDir, "adapters/mobile/open-device.sh");
165
180
  return spawnScriptStreaming(
@@ -47,7 +47,7 @@ const mobileSurface = {
47
47
  return [{ label: "metro", path: recipeRuntimePath(target, "metro.log") }];
48
48
  },
49
49
  appLogSource(target) {
50
- return { label: "app", path: recipeRuntimePath(target, "metro.log") };
50
+ return { label: "app", path: recipeRuntimePath(target, "app-console.log") };
51
51
  },
52
52
  hints: {
53
53
  launch: "mm-harness launch ios",
@@ -7,7 +7,6 @@ import {
7
7
  resolveExtensionRuntimeContext,
8
8
  resolveExtensionRuntimePorts,
9
9
  resolveSlotPortsByRepo,
10
- resolveFarmslotPortsByRepo,
11
10
  resolveMobileRuntimeContext,
12
11
  resolveMobileRuntimePorts,
13
12
  resolveMobileSlotDefaults
@@ -20,7 +19,6 @@ export {
20
19
  resolveDefaultExtensionPorts,
21
20
  resolveExtensionRuntimeContext,
22
21
  resolveExtensionRuntimePorts,
23
- resolveFarmslotPortsByRepo,
24
22
  resolveMobileRuntimeContext,
25
23
  resolveMobileRuntimePorts,
26
24
  resolveMobileSlotDefaults,
@@ -105,7 +105,13 @@ function isExtensionWatcherLive(target) {
105
105
  function stopExtensionWatcher(target) {
106
106
  const runtimeAbs = path.join(path.resolve(target), recipeRuntimeDir());
107
107
  const webpackPidFile = path.join(runtimeAbs, "recipe-harness-webpack.pid");
108
+ const syncPidFile = path.join(runtimeAbs, "recipe-harness-webpack-sync.pid");
108
109
  const ownedPids = extensionWatcherPids(target);
110
+ try {
111
+ const syncPid = fs.readFileSync(syncPidFile, "utf8").trim();
112
+ if (/^\d+$/u.test(syncPid) && processAlive(Number(syncPid))) ownedPids.push(Number(syncPid));
113
+ } catch {
114
+ }
109
115
  for (const pid of ownedPids) {
110
116
  try {
111
117
  process.kill(pid, "SIGTERM");
@@ -113,6 +119,7 @@ function stopExtensionWatcher(target) {
113
119
  }
114
120
  }
115
121
  fs.rmSync(webpackPidFile, { force: true });
122
+ fs.rmSync(syncPidFile, { force: true });
116
123
  if (ownedPids.length > 0) {
117
124
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 2e3);
118
125
  for (const pid of ownedPids) {
@@ -164,6 +171,7 @@ function stopExtensionRuntime(target) {
164
171
  let removedMarkers = 0;
165
172
  for (const name of [
166
173
  "recipe-harness-webpack.pid",
174
+ "recipe-harness-webpack-sync.pid",
167
175
  "webpack.pid",
168
176
  "browser.pid",
169
177
  "chromium.pid",
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { detectAdapter, handleHarness } from "./harness.js";
2
- import { handleActions, handleManifest } from "./commands/manifest.js";
2
+ import { handleActions } from "./commands/manifest.js";
3
3
  import { handleDoctor } from "./commands/doctor.js";
4
4
  import { handleProvision } from "./commands/provision.js";
5
5
  import { handleRuntimeHealth } from "./commands/runtime-health.js";
@@ -25,7 +25,6 @@ import { handleLast } from "./commands/last.js";
25
25
  import { parseArgs, targetPath } from "./commands/parse-args.js";
26
26
  import { runOneNode } from "./commands/run-engine.js";
27
27
  const COMMANDS = {
28
- manifest: handleManifest,
29
28
  actions: handleActions,
30
29
  doctor: handleDoctor,
31
30
  "runtime-health": handleRuntimeHealth,
@@ -36,7 +35,7 @@ const COMMANDS = {
36
35
  run: handleRun,
37
36
  "self-test": handleSelfTest
38
37
  };
39
- const OVERLAY_COMMANDS = ["install", "verify", "cleanup", "live"];
38
+ const OVERLAY_COMMANDS = ["install", "verify", "cleanup"];
40
39
  function usage() {
41
40
  console.error(`mm-harness \u2014 the MetaMask recipe harness: launch the app, prove behavior, manage the runtime overlay.
42
41
  Run it from inside a MetaMask checkout; the platform (mobile | extension | core) is auto-detected.
@@ -63,9 +62,6 @@ PROVE \u2014 run recipes and inspect capabilities:
63
62
  mm-harness check diff --profile fast --artifacts-dir artifacts/validation
64
63
  actions Describe the actions a manifest declares.
65
64
  mm-harness actions --adapter mobile
66
- manifest Print/validate the action manifest for an adapter.
67
- mm-harness manifest --adapter extension --json
68
-
69
65
  RUNTIME OVERLAY \u2014 install/verify/clean the per-checkout runtime overlay:
70
66
  provision Install the cached Runway mobile dev client (artifact cache + sim install).
71
67
  mm-harness provision runway ios --adapter mobile
@@ -73,12 +69,8 @@ RUNTIME OVERLAY \u2014 install/verify/clean the per-checkout runtime overlay:
73
69
  mm-harness install # inside a checkout, auto-detected
74
70
  verify Check the overlay/runtime is present and healthy (no launch).
75
71
  mm-harness verify
76
- live Launch/reuse the app, then verify live control end-to-end.
77
- mm-harness live
78
72
  cleanup Remove the installed overlay and restore the checkout.
79
73
  mm-harness cleanup
80
- # Open sub-question (Arthur): keep these top-level (mm-harness install) or group
81
- # them under an \`overlay\` command (mm-harness overlay install)? Top-level for now.
82
74
 
83
75
  ADVANCED \u2014 internal runtime probes (rarely typed by hand):
84
76
  runtime-health runtime-decision runtime-launch
@@ -89,7 +81,7 @@ ONE bin: mm-harness is the only command. No per-platform binaries \u2014 platfor
89
81
  auto-detected, the positional target forces it (mm-harness launch ios), and
90
82
  platform-specific needs are FLAGS on the same command (e.g. --sidebar, --full-build).
91
83
  Human happy path = the bare command; agents add depth via flags (--json, --target, ports).
92
- See docs/MENTAL-MODEL.md (overview) and docs/CLI-SPEC.md (full contract).
84
+ See README.md for the current workflow and docs/CHEATSHEET.md for examples.
93
85
  `);
94
86
  }
95
87
  async function main(argv) {
@@ -33,6 +33,7 @@ import {
33
33
  validateRecipeAdapterAware
34
34
  } from "./run-engine.js";
35
35
  import { acquireCheckoutLock } from "../checkout-lock.js";
36
+ import { formatRunDiagnosticsForHuman, readRunDiagnosticsDocument } from "../run-diagnostics.js";
36
37
  async function handleCall(argv) {
37
38
  if (argv.includes("--list")) {
38
39
  const { options: options2 } = parseArgs(argv, "call");
@@ -206,15 +207,19 @@ async function handleCall(argv) {
206
207
  const rendered = callOutput !== void 0 ? `
207
208
  Result:
208
209
  ${formatCallOutput(callOutput)}` : "";
209
- const sideFindingTotal = result.sideFindings?.counts.total ?? 0;
210
210
  const out = (style, text) => color(style, text, { stream: process.stdout });
211
- const sideFindings = sideFindingTotal > 0 ? `${out("label", "Side findings:")} REVIEW ${sideFindingTotal} distinct application warning/error event(s); see ${out("path", result.diagnosticsPath ?? "diagnostics.json")} (non-blocking)
212
- ` : "";
211
+ const diagnostics = formatRunDiagnosticsForHuman(
212
+ readRunDiagnosticsDocument(result.diagnosticsPath),
213
+ adapter
214
+ );
215
+ const renderedDiagnostics = `${out("label", "Diagnostics:")}
216
+ ${diagnostics.map((line) => ` ${line}`).join("\n")}
217
+ `;
213
218
  console.log(
214
219
  `${out("label", "call")} ${out("cmd", resolvedAction)}: ${out(result.status === "pass" ? "ok" : "err", result.status)}${rendered ? `
215
220
  ${out("label", "Result:")}
216
221
  ${formatCallOutput(callOutput)}` : ""}
217
- ` + sideFindings + `${out("label", "Artifacts:")} ${out("path", result.artifactManifestPath)}` + (result.status === "fail" ? `
222
+ ` + renderedDiagnostics + `${out("label", "Artifacts:")} ${out("path", result.artifactManifestPath)}` + (result.status === "fail" ? `
218
223
  Next: ${failureUserAction}` : "")
219
224
  );
220
225
  }
@@ -1,7 +1,8 @@
1
1
  import path from "node:path";
2
2
  import { runnerDir } from "../paths.js";
3
+ import { acquireCheckoutLock } from "../checkout-lock.js";
3
4
  import { getAdapterSurface } from "../adapters/surface.js";
4
- import { ADAPTER_DETECT_NEXT, EXIT, flag, parseFlags, resolveAdapter, spawnScript, targetOf, usageOut } from "./shared.js";
5
+ import { ADAPTER_DETECT_NEXT, checkoutBusyOut, EXIT, flag, parseFlags, resolveAdapter, spawnScript, targetOf, usageOut } from "./shared.js";
5
6
  const DEBUG_BOOLEANS = /* @__PURE__ */ new Set(["worker", "devMenu", "json", "noOpen"]);
6
7
  async function handleDebug(argv) {
7
8
  const { options } = parseFlags(argv, DEBUG_BOOLEANS);
@@ -31,37 +32,43 @@ async function handleDebug(argv) {
31
32
  "mm-harness debug --worker"
32
33
  );
33
34
  }
34
- 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
- }
43
- if (json) debugArgs.push("--json");
44
- if (noOpen) debugArgs.push("--no-open");
45
- const result = spawnScript(process.execPath, [openDebug, ...debugArgs], target, json);
46
- if (json) {
47
- const leaf = parseDebugLeaf(result.output);
48
- console.log(
49
- JSON.stringify(
50
- {
51
- ...leaf,
52
- schemaVersion: 1,
53
- command: "debug",
54
- adapter,
55
- mode: worker ? "worker" : devMenu ? "dev-menu" : "default",
56
- opened: leaf.opened === true,
57
- exitCode: result.status === 0 ? EXIT.ok : EXIT.runtime
58
- },
59
- null,
60
- 2
61
- )
62
- );
35
+ const lock = acquireCheckoutLock(target, "debug");
36
+ if ("message" in lock) return checkoutBusyOut(json, "debug", lock.message, lock.path);
37
+ try {
38
+ const openDebug = path.join(runnerDir, "adapters/shared/open-debug.mjs");
39
+ const debugArgs = ["--adapter", adapter];
40
+ if (adapter === "extension") {
41
+ if (process.env.CDP_PORT) debugArgs.push("--cdp-port", process.env.CDP_PORT);
42
+ debugArgs.push("--target", worker ? "worker" : "page");
43
+ } else {
44
+ if (process.env.WATCHER_PORT) debugArgs.push("--port", process.env.WATCHER_PORT);
45
+ debugArgs.push("--action", devMenu ? "dev-menu" : "debug");
46
+ }
47
+ if (json) debugArgs.push("--json");
48
+ if (noOpen) debugArgs.push("--no-open");
49
+ const result = spawnScript(process.execPath, [openDebug, ...debugArgs], target, json);
50
+ if (json) {
51
+ const leaf = parseDebugLeaf(result.output);
52
+ console.log(
53
+ JSON.stringify(
54
+ {
55
+ ...leaf,
56
+ schemaVersion: 1,
57
+ command: "debug",
58
+ adapter,
59
+ mode: worker ? "worker" : devMenu ? "dev-menu" : "default",
60
+ opened: leaf.opened === true,
61
+ exitCode: result.status === 0 ? EXIT.ok : EXIT.runtime
62
+ },
63
+ null,
64
+ 2
65
+ )
66
+ );
67
+ }
68
+ return result.status === 0 ? EXIT.ok : EXIT.runtime;
69
+ } finally {
70
+ lock.release();
63
71
  }
64
- return result.status === 0 ? EXIT.ok : EXIT.runtime;
65
72
  }
66
73
  function parseDebugLeaf(output) {
67
74
  try {
@@ -56,6 +56,14 @@ async function handleFixturesLocked(argv, deps) {
56
56
  if (sub !== "init" && sub !== "sync" && sub !== "set" && sub !== "generate" && sub !== "finalize") {
57
57
  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
58
  }
59
+ if (sub !== "init" && (flag(options, "dev") || flag(options, "force") || str(options, "from"))) {
60
+ return usageOut(
61
+ json,
62
+ "fixtures",
63
+ "--dev, --from, and --force belong to fixtures init; they do not apply a fixture.",
64
+ "mm-harness fixtures init --dev && mm-harness fixtures set"
65
+ );
66
+ }
59
67
  const target = targetOf(options);
60
68
  const adapter = resolveAdapter(options, target);
61
69
  if (!adapter) {
@@ -96,6 +104,14 @@ async function handleFixturesLocked(argv, deps) {
96
104
  return exitCode;
97
105
  }
98
106
  const fixturePath = path.resolve(str(options, "fixture") ?? process.env.RECIPE_WALLET_FIXTURE ?? canonicalFixture);
107
+ if (!fs.existsSync(fixturePath)) {
108
+ return usageOut(
109
+ json,
110
+ "fixtures",
111
+ `no wallet fixture at ${fixturePath}.`,
112
+ "mm-harness fixtures init --dev && mm-harness fixtures set"
113
+ );
114
+ }
99
115
  process.stderr.write(`\u2192 fixtures set ${adapter} \u2014 connecting bridge + applying wallet fixture (can take ~30s)\u2026
100
116
  `);
101
117
  let status;
@@ -129,14 +145,6 @@ async function handleFixturesLocked(argv, deps) {
129
145
  console.error(` Next: ${retryHint}`);
130
146
  }
131
147
  } 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
148
  const previousFixtureEnv = process.env.RECIPE_WALLET_FIXTURE;
141
149
  process.env.RECIPE_WALLET_FIXTURE = fixturePath;
142
150
  try {
@@ -150,7 +158,7 @@ async function handleFixturesLocked(argv, deps) {
150
158
  console.error(` Next: ${retryHint}`);
151
159
  }
152
160
  }
153
- const teaching = `Wallet fixture applied. Want different accounts? Edit: ${canonicalFixture}`;
161
+ const teaching = status === "pass" ? `Wallet fixture applied. Want different accounts? Edit: ${canonicalFixture}` : "Wallet fixture was not applied.";
154
162
  if (json) {
155
163
  console.log(
156
164
  JSON.stringify(
@@ -174,7 +182,7 @@ async function handleFixturesLocked(argv, deps) {
174
182
  2
175
183
  )
176
184
  );
177
- } else {
185
+ } else if (status === "pass") {
178
186
  console.error(teaching);
179
187
  }
180
188
  return status === "pass" ? EXIT.ok : EXIT.runtime;
@@ -35,6 +35,7 @@ import { launchMobile } from "./mobile.js";
35
35
  import { applyDeviceTargeting } from "../device-target.js";
36
36
  import { acquireCheckoutLock } from "../../checkout-lock.js";
37
37
  import { JsonStreamWriter } from "../../json-stream.js";
38
+ import { ensureExtensionConsoleCapture } from "../../adapters/extension/console-capture.js";
38
39
  const LAUNCH_BOOLEANS = /* @__PURE__ */ new Set([
39
40
  "build",
40
41
  "watch",
@@ -427,6 +428,15 @@ async function finishLaunch(jsonOutput, machine, stream, adapter, mobileTarget,
427
428
  });
428
429
  }
429
430
  }
431
+ if (adapter === "extension") {
432
+ try {
433
+ await ensureExtensionConsoleCapture(target);
434
+ } catch (error) {
435
+ if (!machine) {
436
+ console.error(`WARN: Extension console capture unavailable: ${error instanceof Error ? error.message : String(error)}`);
437
+ }
438
+ }
439
+ }
430
440
  if (wantVerify) {
431
441
  stream.phase("verify");
432
442
  const verifyArgs = ["verify", "--adapter", adapter, "--target", target];
@@ -19,9 +19,10 @@ async function handleLogs(argv) {
19
19
  const logSources = surface.logSources(target);
20
20
  const appLogSource = surface.appLogSource(target);
21
21
  const sourceLabels = logSources.map((entry) => entry.label);
22
- const defaultSource = sourceLabels[0];
22
+ const appSourceLabel = appLogSource?.label ?? "app";
23
+ const defaultSource = appLogSource && appSourceLabel !== "app" ? appSourceLabel : sourceLabels[0];
23
24
  const source = str(options, "source") ?? defaultSource;
24
- const validSources = [...sourceLabels, ...appLogSource ? ["app"] : []];
25
+ const validSources = [...sourceLabels, ...appLogSource ? [appSourceLabel] : [], ...appLogSource && appSourceLabel !== "app" ? ["app"] : []];
25
26
  if (!validSources.includes(source)) {
26
27
  return usageOut(json, "logs", `--source must be one of: ${validSources.join(", ")}.`, `mm-harness logs --source ${defaultSource}`);
27
28
  }
@@ -32,11 +33,12 @@ async function handleLogs(argv) {
32
33
  }
33
34
  process.env.RECIPE_LOG_EVENTS = events;
34
35
  }
35
- const requested = source === "app" ? appLogSource : logSources.find((entry) => entry.label === source);
36
- const ordered = source === "app" ? requested ? [requested] : [] : requested ? [requested, ...logSources.filter((entry) => entry !== requested)] : logSources;
36
+ const isAppSource = source === "app" || source === appSourceLabel;
37
+ const requested = isAppSource ? appLogSource : logSources.find((entry) => entry.label === source);
38
+ const ordered = isAppSource ? requested ? [requested] : [] : requested ? [requested] : logSources;
37
39
  const logFile = ordered.find((entry) => fs.existsSync(entry.path))?.path;
38
40
  if (!logFile) {
39
- if (source === "app" && appLogSource) {
41
+ if (isAppSource && appLogSource) {
40
42
  const message = `application console unavailable for this checkout (${path.basename(appLogSource.path)} is absent).`;
41
43
  const userAction = surface.hints.launch;
42
44
  if (json) {
@@ -55,7 +57,7 @@ async function handleLogs(argv) {
55
57
  }
56
58
  return EXIT.runtime;
57
59
  }
58
- const names = source === "app" && appLogSource ? path.basename(appLogSource.path) : logSources.map((entry) => path.basename(entry.path)).join(" / ");
60
+ const names = isAppSource && appLogSource ? path.basename(appLogSource.path) : logSources.map((entry) => path.basename(entry.path)).join(" / ");
59
61
  return usageOut(
60
62
  json,
61
63
  "logs",
@@ -1,25 +1,14 @@
1
1
  import { loadActionManifest, validateManifest } from "../manifest.js";
2
2
  import { EXIT } from "./shared.js";
3
- import {
4
- actionManifestPathOption,
5
- isRecord,
6
- optionFlag,
7
- optionString,
8
- resolveAdapter
9
- } from "./parse-args.js";
10
- async function handleManifest({ options }) {
11
- const { adapter } = resolveAdapter(options);
12
- const actionManifestPath = actionManifestPathOption(options, adapter);
13
- const manifest = loadActionManifest(adapter, optionString(options, "actionManifest"));
14
- await validateManifest(manifest);
15
- if (optionFlag(options, "json")) console.log(JSON.stringify(manifest, null, 2));
16
- else console.log(actionManifestPath);
17
- return 0;
18
- }
3
+ import { isRecord, optionFlag, optionString, resolveAdapter } from "./parse-args.js";
19
4
  async function handleActions({ options, positional }) {
20
5
  const { adapter } = resolveAdapter(options);
21
6
  const manifest = loadActionManifest(adapter, optionString(options, "actionManifest"));
22
7
  await validateManifest(manifest);
8
+ if (optionFlag(options, "raw")) {
9
+ console.log(JSON.stringify(manifest, null, 2));
10
+ return EXIT.ok;
11
+ }
23
12
  const json = optionFlag(options, "json");
24
13
  const action = optionString(options, "action");
25
14
  const query = positional[0]?.trim();
@@ -226,7 +215,6 @@ export {
226
215
  findRelatedActions,
227
216
  fuzzyResolveActions,
228
217
  handleActions,
229
- handleManifest,
230
218
  searchActions,
231
219
  summarizeActionCategories
232
220
  };
@@ -200,12 +200,7 @@ function libraryRecipeNames(adapter) {
200
200
  return [...new Set(names)].sort();
201
201
  }
202
202
  function recipeNameCandidates(recipeArg, adapter) {
203
- const names = [recipeArg, ...LEGACY_RECIPE_ALIASES[recipeArg] ?? []];
204
- const candidates = [];
205
- for (const name of names) {
206
- candidates.push(...singleRecipeNameCandidates(name, adapter));
207
- }
208
- return [...new Set(candidates)];
203
+ return singleRecipeNameCandidates(recipeArg, adapter);
209
204
  }
210
205
  function singleRecipeNameCandidates(name, adapter) {
211
206
  if (name.endsWith(".recipe.json")) {
@@ -231,32 +226,6 @@ function domainPathForRecipeName(name) {
231
226
  function uniqueStrings(values) {
232
227
  return [...new Set(values)];
233
228
  }
234
- const LEGACY_RECIPE_ALIASES = {
235
- "action-validation": ["runner.action-validation"],
236
- "action-validation.extension.recipe.json": ["runner.action-validation.extension.recipe.json"],
237
- "action-validation.mobile.recipe.json": ["runner.action-validation.mobile.recipe.json"],
238
- "app-lifecycle-android-smoke": ["app.lifecycle.android-smoke"],
239
- "app-lifecycle-android-smoke.mobile.recipe.json": ["app.lifecycle.android-smoke.mobile.recipe.json"],
240
- "order-lifecycle": ["perps.order-lifecycle"],
241
- "order-lifecycle.core.recipe.json": ["perps.order-lifecycle.core.recipe.json"],
242
- "perps-lifecycle": ["perps.lifecycle"],
243
- "perps-lifecycle.recipe.json": ["perps.lifecycle.recipe.json"],
244
- "perps-performance": ["perps.performance"],
245
- "perps-performance-background-resume": ["perps.performance.background-resume"],
246
- "perps-performance-background-resume.mobile.recipe.json": ["perps.performance.background-resume.mobile.recipe.json"],
247
- "perps-performance-cold-start": ["perps.performance.cold-start"],
248
- "perps-performance-cold-start.mobile.recipe.json": ["perps.performance.cold-start.mobile.recipe.json"],
249
- "perps-performance-warm-start": ["perps.performance.warm-start"],
250
- "perps-performance-warm-start.mobile.recipe.json": ["perps.performance.warm-start.mobile.recipe.json"],
251
- "perps-performance.mobile.recipe.json": ["perps.performance.mobile.recipe.json"],
252
- "read-markets": ["perps.read-markets"],
253
- "read-markets.core.recipe.json": ["perps.read-markets.core.recipe.json"],
254
- "smoke": ["runner.smoke"],
255
- "smoke.extension.recipe.json": ["runner.smoke.extension.recipe.json"],
256
- "smoke.mobile.recipe.json": ["runner.smoke.mobile.recipe.json"],
257
- "trading-lifecycle": ["perps.trading-lifecycle"],
258
- "trading-lifecycle.core.recipe.json": ["perps.trading-lifecycle.core.recipe.json"]
259
- };
260
229
  async function validateRunRecipeStatic(recipeArg, adapter, options) {
261
230
  const librarySources = await resolveMetaMaskLibrarySources(optionString(options, "library"));
262
231
  const resolved = resolveRunRecipeArg(recipeArg, adapter, librarySources);
@@ -34,7 +34,7 @@ function renderRunReport(summary, entries) {
34
34
  lines.push(
35
35
  "",
36
36
  "## Side findings",
37
- `- REVIEW ${sideFindingTotal} distinct application warning/error event(s); see diagnostics.json (non-blocking)`
37
+ `- REVIEW ${sideFindingTotal} distinct application warning/error event(s) (non-blocking; expanded below and stored in diagnostics.json)`
38
38
  );
39
39
  }
40
40
  lines.push("", "## Steps");
@@ -31,6 +31,7 @@ import { coreDependencyBlock } from "./core-readiness.js";
31
31
  import { writeRunReport } from "./run-report.js";
32
32
  import { acquireCheckoutLock } from "../checkout-lock.js";
33
33
  import { JsonStreamWriter } from "../json-stream.js";
34
+ import { formatRunDiagnosticsForHuman, readRunDiagnosticsDocument } from "../run-diagnostics.js";
34
35
  async function handleRun(parsed) {
35
36
  const stream = new JsonStreamWriter("run", optionFlag(parsed.options, "jsonStream"));
36
37
  const restoreStdout = stream.isolateStdout();
@@ -211,15 +212,61 @@ async function handleRunInner({ positional, options }, stream) {
211
212
  console.log(out("label", "summary:"));
212
213
  for (const line of report.preview) console.log(` ${formatPreviewLine(line, out)}`);
213
214
  }
214
- console.log(`${out("label", "report:")} ${out("path", report.path)}`);
215
- console.log(`${out("label", "artifacts:")} ${out("path", result.artifactManifestPath)}`);
216
- if (result.status === "fail") console.error(` Next: ${failureUserAction}`);
215
+ const artifacts = runArtifactInventory(result.artifactManifestPath);
216
+ console.log(out("label", `artifacts (${artifacts.length}):`));
217
+ for (const artifact of artifacts) {
218
+ console.log(` ${out("dim", `${artifact.label}:`)} ${out("path", artifact.absolutePath)}`);
219
+ }
220
+ if (result.status === "fail") {
221
+ console.error(` Next: ${failureUserAction}`);
222
+ }
223
+ console.log(out("label", "diagnostics:"));
224
+ const diagnostics = readRunDiagnosticsDocument(result.diagnosticsPath);
225
+ for (const line of formatRunDiagnosticsForHuman(diagnostics, adapter)) {
226
+ console.log(` ${formatDiagnosticLine(line, out)}`);
227
+ }
217
228
  }
218
229
  return exitCode;
219
230
  } finally {
220
231
  lock.release();
221
232
  }
222
233
  }
234
+ function formatDiagnosticLine(line, out) {
235
+ const match = /^(CLEAN|REVIEW|UNAVAILABLE|N\/A|WARNING|ERROR|EXCEPTION)(.*)$/u.exec(line);
236
+ if (!match) return line;
237
+ const status = match[1];
238
+ const style = status === "CLEAN" ? "ok" : status === "ERROR" || status === "EXCEPTION" ? "err" : "warn";
239
+ return `${out(style, status)}${match[2]}`;
240
+ }
241
+ function runArtifactInventory(manifestPathValue) {
242
+ const manifestPath = path.resolve(String(manifestPathValue));
243
+ const root = path.dirname(manifestPath);
244
+ let manifest;
245
+ try {
246
+ manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
247
+ } catch {
248
+ return [{ absolutePath: manifestPath, basename: path.basename(manifestPath), label: "Artifact manifest" }];
249
+ }
250
+ const entries = isRecord(manifest) && Array.isArray(manifest.artifacts) ? manifest.artifacts : [];
251
+ const artifacts = [];
252
+ for (const entry of entries) {
253
+ if (!isRecord(entry) || typeof entry.path !== "string" || entry.path.length === 0) continue;
254
+ const absolutePath = path.resolve(root, entry.path);
255
+ const basename = path.basename(absolutePath);
256
+ const label = typeof entry.label === "string" && entry.label.length > 0 ? entry.label : basename;
257
+ artifacts.push({ absolutePath, basename, label });
258
+ }
259
+ artifacts.push({ absolutePath: manifestPath, basename: path.basename(manifestPath), label: "Artifact manifest" });
260
+ const priority = /* @__PURE__ */ new Map([
261
+ ["diagnostics.json", 0],
262
+ ["report.md", 1],
263
+ ["summary.json", 2],
264
+ ["trace.json", 3],
265
+ ["recipe.json", 4],
266
+ ["artifact-manifest.json", 100]
267
+ ]);
268
+ return artifacts.filter((artifact, index, all) => all.findIndex((candidate) => candidate.absolutePath === artifact.absolutePath) === index).sort((left, right) => (priority.get(left.basename) ?? 50) - (priority.get(right.basename) ?? 50));
269
+ }
223
270
  function formatPreviewLine(line, out) {
224
271
  const match = /^(PASS|FAIL)\s+(.+)$/u.exec(line);
225
272
  if (!match) return line;
@@ -34,7 +34,7 @@ function parseBridgeEntries(raw) {
34
34
  deviceName: typeof e.deviceName === "string" ? e.deviceName : "",
35
35
  platform: typeof e.platform === "string" ? e.platform : "",
36
36
  // agenticPresent is only set when the bridge explicitly sends it (false = absent).
37
- // Omitted by older bridge versions → leave undefined so caller treats as present.
37
+ // An omitted value remains unknown.
38
38
  ...typeof e.agenticPresent === "boolean" ? { agenticPresent: e.agenticPresent } : {}
39
39
  }));
40
40
  }