@deeeed/metamask-harness 0.17.0 → 0.17.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +90 -140
  3. package/adapters/core/cleanup.sh +0 -0
  4. package/adapters/core/inject.sh +0 -0
  5. package/adapters/extension/cleanup.mjs +0 -0
  6. package/adapters/extension/console-tail.mjs +51 -24
  7. package/adapters/extension/ensure-browser.sh +0 -0
  8. package/adapters/extension/inject.mjs +5 -9
  9. package/adapters/extension/launch-browser.cjs +0 -0
  10. package/adapters/extension/launch-webpack.cjs +83 -0
  11. package/adapters/extension/launch.sh +8 -11
  12. package/adapters/extension/live.sh +2 -0
  13. package/adapters/extension/readiness.mjs +0 -0
  14. package/adapters/extension/reattach.sh +1 -0
  15. package/adapters/extension/refresh-build.sh +0 -0
  16. package/adapters/extension/seed-fixture.sh +4 -12
  17. package/adapters/extension/sidepanel-toggle.sh +4 -1
  18. package/adapters/extension/snapshot-dist.sh +0 -0
  19. package/adapters/extension/stamp-runtime-title.cjs +49 -0
  20. package/adapters/extension/start-watch.sh +16 -4
  21. package/adapters/extension/stop-viewers.sh +1 -1
  22. package/adapters/extension/sync-webpack-dist.cjs +105 -0
  23. package/adapters/extension/verify.sh +1 -1
  24. package/adapters/extension/wallet-fixture-state.cjs +0 -0
  25. package/adapters/manifest.json +46 -38
  26. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +0 -0
  27. package/adapters/mobile/bridge-runtime/console-forwarder.cjs +1 -1
  28. package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +2 -3
  29. package/adapters/mobile/bridge-runtime/setup-wallet.sh +0 -0
  30. package/adapters/mobile/cleanup.sh +16 -176
  31. package/adapters/mobile/inject.sh +88 -671
  32. package/adapters/mobile/launch-console-forwarder.cjs +39 -0
  33. package/adapters/mobile/lib/metro-listener.sh +0 -0
  34. package/adapters/mobile/lib/tmux-viewer.sh +0 -0
  35. package/adapters/mobile/open-device.sh +0 -0
  36. package/adapters/mobile/prewarm-bundle.sh +0 -0
  37. package/adapters/mobile/start-console-forwarder.sh +70 -0
  38. package/adapters/mobile/start-metro.sh +0 -41
  39. package/adapters/mobile/stop-metro.sh +0 -0
  40. package/adapters/mobile/verify.sh +4 -61
  41. package/adapters/mobile/wait-for-bridge.sh +0 -0
  42. package/adapters/mobile/yarn-setup.sh +0 -0
  43. package/adapters/shared/activate-repo-node.sh +0 -0
  44. package/adapters/shared/activate-repo-ruby.sh +0 -0
  45. package/adapters/shared/cli-ux.sh +0 -0
  46. package/adapters/shared/ensure-runner-deps.sh +0 -0
  47. package/adapters/shared/harness-path.sh +0 -0
  48. package/adapters/shared/hash-helpers.sh +0 -0
  49. package/adapters/shared/install-repo-deps.sh +0 -0
  50. package/adapters/shared/json-field.sh +0 -0
  51. package/adapters/shared/open-debug.mjs +90 -36
  52. package/adapters/shared/open-log-window.sh +0 -0
  53. package/adapters/shared/reap-checkout-metros.sh +0 -0
  54. package/adapters/shared/resolve-slot-ports-core.mjs +0 -6
  55. package/adapters/shared/resolve-slot-ports.mjs +0 -0
  56. package/adapters/shared/resolve-slot-ports.sh +0 -5
  57. package/adapters/shared/sync-wallet-fixture.sh +0 -0
  58. package/adapters/shared/tmux-session.sh +0 -0
  59. package/adapters/shared/tmux-viewer.sh +0 -0
  60. package/adapters/shared/update-check-worker.mjs +37 -0
  61. package/dist/adapters/extension/console-capture.js +84 -0
  62. package/dist/adapters/extension/runtime-decision.js +14 -2
  63. package/dist/adapters/extension/surface.js +3 -2
  64. package/dist/adapters/mobile/prepare.js +22 -7
  65. package/dist/adapters/mobile/surface.js +1 -1
  66. package/dist/adapters/resolve-slot-ports.js +0 -2
  67. package/dist/adapters/slot-ports.js +8 -0
  68. package/dist/cli.js +3 -11
  69. package/dist/commands/call.js +9 -4
  70. package/dist/commands/debug.js +38 -31
  71. package/dist/commands/fixtures.js +18 -10
  72. package/dist/commands/launch/index.js +10 -0
  73. package/dist/commands/logs.js +8 -6
  74. package/dist/commands/manifest.js +5 -17
  75. package/dist/commands/run-engine.js +1 -32
  76. package/dist/commands/run-report.js +1 -1
  77. package/dist/commands/run.js +50 -3
  78. package/dist/commands/status-probe.js +1 -1
  79. package/dist/commands/update.js +15 -28
  80. package/dist/doctor.js +1 -4
  81. package/dist/harness.js +5 -123
  82. package/dist/live-adapter-contract.js +6 -20
  83. package/dist/mm-harness-cli.js +6 -43
  84. package/dist/paths.js +2 -5
  85. package/dist/run-diagnostics.js +35 -40
  86. package/dist/runtime-context.js +26 -8
  87. package/docs/CHEATSHEET.md +39 -52
  88. package/docs/CLI-ERGONOMICS-HUMAN-QA.md +48 -104
  89. package/docs/CODE-MAP.md +1 -1
  90. package/docs/VIDEO-DEMO-VALIDATION.md +62 -0
  91. package/docs/architecture.md +65 -375
  92. package/docs/live-adapter-contract.md +3 -1
  93. package/library/actions/core/perps/_controller.mjs +10 -55
  94. package/library/manifests/mobile.action-manifest.json +1 -1
  95. package/package.json +2 -2
  96. package/scripts/completions.sh +0 -0
  97. package/scripts/install-completions.sh +0 -0
  98. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +0 -185
  99. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +0 -1662
  100. package/adapters/mobile/overlay/compat/README.md +0 -28
  101. package/adapters/mobile/overlay/compat/rn81-message-event-source.patch +0 -42
  102. package/adapters/shared/resolve-farmslot-ports-core.mjs +0 -3
  103. package/adapters/shared/resolve-farmslot-ports.mjs +0 -5
  104. package/adapters/shared/resolve-farmslot-ports.sh +0 -7
  105. package/docs/CLI-SPEC.md +0 -1142
  106. package/docs/MENTAL-MODEL.md +0 -295
@@ -1,4 +1,4 @@
1
- import { spawnSync } from "node:child_process";
1
+ import { spawn, spawnSync } from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
@@ -6,7 +6,7 @@ import { runnerDir } from "../paths.js";
6
6
  import { EXIT, flag, parseFlags } from "./shared.js";
7
7
  const PACKAGE_NAME = "@deeeed/metamask-harness";
8
8
  const NUDGE_INTERVAL_MS = 24 * 60 * 60 * 1e3;
9
- const NUDGE_FETCH_TIMEOUT_MS = 300;
9
+ const NUDGE_FETCH_TIMEOUT_MS = 2e3;
10
10
  function currentVersion() {
11
11
  try {
12
12
  const pkg = JSON.parse(fs.readFileSync(path.join(runnerDir, "package.json"), "utf8"));
@@ -56,24 +56,6 @@ function fetchLatest(timeoutMs) {
56
56
  if (!latest) return { error: { kind: "unreachable", message: "npm returned no dist-tag for latest" } };
57
57
  return { latest };
58
58
  }
59
- async function fetchLatestAsync(timeoutMs) {
60
- const controller = new AbortController();
61
- const timer = setTimeout(() => controller.abort(), timeoutMs);
62
- try {
63
- const url = `https://registry.npmjs.org/-/package/${encodeURIComponent(PACKAGE_NAME)}/dist-tags`;
64
- const res = await fetch(url, { signal: controller.signal });
65
- if (!res.ok) return { error: { kind: "unreachable", message: `registry returned HTTP ${res.status}` } };
66
- const data = await res.json();
67
- const latest = data["latest"];
68
- if (!latest) return { error: { kind: "unreachable", message: "npm registry returned no latest dist-tag" } };
69
- return { latest };
70
- } catch (err) {
71
- const msg = err.name === "AbortError" ? `registry fetch timed out after ${timeoutMs}ms` : err.message;
72
- return { error: { kind: "unreachable", message: msg } };
73
- } finally {
74
- clearTimeout(timer);
75
- }
76
- }
77
59
  function teachFailure(json, current, code, message, userAction) {
78
60
  if (json) {
79
61
  console.log(
@@ -211,23 +193,28 @@ function nudgeDisabled() {
211
193
  function nudgeLine(current, latest) {
212
194
  return isNewer(latest, current) ? `mm-harness ${current} \u2192 ${latest} available \xB7 run: mm-harness update` : null;
213
195
  }
214
- async function maybeNudge(now = Date.now()) {
196
+ function maybeNudge(now = Date.now()) {
215
197
  if (nudgeDisabled()) return;
216
198
  const file = cacheFile();
217
199
  const cache = readCache(file);
218
200
  const current = currentVersion();
219
- let latest = cache?.latest ?? "";
220
201
  process.once("exit", () => {
221
- const line = nudgeLine(current, latest);
202
+ const line = nudgeLine(current, cache?.latest ?? "");
222
203
  if (line) process.stderr.write(`${line}
223
204
  `);
224
205
  });
225
206
  if (!cache || now - cache.lastCheck >= NUDGE_INTERVAL_MS) {
226
- writeCache(file, { lastCheck: now, latest });
227
- const fetched = await fetchLatestAsync(NUDGE_FETCH_TIMEOUT_MS);
228
- if (fetched.latest) {
229
- latest = fetched.latest;
230
- writeCache(file, { lastCheck: now, latest });
207
+ writeCache(file, { lastCheck: now, latest: cache?.latest ?? "" });
208
+ const worker = path.join(runnerDir, "adapters", "shared", "update-check-worker.mjs");
209
+ if (!fs.existsSync(worker)) return;
210
+ try {
211
+ const child = spawn(
212
+ process.execPath,
213
+ [worker, file, String(NUDGE_FETCH_TIMEOUT_MS)],
214
+ { detached: true, stdio: "ignore" }
215
+ );
216
+ child.unref();
217
+ } catch {
231
218
  }
232
219
  }
233
220
  }
package/dist/doctor.js CHANGED
@@ -93,10 +93,7 @@ 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"] },
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"] }
96
+ { key: "cdpPort", envVars: ["RECIPE_CDP_PORT", "CDP_PORT"], envVar: "CDP_PORT", customize: "doctor --fix claims one; --cdp-port overrides", adapters: ["extension"] }
100
97
  ];
101
98
  function runtimeContextSummary(target, adapter) {
102
99
  const contextPath = resolveRuntimeContextPath(target);
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
 
@@ -167,7 +162,7 @@ function readRuntimeContextField(contextPath, field) {
167
162
  if (node === void 0 || node === null || node === "") return void 0;
168
163
  return String(node);
169
164
  }
170
- function applyExtensionRuntimeEnv(target, action, args) {
165
+ function applyExtensionRuntimeEnv(target, args) {
171
166
  const contextPath = resolveRuntimeContextPath(target);
172
167
  const contextExists = fs.existsSync(contextPath);
173
168
  if (contextExists) {
@@ -180,19 +175,6 @@ function applyExtensionRuntimeEnv(target, action, args) {
180
175
  const extensionId = readRuntimeContextField(contextPath, "extensionId");
181
176
  if (extensionId) process.env.RECIPE_HARNESS_EXTENSION_ID = extensionId;
182
177
  }
183
- if (process.env.RECIPE_RUNTIME_START_APPROVED === void 0) {
184
- const approved = readRuntimeContextField(contextPath, "runtimeStart.approved");
185
- if (approved === "true" || approved === "True" || approved === "1") process.env.RECIPE_RUNTIME_START_APPROVED = "1";
186
- else if (approved === "false" || approved === "False" || approved === "0") process.env.RECIPE_RUNTIME_START_APPROVED = "0";
187
- }
188
- if (!process.env.RECIPE_RUNTIME_START_CMD) {
189
- const command = readRuntimeContextField(contextPath, "runtimeStart.command");
190
- if (command) process.env.RECIPE_RUNTIME_START_CMD = command;
191
- }
192
- if (!process.env.RECIPE_RUNTIME_READY_URL) {
193
- const readyUrl = readRuntimeContextField(contextPath, "runtimeStart.readyUrl");
194
- if (readyUrl) process.env.RECIPE_RUNTIME_READY_URL = readyUrl;
195
- }
196
178
  } else {
197
179
  delete process.env.RECIPE_RUNTIME_CONTEXT;
198
180
  }
@@ -206,11 +188,6 @@ function applyExtensionRuntimeEnv(target, action, args) {
206
188
  result = [...result, "--cdp-port", cdpPort];
207
189
  }
208
190
  }
209
- if (action === "live" && !hasArg(result, "--prepare-cmd")) {
210
- if (process.env.RECIPE_RUNTIME_START_APPROVED === "1" && process.env.RECIPE_RUNTIME_START_CMD) {
211
- result = [...result, "--prepare-cmd", process.env.RECIPE_RUNTIME_START_CMD];
212
- }
213
- }
214
191
  return result;
215
192
  }
216
193
  function resolveEntry(base, candidates, mode) {
@@ -287,82 +264,6 @@ Next: mm-harness install --platform core --target ${target}`
287
264
  );
288
265
  return { command, prefixArgs: [] };
289
266
  }
290
- async function handleMobileLive(target, forwardArgs, json, autoDetected) {
291
- const platform = argValue(forwardArgs, "--platform") ?? "ios";
292
- const watcherPortStr = argValue(forwardArgs, "--watcher-port") ?? process.env.WATCHER_PORT;
293
- const watcherPort = watcherPortStr ? parseInt(watcherPortStr, 10) : void 0;
294
- const start = Date.now();
295
- if (!json) {
296
- const detected = autoDetected ? ", auto-detected" : "";
297
- console.error(`\u2192 live (mobile${detected}) \u2014 target: ${target}`);
298
- }
299
- const prepResult = await prepareMobile(target, { platform, json, watcherPort });
300
- if (prepResult.status !== 0) {
301
- const elapsed2 = ((Date.now() - start) / 1e3).toFixed(1);
302
- if (json) {
303
- console.log(
304
- harnessSummary("live", "mobile", target, "fail", prepResult.status, autoDetected, {
305
- code: "MOBILE_PREPARE_FAILED",
306
- message: `mobile prepare failed (exit ${prepResult.status})`,
307
- userAction: failureHint("mobile", "live")
308
- })
309
- );
310
- } else {
311
- console.error(
312
- `\u2717 live mobile failed (exit ${prepResult.status}, ${elapsed2}s)
313
- ${failureHint("mobile", "live")}`
314
- );
315
- }
316
- return prepResult.status;
317
- }
318
- const installedVerify = path.join(recipeHarnessPath(target, "mobile"), "scripts", "verify.sh");
319
- const verifySh = fs.existsSync(installedVerify) ? installedVerify : resolveEntry(runnerDir, ["adapters/mobile/verify.sh", "scripts/mobile/verify.sh"], "file");
320
- const verifyArgs = hasArg(forwardArgs, "--no-auto-start") ? [...forwardArgs] : ["--no-auto-start", ...forwardArgs];
321
- if (shellLeafMissing(verifySh)) {
322
- const message = missingShellLeafMessage(verifySh);
323
- if (json) {
324
- console.log(
325
- harnessSummary("live", "mobile", target, "fail", 1, autoDetected, {
326
- code: "HARNESS_SPAWN_FAILED",
327
- message,
328
- userAction: failureHint("mobile", "live")
329
- })
330
- );
331
- } else {
332
- console.error(`\u2717 ${message}`);
333
- }
334
- return 1;
335
- }
336
- const { bin: verifyBin, args: verifySpawnArgs } = resolveLeafInvoke(verifySh, verifyArgs);
337
- const result = spawnSync(verifyBin, verifySpawnArgs, {
338
- stdio: json ? ["inherit", 2, "inherit"] : "inherit",
339
- env: process.env
340
- });
341
- const elapsed = ((Date.now() - start) / 1e3).toFixed(1);
342
- const exitCode = result.status ?? 1;
343
- if (json) {
344
- console.log(
345
- harnessSummary(
346
- "live",
347
- "mobile",
348
- target,
349
- exitCode === 0 ? "pass" : "fail",
350
- exitCode,
351
- autoDetected,
352
- exitCode === 0 ? void 0 : { code: "MOBILE_VERIFY_FAILED", message: `mobile live verify failed (exit ${exitCode})`, userAction: failureHint("mobile", "live") }
353
- )
354
- );
355
- } else if (exitCode === 0) {
356
- console.error(`\u2713 live mobile passed (${elapsed}s)
357
- Next: ${successNext("mobile", "live", target)}`);
358
- } else {
359
- console.error(
360
- `\u2717 live mobile failed (exit ${exitCode}, ${elapsed}s)
361
- ${failureHint("mobile", "live")}`
362
- );
363
- }
364
- return exitCode;
365
- }
366
267
  async function handleHarness(argv) {
367
268
  const action = argv[0];
368
269
  if (!action || action === "-h" || action === "--help") {
@@ -394,32 +295,13 @@ async function handleHarness(argv) {
394
295
  }
395
296
  return 2;
396
297
  }
397
- if (!isValidAdapterAction(adapter, harnessAction)) {
398
- if (json) {
399
- console.log(
400
- harnessSummary(harnessAction, adapter, target, "fail", 2, parsedAdapter === void 0, {
401
- code: "UNSUPPORTED_ACTION",
402
- message: `${harnessAction} is not supported for ${adapter} (core supports install, verify, cleanup).`,
403
- userAction: "run mm-harness doctor to see which actions are supported for this adapter"
404
- })
405
- );
406
- } else {
407
- console.error(
408
- `\u2717 ${harnessAction} is not supported for ${adapter} (core supports install, verify, cleanup).`
409
- );
410
- }
411
- return 2;
412
- }
413
298
  const autoDetected = parsedAdapter === void 0;
414
299
  let forwardArgs = hasArg(forward, "--target") ? [...forward] : ["--target", target, ...forward];
415
300
  if (harnessAction === "install" && hasArg(forward, "--runway")) {
416
301
  return handleRunwayInstall(adapter, target, forward, json);
417
302
  }
418
- if (adapter === "extension" && (harnessAction === "live" || harnessAction === "verify")) {
419
- forwardArgs = applyExtensionRuntimeEnv(target, harnessAction, forwardArgs);
420
- }
421
- if (adapter === "mobile" && harnessAction === "live") {
422
- return handleMobileLive(target, forwardArgs, json, autoDetected);
303
+ if (adapter === "extension" && harnessAction === "verify") {
304
+ forwardArgs = applyExtensionRuntimeEnv(target, forwardArgs);
423
305
  }
424
306
  const dispatch = resolveHarnessDispatch(adapter, harnessAction, target);
425
307
  if ("error" in dispatch) {
@@ -587,7 +469,7 @@ function harnessSummary(action, adapter, target, status, exitCode, autoDetected,
587
469
  status,
588
470
  exitCode,
589
471
  // Error contract: every --json failure carries a stable machine code + human
590
- // message (CLI-SPEC.md §5.1). userAction is included when present so callers
472
+ // message. userAction is included when present so callers
591
473
  // can surface the reachable escape without parsing the human message.
592
474
  ...status === "fail" && error ? { error } : {},
593
475
  ...status === "pass" && adapter ? { next: successNext(adapter, action, target) } : {}
@@ -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
  }
@@ -424,8 +424,8 @@ Example:
424
424
  --full Raw log tail (default = compact) (env: RECIPE_LOG_UI)
425
425
  --window (Re)open the read-only tmux tail window for the resolved dev-server port (leaves the process untouched)
426
426
  --events <n> Compact event count (default 10) (env: RECIPE_LOG_EVENTS)
427
- --source <label> Log source per adapter \u2014 mobile: metro|app (default metro);
428
- 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.
429
429
  Core is headless (teaching error).
430
430
  --adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
431
431
  --target <path> Checkout path (default: cwd)
@@ -541,21 +541,6 @@ function assertPublicContractMatchesSurface() {
541
541
  }
542
542
  }
543
543
  assertPublicContractMatchesSurface();
544
- const RETIRED = [
545
- {
546
- name: "live",
547
- message: `mm-harness live is retired (exit 2).
548
-
549
- Replacement: mm-harness launch --verify (install overlay \u2192 launch \u2192 CDP/bridge poll \u2192 smoke verify).`
550
- },
551
- {
552
- name: "manifest",
553
- message: `mm-harness manifest is retired (exit 2).
554
-
555
- Replacement: mm-harness actions --raw (works now \u2014 dumps the raw action registry JSON,
556
- identical to the old \`manifest --json\`). Manifest validation moved into doctor / run --plan.`
557
- }
558
- ];
559
544
  const HELP_GROUPS = [
560
545
  {
561
546
  title: "DAILY LOOP",
@@ -658,7 +643,7 @@ function groupedHelp() {
658
643
  lines.push("DEV/PROD \u2014 the global npm install is prod; set MM_HARNESS_BIN to a dev checkout to override:");
659
644
  lines.push(" MM_HARNESS_BIN=/path/to/checkout/bin/mm-harness # unset = the installed/global bin");
660
645
  lines.push("");
661
- 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.");
662
647
  return `${lines.join("\n")}
663
648
  `;
664
649
  }
@@ -670,11 +655,6 @@ async function delegate(argv) {
670
655
  return error !== null && typeof error === "object" && "exitCode" in error && typeof error.exitCode === "number" ? error.exitCode : 1;
671
656
  }
672
657
  }
673
- function translateActionsRaw(argv) {
674
- const rest = argv.slice(1).filter((arg) => arg !== "--raw");
675
- const withJson = rest.includes("--json") ? rest : [...rest, "--json"];
676
- return ["manifest", ...withJson];
677
- }
678
658
  const pkgVersion = (() => {
679
659
  try {
680
660
  return JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8")).version ?? "unknown";
@@ -692,8 +672,7 @@ for (const command of REAL) {
692
672
  if (command.name === "update") {
693
673
  process.exit(await handleUpdate(rawArgv.slice(1)));
694
674
  }
695
- const argv = command.name === "actions" && rawArgv.includes("--raw") ? translateActionsRaw(rawArgv) : rawArgv;
696
- process.exit(await withCommandJournal(command.name, rawArgv, () => delegate(argv)));
675
+ process.exit(await withCommandJournal(command.name, rawArgv, () => delegate(rawArgv)));
697
676
  });
698
677
  }
699
678
  const HIDDEN = [
@@ -715,24 +694,9 @@ program.command("completions").description("Install/print bundled shell tab-comp
715
694
  const result = spawnSync("bash", [script, ...rawArgv.slice(1)], { stdio: "inherit" });
716
695
  process.exit(result.status ?? 1);
717
696
  });
718
- for (const retired of RETIRED) {
719
- program.command(retired.name).description(`${retired.name} is retired (see teaching error)`).allowUnknownOption().helpOption(false).argument("[args...]").action(async () => {
720
- if (retired.name === "live") {
721
- const adapterIdx = rawArgv.indexOf("--adapter");
722
- const aliasIdx = rawArgv.indexOf("-a");
723
- const adapterVal = adapterIdx !== -1 ? rawArgv[adapterIdx + 1] : aliasIdx !== -1 ? rawArgv[aliasIdx + 1] : void 0;
724
- if (adapterVal === "mobile" || adapterVal === "android") {
725
- process.exit(await delegate(rawArgv));
726
- return;
727
- }
728
- }
729
- console.error(retired.message);
730
- process.exit(2);
731
- });
732
- }
733
697
  const NUDGE_SKIP = ["update", "completions", "completion-candidates"];
734
698
  if (rawArgv.length > 0 && !NUDGE_SKIP.includes(rawArgv[0])) {
735
- setImmediate(() => void maybeNudge());
699
+ maybeNudge();
736
700
  }
737
701
  function hasPassthroughHelp(argv) {
738
702
  const divider = argv.indexOf("--");
@@ -794,8 +758,7 @@ if (rawArgv.length === 0) {
794
758
  }
795
759
  const preflightBypass = /* @__PURE__ */ new Set([
796
760
  ...HIDDEN,
797
- "completions",
798
- ...RETIRED.map((command) => command.name)
761
+ "completions"
799
762
  ]);
800
763
  if (!preflightBypass.has(rawArgv[0] ?? "")) {
801
764
  const usageError = validatePublicInvocation(rawArgv, PUBLIC_COMMAND_EXAMPLES);
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);
@@ -1,26 +1,54 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { createHash } from "node:crypto";
4
- import { spawn, spawnSync } from "node:child_process";
4
+ import { spawnSync } from "node:child_process";
5
5
  import { getAdapterSurface } from "./adapters/surface.js";
6
6
  import { runnerDir } from "./paths.js";
7
+ import { ensureExtensionConsoleCapture } from "./adapters/extension/console-capture.js";
7
8
  const MAX_CAPTURE_BYTES = 512 * 1024;
8
9
  const MAX_FINDINGS = 20;
9
10
  const MAX_PREVIEW_CHARS = 320;
11
+ function readRunDiagnosticsDocument(diagnosticsPath) {
12
+ if (typeof diagnosticsPath !== "string" || diagnosticsPath.length === 0) return null;
13
+ try {
14
+ const document = JSON.parse(fs.readFileSync(diagnosticsPath, "utf8"));
15
+ return isRunDiagnosticsDocument(document) ? document : null;
16
+ } catch {
17
+ return null;
18
+ }
19
+ }
20
+ function formatRunDiagnosticsForHuman(diagnostics, adapter) {
21
+ if (!diagnostics) {
22
+ return [adapter === "core" ? "N/A \u2014 Core is headless and has no application log source." : "UNAVAILABLE \u2014 application diagnostics could not be collected for this run."];
23
+ }
24
+ const status = diagnostics.status === "clean" ? "CLEAN" : diagnostics.status === "review" ? "REVIEW" : "UNAVAILABLE";
25
+ const lines = [`${status} \u2014 ${diagnostics.note}`];
26
+ for (const finding of diagnostics.findings) {
27
+ const count = finding.count > 1 ? ` \xD7${finding.count}` : "";
28
+ lines.push(`${finding.level.toUpperCase()}${count} \u2014 ${finding.preview}`);
29
+ }
30
+ return lines;
31
+ }
10
32
  async function beginRunDiagnostics(adapter, projectRoot) {
11
33
  const source = getAdapterSurface(adapter).appLogSource(projectRoot);
12
34
  if (!source) return null;
13
35
  const mobileIssueBuffer = adapter === "mobile" && armMobileIssueBuffer(projectRoot) ? { projectRoot } : void 0;
14
- const extensionConsoleCapture = adapter === "extension" ? await startExtensionConsoleCapture(source, projectRoot) : void 0;
36
+ if (adapter === "extension") {
37
+ await ensureExtensionConsoleCapture(projectRoot).catch(() => void 0);
38
+ }
15
39
  const stat = safeStat(source.path);
16
40
  return {
17
41
  source,
18
42
  offset: stat?.size ?? 0,
19
43
  ...stat ? { inode: stat.ino } : {},
20
- ...mobileIssueBuffer ? { mobileIssueBuffer } : {},
21
- ...extensionConsoleCapture ? { extensionConsoleCapture } : {}
44
+ ...mobileIssueBuffer ? { mobileIssueBuffer } : {}
22
45
  };
23
46
  }
47
+ function isRunDiagnosticsDocument(value) {
48
+ if (!isRecord(value) || value.schemaVersion !== 1 || value.scope !== "recipe-run-application") return false;
49
+ if (value.status !== "clean" && value.status !== "review" && value.status !== "unavailable") return false;
50
+ return typeof value.note === "string" && Array.isArray(value.findings);
51
+ }
24
52
  function finishRunDiagnostics(baseline, result) {
25
53
  if (!baseline) return result;
26
54
  try {
@@ -56,9 +84,7 @@ function finishRunDiagnostics(baseline, result) {
56
84
  }
57
85
  }
58
86
  function stopRunDiagnostics(baseline) {
59
- const capture = baseline?.extensionConsoleCapture;
60
- if (!capture || capture.exitCode !== null || capture.signalCode !== null) return;
61
- capture.kill("SIGTERM");
87
+ void baseline;
62
88
  }
63
89
  function collectRunDiagnostics(baseline, bufferedIssues) {
64
90
  const stat = safeStat(baseline.source.path);
@@ -157,39 +183,6 @@ function runMobileIssueCommand(projectRoot, command) {
157
183
  return null;
158
184
  }
159
185
  }
160
- async function startExtensionConsoleCapture(source, projectRoot) {
161
- const cdpPort = process.env.CDP_PORT ?? process.env.RECIPE_CDP_PORT;
162
- if (!cdpPort) return void 0;
163
- fs.mkdirSync(path.dirname(source.path), { recursive: true });
164
- const before = safeStat(source.path)?.size ?? 0;
165
- const script = path.join(runnerDir, "adapters", "extension", "console-tail.mjs");
166
- const child = spawn(process.execPath, [script, "--cdp-port", cdpPort, "--log", source.path], {
167
- cwd: projectRoot,
168
- stdio: "ignore"
169
- });
170
- const attached = await waitForExtensionAttachment(source.path, before, child, 3e3);
171
- if (attached) {
172
- await delay(50);
173
- return child;
174
- }
175
- if (child.exitCode === null && child.signalCode === null) child.kill("SIGTERM");
176
- return void 0;
177
- }
178
- async function waitForExtensionAttachment(logPath, offset, child, timeoutMs) {
179
- const deadline = Date.now() + timeoutMs;
180
- while (Date.now() < deadline && child.exitCode === null && child.signalCode === null) {
181
- const stat = safeStat(logPath);
182
- if (stat && stat.size > offset) {
183
- const appended = readSlice(logPath, offset, Math.min(stat.size - offset, 64 * 1024));
184
- if (appended.includes("[attached]")) return true;
185
- }
186
- await delay(50);
187
- }
188
- return false;
189
- }
190
- function delay(ms) {
191
- return new Promise((resolve) => setTimeout(resolve, ms));
192
- }
193
186
  function redactPreview(value) {
194
187
  return value.replace(/\b(Bearer)\s+\S+/giu, "$1 [REDACTED]").replace(/\b(password|passphrase|mnemonic|seed(?:Phrase)?|privateKey|secret|token|authorization)\b\s*[:=]\s*(?:"[^"]*"|'[^']*'|\S+)/giu, "$1=[REDACTED]").replace(/\b(?:0x)?[a-f0-9]{64,}\b/giu, "[REDACTED_HEX]").replace(/(https?:\/\/[^\s?]+)\?\S+/giu, "$1?[REDACTED_QUERY]").slice(0, MAX_PREVIEW_CHARS);
195
188
  }
@@ -267,5 +260,7 @@ export {
267
260
  beginRunDiagnostics,
268
261
  collectRunDiagnostics,
269
262
  finishRunDiagnostics,
263
+ formatRunDiagnosticsForHuman,
264
+ readRunDiagnosticsDocument,
270
265
  stopRunDiagnostics
271
266
  };
@@ -53,6 +53,32 @@ function readExistingContext(contextPath, repoRoot, runtimeDir, adapter) {
53
53
  return { context, existed: true };
54
54
  }
55
55
  function validExistingContext(context, repoRoot, runtimeDir, adapter) {
56
+ const allowedFields = /* @__PURE__ */ new Set([
57
+ "schemaVersion",
58
+ "repoRoot",
59
+ "project",
60
+ "slotId",
61
+ "machine",
62
+ "runtimeOwner",
63
+ "strict",
64
+ "platform",
65
+ "runtimeDir",
66
+ "gitDirty",
67
+ "simulator",
68
+ "adbSerial",
69
+ "distDir",
70
+ "extensionId",
71
+ "gitHead",
72
+ "gitBranch",
73
+ "gitCommit",
74
+ "createdAt",
75
+ "session",
76
+ "cdpPort",
77
+ "watcherPort",
78
+ "metroPort",
79
+ "devServerPort"
80
+ ]);
81
+ if (Object.keys(context).some((field) => !allowedFields.has(field))) return false;
56
82
  if (context.schemaVersion !== void 0 && context.schemaVersion !== 1) return false;
57
83
  if (context.repoRoot !== void 0 && (typeof context.repoRoot !== "string" || path.resolve(context.repoRoot) !== repoRoot)) return false;
58
84
  if (context.platform !== void 0 && context.platform !== adapter) return false;
@@ -82,14 +108,6 @@ function validExistingContext(context, repoRoot, runtimeDir, adapter) {
82
108
  const value = context[field];
83
109
  if (value !== void 0 && (!Number.isInteger(value) || Number(value) < 1 || Number(value) > 65535)) return false;
84
110
  }
85
- if (context.runtimeStart !== void 0) {
86
- if (!context.runtimeStart || typeof context.runtimeStart !== "object" || Array.isArray(context.runtimeStart)) return false;
87
- const runtimeStart = context.runtimeStart;
88
- if (runtimeStart.approved !== void 0 && typeof runtimeStart.approved !== "boolean") return false;
89
- for (const field of ["command", "readyUrl"]) {
90
- if (runtimeStart[field] !== void 0 && (typeof runtimeStart[field] !== "string" || runtimeStart[field].length === 0)) return false;
91
- }
92
- }
93
111
  const forbiddenResources = adapter === "core" ? ["cdpPort", "watcherPort", "metroPort", "devServerPort", "simulator", "adbSerial", "extensionId"] : adapter === "extension" ? ["metroPort", "simulator", "adbSerial"] : ["cdpPort", "extensionId"];
94
112
  if (forbiddenResources.some((field) => context[field] !== void 0)) return false;
95
113
  return true;