@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
package/dist/doctor.js CHANGED
@@ -93,7 +93,10 @@ function fixtureFileSummary(file, adapter, reportedPath = file) {
93
93
  const RUNTIME_CONTEXT_FIELDS = [
94
94
  { key: "slotId", envVars: ["RECIPE_SLOT_ID"], envVar: "RECIPE_SLOT_ID", customize: "doctor --fix creates local identity; Farmslot may override", adapters: ["mobile", "extension", "core"] },
95
95
  { key: "extensionId", envVars: ["RECIPE_HARNESS_EXTENSION_ID"], envVar: "RECIPE_HARNESS_EXTENSION_ID", customize: "resolved from the built extension", adapters: ["extension"] },
96
- { key: "cdpPort", envVars: ["RECIPE_CDP_PORT", "CDP_PORT"], envVar: "CDP_PORT", customize: "doctor --fix claims one; --cdp-port overrides", adapters: ["extension"] }
96
+ { key: "cdpPort", envVars: ["RECIPE_CDP_PORT", "CDP_PORT"], envVar: "CDP_PORT", customize: "doctor --fix claims one; --cdp-port overrides", adapters: ["extension"] },
97
+ { key: "runtimeStart.approved", envVars: ["RECIPE_RUNTIME_START_APPROVED"], envVar: "RECIPE_RUNTIME_START_APPROVED", customize: "optional orchestrator-provided legacy live context", adapters: ["mobile", "extension"] },
98
+ { key: "runtimeStart.command", envVars: [], envVar: null, customize: "optional orchestrator-provided legacy live context", adapters: ["mobile", "extension"] },
99
+ { key: "runtimeStart.readyUrl", envVars: ["RECIPE_RUNTIME_READY_URL"], envVar: "RECIPE_RUNTIME_READY_URL", customize: "optional orchestrator-provided legacy live context", adapters: ["mobile", "extension"] }
97
100
  ];
98
101
  function runtimeContextSummary(target, adapter) {
99
102
  const contextPath = resolveRuntimeContextPath(target);
package/dist/harness.js CHANGED
@@ -4,8 +4,13 @@ import path from "node:path";
4
4
  import { resolveLeafInvoke, shellLeafMissing, missingShellLeafMessage } from "./leaf-invoke.js";
5
5
  import { resolveExtensionSlotPorts } from "./adapters/slot-ports.js";
6
6
  import { recipeHarnessPath, recipeRuntimeDir, runnerDir } from "./paths.js";
7
- const HARNESS_ACTIONS = ["install", "verify", "cleanup"];
7
+ import { prepareMobile } from "./adapters/mobile/prepare.js";
8
+ const HARNESS_ACTIONS = ["install", "verify", "cleanup", "live"];
8
9
  const ADAPTERS = ["mobile", "extension", "core"];
10
+ function isValidAdapterAction(adapter, action) {
11
+ if (adapter === "core") return action === "install" || action === "verify" || action === "cleanup";
12
+ return true;
13
+ }
9
14
  function harnessUsage() {
10
15
  console.error(`mm-harness \u2014 install and validate the MetaMask recipe runtime in a checkout.
11
16
 
@@ -45,12 +50,6 @@ function failureHint(adapter, action) {
45
50
  const helper = adapter === "mobile" ? "mm-harness launch ios # or launch android" : "mm-harness launch";
46
51
  return `Read the error above for the specific cause. To (re)start the runtime, run: ${helper}`;
47
52
  }
48
- function successNext(adapter, action, target) {
49
- const base = `--adapter ${adapter} --target ${shellQuote(target)}`;
50
- if (action === "install") return `mm-harness verify ${base}`;
51
- if (action === "cleanup") return `mm-harness install ${base}`;
52
- return `mm-harness status --target ${shellQuote(target)} --json`;
53
- }
54
53
  function hasArg(args, needle) {
55
54
  return args.some((arg) => arg === needle || arg.startsWith(`${needle}=`));
56
55
  }
@@ -71,7 +70,6 @@ function parseHarnessArgs(args) {
71
70
  const forward = [];
72
71
  let adapter;
73
72
  let json = false;
74
- let quiet = false;
75
73
  let separator = false;
76
74
  for (let i = 0; i < args.length; i += 1) {
77
75
  const arg = args[i];
@@ -87,10 +85,6 @@ function parseHarnessArgs(args) {
87
85
  json = true;
88
86
  continue;
89
87
  }
90
- if (arg === "--quiet") {
91
- quiet = true;
92
- continue;
93
- }
94
88
  if (arg === "--adapter" || arg === "--platform") {
95
89
  const value = args[i + 1];
96
90
  if (arg === "--adapter") {
@@ -121,7 +115,7 @@ function parseHarnessArgs(args) {
121
115
  }
122
116
  forward.push(arg);
123
117
  }
124
- return { adapter, json, quiet, forward };
118
+ return { adapter, json, forward };
125
119
  }
126
120
  const ADAPTER_DETECT_NEXT = "cd into a MetaMask checkout or pass --target <path>, or force it with --adapter <mobile|extension|core>";
127
121
  function detectAdapter(target) {
@@ -167,7 +161,7 @@ function readRuntimeContextField(contextPath, field) {
167
161
  if (node === void 0 || node === null || node === "") return void 0;
168
162
  return String(node);
169
163
  }
170
- function applyExtensionRuntimeEnv(target, args) {
164
+ function applyExtensionRuntimeEnv(target, action, args) {
171
165
  const contextPath = resolveRuntimeContextPath(target);
172
166
  const contextExists = fs.existsSync(contextPath);
173
167
  if (contextExists) {
@@ -180,6 +174,19 @@ function applyExtensionRuntimeEnv(target, args) {
180
174
  const extensionId = readRuntimeContextField(contextPath, "extensionId");
181
175
  if (extensionId) process.env.RECIPE_HARNESS_EXTENSION_ID = extensionId;
182
176
  }
177
+ if (process.env.RECIPE_RUNTIME_START_APPROVED === void 0) {
178
+ const approved = readRuntimeContextField(contextPath, "runtimeStart.approved");
179
+ if (approved === "true" || approved === "True" || approved === "1") process.env.RECIPE_RUNTIME_START_APPROVED = "1";
180
+ else if (approved === "false" || approved === "False" || approved === "0") process.env.RECIPE_RUNTIME_START_APPROVED = "0";
181
+ }
182
+ if (!process.env.RECIPE_RUNTIME_START_CMD) {
183
+ const command = readRuntimeContextField(contextPath, "runtimeStart.command");
184
+ if (command) process.env.RECIPE_RUNTIME_START_CMD = command;
185
+ }
186
+ if (!process.env.RECIPE_RUNTIME_READY_URL) {
187
+ const readyUrl = readRuntimeContextField(contextPath, "runtimeStart.readyUrl");
188
+ if (readyUrl) process.env.RECIPE_RUNTIME_READY_URL = readyUrl;
189
+ }
183
190
  } else {
184
191
  delete process.env.RECIPE_RUNTIME_CONTEXT;
185
192
  }
@@ -193,6 +200,11 @@ function applyExtensionRuntimeEnv(target, args) {
193
200
  result = [...result, "--cdp-port", cdpPort];
194
201
  }
195
202
  }
203
+ if (action === "live" && !hasArg(result, "--prepare-cmd")) {
204
+ if (process.env.RECIPE_RUNTIME_START_APPROVED === "1" && process.env.RECIPE_RUNTIME_START_CMD) {
205
+ result = [...result, "--prepare-cmd", process.env.RECIPE_RUNTIME_START_CMD];
206
+ }
207
+ }
196
208
  return result;
197
209
  }
198
210
  function resolveEntry(base, candidates, mode) {
@@ -269,6 +281,81 @@ Next: mm-harness install --platform core --target ${target}`
269
281
  );
270
282
  return { command, prefixArgs: [] };
271
283
  }
284
+ async function handleMobileLive(target, forwardArgs, json, autoDetected) {
285
+ const platform = argValue(forwardArgs, "--platform") ?? "ios";
286
+ const watcherPortStr = argValue(forwardArgs, "--watcher-port") ?? process.env.WATCHER_PORT;
287
+ const watcherPort = watcherPortStr ? parseInt(watcherPortStr, 10) : void 0;
288
+ const start = Date.now();
289
+ if (!json) {
290
+ const detected = autoDetected ? ", auto-detected" : "";
291
+ console.error(`\u2192 live (mobile${detected}) \u2014 target: ${target}`);
292
+ }
293
+ const prepResult = await prepareMobile(target, { platform, json, watcherPort });
294
+ if (prepResult.status !== 0) {
295
+ const elapsed2 = ((Date.now() - start) / 1e3).toFixed(1);
296
+ if (json) {
297
+ console.log(
298
+ harnessSummary("live", "mobile", target, "fail", prepResult.status, autoDetected, {
299
+ code: "MOBILE_PREPARE_FAILED",
300
+ message: `mobile prepare failed (exit ${prepResult.status})`,
301
+ userAction: failureHint("mobile", "live")
302
+ })
303
+ );
304
+ } else {
305
+ console.error(
306
+ `\u2717 live mobile failed (exit ${prepResult.status}, ${elapsed2}s)
307
+ ${failureHint("mobile", "live")}`
308
+ );
309
+ }
310
+ return prepResult.status;
311
+ }
312
+ const installedVerify = path.join(recipeHarnessPath(target, "mobile"), "scripts", "verify.sh");
313
+ const verifySh = fs.existsSync(installedVerify) ? installedVerify : resolveEntry(runnerDir, ["adapters/mobile/verify.sh", "scripts/mobile/verify.sh"], "file");
314
+ const verifyArgs = hasArg(forwardArgs, "--no-auto-start") ? [...forwardArgs] : ["--no-auto-start", ...forwardArgs];
315
+ if (shellLeafMissing(verifySh)) {
316
+ const message = missingShellLeafMessage(verifySh);
317
+ if (json) {
318
+ console.log(
319
+ harnessSummary("live", "mobile", target, "fail", 1, autoDetected, {
320
+ code: "HARNESS_SPAWN_FAILED",
321
+ message,
322
+ userAction: failureHint("mobile", "live")
323
+ })
324
+ );
325
+ } else {
326
+ console.error(`\u2717 ${message}`);
327
+ }
328
+ return 1;
329
+ }
330
+ const { bin: verifyBin, args: verifySpawnArgs } = resolveLeafInvoke(verifySh, verifyArgs);
331
+ const result = spawnSync(verifyBin, verifySpawnArgs, {
332
+ stdio: json ? ["inherit", 2, "inherit"] : "inherit",
333
+ env: process.env
334
+ });
335
+ const elapsed = ((Date.now() - start) / 1e3).toFixed(1);
336
+ const exitCode = result.status ?? 1;
337
+ if (json) {
338
+ console.log(
339
+ harnessSummary(
340
+ "live",
341
+ "mobile",
342
+ target,
343
+ exitCode === 0 ? "pass" : "fail",
344
+ exitCode,
345
+ autoDetected,
346
+ exitCode === 0 ? void 0 : { code: "MOBILE_VERIFY_FAILED", message: `mobile live verify failed (exit ${exitCode})`, userAction: failureHint("mobile", "live") }
347
+ )
348
+ );
349
+ } else if (exitCode === 0) {
350
+ console.error(`\u2713 live mobile passed (${elapsed}s)`);
351
+ } else {
352
+ console.error(
353
+ `\u2717 live mobile failed (exit ${exitCode}, ${elapsed}s)
354
+ ${failureHint("mobile", "live")}`
355
+ );
356
+ }
357
+ return exitCode;
358
+ }
272
359
  async function handleHarness(argv) {
273
360
  const action = argv[0];
274
361
  if (!action || action === "-h" || action === "--help") {
@@ -281,14 +368,12 @@ async function handleHarness(argv) {
281
368
  return 2;
282
369
  }
283
370
  const harnessAction = action;
284
- const { adapter: parsedAdapter, json, quiet, forward } = parseHarnessArgs(argv.slice(1));
371
+ const { adapter: parsedAdapter, json, forward } = parseHarnessArgs(argv.slice(1));
285
372
  const rawTarget = argValue(forward, "--target");
286
373
  const target = path.resolve(rawTarget ?? process.cwd());
287
374
  const adapter = parsedAdapter ?? detectAdapter(target);
288
375
  if (!adapter || !isAdapter(adapter)) {
289
- if (quiet) {
290
- return 2;
291
- } else if (json) {
376
+ if (json) {
292
377
  const detectError = adapter ? { code: "UNSUPPORTED_PLATFORM", message: `unsupported platform: ${adapter}`, userAction: "pass --adapter <mobile|extension|core> to specify a supported adapter" } : { code: "ADAPTER_DETECTION_FAILED", message: `could not detect the MetaMask repo type for ${target}`, userAction: ADAPTER_DETECT_NEXT };
293
378
  console.log(harnessSummary(harnessAction, void 0, target, "fail", 2, false, detectError));
294
379
  } else {
@@ -302,19 +387,36 @@ async function handleHarness(argv) {
302
387
  }
303
388
  return 2;
304
389
  }
390
+ if (!isValidAdapterAction(adapter, harnessAction)) {
391
+ if (json) {
392
+ console.log(
393
+ harnessSummary(harnessAction, adapter, target, "fail", 2, parsedAdapter === void 0, {
394
+ code: "UNSUPPORTED_ACTION",
395
+ message: `${harnessAction} is not supported for ${adapter} (core supports install, verify, cleanup).`,
396
+ userAction: "run mm-harness doctor to see which actions are supported for this adapter"
397
+ })
398
+ );
399
+ } else {
400
+ console.error(
401
+ `\u2717 ${harnessAction} is not supported for ${adapter} (core supports install, verify, cleanup).`
402
+ );
403
+ }
404
+ return 2;
405
+ }
305
406
  const autoDetected = parsedAdapter === void 0;
306
407
  let forwardArgs = hasArg(forward, "--target") ? [...forward] : ["--target", target, ...forward];
307
408
  if (harnessAction === "install" && hasArg(forward, "--runway")) {
308
409
  return handleRunwayInstall(adapter, target, forward, json);
309
410
  }
310
- if (adapter === "extension" && harnessAction === "verify") {
311
- forwardArgs = applyExtensionRuntimeEnv(target, forwardArgs);
411
+ if (adapter === "extension" && (harnessAction === "live" || harnessAction === "verify")) {
412
+ forwardArgs = applyExtensionRuntimeEnv(target, harnessAction, forwardArgs);
413
+ }
414
+ if (adapter === "mobile" && harnessAction === "live") {
415
+ return handleMobileLive(target, forwardArgs, json, autoDetected);
312
416
  }
313
417
  const dispatch = resolveHarnessDispatch(adapter, harnessAction, target);
314
418
  if ("error" in dispatch) {
315
- if (quiet) {
316
- return 1;
317
- } else if (json) {
419
+ if (json) {
318
420
  console.log(
319
421
  harnessSummary(harnessAction, adapter, target, "fail", 1, autoDetected, {
320
422
  code: "DISPATCH_UNAVAILABLE",
@@ -330,16 +432,14 @@ async function handleHarness(argv) {
330
432
  }
331
433
  return 1;
332
434
  }
333
- if (!json && !quiet) {
435
+ if (!json) {
334
436
  const detected = autoDetected ? ", auto-detected" : "";
335
437
  console.error(`\u2192 ${harnessAction} (${adapter}${detected}) \u2014 target: ${target}`);
336
438
  }
337
439
  const start = Date.now();
338
440
  if (shellLeafMissing(dispatch.command)) {
339
441
  const message = missingShellLeafMessage(dispatch.command);
340
- if (quiet) {
341
- return 1;
342
- } else if (json) {
442
+ if (json) {
343
443
  console.log(
344
444
  harnessSummary(harnessAction, adapter, target, "fail", 1, autoDetected, {
345
445
  code: "HARNESS_SPAWN_FAILED",
@@ -359,14 +459,12 @@ async function handleHarness(argv) {
359
459
  const result = spawnSync(dispatchBin, dispatchArgs, {
360
460
  // Default: stream child output verbatim (byte-identical to the skill path).
361
461
  // --json: route child output to our stderr so stdout carries only the summary.
362
- stdio: quiet ? "ignore" : json ? ["inherit", 2, "inherit"] : "inherit",
462
+ stdio: json ? ["inherit", 2, "inherit"] : "inherit",
363
463
  env: process.env
364
464
  });
365
465
  const seconds = ((Date.now() - start) / 1e3).toFixed(1);
366
466
  if (result.error) {
367
- if (quiet) {
368
- return 1;
369
- } else if (json) {
467
+ if (json) {
370
468
  console.log(
371
469
  harnessSummary(harnessAction, adapter, target, "fail", 1, autoDetected, {
372
470
  code: "HARNESS_SPAWN_FAILED",
@@ -381,9 +479,7 @@ async function handleHarness(argv) {
381
479
  return 1;
382
480
  }
383
481
  const exitCode = result.status ?? 1;
384
- if (quiet) {
385
- return exitCode;
386
- } else if (json) {
482
+ if (json) {
387
483
  console.log(
388
484
  harnessSummary(
389
485
  harnessAction,
@@ -396,8 +492,7 @@ async function handleHarness(argv) {
396
492
  )
397
493
  );
398
494
  } else if (exitCode === 0) {
399
- console.error(`\u2713 ${harnessAction} ${adapter} passed (${seconds}s)
400
- Next: ${successNext(adapter, harnessAction, target)}`);
495
+ console.error(`\u2713 ${harnessAction} ${adapter} passed (${seconds}s)`);
401
496
  } else {
402
497
  console.error(`\u2717 ${harnessAction} ${adapter} failed (exit ${exitCode}, ${seconds}s)
403
498
  ${failureHint(adapter, harnessAction)}`);
@@ -427,10 +522,8 @@ async function handleRunwayInstall(adapter, target, forward, json) {
427
522
  resolveOnly: hasArg(forward, "--resolve-only"),
428
523
  rerunCommand
429
524
  });
430
- const next = result.status === "pass" && adapter === "mobile" && result.resolveOnly !== true ? `mm-harness launch ${String(result.platform ?? "ios")} --adapter mobile --target ${shellQuote(target)}` : void 0;
431
- const output = next ? { ...result, next } : result;
432
525
  if (json) {
433
- console.log(JSON.stringify(output, null, 2));
526
+ console.log(JSON.stringify(result, null, 2));
434
527
  } else if (result.status === "pass") {
435
528
  const cache = typeof result.cache === "object" && result.cache ? result.cache : void 0;
436
529
  const simulator = typeof result.simulator === "object" && result.simulator ? result.simulator : void 0;
@@ -441,7 +534,6 @@ async function handleRunwayInstall(adapter, target, forward, json) {
441
534
  const action = result.skipped ? "already provisioned" : "installed Runway app";
442
535
  console.error(`\u2713 ${action} for ${adapter} ${result.platform ?? ""} simulator=${simulator?.name ?? "unknown"} cache=${cache?.status ?? "skip"}`);
443
536
  }
444
- if (next) console.error(` Next: ${next}`);
445
537
  } else {
446
538
  console.error(`\u2717 mm-harness install --runway: ${result.error?.message ?? "runway install failed"}
447
539
  Next: ${result.error?.userAction ?? rerunCommand}`);
@@ -484,10 +576,9 @@ function harnessSummary(action, adapter, target, status, exitCode, autoDetected,
484
576
  status,
485
577
  exitCode,
486
578
  // Error contract: every --json failure carries a stable machine code + human
487
- // message. userAction is included when present so callers
579
+ // message (CLI-SPEC.md §5.1). userAction is included when present so callers
488
580
  // can surface the reachable escape without parsing the human message.
489
- ...status === "fail" && error ? { error } : {},
490
- ...status === "pass" && adapter ? { next: successNext(adapter, action, target) } : {}
581
+ ...status === "fail" && error ? { error } : {}
491
582
  });
492
583
  }
493
584
  export {
@@ -103,7 +103,7 @@ function checkHealBounds(target, output, state) {
103
103
  return {
104
104
  code: "SAME_RECOVERY_TWICE",
105
105
  exitCode: EXIT.bounded,
106
- message: state.recovered.length > 0 ? "one recovery already succeeded this invocation \u2014 bounded policy refuses a second recovery." : "one recovery was already attempted this invocation \u2014 refusing to loop.",
106
+ message: "same recovery already failed once this invocation \u2014 refusing to loop.",
107
107
  originalError
108
108
  };
109
109
  }
@@ -46,16 +46,29 @@ function candidatePaths(platform, action) {
46
46
  }
47
47
  pushDomainDispatcherFiles(files, root, platform, family);
48
48
  }
49
+ for (const candidateStem of stems) {
50
+ pushFlatCandidateFiles(files, root, platform, candidateStem);
51
+ }
49
52
  }
50
53
  return files;
51
54
  }
52
55
  function pushCandidateFiles(files, root, platform, family, stem) {
53
- files.push(path.join(root, platform, family, `${stem}.mjs`));
54
- files.push(path.join(root, "shared", family, `${stem}.mjs`));
56
+ for (const extension of ["mjs", "js", "sh"]) {
57
+ files.push(path.join(root, platform, family, `${stem}.${extension}`));
58
+ files.push(path.join(root, "shared", family, `${stem}.${extension}`));
59
+ }
55
60
  }
56
61
  function pushDomainDispatcherFiles(files, root, platform, family) {
57
- files.push(path.join(root, platform, family, `${family}.mjs`));
58
- files.push(path.join(root, "shared", family, `${family}.mjs`));
62
+ for (const extension of ["mjs", "js", "sh"]) {
63
+ files.push(path.join(root, platform, family, `${family}.${extension}`));
64
+ files.push(path.join(root, "shared", family, `${family}.${extension}`));
65
+ }
66
+ }
67
+ function pushFlatCandidateFiles(files, root, platform, stem) {
68
+ for (const extension of ["mjs", "js", "sh"]) {
69
+ files.push(path.join(root, platform, `${stem}.${extension}`));
70
+ files.push(path.join(root, "shared", `${stem}.${extension}`));
71
+ }
59
72
  }
60
73
  async function firstExecutablePath(paths) {
61
74
  for (const file of paths) {
@@ -107,8 +120,9 @@ function runProcess(command, args, options) {
107
120
  });
108
121
  }
109
122
  function commandFor(file, projectRoot, platform) {
123
+ if (file.endsWith(".sh")) return { command: "bash", args: [file] };
110
124
  const needsTsx = platform === "core" || importsSourceTypescript(file);
111
- if (!needsTsx) {
125
+ if (!needsTsx && (file.endsWith(".mjs") || file.endsWith(".js"))) {
112
126
  return { command: process.execPath, args: [file] };
113
127
  }
114
128
  const tsxBin = resolveTsxBin(projectRoot);
@@ -151,7 +165,7 @@ function importsSourceTypescriptFrom(file, visited) {
151
165
  const specifier = match[1] ?? "";
152
166
  if (specifier.endsWith(".ts")) return true;
153
167
  if (!specifier.startsWith(".")) continue;
154
- if (!specifier.endsWith(".mjs")) continue;
168
+ if (!specifier.endsWith(".mjs") && !specifier.endsWith(".js")) continue;
155
169
  if (importsSourceTypescriptFrom(path.resolve(path.dirname(absolute), specifier), visited)) {
156
170
  return true;
157
171
  }