@deeeed/metamask-harness 0.84.0 → 0.85.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 (60) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/adapters/extension/console-tail.mjs +11 -7
  3. package/adapters/extension/ensure-browser.sh +17 -18
  4. package/adapters/mobile/start-console-forwarder.sh +8 -0
  5. package/adapters/shared/resolve-slot-ports-core.mjs +48 -6
  6. package/dist/adapters/extension/actions.js +5 -0
  7. package/dist/adapters/extension/console-capture.js +18 -0
  8. package/dist/adapters/extension/fixtures.js +2 -0
  9. package/dist/adapters/extension/overlay.js +5 -4
  10. package/dist/adapters/extension/runtime.js +2 -0
  11. package/dist/adapters/mobile/launch.js +19 -2
  12. package/dist/adapters/mobile/provision.js +4 -2
  13. package/dist/adapters/mobile/run.js +2 -1
  14. package/dist/adapters/mobile/verify.js +15 -7
  15. package/dist/adapters/web-dapp/actions.js +7 -5
  16. package/dist/adapters.js +5 -3
  17. package/dist/command-contract.js +28 -19
  18. package/dist/commands/check.js +5 -7
  19. package/dist/commands/debug.js +2 -2
  20. package/dist/commands/doctor.js +2 -3
  21. package/dist/commands/domain.js +4 -3
  22. package/dist/commands/fixtures.js +3 -3
  23. package/dist/commands/help.js +6 -5
  24. package/dist/commands/launch/extension.js +9 -7
  25. package/dist/commands/logs.js +2 -2
  26. package/dist/commands/prepare.js +4 -4
  27. package/dist/commands/provision.js +2 -2
  28. package/dist/commands/review.js +8 -10
  29. package/dist/commands/runtime-launch.js +2 -0
  30. package/dist/commands/status.js +4 -2
  31. package/dist/commands/task-init.js +2 -3
  32. package/dist/console-classifier.js +3 -2
  33. package/dist/doctor.js +6 -2
  34. package/dist/mm-harness-cli.js +19 -6
  35. package/dist/paths.js +9 -4
  36. package/dist/recipe-security.js +1 -1
  37. package/dist/review/knowledge.js +10 -2
  38. package/dist/runner.js +18 -3
  39. package/docs/CONTRIBUTING.md +8 -0
  40. package/docs/RECIPES.md +14 -1
  41. package/docs/cli-contract.md +7 -6
  42. package/library/actions/extension/app/assert_no_console_errors.mjs +95 -0
  43. package/library/actions/extension/platform/cdp.mjs +26 -0
  44. package/library/actions/mobile/app/assert_no_console_errors.mjs +129 -0
  45. package/library/actions/shared/console-assert.mjs +174 -0
  46. package/library/actions/shared/console-collector.mjs +10 -3
  47. package/library/actions/shared/console-findings.mjs +108 -7
  48. package/library/actions/web-dapp/app/assert_no_console_errors.mjs +10 -54
  49. package/library/actions/web-dapp/platform/hud.mjs +2 -9
  50. package/library/actions/web-dapp/platform/page.mjs +62 -16
  51. package/library/actions/web-dapp/platform/runtime.mjs +1 -1
  52. package/library/diagnostics/console-allowlist.json +13 -0
  53. package/library/manifests/extension.action-manifest.json +36 -0
  54. package/library/manifests/mobile.action-manifest.json +36 -0
  55. package/library/manifests/web-dapp.action-manifest.json +1 -1
  56. package/library/recipes/core/health/check.recipe.json +40 -0
  57. package/library/recipes/extension/health/check.recipe.json +71 -0
  58. package/library/recipes/mobile/health/check.recipe.json +71 -0
  59. package/package.json +9 -9
  60. package/site/assets/help-recipes.json +2 -1
@@ -19,6 +19,12 @@ const adapterIds = (tokens) => adapterChoices(optionValues(tokens, "--library"),
19
19
  env: operatorLibraryEnv()
20
20
  });
21
21
  const ADAPTER = { "--adapter": value(adapterIds) };
22
+ function selectableAdapters(libraries = []) {
23
+ return adapterIds(libraries.flatMap((entry) => ["--library", entry]));
24
+ }
25
+ function withAdapterList(text, libraries = []) {
26
+ return text.replaceAll("{adapters}", selectableAdapters(libraries).join("|"));
27
+ }
22
28
  const MOBILE_PLATFORM = { "--platform": value(["ios", "android"]) };
23
29
  const ADAPTER_OR_MOBILE_PLATFORM = { "--platform": value((tokens) => [...adapterIds(tokens), "ios", "android"]) };
24
30
  const DEVICE = { "--device": value() };
@@ -88,7 +94,7 @@ const PUBLIC_COMMANDS = [
88
94
 
89
95
  Loads the complete recipe guide shipped with this harness.
90
96
 
91
- --adapter <mobile|extension|core|terminal> Target adapter (auto-detected inside a checkout)
97
+ --adapter <{adapters}> Target adapter (auto-detected inside a checkout)
92
98
  --target <path> Checkout path (default: cwd)
93
99
  --json Load the same guide as structured agent context
94
100
 
@@ -289,6 +295,7 @@ Example:
289
295
  Home dashboard for the current checkout \u2014 the detected adapter, the next command
290
296
  to run, and (mobile only) the connected devices with live app state. Aliases: health, home.
291
297
 
298
+ --adapter <{adapters}> Target adapter (auto-detected inside a checkout)
292
299
  --target <path> Checkout path (default: cwd)
293
300
  --json Machine-readable envelope { adapter, target, view, devices[], next }
294
301
  --fast Skip all live-state probes (instant output, safe for scripts)
@@ -334,7 +341,7 @@ Example:
334
341
  mm-harness status --fast
335
342
  mm-harness status --watch`,
336
343
  contract: {
337
- options: options(HELP, JSON, TARGET, DEVICE, {
344
+ options: options(HELP, JSON, TARGET, ADAPTER, ADAPTER_OR_MOBILE_PLATFORM, DEVICE, {
338
345
  "--fast": bool(),
339
346
  "--all-devices": bool(),
340
347
  "--watch": bool(),
@@ -429,7 +436,7 @@ Example:
429
436
  refuses with the numbered choice when that is ambiguous.
430
437
 
431
438
  --target <path> Checkout path (default: cwd)
432
- --platform <mobile|extension|core|terminal> Force the platform (auto-detected inside a checkout)
439
+ --platform <{adapters}> Force the platform (auto-detected inside a checkout)
433
440
  --mobile-platform <ios|android> Mobile target when status cannot decide it
434
441
  --clear-metro Mobile only: clear Metro's transform cache in the launch step
435
442
  --artifacts-dir <dir> Where sandbox.json and the per-step reports land
@@ -633,7 +640,7 @@ Materialize:
633
640
  --matrix Compare action availability across Mobile, Extension, Core, and Terminal
634
641
  --categories List compact action categories and counts
635
642
  --category <name> List only one category (for example ui, wallet, or perps)
636
- --adapter <mobile|extension|core|terminal> Target adapter (auto-detected inside a checkout)
643
+ --adapter <{adapters}> Target adapter (auto-detected inside a checkout)
637
644
  --target <path> Checkout path (default: cwd)
638
645
  --raw Dump raw action registry JSON
639
646
  --json Machine-readable output
@@ -674,7 +681,7 @@ Example:
674
681
  core headless \u2014 no dev server to stop (teaching error)
675
682
 
676
683
  --port <port> Metro/webpack port override; Extension CDP remains checkout-context scoped
677
- --adapter <mobile|extension|core|terminal> Target adapter (auto-detected inside a checkout)
684
+ --adapter <{adapters}> Target adapter (auto-detected inside a checkout)
678
685
  --target <path> Checkout path (default: cwd)
679
686
  --json Machine-readable output
680
687
 
@@ -707,7 +714,7 @@ Example:
707
714
  --sort <domain|name|library> With --list: grouping/order (default: domain; JSON: name)
708
715
  --device <udid|serial|name> Mobile only: target this device (sets IOS_SIMULATOR / ADB_SERIAL and updates agentic-runtime.json). Without it, >1 connected mobile device fails fast and lists them. Do not shut down other devices to force selection.
709
716
  --cdp-port <port> Extension only: bind the action to this live browser runtime.
710
- --adapter <mobile|extension|core|terminal> Target adapter (auto-detected inside a checkout)
717
+ --adapter <{adapters}> Target adapter (auto-detected inside a checkout)
711
718
  --target <path> Checkout path (default: cwd)
712
719
  --artifacts-dir <dir> Where to write evidence (default: <task>/artifacts/calls/<action>-<id>
713
720
  inside a task, else temp/recipe/calls/<action>-<id>)
@@ -765,7 +772,7 @@ Discovery:
765
772
 
766
773
  Common overrides:
767
774
  --device <udid|serial|name> Mobile only: target this device (sets IOS_SIMULATOR / ADB_SERIAL and updates agentic-runtime.json). Without it, >1 connected mobile device fails fast and lists them. Do not shut down other devices to force selection.
768
- --adapter <mobile|extension|core|terminal> Target adapter (auto-detected inside a checkout)
775
+ --adapter <{adapters}> Target adapter (auto-detected inside a checkout)
769
776
  --platform <ios|android> Mobile platform preference; use --adapter for the product adapter
770
777
  --target <path> Checkout path (default: cwd)
771
778
  --artifacts-dir <dir> Override evidence output (default: active task/artifacts, otherwise a checkout-local temp/recipe/runs/<run>)
@@ -778,7 +785,7 @@ Common overrides:
778
785
  --json Machine-readable output
779
786
  --json-stream Line-flushed JSONL progress + terminal event
780
787
  --record-video=full-run Record a video of the run
781
- --hud <auto|show|hide> Recipe HUD policy (auto shows normal Mobile recipes and a headful terminal browser; hide is explicit)
788
+ --hud <show|hide> Recipe HUD: each node's intent drawn over the app on every adapter, headless included (default: show)
782
789
 
783
790
  Advanced integrations:
784
791
  runtime: --cdp-port, --watcher-port, --runtime-dir, --slot,
@@ -801,7 +808,7 @@ Example:
801
808
  "--plan": bool(),
802
809
  "--proof": bool(),
803
810
  "--describe": bool(),
804
- "--hud": value(["auto", "show", "hide"])
811
+ "--hud": value(["show", "hide"])
805
812
  }),
806
813
  positionals: [{ label: "recipe" }],
807
814
  minimumPositionals: 1,
@@ -850,7 +857,7 @@ Example:
850
857
  --cdp-port <port> Extension CDP port for the liveness probe (env: CDP_PORT / RECIPE_CDP_PORT)
851
858
  --device <udid|serial|name> Mobile only: target this device (env: IOS_SIMULATOR / ADB_SERIAL). doctor reports the connected devices; it never gates on ambiguity.
852
859
  --all-devices Mobile only: show every connected device instead of the slot-scoped target
853
- --adapter <mobile|extension|core|terminal> Target adapter (auto-detected inside a checkout)
860
+ --adapter <{adapters}> Target adapter (auto-detected inside a checkout)
854
861
  --target <path> Checkout path (default: cwd)
855
862
  --runtime-dir <dir> Runtime dir containing agentic-runtime.json (relative to target)
856
863
  --json Machine-readable output (incompatible with --print-ready)
@@ -882,7 +889,7 @@ Example:
882
889
  when available. Does not launch an app or run a recipe.
883
890
 
884
891
  --target <path> Checkout path (default: cwd)
885
- --adapter <mobile|extension|core|terminal> Adapter label (auto-detected when possible)
892
+ --adapter <{adapters}> Adapter label (auto-detected when possible)
886
893
  --base <ref> Diff base (default: PR base, then remote HEAD, then repo fallback)
887
894
  --profile <fast|full> Validation depth (default: fast)
888
895
  --fix Fix changed files, then validate them
@@ -929,11 +936,11 @@ Example:
929
936
  Optional advice:
930
937
  advisor-status --json
931
938
  Resolve saved settings and overrides without reading tasks or calling a model.
932
- plan --task <task.json> --adapter <mobile|extension|core|terminal> --advisor typesafe
939
+ plan --task <task.json> --adapter <{adapters}> --advisor typesafe
933
940
  Assess each acceptance criterion before a recipe exists. Task JSON contains
934
941
  acceptanceCriteria: ["criterion", "..."] (1\u201320 entries), with optional title/description.
935
942
  Returns known candidate refs for reuse/composition, or an explicit discovery gap.
936
- advise --recipe <recipe.json> --task <TASK.md> --adapter <mobile|extension|core|terminal>
943
+ advise --recipe <recipe.json> --task <TASK.md> --adapter <{adapters}>
937
944
  --advisor <provider|off> Override saved policy for this task (currently: typesafe)
938
945
  --advisor-model <model> Override the provider default model or alias
939
946
  --advisor-stages <csv> Select plan,draft,evidence (saved/default: plan)
@@ -983,7 +990,7 @@ Example:
983
990
  provisioning path only: artifact cache + simulator create + simctl install.
984
991
  JavaScript dependencies and Metro remain dispatch-time launch concerns.
985
992
 
986
- --adapter <mobile|extension|core|terminal> Target adapter (mobile supported; extension/core/terminal teach)
993
+ --adapter <{adapters}> Target adapter (mobile supported; the others teach)
987
994
  --target <path> Slot checkout path (default: cwd)
988
995
  --platform <ios> Platform (default ios)
989
996
  --simulator <name|udid> Override agentic-runtime.json simulator (alias: --device)
@@ -1027,7 +1034,7 @@ Example:
1027
1034
  artifact cache + simulator create + simctl install. JavaScript dependencies
1028
1035
  and Metro remain dispatch-time launch concerns.
1029
1036
 
1030
- --adapter <mobile|extension|core|terminal> Target adapter (auto-detected inside a checkout)
1037
+ --adapter <{adapters}> Target adapter (auto-detected inside a checkout)
1031
1038
  --target <path> Checkout path (default: cwd)
1032
1039
  --runway Thin mobile Runway artifact install instead of overlay install
1033
1040
  --platform <ios> Runway platform (default ios)
@@ -1062,7 +1069,7 @@ Example:
1062
1069
  Extension live checks infer the checkout CDP port from runtime context,
1063
1070
  pool, or slot suffix when --cdp-port is omitted.
1064
1071
 
1065
- --adapter <mobile|extension|core|terminal> Target adapter (auto-detected inside a checkout)
1072
+ --adapter <{adapters}> Target adapter (auto-detected inside a checkout)
1066
1073
  --target <path> Checkout path (default: cwd)
1067
1074
  --json Machine-readable output
1068
1075
 
@@ -1082,7 +1089,7 @@ Example:
1082
1089
 
1083
1090
  Remove the installed overlay and restore the checkout.
1084
1091
 
1085
- --adapter <mobile|extension|core|terminal> Target adapter (auto-detected inside a checkout)
1092
+ --adapter <{adapters}> Target adapter (auto-detected inside a checkout)
1086
1093
  --target <path> Checkout path (default: cwd)
1087
1094
 
1088
1095
  Example:
@@ -1235,7 +1242,7 @@ Example:
1235
1242
  --source <label> Log source per adapter \u2014 mobile: metro|app;
1236
1243
  extension: extension|dapp|webpack|watcher|rebuild.
1237
1244
  Core is headless (teaching error).
1238
- --adapter <mobile|extension|core|terminal> Target adapter (auto-detected inside a checkout)
1245
+ --adapter <{adapters}> Target adapter (auto-detected inside a checkout)
1239
1246
  --target <path> Checkout path (default: cwd)
1240
1247
  --json Machine-readable output
1241
1248
 
@@ -1458,5 +1465,7 @@ export {
1458
1465
  HIDDEN_COMMANDS,
1459
1466
  PUBLIC_COMMANDS,
1460
1467
  REPLACED_OPTIONS,
1461
- explainMissingAdapterLibrary
1468
+ explainMissingAdapterLibrary,
1469
+ selectableAdapters,
1470
+ withAdapterList
1462
1471
  };
@@ -1,8 +1,7 @@
1
- import { detectAdapter, EXIT, usageOut, optionFlag, optionString, parseArgs, shellQuote } from "@farmslot/recipe-cli/harness";
1
+ import { EXIT, contextAdapter, usageOut, optionFlag, optionString, parseArgs, shellQuote } from "@farmslot/recipe-cli/harness";
2
2
  import { spawnSync } from "node:child_process";
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
- import "../adapters/surface.js";
6
5
  const USAGE = "mm-harness check diff [--fix] [--target <repo>] [--adapter <adapter>] [--base <ref>] [--profile fast|full] [--artifacts-dir <dir>] [--json]";
7
6
  const JS_TS_EXT_RE = /\.(?:c|m)?jsx?$|\.tsx?$/u;
8
7
  const FORMAT_EXT_RE = /\.(?:c|m)?jsx?$|\.tsx?$|\.json$|\.md$|\.css$|\.scss$|\.ya?ml$/u;
@@ -20,12 +19,11 @@ async function handleCheck(argv) {
20
19
  return usageOut(json, "check", message, USAGE);
21
20
  }
22
21
  const target = path.resolve(optionString(options, "target") ?? process.cwd());
23
- const explicitAdapter = optionString(options, "adapter") ?? optionString(options, "platform");
24
- const detected = explicitAdapter ?? detectAdapter(target);
25
- if (detected === "ios" || detected === "android") {
26
- return usageOut(json, "check", `unsupported adapter '${detected}'`, USAGE);
22
+ const platform = optionString(options, "adapter") ? void 0 : optionString(options, "platform");
23
+ if (platform === "ios" || platform === "android") {
24
+ return usageOut(json, "check", `unsupported adapter '${platform}'`, USAGE);
27
25
  }
28
- const adapter = detected;
26
+ const adapter = contextAdapter(target);
29
27
  const profileInput = optionString(options, "profile");
30
28
  if (profileInput && profileInput !== "fast" && profileInput !== "full") {
31
29
  return usageOut(json, "check", "--profile must be fast or full", USAGE);
@@ -1,4 +1,4 @@
1
- import { adapterDetectNext, EXIT, flag, parseFlags, resolveFlagsAdapter, spawnScript, targetOf, usageOut } from "@farmslot/recipe-cli/harness";
1
+ import { adapterDetectNext, EXIT, flag, parseFlags, contextAdapter, spawnScript, targetOf, usageOut } from "@farmslot/recipe-cli/harness";
2
2
  import path from "node:path";
3
3
  import { runnerDir } from "../paths.js";
4
4
  import { getAdapterSurface } from "../adapters/surface.js";
@@ -7,7 +7,7 @@ async function handleDebug(argv) {
7
7
  const { options } = parseFlags(argv, DEBUG_BOOLEANS);
8
8
  const json = flag(options, "json");
9
9
  const target = targetOf(options);
10
- const adapter = resolveFlagsAdapter(options, target);
10
+ const adapter = contextAdapter(target);
11
11
  if (!adapter) return usageOut(json, "debug", `could not detect the MetaMask repo type for ${target}`, adapterDetectNext());
12
12
  const surface = getAdapterSurface(adapter);
13
13
  if (surface.headless) return usageOut(json, "debug", `${adapter} is headless; there is no debug console.`, surface.hints.relaunch);
@@ -2,7 +2,7 @@ import { execFileSync } from "node:child_process";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { featureFlagReport } from "../feature-flags.js";
5
- import { adapterPluginChecks, color, acquireCheckoutLock, detectAdapter, resolveRuntimeContextPath, ensureOverlay, newHealState, recipeRunning, adapterDetectNext, checkoutBusyOut, EXIT, usageOut, actionManifestPathOption, applyRuntimeDirOption, optionFlag, optionString, shellQuote, targetPath } from "@farmslot/recipe-cli/harness";
5
+ import { adapterPluginChecks, color, acquireCheckoutLock, contextAdapter, resolveRuntimeContextPath, ensureOverlay, newHealState, recipeRunning, adapterDetectNext, checkoutBusyOut, EXIT, usageOut, actionManifestPathOption, applyRuntimeDirOption, optionFlag, optionString, shellQuote, targetPath } from "@farmslot/recipe-cli/harness";
6
6
  import { collectCheckoutView, renderCheckoutView } from "../checkout-view.js";
7
7
  import { createDoctorReport, fixtureSummary, requiredDoctorCheckSummary } from "../doctor.js";
8
8
  import { assertAdapter, runnerDir } from "../paths.js";
@@ -33,8 +33,7 @@ async function handleDoctor({ options }) {
33
33
  const expectLive = optionFlag(options, "expectLive") || printReady;
34
34
  const allDevices = optionFlag(options, "allDevices");
35
35
  const platformOption = optionString(options, "platform");
36
- const explicitAdapter = optionString(options, "adapter") ?? (platformOption === "ios" || platformOption === "android" ? "mobile" : platformOption);
37
- const adapter = explicitAdapter ?? detectAdapter(target);
36
+ const adapter = contextAdapter(target);
38
37
  if (!adapter) {
39
38
  return usageOut(json, "doctor", `could not detect the MetaMask repo type for ${target}`, adapterDetectNext());
40
39
  }
@@ -4,7 +4,8 @@ import {
4
4
  classifyDomain,
5
5
  configPath,
6
6
  discoverLibraries,
7
- readConfig
7
+ readConfig,
8
+ REVIEW_ADAPTERS
8
9
  } from "../review/knowledge.js";
9
10
  import { adapterFromOptions } from "./review.js";
10
11
  async function handleDomain(argv) {
@@ -12,14 +13,14 @@ async function handleDomain(argv) {
12
13
  const target = targetPath(parsed.options);
13
14
  const json = optionFlag(parsed.options, "json");
14
15
  const declared = optionString(parsed.options, "domain") ?? process.env.MM_HARNESS_DOMAIN;
15
- const detected = adapterFromOptions(parsed.options, target);
16
+ const detected = adapterFromOptions();
16
17
  const adapter = detected === "unscoped" ? void 0 : detected;
17
18
  if (declared) {
18
19
  emit(json, { domain: declared, source: "declared", adapter: adapter ?? null, target });
19
20
  return 0;
20
21
  }
21
22
  if (!adapter) {
22
- throw usageError("No MetaMask checkout detected: pass --adapter <mobile|extension|core|terminal> or --domain <name>.");
23
+ throw usageError(`No MetaMask checkout detected: pass --adapter <${REVIEW_ADAPTERS.join("|")}> or --domain <name>.`);
23
24
  }
24
25
  const config = readConfig(configPath());
25
26
  const libraries = discoverLibraries({ target, config });
@@ -4,7 +4,7 @@ import path from "node:path";
4
4
  import { runnerDir, walletFixturePath } from "../paths.js";
5
5
  import { getAdapterSurface } from "../adapters/surface.js";
6
6
  import { fixtureFileSummary, fixtureSummary } from "../doctor.js";
7
- import { acquireCheckoutLock, adapterDetectNext, checkoutBusyOut, EXIT, flag, parseFlags, resolveFlagsAdapter, scriptOverride, spawnScript, spawnScriptStreaming, str, targetOf, usageOut } from "@farmslot/recipe-cli/harness";
7
+ import { acquireCheckoutLock, adapterDetectNext, checkoutBusyOut, EXIT, flag, parseFlags, contextAdapter, scriptOverride, spawnScript, spawnScriptStreaming, str, targetOf, usageOut } from "@farmslot/recipe-cli/harness";
8
8
  import { applyDeviceTargeting } from "./device-target.js";
9
9
  import { FeatureFlagSpecError, fixtureFeatureFlags, parseRemoteFlagSpec } from "../feature-flags.js";
10
10
  const FIXTURES_BOOLEANS = /* @__PURE__ */ new Set(["dev", "force", "json"]);
@@ -38,7 +38,7 @@ function fixturesStatus(options) {
38
38
  );
39
39
  }
40
40
  const target = targetOf(options);
41
- const adapter = resolveFlagsAdapter(options, target);
41
+ const adapter = contextAdapter(target);
42
42
  if (!adapter) {
43
43
  return usageOut(json, "fixtures", `could not detect the MetaMask repo type for ${target}`, adapterDetectNext());
44
44
  }
@@ -114,7 +114,7 @@ async function handleFixturesLocked(argv) {
114
114
  );
115
115
  }
116
116
  const target = targetOf(options);
117
- const adapter = resolveFlagsAdapter(options, target);
117
+ const adapter = contextAdapter(target);
118
118
  if (!adapter) {
119
119
  return usageOut(json, "fixtures", `could not detect the MetaMask repo type for ${target}`, adapterDetectNext());
120
120
  }
@@ -1,9 +1,10 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { color, detectAdapter, optionString, parseArgs, targetPath } from "@farmslot/recipe-cli/harness";
3
+ import { color, contextAdapter, parseArgs, targetPath } from "@farmslot/recipe-cli/harness";
4
+ import { selectableAdapters } from "../command-contract.js";
4
5
  import { handleHelpReview } from "./review.js";
5
6
  import "../adapters/surface.js";
6
- const UNSCOPED_NEXT = "cd into a MetaMask checkout or pass --adapter <mobile|extension|core|terminal>";
7
+ const unscopedNext = () => `cd into a MetaMask checkout or pass --adapter <${selectableAdapters().join("|")}>`;
7
8
  function validHelpLine(value) {
8
9
  return typeof value === "string" || Boolean(value && typeof value.text === "string" && Array.isArray(value.adapters) && value.adapters.length > 0 && value.adapters.every((adapter) => ["mobile", "extension", "core", "web-dapp"].includes(adapter)));
9
10
  }
@@ -35,7 +36,7 @@ function recipeHelpPayload(topic, harnessVersion, adapter, target) {
35
36
  harnessVersion,
36
37
  recipeProtocolVersion: topic.recipeProtocolVersion,
37
38
  adapter,
38
- ...adapter === "unscoped" ? { next: UNSCOPED_NEXT } : {},
39
+ ...adapter === "unscoped" ? { next: unscopedNext() } : {},
39
40
  target,
40
41
  topic: topic.topic,
41
42
  title: topic.title,
@@ -58,7 +59,7 @@ function renderRecipeHelp(topic, harnessVersion, adapter) {
58
59
  paint("warn", topic.scopeNotice),
59
60
  `Visual tutorial: ${paint("cmd", "mm-harness tutorial")}`
60
61
  ];
61
- if (adapter === "unscoped") lines.push(paint("warn", `No MetaMask checkout detected. Next: ${UNSCOPED_NEXT}.`));
62
+ if (adapter === "unscoped") lines.push(paint("warn", `No MetaMask checkout detected. Next: ${unscopedNext()}.`));
62
63
  for (const [index, section] of scopedSections(topic, adapter).entries()) {
63
64
  lines.push("", paint("label", `${index + 1}. ${section.title}`), ` ${section.instruction}`);
64
65
  for (const detail of section.details) lines.push(` - ${detail}`);
@@ -72,7 +73,7 @@ function handleHelp(argv, context) {
72
73
  const parsed = parseArgs(argv);
73
74
  if (parsed.positional[0] === "review") return handleHelpReview(argv, context.harnessVersion);
74
75
  const target = targetPath(parsed.options);
75
- const adapter = optionString(parsed.options, "adapter") ?? detectAdapter(target) ?? "unscoped";
76
+ const adapter = contextAdapter(target) ?? "unscoped";
76
77
  const topic = loadRecipeHelp(context.packageRoot);
77
78
  const payload = recipeHelpPayload(topic, context.harnessVersion, adapter, target);
78
79
  if (parsed.options.json === true) {
@@ -24,6 +24,7 @@ import {
24
24
  import { checkExtensionRuntimeHealth } from "../../adapters/extension/runtime.js";
25
25
  import { ensureHarnessFresh } from "../../adapters/harness-freshness.js";
26
26
  import { isExtensionWatcherLive, stopExtensionWatcher } from "../../adapters/slot-ports.js";
27
+ import { recordExtensionConsoleLaunch } from "../../adapters/extension/console-capture.js";
27
28
  import { readExtensionPinnedFlags } from "../../adapters/extension/feature-flags.js";
28
29
  import { sameFeatureFlags } from "../../feature-flags.js";
29
30
  const { CdpSession } = await importRecipeHarnessRuntimeCdp();
@@ -108,6 +109,14 @@ Next: ${block.userAction}`
108
109
  }
109
110
  await ensureHarnessFresh(target, "extension");
110
111
  restoreRequestedPorts();
112
+ if (!reusable && artifactLaunchable && remoteFlags) {
113
+ return {
114
+ status: EXIT.infra,
115
+ output: `REMOTE_FLAG_NOT_PINNABLE: the remote feature-flag pin '${remoteFlags.spec}' (from --remote-flag and the wallet fixture's remoteFeatureFlags) cannot be applied to a verified release artifact; the artifact's manifest is signed evidence and is never rewritten.
116
+ Next: drop --remote-flag and remove remoteFeatureFlags from the wallet fixture to validate the artifact as shipped, or validate the variant on a source checkout launched without the artifact.`
117
+ };
118
+ }
119
+ recordExtensionConsoleLaunch(target);
111
120
  if (wantWatch) {
112
121
  const startWatchSh = path.join(runnerDir, "adapters/extension/start-watch.sh");
113
122
  const watchArgs = ["--target", target, "--runner-bin", path.join(runnerDir, "bin/mm-harness")];
@@ -128,13 +137,6 @@ Next: ${block.userAction}`
128
137
  return extensionReattach(target, displayMode);
129
138
  }
130
139
  if (artifactLaunchable) {
131
- if (remoteFlags) {
132
- return {
133
- status: EXIT.infra,
134
- output: `REMOTE_FLAG_NOT_PINNABLE: the remote feature-flag pin '${remoteFlags.spec}' (from --remote-flag and the wallet fixture's remoteFeatureFlags) cannot be applied to a verified release artifact; the artifact's manifest is signed evidence and is never rewritten.
135
- Next: drop --remote-flag and remove remoteFeatureFlags from the wallet fixture to validate the artifact as shipped, or validate the variant on a source checkout launched without the artifact.`
136
- };
137
- }
138
140
  return extensionLaunchReleaseArtifact(target, artifact);
139
141
  }
140
142
  return extensionLaunchDevelopment(target, remoteFlags);
@@ -1,4 +1,4 @@
1
- import { adapterDetectNext, EXIT, flag, parseFlags, resolveFlagsAdapter, spawnInherit, spawnScript, str, targetOf, usageOut } from "@farmslot/recipe-cli/harness";
1
+ import { adapterDetectNext, EXIT, flag, parseFlags, contextAdapter, spawnInherit, spawnScript, str, targetOf, usageOut } from "@farmslot/recipe-cli/harness";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { runnerDir } from "../paths.js";
@@ -8,7 +8,7 @@ async function handleLogs(argv) {
8
8
  const { options } = parseFlags(argv, LOGS_BOOLEANS);
9
9
  const json = flag(options, "json");
10
10
  const target = targetOf(options);
11
- const adapter = resolveFlagsAdapter(options, target);
11
+ const adapter = contextAdapter(target);
12
12
  if (!adapter) {
13
13
  return usageOut(json, "logs", `could not detect the MetaMask repo type for ${target}`, adapterDetectNext());
14
14
  }
@@ -1,4 +1,4 @@
1
- import { acquireCheckoutLock, trackCheckoutChild, recordCommandStage, color, stripAnsi, harnessExecutable, recipeRuntimeDir, detectAdapter, optionFlag, optionString, parseArgs, targetPath, usageError, EXIT } from "@farmslot/recipe-cli/harness";
1
+ import { acquireCheckoutLock, trackCheckoutChild, recordCommandStage, color, stripAnsi, harnessExecutable, recipeRuntimeDir, contextAdapter, optionFlag, optionString, parseArgs, targetPath, usageError, EXIT } from "@farmslot/recipe-cli/harness";
2
2
  import { spawn } from "node:child_process";
3
3
  import { OperationOutputTail } from "@farmslot/recipe-runner/runtime/operation";
4
4
  import fs from "node:fs";
@@ -265,8 +265,8 @@ function mobilePlatformOption(value) {
265
265
  Next: ${USAGE}`
266
266
  );
267
267
  }
268
- function resolvePlatform(requested, target) {
269
- const platform = requested ?? detectAdapter(target);
268
+ function resolvePlatform(target) {
269
+ const platform = contextAdapter(target);
270
270
  const surface = platform && adapterRegistry.has(platform) ? getAdapterSurface(platform) : void 0;
271
271
  if (surface?.prepare) {
272
272
  return surface;
@@ -296,7 +296,7 @@ async function handlePrepareLocked(argv) {
296
296
  const { options } = parseArgs(argv);
297
297
  const json = optionFlag(options, "json");
298
298
  const target = targetPath(options);
299
- const surface = resolvePlatform(optionString(options, "platform"), target);
299
+ const surface = resolvePlatform(target);
300
300
  const platform = surface.id;
301
301
  const artifactsDir = path.resolve(
302
302
  optionString(options, "artifactsDir") ?? path.join(target, recipeRuntimeDir())
@@ -1,11 +1,11 @@
1
- import { detectAdapter, adapterDetectNext, usageOut, applyWatcherPortOption, optionFlag, optionString, shellQuote, shellQuoteArg, targetPath } from "@farmslot/recipe-cli/harness";
1
+ import { contextAdapter, adapterDetectNext, usageOut, applyWatcherPortOption, optionFlag, optionString, shellQuote, shellQuoteArg, targetPath } from "@farmslot/recipe-cli/harness";
2
2
  import { assertAdapter } from "../paths.js";
3
3
  import { getAdapterSurface } from "../adapters/surface.js";
4
4
  async function handleProvision({ positional, options, rawArgv }) {
5
5
  applyWatcherPortOption(options);
6
6
  const json = optionFlag(options, "json");
7
7
  const target = targetPath(options);
8
- const adapter = optionString(options, "adapter") ?? detectAdapter(target);
8
+ const adapter = contextAdapter(target);
9
9
  if (!adapter) {
10
10
  return usageOut(json, "provision", `could not detect the MetaMask repo type for ${target}`, adapterDetectNext());
11
11
  }
@@ -1,4 +1,4 @@
1
- import { detectAdapter, optionFlag, optionString, parseArgs, targetPath, usageError } from "@farmslot/recipe-cli/harness";
1
+ import { harnessContext, optionFlag, optionString, parseArgs, targetPath, usageError } from "@farmslot/recipe-cli/harness";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { runnerDir } from "../paths.js";
@@ -15,7 +15,6 @@ import {
15
15
  resolveReference,
16
16
  defaultBaseRef
17
17
  } from "../review/knowledge.js";
18
- import "../adapters/surface.js";
19
18
  const EXIT_OK = 0;
20
19
  let cachedHarnessVersion;
21
20
  function harnessVersion() {
@@ -25,18 +24,17 @@ function harnessVersion() {
25
24
  }
26
25
  return cachedHarnessVersion;
27
26
  }
28
- function adapterFromOptions(options, target) {
29
- const raw = optionString(options, "adapter") ?? optionString(options, "platform");
30
- if (raw !== void 0) {
31
- if (raw !== "mobile" && raw !== "extension" && raw !== "core" && raw !== "terminal") throw usageError(`--adapter must be mobile, extension, core, or terminal (got ${raw}).`);
32
- return raw;
27
+ function adapterFromOptions() {
28
+ const adapter = harnessContext()?.adapter;
29
+ const scope = REVIEW_ADAPTERS.find((id) => id === adapter?.value);
30
+ if (adapter?.source === "flag" && !scope) {
31
+ throw usageError(`--adapter must be one of ${REVIEW_ADAPTERS.join(", ")} (got ${adapter.value}).`);
33
32
  }
34
- const detected = detectAdapter(target);
35
- return REVIEW_ADAPTERS.find((adapter) => adapter === detected) ?? "unscoped";
33
+ return scope ?? "unscoped";
36
34
  }
37
35
  function resolveReviewContext(options, { fetch = true } = {}) {
38
36
  const target = targetPath(options);
39
- const adapter = adapterFromOptions(options, target);
37
+ const adapter = adapterFromOptions();
40
38
  const domain = optionString(options, "domain") ?? process.env.MM_HARNESS_DOMAIN;
41
39
  const config = readConfig(configPath());
42
40
  const context = { target, adapter, ...domain ? { domain } : {} };
@@ -10,6 +10,7 @@ import {
10
10
  writeMobileReleaseArtifactState
11
11
  } from "../adapters/mobile/release-artifact-state.js";
12
12
  import { extensionDepsBlock } from "./launch/extension.js";
13
+ import { recordExtensionConsoleLaunch } from "../adapters/extension/console-capture.js";
13
14
  import { isExtensionDistStale } from "../adapters/extension/runtime-decision.js";
14
15
  import { FeatureFlagSpecError, parseRemoteFlagSpec } from "../feature-flags.js";
15
16
  async function handleRuntimeLaunch(args) {
@@ -186,6 +187,7 @@ async function handleRuntimeLaunchLocked({ options }) {
186
187
  cdpPort
187
188
  }));
188
189
  }
190
+ recordExtensionConsoleLaunch(target);
189
191
  const result = await spawnScriptStreaming(command[0], command.slice(1), target, {
190
192
  forward: jsonMode ? "stderr" : "original",
191
193
  stdin: jsonMode ? "ignore" : "inherit",
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { featureFlagReport } from "../feature-flags.js";
4
- import { color, detectAdapter, adapterDetectNext, EXIT, usageOut, optionFlag, optionString, targetPath } from "@farmslot/recipe-cli/harness";
4
+ import { color, contextAdapter, harnessContextField, adapterDetectNext, EXIT, usageOut, optionFlag, optionString, targetPath } from "@farmslot/recipe-cli/harness";
5
5
  import { collectCheckoutView, renderCheckoutView } from "../checkout-view.js";
6
6
  import { assertAdapter } from "../paths.js";
7
7
  import { getAdapterSurface } from "../adapters/surface.js";
@@ -24,7 +24,7 @@ async function handleStatus({ options }) {
24
24
  }
25
25
  const fast = optionFlag(options, "fast");
26
26
  const allDevices = optionFlag(options, "allDevices");
27
- const adapter = detectAdapter(target);
27
+ const adapter = contextAdapter(target);
28
28
  if (!adapter) {
29
29
  return usageOut(json, "status", `could not detect the MetaMask repo type for ${target}`, adapterDetectNext());
30
30
  }
@@ -58,6 +58,7 @@ async function handleStatus({ options }) {
58
58
  command: "status",
59
59
  adapter,
60
60
  target,
61
+ ...harnessContextField(),
61
62
  view,
62
63
  devices: liveView.devicesWithLive,
63
64
  featureFlags: featureFlagReport(liveView.featureFlagOverrides),
@@ -79,6 +80,7 @@ async function handleStatus({ options }) {
79
80
  command: "status",
80
81
  adapter,
81
82
  target,
83
+ ...harnessContextField(),
82
84
  view,
83
85
  devices,
84
86
  featureFlags: staticFeatureFlags,
@@ -1,7 +1,6 @@
1
- import { harnessExecutable, detectAdapter, EXIT } from "@farmslot/recipe-cli/harness";
1
+ import { harnessExecutable, contextAdapter, EXIT } from "@farmslot/recipe-cli/harness";
2
2
  import { spawnSync } from "node:child_process";
3
3
  import { createRequire } from "node:module";
4
- import "../adapters/surface.js";
5
4
  const require2 = createRequire(import.meta.url);
6
5
  const USAGE = "mm-harness task init <task-dir> --flow f --template id --title t [options]";
7
6
  function hasOption(argv, name) {
@@ -14,7 +13,7 @@ function metamaskDefaults(argv) {
14
13
  defaults.push("--mark-command", `${harnessExecutable()} checklist mark`);
15
14
  }
16
15
  if (!hasOption(argv, "--platform")) {
17
- const adapter = detectAdapter(process.cwd());
16
+ const adapter = contextAdapter(process.cwd());
18
17
  if (adapter) defaults.push("--platform", adapter);
19
18
  }
20
19
  return defaults;
@@ -4,13 +4,14 @@ import {
4
4
  consoleRecords,
5
5
  libraryRootsFromEnv,
6
6
  loadConsoleAllowlist,
7
- messageSignature
7
+ messageSignature,
8
+ withBundledLibraryRoot
8
9
  } from "../library/actions/shared/console-findings.mjs";
9
10
  const consoleClassifier = {
10
11
  records: (text) => consoleRecords(text),
11
12
  classify: (record) => classifyConsoleRecord(record),
12
13
  signature: (message) => messageSignature(message),
13
- loadAllowlist: (libraryRoots) => loadConsoleAllowlist([...libraryRoots]),
14
+ loadAllowlist: (libraryRoots) => loadConsoleAllowlist(withBundledLibraryRoot([...libraryRoots])),
14
15
  match: (event, allowlist) => allowlistMatch(event, allowlist),
15
16
  libraryRoots: () => libraryRootsFromEnv()
16
17
  };
package/dist/doctor.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { RECIPE_CLI_VERSION } from "@farmslot/recipe-cli";
4
- import { color, harnessExecutable, recipeHarnessRoot, recipeRuntimeDir, readRuntimeContextField, resolveRuntimeContextPath } from "@farmslot/recipe-cli/harness";
4
+ import { color, harnessContext, harnessContextField, harnessExecutable, recipeHarnessRoot, recipeRuntimeDir, readRuntimeContextField, resolveRuntimeContextPath } from "@farmslot/recipe-cli/harness";
5
5
  import { normalizeFeatureFlagRecord } from "./feature-flags.js";
6
6
  import { getAdapterSurface } from "./adapters/surface.js";
7
7
  import { isPin } from "./harness-pin.js";
@@ -107,10 +107,13 @@ function runtimeContextSummary(target, adapter) {
107
107
  const file = envOverride ?? path.relative(target, contextPath);
108
108
  const fields = {};
109
109
  const specs = adapter ? RUNTIME_CONTEXT_FIELDS.filter((spec) => spec.adapters.includes(adapter)) : RUNTIME_CONTEXT_FIELDS;
110
+ const filled = new Set(
111
+ Object.values(harnessContext()?.ports ?? {}).flatMap((port) => port?.filled ? port.names ?? [] : [])
112
+ );
110
113
  for (const spec of specs) {
111
114
  const envName = spec.envVars.find((name) => {
112
115
  const value = process.env[name];
113
- return value !== void 0 && value !== "";
116
+ return value !== void 0 && value !== "" && !filled.has(name);
114
117
  });
115
118
  if (envName !== void 0) {
116
119
  fields[spec.key] = { value: process.env[envName], source: "env", envVar: envName, customize: spec.customize };
@@ -165,6 +168,7 @@ function createDoctorReport(adapter, target, manifestValidation, actionManifestP
165
168
  requiredChecks,
166
169
  adapter,
167
170
  target,
171
+ ...harnessContextField(),
168
172
  runner: runnerProvenance(actionManifestPath),
169
173
  compatibilityMode: mode,
170
174
  shape: repoShape(target),