@deeeed/metamask-harness 0.16.0 → 0.17.1

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 (119) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +90 -137
  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/dist/adapters/extension/console-capture.js +84 -0
  61. package/dist/adapters/extension/runtime-decision.js +14 -2
  62. package/dist/adapters/extension/surface.js +3 -2
  63. package/dist/adapters/mobile/prepare.js +22 -7
  64. package/dist/adapters/mobile/surface.js +1 -1
  65. package/dist/adapters/resolve-slot-ports.js +0 -2
  66. package/dist/adapters/slot-ports.js +8 -0
  67. package/dist/cli.js +5 -11
  68. package/dist/command-contract.js +441 -0
  69. package/dist/command-journal.js +225 -0
  70. package/dist/commands/call.js +43 -20
  71. package/dist/commands/check.js +9 -3
  72. package/dist/commands/debug.js +38 -31
  73. package/dist/commands/device-target.js +27 -12
  74. package/dist/commands/doctor.js +19 -6
  75. package/dist/commands/fixtures.js +19 -11
  76. package/dist/commands/last.js +52 -0
  77. package/dist/commands/launch/index.js +166 -59
  78. package/dist/commands/logs.js +8 -6
  79. package/dist/commands/manifest.js +114 -29
  80. package/dist/commands/parse-args.js +1 -0
  81. package/dist/commands/provision.js +10 -3
  82. package/dist/commands/run-engine.js +27 -42
  83. package/dist/commands/run-report.js +1 -1
  84. package/dist/commands/run.js +243 -41
  85. package/dist/commands/shared.js +11 -1
  86. package/dist/commands/status-probe.js +1 -1
  87. package/dist/commands/status.js +1 -1
  88. package/dist/commands/stop.js +7 -2
  89. package/dist/doctor.js +1 -4
  90. package/dist/harness.js +19 -125
  91. package/dist/json-stream.js +57 -0
  92. package/dist/live-adapter-contract.js +6 -20
  93. package/dist/mm-harness-cli.js +111 -41
  94. package/dist/paths.js +2 -5
  95. package/dist/run-diagnostics.js +35 -40
  96. package/dist/runner.js +32 -1
  97. package/dist/runtime-context.js +26 -8
  98. package/docs/CHEATSHEET.md +39 -52
  99. package/docs/CLI-ERGONOMICS-AUDIT.md +32 -0
  100. package/docs/CLI-ERGONOMICS-HUMAN-QA.md +48 -0
  101. package/docs/CODE-MAP.md +1 -1
  102. package/docs/UX-PRINCIPLES.md +2 -0
  103. package/docs/VIDEO-DEMO-VALIDATION.md +62 -0
  104. package/docs/architecture.md +65 -375
  105. package/docs/live-adapter-contract.md +3 -1
  106. package/library/actions/core/perps/_controller.mjs +10 -55
  107. package/library/manifests/mobile.action-manifest.json +1 -1
  108. package/package.json +3 -2
  109. package/scripts/completions.sh +0 -0
  110. package/scripts/install-completions.sh +0 -0
  111. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +0 -185
  112. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +0 -1662
  113. package/adapters/mobile/overlay/compat/README.md +0 -28
  114. package/adapters/mobile/overlay/compat/rn81-message-event-source.patch +0 -42
  115. package/adapters/shared/resolve-farmslot-ports-core.mjs +0 -3
  116. package/adapters/shared/resolve-farmslot-ports.mjs +0 -5
  117. package/adapters/shared/resolve-farmslot-ports.sh +0 -7
  118. package/docs/CLI-SPEC.md +0 -1102
  119. package/docs/MENTAL-MODEL.md +0 -295
package/dist/harness.js CHANGED
@@ -4,13 +4,8 @@ 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
- import { prepareMobile } from "./adapters/mobile/prepare.js";
8
- const HARNESS_ACTIONS = ["install", "verify", "cleanup", "live"];
7
+ const HARNESS_ACTIONS = ["install", "verify", "cleanup"];
9
8
  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
- }
14
9
  function harnessUsage() {
15
10
  console.error(`mm-harness \u2014 install and validate the MetaMask recipe runtime in a checkout.
16
11
 
@@ -50,6 +45,12 @@ function failureHint(adapter, action) {
50
45
  const helper = adapter === "mobile" ? "mm-harness launch ios # or launch android" : "mm-harness launch";
51
46
  return `Read the error above for the specific cause. To (re)start the runtime, run: ${helper}`;
52
47
  }
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
+ }
53
54
  function hasArg(args, needle) {
54
55
  return args.some((arg) => arg === needle || arg.startsWith(`${needle}=`));
55
56
  }
@@ -161,7 +162,7 @@ function readRuntimeContextField(contextPath, field) {
161
162
  if (node === void 0 || node === null || node === "") return void 0;
162
163
  return String(node);
163
164
  }
164
- function applyExtensionRuntimeEnv(target, action, args) {
165
+ function applyExtensionRuntimeEnv(target, args) {
165
166
  const contextPath = resolveRuntimeContextPath(target);
166
167
  const contextExists = fs.existsSync(contextPath);
167
168
  if (contextExists) {
@@ -174,19 +175,6 @@ function applyExtensionRuntimeEnv(target, action, args) {
174
175
  const extensionId = readRuntimeContextField(contextPath, "extensionId");
175
176
  if (extensionId) process.env.RECIPE_HARNESS_EXTENSION_ID = extensionId;
176
177
  }
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
- }
190
178
  } else {
191
179
  delete process.env.RECIPE_RUNTIME_CONTEXT;
192
180
  }
@@ -200,11 +188,6 @@ function applyExtensionRuntimeEnv(target, action, args) {
200
188
  result = [...result, "--cdp-port", cdpPort];
201
189
  }
202
190
  }
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
- }
208
191
  return result;
209
192
  }
210
193
  function resolveEntry(base, candidates, mode) {
@@ -281,81 +264,6 @@ Next: mm-harness install --platform core --target ${target}`
281
264
  );
282
265
  return { command, prefixArgs: [] };
283
266
  }
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
- }
359
267
  async function handleHarness(argv) {
360
268
  const action = argv[0];
361
269
  if (!action || action === "-h" || action === "--help") {
@@ -387,32 +295,13 @@ async function handleHarness(argv) {
387
295
  }
388
296
  return 2;
389
297
  }
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
- }
406
298
  const autoDetected = parsedAdapter === void 0;
407
299
  let forwardArgs = hasArg(forward, "--target") ? [...forward] : ["--target", target, ...forward];
408
300
  if (harnessAction === "install" && hasArg(forward, "--runway")) {
409
301
  return handleRunwayInstall(adapter, target, forward, json);
410
302
  }
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);
303
+ if (adapter === "extension" && harnessAction === "verify") {
304
+ forwardArgs = applyExtensionRuntimeEnv(target, forwardArgs);
416
305
  }
417
306
  const dispatch = resolveHarnessDispatch(adapter, harnessAction, target);
418
307
  if ("error" in dispatch) {
@@ -492,7 +381,8 @@ async function handleHarness(argv) {
492
381
  )
493
382
  );
494
383
  } else if (exitCode === 0) {
495
- console.error(`\u2713 ${harnessAction} ${adapter} passed (${seconds}s)`);
384
+ console.error(`\u2713 ${harnessAction} ${adapter} passed (${seconds}s)
385
+ Next: ${successNext(adapter, harnessAction, target)}`);
496
386
  } else {
497
387
  console.error(`\u2717 ${harnessAction} ${adapter} failed (exit ${exitCode}, ${seconds}s)
498
388
  ${failureHint(adapter, harnessAction)}`);
@@ -522,8 +412,10 @@ async function handleRunwayInstall(adapter, target, forward, json) {
522
412
  resolveOnly: hasArg(forward, "--resolve-only"),
523
413
  rerunCommand
524
414
  });
415
+ const next = result.status === "pass" && adapter === "mobile" && result.resolveOnly !== true ? `mm-harness launch ${String(result.platform ?? "ios")} --adapter mobile --target ${shellQuote(target)}` : void 0;
416
+ const output = next ? { ...result, next } : result;
525
417
  if (json) {
526
- console.log(JSON.stringify(result, null, 2));
418
+ console.log(JSON.stringify(output, null, 2));
527
419
  } else if (result.status === "pass") {
528
420
  const cache = typeof result.cache === "object" && result.cache ? result.cache : void 0;
529
421
  const simulator = typeof result.simulator === "object" && result.simulator ? result.simulator : void 0;
@@ -534,6 +426,7 @@ async function handleRunwayInstall(adapter, target, forward, json) {
534
426
  const action = result.skipped ? "already provisioned" : "installed Runway app";
535
427
  console.error(`\u2713 ${action} for ${adapter} ${result.platform ?? ""} simulator=${simulator?.name ?? "unknown"} cache=${cache?.status ?? "skip"}`);
536
428
  }
429
+ if (next) console.error(` Next: ${next}`);
537
430
  } else {
538
431
  console.error(`\u2717 mm-harness install --runway: ${result.error?.message ?? "runway install failed"}
539
432
  Next: ${result.error?.userAction ?? rerunCommand}`);
@@ -576,9 +469,10 @@ function harnessSummary(action, adapter, target, status, exitCode, autoDetected,
576
469
  status,
577
470
  exitCode,
578
471
  // Error contract: every --json failure carries a stable machine code + human
579
- // message (CLI-SPEC.md §5.1). userAction is included when present so callers
472
+ // message. userAction is included when present so callers
580
473
  // can surface the reachable escape without parsing the human message.
581
- ...status === "fail" && error ? { error } : {}
474
+ ...status === "fail" && error ? { error } : {},
475
+ ...status === "pass" && adapter ? { next: successNext(adapter, action, target) } : {}
582
476
  });
583
477
  }
584
478
  export {
@@ -0,0 +1,57 @@
1
+ class JsonStreamWriter {
2
+ enabled;
3
+ command;
4
+ completed = false;
5
+ writeLine;
6
+ constructor(command, enabled, output = process.stdout) {
7
+ this.command = command;
8
+ this.enabled = enabled;
9
+ const write = output.write.bind(output);
10
+ this.writeLine = (line) => {
11
+ write(`${line}
12
+ `);
13
+ };
14
+ }
15
+ isolateStdout() {
16
+ if (!this.enabled) return () => void 0;
17
+ const original = process.stdout.write;
18
+ const redirect = ((chunk, encoding, callback) => process.stderr.write(chunk, encoding, callback));
19
+ process.stdout.write = redirect;
20
+ return () => {
21
+ if (process.stdout.write === redirect) process.stdout.write = original;
22
+ };
23
+ }
24
+ emit(event, fields = {}) {
25
+ if (!this.enabled || this.completed) return;
26
+ this.writeLine(JSON.stringify({
27
+ schemaVersion: 1,
28
+ command: this.command,
29
+ event,
30
+ ...fields,
31
+ ts: (/* @__PURE__ */ new Date()).toISOString()
32
+ }));
33
+ }
34
+ phase(phase, fields = {}) {
35
+ this.emit("phase", { phase, ...fields });
36
+ }
37
+ node(nodeId, action, status) {
38
+ this.emit("node", { nodeId, action, status });
39
+ }
40
+ mutation(mutation) {
41
+ this.emit("mutation", { mutation });
42
+ }
43
+ recovery(code) {
44
+ this.emit("recovery", { code });
45
+ }
46
+ error(error) {
47
+ this.emit("error", { error });
48
+ }
49
+ complete(status, exitCode, fields = {}) {
50
+ if (!this.enabled || this.completed) return;
51
+ this.emit("complete", { status, exitCode, ...fields });
52
+ this.completed = true;
53
+ }
54
+ }
55
+ export {
56
+ JsonStreamWriter
57
+ };
@@ -46,29 +46,16 @@ 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
- }
52
49
  }
53
50
  return files;
54
51
  }
55
52
  function pushCandidateFiles(files, root, platform, family, stem) {
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
- }
53
+ files.push(path.join(root, platform, family, `${stem}.mjs`));
54
+ files.push(path.join(root, "shared", family, `${stem}.mjs`));
60
55
  }
61
56
  function pushDomainDispatcherFiles(files, root, platform, family) {
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
- }
57
+ files.push(path.join(root, platform, family, `${family}.mjs`));
58
+ files.push(path.join(root, "shared", family, `${family}.mjs`));
72
59
  }
73
60
  async function firstExecutablePath(paths) {
74
61
  for (const file of paths) {
@@ -120,9 +107,8 @@ function runProcess(command, args, options) {
120
107
  });
121
108
  }
122
109
  function commandFor(file, projectRoot, platform) {
123
- if (file.endsWith(".sh")) return { command: "bash", args: [file] };
124
110
  const needsTsx = platform === "core" || importsSourceTypescript(file);
125
- if (!needsTsx && (file.endsWith(".mjs") || file.endsWith(".js"))) {
111
+ if (!needsTsx) {
126
112
  return { command: process.execPath, args: [file] };
127
113
  }
128
114
  const tsxBin = resolveTsxBin(projectRoot);
@@ -165,7 +151,7 @@ function importsSourceTypescriptFrom(file, visited) {
165
151
  const specifier = match[1] ?? "";
166
152
  if (specifier.endsWith(".ts")) return true;
167
153
  if (!specifier.startsWith(".")) continue;
168
- if (!specifier.endsWith(".mjs") && !specifier.endsWith(".js")) continue;
154
+ if (!specifier.endsWith(".mjs")) continue;
169
155
  if (importsSourceTypescriptFrom(path.resolve(path.dirname(absolute), specifier), visited)) {
170
156
  return true;
171
157
  }
@@ -4,10 +4,17 @@ import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { Command } from "commander";
6
6
  import { color } from "./cli-color.js";
7
+ import { withCommandJournal } from "./command-journal.js";
8
+ import { JsonStreamWriter } from "./json-stream.js";
7
9
  import { handleUpdate, maybeNudge } from "./commands/update.js";
8
10
  import { handleCallHelp } from "./commands/call.js";
9
11
  import { getAdapterSurface } from "./adapters/surface.js";
10
12
  import { detectAdapter } from "./harness.js";
13
+ import {
14
+ PUBLIC_COMMAND_CONTRACTS,
15
+ publicCommandNames,
16
+ validatePublicInvocation
17
+ } from "./command-contract.js";
11
18
  const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
12
19
  globalThis.__MM_HARNESS_WRAPPER__ = true;
13
20
  const { main: recipeMain } = await import("./cli.js");
@@ -71,10 +78,11 @@ Example:
71
78
  name: "actions",
72
79
  summary: "List the action vocabulary + field schemas (--raw dumps the raw action registry JSON).",
73
80
  example: "mm-harness actions --adapter mobile",
74
- helpText: `mm-harness actions [flags]
81
+ helpText: `mm-harness actions [query] [flags]
75
82
 
76
83
  List the action vocabulary + field schemas for the checkout adapter.
77
84
 
85
+ query Search names, categories, fields, and descriptions (typo-tolerant)
78
86
  --action <name> Describe one action; fuzzy-resolves like call (short or full name)
79
87
  --categories List compact action categories and counts
80
88
  --category <name> List only one category (for example ui, wallet, or perps)
@@ -85,6 +93,7 @@ Example:
85
93
 
86
94
  Example:
87
95
  mm-harness actions --adapter mobile
96
+ mm-harness actions positions --adapter mobile
88
97
  mm-harness actions --adapter mobile --categories --json
89
98
  mm-harness actions --adapter mobile --category ui --json
90
99
  mm-harness actions --adapter mobile --action assert_orders
@@ -176,12 +185,29 @@ Example:
176
185
  --library <name=path> Add/override a recipe-library source (repeatable)
177
186
  --heal <off|infra-only|auto> Healing policy (default: infra-only); auto-ensures the overlay
178
187
  --json Machine-readable output
188
+ --json-stream Line-flushed JSONL progress + terminal event
179
189
  --record-video=full-run Record a video of the run
180
190
 
181
191
  Example:
182
192
  mm-harness run recipe.json --plan --adapter mobile
183
193
  mm-harness run recipe.json --adapter extension --artifacts-dir ./out`
184
194
  },
195
+ {
196
+ name: "last",
197
+ summary: "Show the last significant command, verdict, timestamps, and evidence paths for this checkout.",
198
+ example: "mm-harness last --json",
199
+ helpText: `mm-harness last [flags]
200
+
201
+ Read the atomic per-checkout resumability journal. Discovery commands do not
202
+ replace it, and an interrupted process remains recorded as verdict=running.
203
+
204
+ --target <path> Checkout path (default: cwd)
205
+ --runtime-dir <dir> Runtime dir containing last-command.json
206
+ --json Machine-readable envelope
207
+
208
+ Example:
209
+ mm-harness last --json`
210
+ },
185
211
  {
186
212
  name: "doctor",
187
213
  summary: "Readiness check for a checkout \u2014 no app launch. --fix repairs the overlay/runtime without launching.",
@@ -379,6 +405,7 @@ Example:
379
405
  --adapter <mobile|extension> Target adapter (auto-detected inside a checkout)
380
406
  --target <path> Checkout path (default: cwd)
381
407
  --json Machine-readable summary (recovered[] / mutations[] / phase)
408
+ --json-stream Line-flushed JSONL progress + terminal event
382
409
 
383
410
  Example:
384
411
  mm-harness launch ios
@@ -397,8 +424,8 @@ Example:
397
424
  --full Raw log tail (default = compact) (env: RECIPE_LOG_UI)
398
425
  --window (Re)open the read-only tmux tail window for the resolved dev-server port (leaves the process untouched)
399
426
  --events <n> Compact event count (default 10) (env: RECIPE_LOG_EVENTS)
400
- --source <label> Log source per adapter \u2014 mobile: metro|app (default metro);
401
- extension: webpack|watcher|rebuild|app (default webpack).
427
+ --source <label> Log source per adapter \u2014 mobile: metro|app;
428
+ extension: extension|dapp|webpack|watcher|rebuild.
402
429
  Core is headless (teaching error).
403
430
  --adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
404
431
  --target <path> Checkout path (default: cwd)
@@ -477,6 +504,7 @@ Example:
477
504
  --cdp-port <port> finalize: CDP port of the running extension
478
505
  --extension-dir <path> finalize: loaded extension dist (e.g. dist/chrome)
479
506
  --extension-id-file <path> finalize: optional file to read/write the resolved extension id
507
+ --action-manifest <path> Extension set: override the wallet action manifest
480
508
  --adapter <mobile|extension> Target adapter (auto-detected inside a checkout)
481
509
  --target <path> Checkout path (default: cwd)
482
510
  --device <udid|serial|name> Mobile only: target this device for sync/set
@@ -491,21 +519,28 @@ Example:
491
519
  mm-harness fixtures finalize --fixture wallet-fixture.json --state fixture-state.json --cdp-port 6661 --extension-dir dist/chrome`
492
520
  }
493
521
  ];
494
- const RETIRED = [
495
- {
496
- name: "live",
497
- message: `mm-harness live is retired (exit 2).
498
-
499
- Replacement: mm-harness launch --verify (install overlay \u2192 launch \u2192 CDP/bridge poll \u2192 smoke verify).`
500
- },
501
- {
502
- name: "manifest",
503
- message: `mm-harness manifest is retired (exit 2).
504
-
505
- Replacement: mm-harness actions --raw (works now \u2014 dumps the raw action registry JSON,
506
- identical to the old \`manifest --json\`). Manifest validation moved into doctor / run --plan.`
522
+ const PUBLIC_COMMAND_EXAMPLES = Object.fromEntries(
523
+ REAL.map((command) => [command.name, command.example])
524
+ );
525
+ function assertPublicContractMatchesSurface() {
526
+ const registered = REAL.map((command) => command.name).sort();
527
+ const contracted = publicCommandNames().sort();
528
+ if (registered.join("\n") !== contracted.join("\n")) {
529
+ throw new Error(
530
+ `public command contract drift: registered=[${registered.join(", ")}], contracted=[${contracted.join(", ")}]`
531
+ );
507
532
  }
508
- ];
533
+ for (const command of REAL) {
534
+ const registeredAliases = [...command.aliases ?? []].sort();
535
+ const contractedAliases = [...PUBLIC_COMMAND_CONTRACTS[command.name]?.aliases ?? []].sort();
536
+ if (registeredAliases.join("\n") !== contractedAliases.join("\n")) {
537
+ throw new Error(
538
+ `public command alias contract drift for ${command.name}: registered=[${registeredAliases.join(", ")}], contracted=[${contractedAliases.join(", ")}]`
539
+ );
540
+ }
541
+ }
542
+ }
543
+ assertPublicContractMatchesSurface();
509
544
  const HELP_GROUPS = [
510
545
  {
511
546
  title: "DAILY LOOP",
@@ -520,7 +555,7 @@ const HELP_GROUPS = [
520
555
  {
521
556
  title: "PROVE",
522
557
  blurb: "run recipes and inspect readiness",
523
- commands: ["run", "doctor", "check", "checklist", "recipe-quality"]
558
+ commands: ["run", "last", "doctor", "check", "checklist", "recipe-quality"]
524
559
  },
525
560
  {
526
561
  title: "RUNTIME OVERLAY",
@@ -608,7 +643,7 @@ function groupedHelp() {
608
643
  lines.push("DEV/PROD \u2014 the global npm install is prod; set MM_HARNESS_BIN to a dev checkout to override:");
609
644
  lines.push(" MM_HARNESS_BIN=/path/to/checkout/bin/mm-harness # unset = the installed/global bin");
610
645
  lines.push("");
611
- lines.push("See docs/MENTAL-MODEL.md (overview) and docs/CLI-SPEC.md (full contract).");
646
+ lines.push("See README.md for the workflow and docs/CHEATSHEET.md for examples.");
612
647
  return `${lines.join("\n")}
613
648
  `;
614
649
  }
@@ -620,11 +655,6 @@ async function delegate(argv) {
620
655
  return error !== null && typeof error === "object" && "exitCode" in error && typeof error.exitCode === "number" ? error.exitCode : 1;
621
656
  }
622
657
  }
623
- function translateActionsRaw(argv) {
624
- const rest = argv.slice(1).filter((arg) => arg !== "--raw");
625
- const withJson = rest.includes("--json") ? rest : [...rest, "--json"];
626
- return ["manifest", ...withJson];
627
- }
628
658
  const pkgVersion = (() => {
629
659
  try {
630
660
  return JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8")).version ?? "unknown";
@@ -642,8 +672,7 @@ for (const command of REAL) {
642
672
  if (command.name === "update") {
643
673
  process.exit(await handleUpdate(rawArgv.slice(1)));
644
674
  }
645
- const argv = command.name === "actions" && rawArgv.includes("--raw") ? translateActionsRaw(rawArgv) : rawArgv;
646
- process.exit(await delegate(argv));
675
+ process.exit(await withCommandJournal(command.name, rawArgv, () => delegate(rawArgv)));
647
676
  });
648
677
  }
649
678
  const HIDDEN = [
@@ -665,21 +694,6 @@ program.command("completions").description("Install/print bundled shell tab-comp
665
694
  const result = spawnSync("bash", [script, ...rawArgv.slice(1)], { stdio: "inherit" });
666
695
  process.exit(result.status ?? 1);
667
696
  });
668
- for (const retired of RETIRED) {
669
- program.command(retired.name).description(`${retired.name} is retired (see teaching error)`).allowUnknownOption().helpOption(false).argument("[args...]").action(async () => {
670
- if (retired.name === "live") {
671
- const adapterIdx = rawArgv.indexOf("--adapter");
672
- const aliasIdx = rawArgv.indexOf("-a");
673
- const adapterVal = adapterIdx !== -1 ? rawArgv[adapterIdx + 1] : aliasIdx !== -1 ? rawArgv[aliasIdx + 1] : void 0;
674
- if (adapterVal === "mobile" || adapterVal === "android") {
675
- process.exit(await delegate(rawArgv));
676
- return;
677
- }
678
- }
679
- console.error(retired.message);
680
- process.exit(2);
681
- });
682
- }
683
697
  const NUDGE_SKIP = ["update", "completions", "completion-candidates"];
684
698
  if (rawArgv.length > 0 && !NUDGE_SKIP.includes(rawArgv[0])) {
685
699
  setImmediate(() => void maybeNudge());
@@ -696,10 +710,66 @@ function isCallActionHelp(argv) {
696
710
  const scope = divider === -1 ? argv : argv.slice(0, divider);
697
711
  return scope.includes("--help") || scope.includes("-h");
698
712
  }
713
+ function jsonRequestedBeforePassthrough(argv) {
714
+ const divider = argv.indexOf("--");
715
+ const scope = divider === -1 ? argv : argv.slice(0, divider);
716
+ return scope.some((argument) => argument === "--json" || argument.startsWith("--json="));
717
+ }
718
+ function jsonStreamRequestedBeforePassthrough(argv) {
719
+ const divider = argv.indexOf("--");
720
+ const scope = divider === -1 ? argv : argv.slice(0, divider);
721
+ return scope.includes("--json-stream");
722
+ }
723
+ function emitUsageError(error, json, jsonStream) {
724
+ if (jsonStream) {
725
+ const stream = new JsonStreamWriter(error.command, true);
726
+ stream.error({ code: error.code, message: error.message, userAction: error.userAction });
727
+ stream.complete("fail", 2);
728
+ } else if (json) {
729
+ process.stdout.write(
730
+ `${JSON.stringify(
731
+ {
732
+ schemaVersion: 1,
733
+ command: error.command,
734
+ status: "fail",
735
+ error: {
736
+ code: error.code,
737
+ message: error.message,
738
+ userAction: error.userAction
739
+ },
740
+ exitCode: 2
741
+ },
742
+ null,
743
+ 2
744
+ )}
745
+ `
746
+ );
747
+ } else {
748
+ const scope = error.command === "mm-harness" ? "" : ` ${error.command}`;
749
+ process.stderr.write(`\u2717 mm-harness${scope}: ${error.message}
750
+ Next: ${error.userAction}
751
+ `);
752
+ }
753
+ process.exit(2);
754
+ }
699
755
  if (rawArgv.length === 0) {
700
756
  process.stdout.write(groupedHelp());
701
757
  process.exit(0);
702
758
  }
759
+ const preflightBypass = /* @__PURE__ */ new Set([
760
+ ...HIDDEN,
761
+ "completions"
762
+ ]);
763
+ if (!preflightBypass.has(rawArgv[0] ?? "")) {
764
+ const usageError = validatePublicInvocation(rawArgv, PUBLIC_COMMAND_EXAMPLES);
765
+ if (usageError) {
766
+ emitUsageError(
767
+ usageError,
768
+ jsonRequestedBeforePassthrough(rawArgv),
769
+ jsonStreamRequestedBeforePassthrough(rawArgv)
770
+ );
771
+ }
772
+ }
703
773
  if (hasPassthroughHelp(rawArgv)) {
704
774
  process.exit(await delegate(rawArgv));
705
775
  }
package/dist/paths.js CHANGED
@@ -47,10 +47,7 @@ function recipeWatchLogCandidates() {
47
47
  }
48
48
  function resolveLocalProtocolRoot() {
49
49
  const candidates = [
50
- // Orchestrator-neutral name is primary; FARMSLOT_ROOT stays a back-compat alias
51
- // for one release (the installer injects METAMASK_RUNNER_PROTOCOL_ROOT).
52
50
  process.env.METAMASK_RUNNER_PROTOCOL_ROOT,
53
- process.env.FARMSLOT_ROOT,
54
51
  readConfiguredProtocolRoot(),
55
52
  findProtocolRoot(runnerDir),
56
53
  findProtocolRoot(process.cwd())
@@ -62,7 +59,7 @@ function resolveRequiredLocalProtocolRoot(reason) {
62
59
  const root = resolveLocalProtocolRoot();
63
60
  if (!root) {
64
61
  throw new Error(
65
- `${reason} requires a local protocol/runtime checkout. Set METAMASK_RUNNER_PROTOCOL_ROOT (or the legacy FARMSLOT_ROOT), or create .farmslot-root for this dev-only path.`
62
+ `${reason} requires a local protocol/runtime checkout. Set METAMASK_RUNNER_PROTOCOL_ROOT or create .farmslot-root for this dev-only path.`
66
63
  );
67
64
  }
68
65
  return root;
@@ -152,7 +149,7 @@ async function importProtocolPackage(packageName, localSourceEntry) {
152
149
  const root = resolveLocalProtocolRoot();
153
150
  if (!root) {
154
151
  throw new Error(
155
- `${packageName} is not installed. Install @farmslot/* packages normally, or set FARMSLOT_ROOT/use npm run dev:link-farmslot while co-developing protocol packages locally.`
152
+ `${packageName} is not installed. Install @farmslot/* packages normally, or set METAMASK_RUNNER_PROTOCOL_ROOT/use npm run dev:link-farmslot while co-developing protocol packages locally.`
156
153
  );
157
154
  }
158
155
  return import(pathToFileURL(path.join(root, localSourceEntry)).href);