@deeeed/metamask-harness 0.17.5 → 0.18.0

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 (132) hide show
  1. package/CHANGELOG.md +872 -0
  2. package/README.md +94 -140
  3. package/adapters/extension/console-tail.mjs +55 -24
  4. package/adapters/extension/ensure-browser.sh +6 -1
  5. package/adapters/extension/inject.mjs +6 -9
  6. package/adapters/extension/launch-browser.cjs +7 -1
  7. package/adapters/extension/launch-webpack.cjs +83 -0
  8. package/adapters/extension/launch.sh +8 -11
  9. package/adapters/extension/lib/chrome-args.cjs +8 -1
  10. package/adapters/extension/lib/macos-focus.cjs +32 -0
  11. package/adapters/extension/live.sh +12 -21
  12. package/adapters/extension/reattach.sh +1 -0
  13. package/adapters/extension/seed-fixture.sh +4 -12
  14. package/adapters/extension/sidepanel-toggle.sh +4 -1
  15. package/adapters/extension/stamp-runtime-title.cjs +58 -0
  16. package/adapters/extension/start-watch.sh +17 -4
  17. package/adapters/extension/stop-viewers.sh +1 -1
  18. package/adapters/extension/sync-webpack-dist.cjs +107 -0
  19. package/adapters/extension/verify.sh +1 -1
  20. package/adapters/manifest.json +53 -37
  21. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +16 -0
  22. package/adapters/mobile/bridge-runtime/console-forwarder.cjs +1 -1
  23. package/adapters/mobile/bridge-runtime/lib/match-bridge-target.cjs +3 -1
  24. package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +2 -3
  25. package/adapters/mobile/cleanup.sh +16 -176
  26. package/adapters/mobile/inject.sh +90 -671
  27. package/adapters/mobile/launch-console-forwarder.cjs +39 -0
  28. package/adapters/mobile/open-device.sh +45 -7
  29. package/adapters/mobile/start-console-forwarder.sh +70 -0
  30. package/adapters/mobile/start-metro.sh +0 -41
  31. package/adapters/mobile/verify.sh +19 -64
  32. package/adapters/mobile/wait-for-bridge.sh +22 -8
  33. package/adapters/shared/resolve-slot-ports-core.mjs +17 -6
  34. package/adapters/shared/resolve-slot-ports.sh +17 -5
  35. package/adapters/shared/update-check-worker.mjs +43 -0
  36. package/bin/mm-harness +21 -2
  37. package/dist/adapters/core/surface.js +4 -1
  38. package/dist/adapters/extension/console-capture.js +95 -0
  39. package/dist/adapters/extension/product-config.js +110 -0
  40. package/dist/adapters/extension/runtime-decision.js +20 -2
  41. package/dist/adapters/extension/surface.js +22 -3
  42. package/dist/adapters/mobile/perps-env.js +43 -3
  43. package/dist/adapters/mobile/prepare.js +39 -7
  44. package/dist/adapters/mobile/surface.js +5 -2
  45. package/dist/adapters/resolve-slot-ports.js +2 -2
  46. package/dist/adapters/slot-ports.js +13 -16
  47. package/dist/adapters.js +56 -16
  48. package/dist/checkout-lock.js +27 -2
  49. package/dist/cli-color.js +19 -0
  50. package/dist/cli-commands.js +1 -1
  51. package/dist/cli.js +7 -14
  52. package/dist/command-contract.js +451 -0
  53. package/dist/command-journal.js +225 -0
  54. package/dist/commands/call.js +170 -50
  55. package/dist/commands/check.js +9 -3
  56. package/dist/commands/completion-candidates.js +20 -13
  57. package/dist/commands/device-target.js +27 -12
  58. package/dist/commands/doctor.js +106 -25
  59. package/dist/commands/fixtures.js +92 -34
  60. package/dist/commands/flows.js +39 -10
  61. package/dist/commands/last.js +52 -0
  62. package/dist/commands/launch/extension.js +38 -15
  63. package/dist/commands/launch/index.js +207 -63
  64. package/dist/commands/list-executables.js +151 -29
  65. package/dist/commands/logs.js +8 -6
  66. package/dist/commands/manifest.js +270 -35
  67. package/dist/commands/parse-args.js +13 -1
  68. package/dist/commands/provision.js +10 -3
  69. package/dist/commands/run-engine.js +435 -105
  70. package/dist/commands/run-report.js +12 -3
  71. package/dist/commands/run.js +355 -50
  72. package/dist/commands/shared.js +75 -3
  73. package/dist/commands/status-probe.js +4 -1
  74. package/dist/commands/status.js +2 -1
  75. package/dist/commands/stop.js +7 -2
  76. package/dist/commands/update.js +16 -28
  77. package/dist/completions-cache.js +1 -1
  78. package/dist/doctor.js +57 -10
  79. package/dist/harness.js +47 -137
  80. package/dist/heal-bounds.js +2 -2
  81. package/dist/json-stream.js +57 -0
  82. package/dist/live-adapter-contract.js +138 -32
  83. package/dist/manifest.js +161 -1
  84. package/dist/mm-harness-cli.js +135 -54
  85. package/dist/paths.js +2 -5
  86. package/dist/recipe-security.js +178 -0
  87. package/dist/run-diagnostics.js +261 -0
  88. package/dist/runner.js +117 -8
  89. package/docs/CONTRIBUTING.md +137 -0
  90. package/docs/QA.md +185 -0
  91. package/docs/RECIPES.md +161 -0
  92. package/docs/SECURITY.md +88 -0
  93. package/library/README.md +4 -0
  94. package/library/actions/core/perps/_controller.mjs +10 -55
  95. package/library/actions/core/perps/read_account.mjs +2 -2
  96. package/library/actions/core/perps/read_orders.mjs +2 -1
  97. package/library/actions/core/perps/read_positions.mjs +2 -1
  98. package/library/actions/core/wallet/list_accounts.mjs +95 -0
  99. package/library/actions/extension/platform/cdp.mjs +1 -0
  100. package/library/actions/extension/wallet/list_accounts.mjs +41 -0
  101. package/library/actions/mobile/wallet/list_accounts.mjs +37 -0
  102. package/library/manifests/core.action-manifest.json +68 -7
  103. package/library/manifests/extension.action-manifest.json +53 -0
  104. package/library/manifests/mobile.action-manifest.json +81 -3
  105. package/library/recipes/runner/action-validation.extension.recipe.json +8 -1
  106. package/library/recipes/runner/action-validation.mobile.recipe.json +8 -1
  107. package/library/recipes/runner/smoke.core.recipe.json +27 -0
  108. package/library/recipes/wallet/smoke.extension.recipe.json +42 -0
  109. package/library/recipes/wallet/smoke.mobile.recipe.json +42 -0
  110. package/package.json +12 -5
  111. package/scripts/completions.sh +7 -7
  112. package/scripts/validate-human-outcomes.mjs +169 -0
  113. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +0 -185
  114. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +0 -1662
  115. package/adapters/mobile/overlay/compat/README.md +0 -28
  116. package/adapters/mobile/overlay/compat/rn81-message-event-source.patch +0 -42
  117. package/adapters/shared/resolve-farmslot-ports-core.mjs +0 -3
  118. package/adapters/shared/resolve-farmslot-ports.mjs +0 -5
  119. package/adapters/shared/resolve-farmslot-ports.sh +0 -7
  120. package/docs/ADAPTER-SURFACE.md +0 -119
  121. package/docs/CHEATSHEET.md +0 -61
  122. package/docs/CLI-SPEC.md +0 -1098
  123. package/docs/CODE-MAP.md +0 -62
  124. package/docs/DEBUG-HANDOVER.md +0 -36
  125. package/docs/MENTAL-MODEL.md +0 -295
  126. package/docs/UX-PRINCIPLES.md +0 -64
  127. package/docs/architecture.md +0 -398
  128. package/docs/live-adapter-contract.md +0 -188
  129. package/docs/package-boundaries.md +0 -47
  130. package/docs/perps-flow-catalog.md +0 -235
  131. package/docs/recipe-libraries.md +0 -301
  132. package/docs/runtime-file-conventions.md +0 -36
@@ -1,8 +1,18 @@
1
1
  import { spawn, spawnSync } from "node:child_process";
2
2
  import path from "node:path";
3
3
  import { detectAdapter } from "../harness.js";
4
+ import { colorHumanMessage } from "../cli-color.js";
4
5
  import { resolveLeafInvoke, shellLeafMissing } from "../leaf-invoke.js";
5
6
  const EXIT = { ok: 0, runtime: 1, usage: 2, infra: 3, bounded: 4, validation: 5 };
7
+ function writeInteractiveProgress(json, message, {
8
+ stdoutIsTTY = Boolean(process.stdout.isTTY),
9
+ stream = process.stderr
10
+ } = {}) {
11
+ if (json || !stdoutIsTTY) return false;
12
+ stream.write(`${colorHumanMessage(message, { stream })}
13
+ `);
14
+ return true;
15
+ }
6
16
  function parseFlags(argv, booleans) {
7
17
  const positional = [];
8
18
  const options = {};
@@ -138,11 +148,19 @@ function spawnScriptStreaming(script, args, cwd, options) {
138
148
  let settled = false;
139
149
  let didTimeout = false;
140
150
  let escalation;
151
+ let forwardedParentSignal;
152
+ let parentSignalFallback;
153
+ const parentSignals = ["SIGINT", "SIGTERM"];
154
+ const removeParentSignalHandlers = () => {
155
+ for (const signal of parentSignals) process.removeListener(signal, parentSignalHandlers[signal]);
156
+ };
141
157
  const finish = (result) => {
142
158
  if (settled) return;
143
159
  settled = true;
144
160
  if (timer) clearTimeout(timer);
145
- if (escalation && !didTimeout) clearTimeout(escalation);
161
+ if (escalation) clearTimeout(escalation);
162
+ if (parentSignalFallback) clearTimeout(parentSignalFallback);
163
+ removeParentSignalHandlers();
146
164
  resolve(result);
147
165
  };
148
166
  const signalChildTree = (signal) => {
@@ -152,6 +170,26 @@ function spawnScriptStreaming(script, args, cwd, options) {
152
170
  } catch {
153
171
  }
154
172
  };
173
+ const exitFromForwardedSignal = () => {
174
+ if (!forwardedParentSignal) return;
175
+ const signal = forwardedParentSignal;
176
+ forwardedParentSignal = void 0;
177
+ if (parentSignalFallback) clearTimeout(parentSignalFallback);
178
+ process.kill(process.pid, signal);
179
+ };
180
+ const parentSignalHandlers = Object.fromEntries(parentSignals.map((signal) => [signal, () => {
181
+ if (forwardedParentSignal) return;
182
+ forwardedParentSignal = signal;
183
+ signalChildTree(signal);
184
+ removeParentSignalHandlers();
185
+ parentSignalFallback = setTimeout(() => {
186
+ signalChildTree("SIGKILL");
187
+ exitFromForwardedSignal();
188
+ }, 1e3);
189
+ }]));
190
+ if (ownsProcessGroup) {
191
+ for (const signal of parentSignals) process.once(signal, parentSignalHandlers[signal]);
192
+ }
155
193
  const timeoutMs = spawnOptions.timeoutMs;
156
194
  const timer = Number.isFinite(timeoutMs) && Number(timeoutMs) > 0 ? setTimeout(() => {
157
195
  didTimeout = true;
@@ -182,8 +220,17 @@ function spawnScriptStreaming(script, args, cwd, options) {
182
220
  `);
183
221
  finish({ status: 1, output: message });
184
222
  });
223
+ child.on("exit", () => {
224
+ if (forwardedParentSignal) {
225
+ exitFromForwardedSignal();
226
+ return;
227
+ }
228
+ if (!didTimeout) return;
229
+ signalChildTree("SIGKILL");
230
+ finish({ status: 1, output, timedOut: true, timeoutMs: Number(timeoutMs) });
231
+ });
185
232
  child.on("close", (status) => {
186
- if (didTimeout && escalation) return;
233
+ if (didTimeout) signalChildTree("SIGKILL");
187
234
  finish({
188
235
  status: didTimeout ? 1 : status ?? 1,
189
236
  output,
@@ -208,9 +255,33 @@ function usageOut(json, command, message, userAction) {
208
255
  }
209
256
  return EXIT.usage;
210
257
  }
258
+ function checkoutBusyOut(json, command, message, lockPath) {
259
+ const userAction = `wait for the current owner, or inspect ${lockPath} if its process has exited`;
260
+ if (json) {
261
+ console.log(
262
+ JSON.stringify(
263
+ {
264
+ schemaVersion: 1,
265
+ command,
266
+ status: "fail",
267
+ exitCode: EXIT.bounded,
268
+ recoverable: false,
269
+ error: { code: "SANDBOX_BUSY", message, userAction }
270
+ },
271
+ null,
272
+ 2
273
+ )
274
+ );
275
+ } else {
276
+ console.error(`\u2717 mm-harness ${command}: ${message}
277
+ Next: ${userAction}`);
278
+ }
279
+ return EXIT.bounded;
280
+ }
211
281
  export {
212
282
  ADAPTER_DETECT_NEXT,
213
283
  EXIT,
284
+ checkoutBusyOut,
214
285
  flag,
215
286
  parseFlags,
216
287
  resolveAdapter,
@@ -220,5 +291,6 @@ export {
220
291
  spawnScriptStreaming,
221
292
  str,
222
293
  targetOf,
223
- usageOut
294
+ usageOut,
295
+ writeInteractiveProgress
224
296
  };
@@ -22,6 +22,9 @@ function routeToString(route) {
22
22
  function deriveWalletState(account, route) {
23
23
  const routeStr = (routeToString(route) ?? "").toLowerCase();
24
24
  if (routeStr.includes("onboard")) return "onboarding";
25
+ if (routeStr === "login" || routeStr === "lockscreen") {
26
+ return "locked";
27
+ }
25
28
  if (account !== null && account !== void 0 && typeof account === "object") return "unlocked";
26
29
  return "locked";
27
30
  }
@@ -34,7 +37,7 @@ function parseBridgeEntries(raw) {
34
37
  deviceName: typeof e.deviceName === "string" ? e.deviceName : "",
35
38
  platform: typeof e.platform === "string" ? e.platform : "",
36
39
  // agenticPresent is only set when the bridge explicitly sends it (false = absent).
37
- // Omitted by older bridge versions → leave undefined so caller treats as present.
40
+ // An omitted value remains unknown.
38
41
  ...typeof e.agenticPresent === "boolean" ? { agenticPresent: e.agenticPresent } : {}
39
42
  }));
40
43
  }
@@ -31,7 +31,7 @@ async function handleStatus({ options }) {
31
31
  if (adapter === "mobile") {
32
32
  const dtResult = applyDeviceTargeting("status", adapter, options, { gate: false, rerun: "" });
33
33
  if ("code" in dtResult) {
34
- return usageOut(json, "status", dtResult.message, "mm-harness status --device <id> or --all-devices");
34
+ return usageOut(json, "status", dtResult.message, dtResult.userAction);
35
35
  }
36
36
  }
37
37
  const next = surface.hints.relaunch;
@@ -104,6 +104,7 @@ function renderRuntimeStatus(runtime, out) {
104
104
  );
105
105
  }
106
106
  function nextForRuntime(launchHint, relaunchHint, runtime) {
107
+ if (runtime.nextAction) return runtime.nextAction;
107
108
  if (runtime.decision === "ready") return "mm-harness logs";
108
109
  if (runtime.decision === "relaunch") return launchHint;
109
110
  return relaunchHint;
@@ -5,7 +5,8 @@ import {
5
5
  parseArgs,
6
6
  optionFlag,
7
7
  optionString,
8
- resolveAdapter
8
+ resolveAdapter,
9
+ shellQuote
9
10
  } from "./parse-args.js";
10
11
  async function handleStop(argv) {
11
12
  const { options } = parseArgs(argv, "stop");
@@ -22,6 +23,8 @@ async function handleStop(argv) {
22
23
  if (stop.kind === "headless") {
23
24
  return usageOut(json, "stop", stop.message, stop.userAction);
24
25
  }
26
+ const next = adapter === "extension" ? `${surface.hints.launch} --target ${shellQuote(target)}` : void 0;
27
+ const userAction = `mm-harness status --target ${shellQuote(target)} --json`;
25
28
  if (json) {
26
29
  console.log(
27
30
  JSON.stringify(
@@ -33,7 +36,8 @@ async function handleStop(argv) {
33
36
  status: stop.status === 0 ? "pass" : "fail",
34
37
  ...stop.signalled !== void 0 ? { signalled: stop.signalled } : {},
35
38
  exitCode: stop.status,
36
- ...stop.output ? { output: stop.output } : {}
39
+ ...stop.output ? { output: stop.output } : {},
40
+ ...stop.status === 0 ? next ? { next } : {} : { error: { code: "DEV_SERVER_STOP_FAILED", message: stop.summary, userAction } }
37
41
  },
38
42
  null,
39
43
  2
@@ -43,6 +47,7 @@ async function handleStop(argv) {
43
47
  if (stop.output) process.stderr.write(`${stop.output}
44
48
  `);
45
49
  console.error(`${color(stop.status === 0 ? "ok" : "err", stop.status === 0 ? "\u2713" : "\u2717")} ${stop.summary}`);
50
+ if (stop.status !== 0 || next) console.error(` Next: ${stop.status === 0 ? next : userAction}`);
46
51
  }
47
52
  return stop.status;
48
53
  }
@@ -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,8 @@ 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
+ const REGISTRY_DIST_TAGS_ENDPOINT = `https://registry.npmjs.org/-/package/${encodeURIComponent(PACKAGE_NAME)}/dist-tags`;
10
11
  function currentVersion() {
11
12
  try {
12
13
  const pkg = JSON.parse(fs.readFileSync(path.join(runnerDir, "package.json"), "utf8"));
@@ -56,24 +57,6 @@ function fetchLatest(timeoutMs) {
56
57
  if (!latest) return { error: { kind: "unreachable", message: "npm returned no dist-tag for latest" } };
57
58
  return { latest };
58
59
  }
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
60
  function teachFailure(json, current, code, message, userAction) {
78
61
  if (json) {
79
62
  console.log(
@@ -211,23 +194,28 @@ function nudgeDisabled() {
211
194
  function nudgeLine(current, latest) {
212
195
  return isNewer(latest, current) ? `mm-harness ${current} \u2192 ${latest} available \xB7 run: mm-harness update` : null;
213
196
  }
214
- async function maybeNudge(now = Date.now()) {
197
+ function maybeNudge(now = Date.now()) {
215
198
  if (nudgeDisabled()) return;
216
199
  const file = cacheFile();
217
200
  const cache = readCache(file);
218
201
  const current = currentVersion();
219
- let latest = cache?.latest ?? "";
220
202
  process.once("exit", () => {
221
- const line = nudgeLine(current, latest);
203
+ const line = nudgeLine(current, cache?.latest ?? "");
222
204
  if (line) process.stderr.write(`${line}
223
205
  `);
224
206
  });
225
207
  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 });
208
+ writeCache(file, { lastCheck: now, latest: cache?.latest ?? "" });
209
+ const worker = path.join(runnerDir, "adapters", "shared", "update-check-worker.mjs");
210
+ if (!fs.existsSync(worker)) return;
211
+ try {
212
+ const child = spawn(
213
+ process.execPath,
214
+ [worker, file, String(NUDGE_FETCH_TIMEOUT_MS), REGISTRY_DIST_TAGS_ENDPOINT],
215
+ { detached: true, stdio: "ignore" }
216
+ );
217
+ child.unref();
218
+ } catch {
231
219
  }
232
220
  }
233
221
  }
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { recipeRuntimePath } from "./paths.js";
4
- const COMPLETION_CACHE_VERSION = 1;
4
+ const COMPLETION_CACHE_VERSION = 2;
5
5
  const COMPLETION_CACHE_TTL_MS = 6e4;
6
6
  function completionCachePath(projectRoot) {
7
7
  return recipeRuntimePath(projectRoot, ".completion-cache.json");
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);
@@ -158,12 +155,7 @@ function createDoctorReport(adapter, target, manifestValidation, actionManifestP
158
155
  requiredChecks,
159
156
  adapter,
160
157
  target,
161
- runner: {
162
- name: "@metamask/recipe-runner",
163
- runnerDir,
164
- actionManifestPath,
165
- harnessPackage: "@farmslot/recipe-harness"
166
- },
158
+ runner: runnerProvenance(actionManifestPath),
167
159
  compatibilityMode: mode,
168
160
  shape: repoShape(target),
169
161
  fixture: fixtureSummary(target, adapter),
@@ -172,6 +164,60 @@ function createDoctorReport(adapter, target, manifestValidation, actionManifestP
172
164
  manifestValidation: manifestValidation.summary
173
165
  };
174
166
  }
167
+ function runnerInstallKind(runnerRoot, invokedPath, executablePath) {
168
+ const normalizedRoot = path.normalize(runnerRoot);
169
+ const nodeModulesSegment = `${path.sep}node_modules${path.sep}`;
170
+ const globalNodeModulesSegment = `${path.sep}lib${path.sep}node_modules${path.sep}`;
171
+ if (normalizedRoot.includes(globalNodeModulesSegment)) return "global-install";
172
+ const nodeModulesIndex = normalizedRoot.lastIndexOf(nodeModulesSegment);
173
+ if (nodeModulesIndex >= 0) {
174
+ const nodeModulesRoot = normalizedRoot.slice(
175
+ 0,
176
+ nodeModulesIndex + nodeModulesSegment.length - 1
177
+ );
178
+ const invoked = path.resolve(invokedPath);
179
+ const projectBin = `${path.join(nodeModulesRoot, ".bin")}${path.sep}`;
180
+ if (invoked === path.resolve(executablePath) || invoked.startsWith(projectBin)) {
181
+ return "project-install";
182
+ }
183
+ return "global-install";
184
+ }
185
+ if (path.resolve(invokedPath) !== path.resolve(executablePath)) return "local-link";
186
+ return "source-checkout";
187
+ }
188
+ function runnerProvenance(actionManifestPath) {
189
+ const packagePath = path.join(runnerDir, "package.json");
190
+ const packageDocument = readJsonObject(packagePath);
191
+ const packageName = packageDocument.name;
192
+ const version = packageDocument.version;
193
+ if (packageName !== "@deeeed/metamask-harness") {
194
+ throw new Error(`Expected @deeeed/metamask-harness package name in ${packagePath}`);
195
+ }
196
+ if (typeof version !== "string" || !version) {
197
+ throw new Error(`Expected package version in ${packagePath}`);
198
+ }
199
+ const executablePath = path.resolve(
200
+ process.env.MM_HARNESS_EXECUTABLE ?? path.join(runnerDir, "bin/mm-harness")
201
+ );
202
+ const invokedPath = path.resolve(process.env.MM_HARNESS_INVOKED_AS ?? executablePath);
203
+ const installKind = runnerInstallKind(runnerDir, invokedPath, executablePath);
204
+ const packageSource = runnerDir.includes(`${path.sep}node_modules${path.sep}`) ? "node_modules" : "source-checkout";
205
+ return {
206
+ name: "@metamask/recipe-runner",
207
+ packageName,
208
+ version,
209
+ runnerDir,
210
+ packagePath,
211
+ packageSource,
212
+ installKind,
213
+ linked: installKind === "local-link",
214
+ global: installKind === "global-install",
215
+ invokedPath,
216
+ executablePath,
217
+ actionManifestPath,
218
+ harnessPackage: "@farmslot/recipe-harness"
219
+ };
220
+ }
175
221
  function readJsonObject(file) {
176
222
  const value = readJson(file);
177
223
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -187,5 +233,6 @@ export {
187
233
  renderRuntimeContext,
188
234
  repoShape,
189
235
  requiredDoctorCheckSummary,
236
+ runnerInstallKind,
190
237
  runtimeContextSummary
191
238
  };