@deeeed/metamask-harness 0.17.3 → 0.17.5

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 (114) hide show
  1. package/CHANGELOG.md +6 -96
  2. package/README.md +136 -96
  3. package/adapters/extension/console-tail.mjs +24 -51
  4. package/adapters/extension/ensure-browser.sh +7 -1
  5. package/adapters/extension/inject.mjs +15 -5
  6. package/adapters/extension/launch-browser.cjs +4 -3
  7. package/adapters/extension/launch.sh +11 -8
  8. package/adapters/extension/lib/chrome-args.cjs +24 -0
  9. package/adapters/extension/live.sh +1 -2
  10. package/adapters/extension/reattach.sh +0 -1
  11. package/adapters/extension/seed-fixture.sh +12 -4
  12. package/adapters/extension/sidepanel-toggle.sh +1 -4
  13. package/adapters/extension/start-watch.sh +4 -17
  14. package/adapters/extension/stop-viewers.sh +1 -1
  15. package/adapters/extension/verify.sh +1 -1
  16. package/adapters/manifest.json +45 -53
  17. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +0 -16
  18. package/adapters/mobile/bridge-runtime/console-forwarder.cjs +1 -1
  19. package/adapters/mobile/bridge-runtime/lib/match-bridge-target.cjs +1 -3
  20. package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +3 -2
  21. package/adapters/mobile/cleanup.sh +176 -16
  22. package/adapters/mobile/inject.sh +671 -90
  23. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +185 -0
  24. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +1662 -0
  25. package/adapters/mobile/overlay/compat/README.md +28 -0
  26. package/adapters/mobile/overlay/compat/rn81-message-event-source.patch +42 -0
  27. package/adapters/mobile/start-metro.sh +41 -0
  28. package/adapters/mobile/verify.sh +61 -4
  29. package/adapters/mobile/wait-for-bridge.sh +8 -22
  30. package/adapters/shared/harness-source-fingerprint.mjs +49 -0
  31. package/adapters/shared/install-repo-deps.sh +1 -5
  32. package/adapters/shared/open-debug.mjs +184 -103
  33. package/adapters/shared/resolve-farmslot-ports-core.mjs +3 -0
  34. package/adapters/shared/resolve-farmslot-ports.mjs +5 -0
  35. package/adapters/shared/resolve-farmslot-ports.sh +7 -0
  36. package/adapters/shared/resolve-slot-ports-core.mjs +6 -0
  37. package/adapters/shared/resolve-slot-ports.sh +7 -2
  38. package/bin/mm-harness +0 -7
  39. package/dist/adapters/extension/harness-freshness.js +39 -0
  40. package/dist/adapters/extension/runtime-decision.js +4 -73
  41. package/dist/adapters/extension/surface.js +2 -3
  42. package/dist/adapters/mobile/perps-env.js +3 -43
  43. package/dist/adapters/mobile/prepare.js +7 -22
  44. package/dist/adapters/mobile/source-freshness.js +26 -41
  45. package/dist/adapters/mobile/surface.js +1 -1
  46. package/dist/adapters/resolve-slot-ports.js +2 -0
  47. package/dist/adapters/slot-ports.js +0 -16
  48. package/dist/adapters.js +5 -12
  49. package/dist/cli-commands.js +1 -1
  50. package/dist/cli.js +12 -6
  51. package/dist/commands/call.js +22 -56
  52. package/dist/commands/check.js +3 -9
  53. package/dist/commands/checklist.js +4 -1
  54. package/dist/commands/debug.js +31 -38
  55. package/dist/commands/device-target.js +12 -27
  56. package/dist/commands/doctor.js +23 -77
  57. package/dist/commands/fixtures.js +17 -27
  58. package/dist/commands/launch/extension.js +2 -0
  59. package/dist/commands/launch/index.js +59 -167
  60. package/dist/commands/logs.js +6 -8
  61. package/dist/commands/manifest.js +25 -151
  62. package/dist/commands/parse-args.js +0 -2
  63. package/dist/commands/provision.js +3 -10
  64. package/dist/commands/run-engine.js +69 -71
  65. package/dist/commands/run-report.js +3 -12
  66. package/dist/commands/run.js +35 -245
  67. package/dist/commands/shared.js +3 -55
  68. package/dist/commands/status-probe.js +1 -1
  69. package/dist/commands/status.js +1 -1
  70. package/dist/commands/stop.js +2 -7
  71. package/dist/commands/update.js +28 -16
  72. package/dist/doctor.js +4 -1
  73. package/dist/harness.js +134 -43
  74. package/dist/heal-bounds.js +1 -1
  75. package/dist/live-adapter-contract.js +20 -6
  76. package/dist/mm-harness-cli.js +48 -123
  77. package/dist/paths.js +5 -2
  78. package/dist/runner.js +2 -37
  79. package/dist/runtime-context.js +8 -26
  80. package/docs/ADAPTER-SURFACE.md +112 -19
  81. package/docs/CHEATSHEET.md +52 -39
  82. package/docs/CLI-SPEC.md +1098 -0
  83. package/docs/CODE-MAP.md +1 -1
  84. package/docs/DEBUG-HANDOVER.md +36 -0
  85. package/docs/MENTAL-MODEL.md +295 -0
  86. package/docs/UX-PRINCIPLES.md +1 -3
  87. package/docs/architecture.md +375 -65
  88. package/docs/live-adapter-contract.md +2 -4
  89. package/docs/recipe-libraries.md +275 -45
  90. package/library/README.md +0 -4
  91. package/library/actions/core/perps/_controller.mjs +55 -10
  92. package/library/actions/mobile/platform/bridge.mjs +1 -5
  93. package/library/manifests/core.action-manifest.json +7 -7
  94. package/library/manifests/mobile.action-manifest.json +3 -28
  95. package/package.json +1 -4
  96. package/adapters/extension/launch-webpack.cjs +0 -83
  97. package/adapters/extension/stamp-runtime-title.cjs +0 -49
  98. package/adapters/extension/sync-webpack-dist.cjs +0 -105
  99. package/adapters/mobile/launch-console-forwarder.cjs +0 -39
  100. package/adapters/mobile/start-console-forwarder.sh +0 -70
  101. package/adapters/shared/update-check-worker.mjs +0 -35
  102. package/dist/adapters/extension/console-capture.js +0 -93
  103. package/dist/command-contract.js +0 -441
  104. package/dist/command-journal.js +0 -225
  105. package/dist/commands/last.js +0 -52
  106. package/dist/json-stream.js +0 -57
  107. package/dist/run-diagnostics.js +0 -261
  108. package/docs/CLI-ERGONOMICS-AUDIT.md +0 -32
  109. package/docs/CLI-ERGONOMICS-HUMAN-QA.md +0 -64
  110. package/docs/VIDEO-DEMO-VALIDATION.md +0 -74
  111. package/library/recipes/runner/smoke.core.recipe.json +0 -27
  112. package/library/recipes/wallet/smoke.extension.recipe.json +0 -42
  113. package/library/recipes/wallet/smoke.mobile.recipe.json +0 -42
  114. package/scripts/validate-human-outcomes.mjs +0 -169
@@ -7,7 +7,6 @@ import { recipeRuntimePath, runnerDir } from "../../paths.js";
7
7
  import { getAdapterSurface } from "../../adapters/surface.js";
8
8
  import {
9
9
  ADAPTER_DETECT_NEXT,
10
- checkoutBusyOut,
11
10
  EXIT,
12
11
  flag,
13
12
  parseFlags,
@@ -15,8 +14,7 @@ import {
15
14
  spawnScript,
16
15
  str,
17
16
  targetOf,
18
- usageOut,
19
- writeInteractiveProgress
17
+ usageOut
20
18
  } from "../shared.js";
21
19
  import {
22
20
  RECOVERY_CODE,
@@ -34,8 +32,6 @@ import {
34
32
  import { launchMobile } from "./mobile.js";
35
33
  import { applyDeviceTargeting } from "../device-target.js";
36
34
  import { acquireCheckoutLock } from "../../checkout-lock.js";
37
- import { JsonStreamWriter } from "../../json-stream.js";
38
- import { ensureExtensionConsoleCapture } from "../../adapters/extension/console-capture.js";
39
35
  const LAUNCH_BOOLEANS = /* @__PURE__ */ new Set([
40
36
  "build",
41
37
  "watch",
@@ -43,58 +39,30 @@ const LAUNCH_BOOLEANS = /* @__PURE__ */ new Set([
43
39
  "sidepanel",
44
40
  "fullscreen",
45
41
  "runway",
46
- "json",
47
- "jsonStream"
42
+ "json"
48
43
  ]);
49
44
  const DEFAULT_EXTENSION_DAPP_URL = "https://metamask.github.io/test-dapp/";
50
45
  async function handleLaunch(argv) {
51
46
  const { options } = parseFlags(argv, LAUNCH_BOOLEANS);
52
47
  const json = flag(options, "json");
53
- const stream = new JsonStreamWriter("launch", flag(options, "jsonStream"));
54
- const jsonOutput = json && !stream.enabled;
55
- const restoreStdout = stream.isolateStdout();
56
48
  const target = targetOf(options);
49
+ if (!fs.existsSync(target)) return handleLaunchLocked(argv);
50
+ const lock = acquireCheckoutLock(target, "launch");
51
+ if ("message" in lock) {
52
+ return usageOut(json, "launch", lock.message, `wait for the current owner, or inspect ${lock.path} if its process has exited`);
53
+ }
57
54
  try {
58
- let exitCode;
59
- if (!fs.existsSync(target)) {
60
- exitCode = await handleLaunchLocked(argv, stream);
61
- } else {
62
- const lock = acquireCheckoutLock(target, "launch");
63
- if ("message" in lock) {
64
- const userAction = `wait for the current owner, or inspect ${lock.path} if its process has exited`;
65
- stream.error({ code: "SANDBOX_BUSY", message: lock.message, userAction });
66
- exitCode = checkoutBusyOut(jsonOutput, "launch", lock.message, lock.path);
67
- } else {
68
- try {
69
- exitCode = await handleLaunchLocked(argv, stream);
70
- } finally {
71
- lock.release();
72
- }
73
- }
74
- }
75
- stream.complete(exitCode === EXIT.ok ? "pass" : "fail", exitCode);
76
- return exitCode;
77
- } catch (error) {
78
- const exitCode = error !== null && typeof error === "object" && "exitCode" in error && typeof error.exitCode === "number" ? error.exitCode : EXIT.runtime;
79
- stream.error({
80
- code: exitCode === EXIT.usage ? "CLI_USAGE_ERROR" : "LAUNCH_FAILED",
81
- message: error instanceof Error ? error.message : String(error),
82
- userAction: `mm-harness doctor --target ${shellQuote(target)} --json`
83
- });
84
- stream.complete("fail", exitCode);
85
- throw error;
55
+ return await handleLaunchLocked(argv);
86
56
  } finally {
87
- restoreStdout();
57
+ lock.release();
88
58
  }
89
59
  }
90
- async function handleLaunchLocked(argv, stream) {
60
+ async function handleLaunchLocked(argv) {
91
61
  const { positional, options } = parseFlags(argv, LAUNCH_BOOLEANS);
92
62
  const json = flag(options, "json");
93
- const jsonOutput = json && !stream.enabled;
94
- const machine = json || stream.enabled;
95
63
  const target = targetOf(options);
96
64
  const heal = parseHeal(options, "auto");
97
- if (typeof heal !== "string") return launchUsage(jsonOutput, stream, heal.error, "use --heal off|infra-only|auto");
65
+ if (typeof heal !== "string") return usageOut(json, "launch", heal.error, "use --heal off|infra-only|auto");
98
66
  const posToken = positional[0];
99
67
  const mobileTargetToken = posToken === "ios" || posToken === "android" ? posToken : void 0;
100
68
  const platformFlag = str(options, "platform");
@@ -103,16 +71,15 @@ async function handleLaunchLocked(argv, stream) {
103
71
  const adapterHint = mobileTarget ? "mobile" : void 0;
104
72
  const adapter = resolveAdapter(options, target, adapterHint);
105
73
  if (!adapter) {
106
- return launchUsage(jsonOutput, stream, `could not detect the MetaMask repo type for ${target}`, ADAPTER_DETECT_NEXT);
74
+ return usageOut(json, "launch", `could not detect the MetaMask repo type for ${target}`, ADAPTER_DETECT_NEXT);
107
75
  }
108
- stream.phase("resolve", { target, adapter, platform: mobileTarget ?? null });
109
76
  if (adapter === "core") {
110
- return launchUsage(jsonOutput, stream, "core is headless; there is nothing to launch.", "mm-harness verify");
77
+ return usageOut(json, "launch", "core is headless; there is nothing to launch.", "mm-harness verify");
111
78
  }
112
79
  if (adapter === "mobile" && !mobileTarget) {
113
- return launchUsage(
114
- jsonOutput,
115
- stream,
80
+ return usageOut(
81
+ json,
82
+ "launch",
116
83
  "target is required for mobile.",
117
84
  "mm-harness launch ios or mm-harness launch android"
118
85
  );
@@ -122,15 +89,15 @@ async function handleLaunchLocked(argv, stream) {
122
89
  const wantWatch = flag(options, "watch");
123
90
  const wantRunway = flag(options, "runway");
124
91
  if (wantRunway && adapter !== "mobile") {
125
- return launchUsage(jsonOutput, stream, "runway is mobile-only.", "drop --runway for the extension");
92
+ return usageOut(json, "launch", "runway is mobile-only.", "drop --runway for the extension");
126
93
  }
127
94
  const displayMode = flag(options, "sidepanel") && !flag(options, "fullscreen") ? "sidepanel" : "fullscreen";
128
95
  for (const portFlag of ["cdpPort", "watcherPort"]) {
129
96
  const value = str(options, portFlag);
130
97
  if (value !== void 0 && !/^\d+$/u.test(value)) {
131
- return launchUsage(
132
- jsonOutput,
133
- stream,
98
+ return usageOut(
99
+ json,
100
+ "launch",
134
101
  `--${portFlag === "cdpPort" ? "cdp-port" : "watcher-port"} must be numeric (got: ${value}).`,
135
102
  `pass a numeric port, e.g. --${portFlag === "cdpPort" ? "cdp-port 6663" : "watcher-port 8081"}`
136
103
  );
@@ -138,7 +105,7 @@ async function handleLaunchLocked(argv, stream) {
138
105
  }
139
106
  const envResult = applyLaunchEnvOverrides(options, adapter, mobileTarget, target);
140
107
  if (envResult && "code" in envResult) {
141
- return launchUsage(jsonOutput, stream, envResult.message, envResult.userAction);
108
+ return usageOut(json, "launch", envResult.message, "mm-harness launch android --device <adb-serial|device-name>");
142
109
  }
143
110
  const tier = wantBuild ? "build" : "quick";
144
111
  if (adapter === "extension") {
@@ -147,55 +114,44 @@ async function handleLaunchLocked(argv, stream) {
147
114
  process.env.EXTENSION_START_URL = requestedUrl || DEFAULT_EXTENSION_DAPP_URL;
148
115
  }
149
116
  }
150
- if (!machine && adapter === "extension") {
117
+ if (!json && adapter === "extension") {
151
118
  const modeNote = displayMode === "sidepanel" ? `sidepanel \xB7 dapp ${process.env.EXTENSION_START_URL ?? DEFAULT_EXTENSION_DAPP_URL}` : "fullscreen";
152
119
  const workNote = wantWatch ? "watch only" : tier === "build" ? "clean build" : "quick reuse probe";
153
120
  console.error(
154
121
  `\u2192 extension launch \u2014 ${modeNote} \xB7 CDP :${process.env.CDP_PORT ?? "default"} \xB7 ${workNote}`
155
122
  );
156
123
  }
157
- if (adapter === "mobile") {
158
- const device = str(options, "device") ?? (mobileTarget === "android" ? process.env.ADB_SERIAL ?? process.env.ANDROID_SERIAL : process.env.IOS_SIMULATOR ?? process.env.SIM_UDID) ?? "configured device";
159
- const watcherPort = str(options, "watcherPort") ?? process.env.WATCHER_PORT ?? "auto";
160
- writeInteractiveProgress(
161
- machine,
162
- `\u2192 mobile launch \u2014 ${mobileTarget} \xB7 ${tier === "build" ? "native build" : "quick readiness"} \xB7 ${device} \xB7 Metro ${watcherPort === "auto" ? "auto" : `:${watcherPort}`}`
163
- );
164
- }
165
124
  const state = newHealState();
166
125
  if (tier === "quick" && nativeInputsChanged(target, adapter)) {
167
- return launchUsage(
168
- jsonOutput,
169
- stream,
126
+ return usageOut(
127
+ json,
128
+ "launch",
170
129
  "native build inputs changed since last build.",
171
130
  `mm-harness launch ${adapter === "mobile" ? `${mobileTarget} ` : ""}--build`
172
131
  );
173
132
  }
174
- stream.phase("install");
175
- const ensured = await ensureOverlay(adapter, target, heal, state, machine);
133
+ const ensured = await ensureOverlay(adapter, target, heal, state, json);
176
134
  if (!ensured.ok) {
177
- return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
135
+ return launchFail(json, adapter, mobileTarget, tier, state, {
178
136
  code: "OVERLAY_INSTALL_FAILED",
179
137
  message: ensured.error ?? "overlay install failed",
180
138
  recoverable: false,
181
139
  exitCode: EXIT.infra
182
140
  });
183
141
  }
184
- stream.phase("healthcheck");
185
142
  if (adapter === "extension") {
186
143
  const willBuild = wantWatch || tier === "build" || !await extensionRuntimeReusable(target);
187
144
  if (willBuild) {
188
145
  const depsBlock = extensionDepsBlock(target);
189
146
  if (depsBlock) {
190
147
  if (heal === "off") {
191
- return launchUsage(jsonOutput, stream, depsBlock.message, depsBlock.userAction);
148
+ return usageOut(json, "launch", depsBlock.message, depsBlock.userAction);
192
149
  }
193
150
  const recoveryCode2 = "deps.installed";
194
151
  state.attemptedRecoveries.push(recoveryCode2);
195
- stream.phase("recover");
196
152
  const repaired = await installExtensionDeps(target);
197
153
  if (repaired.status !== 0 || extensionDepsBlock(target)) {
198
- return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
154
+ return launchFail(json, adapter, mobileTarget, tier, state, {
199
155
  code: "DEPENDENCY_INSTALL_FAILED",
200
156
  message: depsBlock.message,
201
157
  recoverable: false,
@@ -209,13 +165,12 @@ async function handleLaunchLocked(argv, stream) {
209
165
  }
210
166
  }
211
167
  }
212
- stream.phase("launch");
213
- let attempt = await executeComposition(adapter, mobileTarget, tier, wantWatch, target, machine, displayMode);
168
+ let attempt = await executeComposition(adapter, mobileTarget, tier, wantWatch, target, json, displayMode);
214
169
  if (attempt.status === 0) {
215
- return await finishLaunch(jsonOutput, machine, stream, adapter, mobileTarget, tier, displayMode, target, state, wantWatch, wantVerify);
170
+ return await finishLaunch(json, adapter, mobileTarget, tier, displayMode, target, state, wantWatch, wantVerify);
216
171
  }
217
172
  if (heal === "off") {
218
- return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
173
+ return launchFail(json, adapter, mobileTarget, tier, state, {
219
174
  code: "LAUNCH_FAILED",
220
175
  message: `launch ${adapter}${mobileTarget ? ` ${mobileTarget}` : ""} failed (healing off)`,
221
176
  recoverable: false,
@@ -225,13 +180,12 @@ async function handleLaunchLocked(argv, stream) {
225
180
  if (adapter === "extension" && tier === "quick" && extensionQuickReattachFailed(attempt.output)) {
226
181
  const recoveryCode2 = "chrome.relaunched";
227
182
  state.attemptedRecoveries.push(recoveryCode2);
228
- stream.phase("recover");
229
- const rebuildAttempt = await executeComposition(adapter, mobileTarget, "build", wantWatch, target, machine, displayMode);
183
+ const rebuildAttempt = await executeComposition(adapter, mobileTarget, "build", wantWatch, target, json, displayMode);
230
184
  if (rebuildAttempt.status === 0) {
231
185
  state.recovered.push(recoveryCode2);
232
- return await finishLaunch(jsonOutput, machine, stream, adapter, mobileTarget, tier, displayMode, target, state, wantWatch, wantVerify);
186
+ return await finishLaunch(json, adapter, mobileTarget, tier, displayMode, target, state, wantWatch, wantVerify);
233
187
  }
234
- return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
188
+ return launchFail(json, adapter, mobileTarget, tier, state, {
235
189
  code: "EXTENSION_RELAUNCH_FAILED",
236
190
  message: "quick reattach failed, and clean relaunch failed too.",
237
191
  recoverable: false,
@@ -240,7 +194,7 @@ async function handleLaunchLocked(argv, stream) {
240
194
  });
241
195
  }
242
196
  if (adapter === "mobile" && mobileProvisioningBlocked(attempt.output)) {
243
- return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
197
+ return launchFail(json, adapter, mobileTarget, tier, state, {
244
198
  code: "MOBILE_PROVISION_REQUIRED",
245
199
  message: `mobile ${mobileTarget ?? "runtime"} is not provisioned for this slot.`,
246
200
  recoverable: false,
@@ -251,7 +205,7 @@ async function handleLaunchLocked(argv, stream) {
251
205
  }
252
206
  const bound = checkHealBounds(target, attempt.output, state);
253
207
  if (bound !== null) {
254
- return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
208
+ return launchFail(json, adapter, mobileTarget, tier, state, {
255
209
  code: bound.code,
256
210
  message: bound.message,
257
211
  recoverable: false,
@@ -262,13 +216,12 @@ async function handleLaunchLocked(argv, stream) {
262
216
  }
263
217
  const recoveryCode = RECOVERY_CODE[adapter];
264
218
  state.attemptedRecoveries.push(recoveryCode);
265
- stream.phase("recover");
266
- attempt = await executeComposition(adapter, mobileTarget, tier, wantWatch, target, machine, displayMode);
219
+ attempt = await executeComposition(adapter, mobileTarget, tier, wantWatch, target, json, displayMode);
267
220
  if (attempt.status === 0) {
268
221
  state.recovered.push(recoveryCode);
269
- return await finishLaunch(jsonOutput, machine, stream, adapter, mobileTarget, tier, displayMode, target, state, wantWatch, wantVerify);
222
+ return await finishLaunch(json, adapter, mobileTarget, tier, displayMode, target, state, wantWatch, wantVerify);
270
223
  }
271
- return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
224
+ return launchFail(json, adapter, mobileTarget, tier, state, {
272
225
  code: "SAME_RECOVERY_TWICE",
273
226
  message: `launch ${adapter}${mobileTarget ? ` ${mobileTarget}` : ""} failed again after ${recoveryCode} recovery \u2014 refusing to loop.`,
274
227
  recoverable: false,
@@ -321,16 +274,14 @@ function applyLaunchEnvOverrides(options, adapter, mobileTarget, target) {
321
274
  return {
322
275
  ok: false,
323
276
  code: "DEVICE_WRONG_PLATFORM",
324
- message: `--device ${device} did not resolve to an Android device for launch android.`,
325
- userAction: `mm-harness status --target ${shellQuote(target)} --all-devices --json`
277
+ message: `--device ${device} did not resolve to an Android device for launch android.`
326
278
  };
327
279
  }
328
280
  if (mobileTarget === "ios" && device && !process.env.IOS_SIMULATOR) {
329
281
  return {
330
282
  ok: false,
331
283
  code: "DEVICE_WRONG_PLATFORM",
332
- message: `--device ${device} did not resolve to an iOS simulator for launch ios.`,
333
- userAction: `mm-harness status --target ${shellQuote(target)} --all-devices --json`
284
+ message: `--device ${device} did not resolve to an iOS simulator for launch ios.`
334
285
  };
335
286
  }
336
287
  }
@@ -376,29 +327,26 @@ async function executeComposition(adapter, mobileTarget, tier, wantWatch, target
376
327
  }
377
328
  return launchExtension(target, tier, wantWatch, displayMode);
378
329
  }
379
- async function finishLaunch(jsonOutput, machine, stream, adapter, mobileTarget, tier, displayMode, target, state, wantWatch, wantVerify, sidepanelRecoveryAllowed = true) {
330
+ async function finishLaunch(json, adapter, mobileTarget, tier, displayMode, target, state, wantWatch, wantVerify, sidepanelRecoveryAllowed = true) {
380
331
  if (adapter === "extension" && displayMode === "sidepanel") {
381
332
  const sidepanelSh = path.join(runnerDir, "adapters/extension/sidepanel-toggle.sh");
382
333
  const sidepanelArgs = ["open"];
383
334
  if (process.env.CDP_PORT) sidepanelArgs.push("--cdp-port", process.env.CDP_PORT);
384
- if (!machine) {
335
+ if (!json) {
385
336
  console.error(
386
337
  `[sidepanel] opening panel beside ${process.env.EXTENSION_START_URL ?? DEFAULT_EXTENSION_DAPP_URL}`
387
338
  );
388
339
  }
389
- const sidepanel = spawnScript(sidepanelSh, sidepanelArgs, target, machine, { REPO: target });
340
+ const sidepanel = spawnScript(sidepanelSh, sidepanelArgs, target, json, { REPO: target });
390
341
  if (sidepanel.status !== 0) {
391
342
  const recoveryCode = "chrome.relaunched";
392
343
  if (sidepanelRecoveryAllowed && tier === "quick" && extensionRuntimeBlocked(sidepanel.output) && !state.attemptedRecoveries.includes(recoveryCode)) {
393
344
  state.attemptedRecoveries.push(recoveryCode);
394
- stream.phase("recover");
395
- const rebuildAttempt = await executeComposition(adapter, mobileTarget, "build", wantWatch, target, machine, displayMode);
345
+ const rebuildAttempt = await executeComposition(adapter, mobileTarget, "build", wantWatch, target, json, displayMode);
396
346
  if (rebuildAttempt.status === 0) {
397
347
  state.recovered.push(recoveryCode);
398
348
  return await finishLaunch(
399
- jsonOutput,
400
- machine,
401
- stream,
349
+ json,
402
350
  adapter,
403
351
  mobileTarget,
404
352
  tier,
@@ -410,7 +358,7 @@ async function finishLaunch(jsonOutput, machine, stream, adapter, mobileTarget,
410
358
  false
411
359
  );
412
360
  }
413
- return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
361
+ return launchFail(json, adapter, mobileTarget, tier, state, {
414
362
  code: "EXTENSION_RELAUNCH_FAILED",
415
363
  message: "sidepanel open found a blocked extension runtime, and clean relaunch failed too.",
416
364
  recoverable: false,
@@ -418,7 +366,7 @@ async function finishLaunch(jsonOutput, machine, stream, adapter, mobileTarget,
418
366
  originalError: rebuildAttempt.output.trim() || void 0
419
367
  });
420
368
  }
421
- return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
369
+ return launchFail(json, adapter, mobileTarget, tier, state, {
422
370
  code: "SIDEPANEL_OPEN_FAILED",
423
371
  message: "app launched but opening the side panel failed.",
424
372
  recoverable: false,
@@ -428,48 +376,16 @@ async function finishLaunch(jsonOutput, machine, stream, adapter, mobileTarget,
428
376
  });
429
377
  }
430
378
  }
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
- }
440
379
  if (wantVerify) {
441
- stream.phase("verify");
442
380
  const verifyArgs = ["verify", "--adapter", adapter, "--target", target];
443
381
  if (adapter === "mobile" && mobileTarget) verifyArgs.push("--platform", mobileTarget);
444
- if (machine) verifyArgs.push("--json");
445
- const exitCode = await handleHarness(verifyArgs);
446
- if (stream.enabled) {
447
- for (const mutation of state.mutations) stream.mutation(mutation);
448
- for (const recovery of state.recovered) stream.recovery(recovery);
449
- stream.complete(exitCode === EXIT.ok ? "pass" : "fail", exitCode, {
450
- adapter,
451
- platform: mobileTarget ?? (adapter === "extension" ? displayMode : null),
452
- tier,
453
- recovered: state.recovered,
454
- mutations: state.mutations
455
- });
456
- }
457
- return exitCode;
382
+ if (json) verifyArgs.push("--json");
383
+ return handleHarness(verifyArgs);
458
384
  }
459
- return launchPass(jsonOutput, stream, adapter, mobileTarget, tier, displayMode, state);
385
+ return launchPass(json, adapter, mobileTarget, tier, displayMode, state);
460
386
  }
461
- function launchPass(json, stream, adapter, mobileTarget, tier, displayMode, state) {
462
- if (stream.enabled) {
463
- for (const mutation of state.mutations) stream.mutation(mutation);
464
- for (const recovery of state.recovered) stream.recovery(recovery);
465
- stream.complete("pass", EXIT.ok, {
466
- adapter,
467
- platform: mobileTarget ?? (adapter === "extension" ? displayMode : null),
468
- tier,
469
- recovered: state.recovered,
470
- mutations: state.mutations
471
- });
472
- } else if (json) {
387
+ function launchPass(json, adapter, mobileTarget, tier, displayMode, state) {
388
+ if (json) {
473
389
  console.log(
474
390
  JSON.stringify(
475
391
  {
@@ -507,32 +423,8 @@ function launchDeviceLabel(mobileTarget) {
507
423
  }
508
424
  return process.env.IOS_SIMULATOR || process.env.ADB_SERIAL || "booted device";
509
425
  }
510
- function launchUsage(json, stream, message, userAction) {
511
- stream.error({ code: "USAGE", message, userAction });
512
- return usageOut(json, "launch", message, userAction);
513
- }
514
- function launchFail(json, stream, adapter, mobileTarget, tier, state, target, failure) {
515
- const userAction = failure.userAction ?? `mm-harness doctor --fix --adapter ${adapter} --target ${shellQuote(target)} --json`;
516
- if (stream.enabled) {
517
- for (const mutation of state.mutations) stream.mutation(mutation);
518
- for (const recovery of state.recovered) stream.recovery(recovery);
519
- stream.error({
520
- code: failure.code,
521
- message: failure.message,
522
- retryable: failure.recoverable,
523
- userAction,
524
- originalError: failure.originalError ?? null
525
- });
526
- stream.complete("fail", failure.exitCode, {
527
- adapter,
528
- platform: mobileTarget ?? null,
529
- tier,
530
- recovered: state.recovered,
531
- mutations: state.mutations,
532
- recoverable: failure.recoverable,
533
- attemptedRecoveries: state.attemptedRecoveries
534
- });
535
- } else if (json) {
426
+ function launchFail(json, adapter, mobileTarget, tier, state, failure) {
427
+ if (json) {
536
428
  console.log(
537
429
  JSON.stringify(
538
430
  {
@@ -552,7 +444,7 @@ function launchFail(json, stream, adapter, mobileTarget, tier, state, target, fa
552
444
  code: failure.code,
553
445
  message: failure.message,
554
446
  retryable: failure.recoverable,
555
- userAction,
447
+ userAction: failure.userAction ?? null,
556
448
  originalError: failure.originalError ?? null
557
449
  }
558
450
  },
@@ -565,8 +457,8 @@ function launchFail(json, stream, adapter, mobileTarget, tier, state, target, fa
565
457
  `\u2717 launch ${adapter}${mobileTarget ? ` ${mobileTarget}` : ""} failed
566
458
  ${failure.message}` + (failure.originalError ? `
567
459
  --- original failure ---
568
- ${failure.originalError}` : "") + `
569
- Next: ${userAction}`
460
+ ${failure.originalError}` : "") + (failure.userAction ? `
461
+ Next: ${failure.userAction}` : "")
570
462
  );
571
463
  }
572
464
  return failure.exitCode;
@@ -19,10 +19,9 @@ 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 appSourceLabel = appLogSource?.label ?? "app";
23
- const defaultSource = appLogSource && appSourceLabel !== "app" ? appSourceLabel : sourceLabels[0];
22
+ const defaultSource = sourceLabels[0];
24
23
  const source = str(options, "source") ?? defaultSource;
25
- const validSources = [...sourceLabels, ...appLogSource ? [appSourceLabel] : [], ...appLogSource && appSourceLabel !== "app" ? ["app"] : []];
24
+ const validSources = [...sourceLabels, ...appLogSource ? ["app"] : []];
26
25
  if (!validSources.includes(source)) {
27
26
  return usageOut(json, "logs", `--source must be one of: ${validSources.join(", ")}.`, `mm-harness logs --source ${defaultSource}`);
28
27
  }
@@ -33,12 +32,11 @@ async function handleLogs(argv) {
33
32
  }
34
33
  process.env.RECIPE_LOG_EVENTS = events;
35
34
  }
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;
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;
39
37
  const logFile = ordered.find((entry) => fs.existsSync(entry.path))?.path;
40
38
  if (!logFile) {
41
- if (isAppSource && appLogSource) {
39
+ if (source === "app" && appLogSource) {
42
40
  const message = `application console unavailable for this checkout (${path.basename(appLogSource.path)} is absent).`;
43
41
  const userAction = surface.hints.launch;
44
42
  if (json) {
@@ -57,7 +55,7 @@ async function handleLogs(argv) {
57
55
  }
58
56
  return EXIT.runtime;
59
57
  }
60
- const names = isAppSource && appLogSource ? path.basename(appLogSource.path) : logSources.map((entry) => path.basename(entry.path)).join(" / ");
58
+ const names = source === "app" && appLogSource ? path.basename(appLogSource.path) : logSources.map((entry) => path.basename(entry.path)).join(" / ");
61
59
  return usageOut(
62
60
  json,
63
61
  "logs",