@deeeed/metamask-harness 0.80.0 → 0.82.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 (118) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/adapters/manifest.json +48 -64
  3. package/adapters/{terminal → web-dapp}/cleanup.mjs +9 -9
  4. package/adapters/{terminal → web-dapp}/inject.mjs +14 -14
  5. package/adapters/{terminal → web-dapp}/launch.mjs +27 -24
  6. package/adapters/web-dapp/lib/blocked-hosts.mjs +24 -0
  7. package/adapters/{terminal → web-dapp}/lib/cdp-client.mjs +1 -1
  8. package/adapters/{terminal → web-dapp}/lib/display.mjs +1 -1
  9. package/adapters/{terminal → web-dapp}/lib/hud.mjs +1 -1
  10. package/adapters/web-dapp/lib/policy.mjs +79 -0
  11. package/adapters/{terminal → web-dapp}/lib/processes.mjs +8 -8
  12. package/adapters/{terminal → web-dapp}/lib/readiness.mjs +32 -34
  13. package/adapters/{terminal → web-dapp}/stop.mjs +5 -4
  14. package/adapters/{terminal → web-dapp}/verify.mjs +9 -6
  15. package/adapters/{terminal → web-dapp}/wallet-host.mjs +44 -227
  16. package/dist/adapters/core/actions.js +4 -1
  17. package/dist/adapters/extension/actions.js +6 -2
  18. package/dist/adapters/extension/network-observer.js +13 -0
  19. package/dist/adapters/extension/run.js +115 -31
  20. package/dist/adapters/mobile/actions.js +4 -1
  21. package/dist/adapters/mobile/network-observer.js +68 -0
  22. package/dist/adapters/mobile/performance-observer.js +1 -3
  23. package/dist/adapters/mobile/run.js +32 -9
  24. package/dist/adapters/perps-order-inputs.js +36 -0
  25. package/dist/adapters/plugin-surface.js +98 -0
  26. package/dist/adapters/proof-failure.js +11 -0
  27. package/dist/adapters/surface.js +1 -1
  28. package/dist/adapters/{terminal → web-dapp}/actions.js +11 -8
  29. package/dist/adapters/web-dapp/console-capture.js +63 -0
  30. package/dist/adapters/web-dapp/doctor-checks.js +33 -0
  31. package/dist/adapters/{terminal → web-dapp}/launch.js +21 -19
  32. package/dist/adapters/web-dapp/overlay.js +21 -0
  33. package/dist/adapters/web-dapp/readiness.js +10 -0
  34. package/dist/adapters/{terminal → web-dapp}/run.js +22 -17
  35. package/dist/adapters/{terminal → web-dapp}/stop.js +5 -4
  36. package/dist/adapters/web-dapp/surface.js +149 -0
  37. package/dist/adapters.js +20 -4
  38. package/dist/cli.js +34 -6
  39. package/dist/command-contract.js +29 -7
  40. package/dist/commands/check.js +32 -8
  41. package/dist/commands/completion-candidates.js +5 -5
  42. package/dist/commands/doctor.js +19 -2
  43. package/dist/commands/farmslot-ready.js +1 -0
  44. package/dist/commands/help.js +1 -1
  45. package/dist/commands/launch/device.js +5 -5
  46. package/dist/commands/self-test.js +10 -5
  47. package/dist/engine.js +47 -0
  48. package/dist/funding-execution-context.js +81 -0
  49. package/dist/manifest.js +48 -24
  50. package/dist/mm-harness-cli.js +20 -3
  51. package/dist/performance-observation.js +7 -15
  52. package/dist/recipe-assistance-context.js +6 -7
  53. package/dist/runner.js +28 -8
  54. package/docs/CONTRIBUTING.md +4 -2
  55. package/docs/RECIPES.md +31 -3
  56. package/docs/cli-contract.md +5 -3
  57. package/library/actions/core/perps/_controller.mjs +24 -28
  58. package/library/actions/core/perps/_rewards.mjs +186 -0
  59. package/library/actions/core/perps/close_positions.mjs +1 -1
  60. package/library/actions/core/perps/place_order.mjs +167 -45
  61. package/library/actions/core/perps/quote_fees.mjs +7 -4
  62. package/library/actions/core/wallet/list_accounts.mjs +1 -1
  63. package/library/actions/extension/perps/close_visible_position.mjs +5 -0
  64. package/library/actions/extension/perps/edit_margin.mjs +4 -0
  65. package/library/actions/extension/perps/mutation-receipt.mjs +95 -3
  66. package/library/actions/extension/perps/perps.mjs +320 -3
  67. package/library/actions/extension/perps/place_order.mjs +3 -1
  68. package/library/actions/extension/perps/update_position_tpsl.mjs +4 -0
  69. package/library/actions/extension/platform/cdp.mjs +71 -3
  70. package/library/actions/mobile/perps/perps.mjs +216 -11
  71. package/library/actions/mobile/perps/place_order.mjs +3 -1
  72. package/library/actions/shared/console-collector.mjs +1 -1
  73. package/library/actions/shared/perps/order-policy.mjs +177 -0
  74. package/library/actions/{core/wallet/_fixture-accounts.mjs → shared/wallet/fixture-accounts.mjs} +18 -3
  75. package/library/actions/{terminal → web-dapp}/app/assert_no_console_errors.mjs +4 -4
  76. package/library/actions/{terminal → web-dapp}/app/launch.mjs +8 -7
  77. package/library/actions/{terminal → web-dapp}/platform/hud.mjs +7 -6
  78. package/library/actions/{terminal → web-dapp}/platform/page.mjs +13 -12
  79. package/library/actions/{terminal → web-dapp}/platform/runtime.mjs +16 -12
  80. package/library/actions/web-dapp/ui/navigate.mjs +24 -0
  81. package/library/actions/web-dapp/wallet/_signature-log.mjs +32 -0
  82. package/library/actions/{terminal → web-dapp}/wallet/assert_signatures.mjs +8 -14
  83. package/library/actions/{terminal → web-dapp}/wallet/list_accounts.mjs +2 -2
  84. package/library/actions/web-dapp/wallet/read_signatures.mjs +38 -0
  85. package/library/manifests/core.action-manifest.json +6 -2
  86. package/library/manifests/extension.action-manifest.json +4 -0
  87. package/library/manifests/mobile.action-manifest.json +12 -0
  88. package/library/manifests/{terminal.action-manifest.json → web-dapp.action-manifest.json} +18 -702
  89. package/package.json +6 -6
  90. package/site/assets/help-recipes.json +1 -0
  91. package/adapters/terminal/lib/mainnet-guard.mjs +0 -177
  92. package/adapters/terminal/lib/origin.mjs +0 -30
  93. package/adapters/terminal/lib/page-script.mjs +0 -246
  94. package/adapters/terminal/lib/strict-wallet.mjs +0 -138
  95. package/dist/adapters/terminal/console-capture.js +0 -59
  96. package/dist/adapters/terminal/doctor-checks.js +0 -22
  97. package/dist/adapters/terminal/overlay.js +0 -18
  98. package/dist/adapters/terminal/readiness.js +0 -11
  99. package/dist/adapters/terminal/surface.js +0 -133
  100. package/dist/commands/call.js +0 -671
  101. package/dist/commands/list-executables.js +0 -245
  102. package/dist/commands/manifest.js +0 -477
  103. package/dist/commands/run-engine.js +0 -1301
  104. package/dist/commands/run.js +0 -730
  105. package/dist/discovery.js +0 -15
  106. package/dist/metamask-action-validation.js +0 -68
  107. package/dist/network-observation.js +0 -290
  108. package/library/actions/terminal/perps/_venue.mjs +0 -242
  109. package/library/actions/terminal/perps/assert_orders.mjs +0 -54
  110. package/library/actions/terminal/perps/assert_positions.mjs +0 -40
  111. package/library/actions/terminal/perps/read_orders.mjs +0 -26
  112. package/library/actions/terminal/perps/read_positions.mjs +0 -25
  113. package/library/actions/terminal/perps/read_snapshot.mjs +0 -40
  114. package/library/actions/terminal/perps/revoke_agent.mjs +0 -58
  115. package/library/actions/terminal/perps/teardown_state.mjs +0 -104
  116. package/library/actions/terminal/ui/navigate.mjs +0 -31
  117. package/library/actions/terminal/wallet/_signature-log.mjs +0 -177
  118. package/library/actions/terminal/wallet/read_signatures.mjs +0 -37
@@ -1,8 +1,9 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import path from "node:path";
3
3
  import { runnerDir } from "../../paths.js";
4
- function stopTerminalBrowserSync(target) {
5
- const args = [path.join(runnerDir, "adapters", "terminal", "stop.mjs"), "--target", path.resolve(target)];
4
+ import { webDappAdapterId } from "../../../adapters/web-dapp/lib/policy.mjs";
5
+ function stopWebDappBrowserSync(target) {
6
+ const args = [path.join(runnerDir, "adapters", "web-dapp", "stop.mjs"), "--target", path.resolve(target)];
6
7
  const cdpPort = process.env.RECIPE_CDP_PORT ?? process.env.CDP_PORT;
7
8
  if (cdpPort) args.push("--cdp-port", cdpPort);
8
9
  const result = spawnSync(process.execPath, args, { encoding: "utf8" });
@@ -10,10 +11,10 @@ function stopTerminalBrowserSync(target) {
10
11
  const status = result.status ?? 1;
11
12
  return {
12
13
  status,
13
- summary: status === 0 ? "terminal browser and wallet host stopped" : "terminal browser stop failed",
14
+ summary: status === 0 ? `${webDappAdapterId()} browser and wallet host stopped` : `${webDappAdapterId()} browser stop failed`,
14
15
  output
15
16
  };
16
17
  }
17
18
  export {
18
- stopTerminalBrowserSync
19
+ stopWebDappBrowserSync
19
20
  };
@@ -0,0 +1,149 @@
1
+ import { shellQuote } from "@farmslot/recipe-cli/harness";
2
+ import path from "node:path";
3
+ import { defineAdapter } from "@farmslot/adapter-sdk";
4
+ import fs from "node:fs";
5
+ import { POLICY_ENV, webDappAdapterId, webDappPolicy } from "../../../adapters/web-dapp/lib/policy.mjs";
6
+ import { WEB_DAPP_CONSOLE_FILES, webDappRuntimeFile } from "./console-capture.js";
7
+ import { stopWebDappBrowserSync } from "./stop.js";
8
+ import { adapterRegistry } from "../registry.js";
9
+ import { webDappActions } from "./actions.js";
10
+ import { webDappHarness, webDappRuntimeContext } from "./overlay.js";
11
+ import { webDappReadiness } from "./readiness.js";
12
+ import { webDappDiagnostics, webDappObservation } from "./run.js";
13
+ function portFromEnv(...names) {
14
+ for (const name of names) {
15
+ const raw = process.env[name];
16
+ if (raw && /^\d+$/u.test(raw)) return Number(raw);
17
+ }
18
+ return void 0;
19
+ }
20
+ function webDappAppPort() {
21
+ return portFromEnv("TERMINAL_APP_PORT", "RECIPE_WATCHER_PORT", "WATCHER_PORT");
22
+ }
23
+ function webDappCdpPort() {
24
+ return portFromEnv("RECIPE_CDP_PORT", "CDP_PORT");
25
+ }
26
+ function webDappSignerFor(target) {
27
+ try {
28
+ const state = JSON.parse(fs.readFileSync(webDappRuntimeFile(target, "browser.json"), "utf8"));
29
+ if (typeof state.signer === "string") return state.signer;
30
+ } catch {
31
+ }
32
+ return process.env.TERMINAL_SIGNER || "extension";
33
+ }
34
+ function bindWebDappPolicy(adapter) {
35
+ if (adapter.extends === "web-dapp" && typeof adapter.policy?.module === "string") {
36
+ process.env[POLICY_ENV] = adapter.policy.module;
37
+ }
38
+ return adapter;
39
+ }
40
+ const webDappSurface = defineAdapter({
41
+ id: "web-dapp",
42
+ sdkVersion: 1,
43
+ headless: false,
44
+ resolveSlotPorts() {
45
+ },
46
+ async runtimeStatus(target) {
47
+ if (!process.env[POLICY_ENV]) {
48
+ return {
49
+ decision: "blocked",
50
+ reasonCode: "web-dapp-venue-policy",
51
+ reasons: ["web-dapp needs a venue policy: select an adapter that extends it"],
52
+ nextAction: "pass --adapter <the adapter that extends web-dapp>, e.g. --adapter terminal, with its recipe library on RECIPE_LIBRARY_PATH",
53
+ deps: "missing",
54
+ devServer: { label: "next", status: "down" }
55
+ };
56
+ }
57
+ const policy = webDappPolicy();
58
+ const { webDappReadiness: webDappReadiness2 } = await import("../../../adapters/web-dapp/lib/readiness.mjs");
59
+ const report = await webDappReadiness2({
60
+ policy,
61
+ target: path.resolve(target),
62
+ appPort: webDappAppPort(),
63
+ cdpPort: webDappCdpPort(),
64
+ signer: process.env.TERMINAL_SIGNER || "extension"
65
+ });
66
+ const blocking = report.checks.filter((check) => check.required && check.status === "fail");
67
+ const devServer = report.checks.find((check) => check.id === "app-dev-server");
68
+ return {
69
+ decision: blocking.length === 0 ? "ready" : "blocked",
70
+ reasonCode: blocking[0]?.id,
71
+ reasons: (blocking.length ? blocking : report.checks).map((check) => `${check.id}: ${check.detail}`),
72
+ nextAction: blocking.length === 0 ? void 0 : blocking[0].id === "app-dev-server" || blocking[0].id === "testnet-forced" ? "start the slot dev server with testnet forced (Farmslot preflight), then retry" : `mm-harness doctor --adapter ${policy.adapterId} --target ${shellQuote(path.resolve(target))} --json`,
73
+ deps: report.checks.find((check) => check.id === "dependencies")?.status === "pass" ? "current" : "missing",
74
+ devServer: {
75
+ label: "next",
76
+ status: devServer?.status === "pass" ? "up" : "down"
77
+ }
78
+ };
79
+ },
80
+ runwayProvision: {
81
+ async run(target, options) {
82
+ return {
83
+ schemaVersion: 1,
84
+ command: "provision",
85
+ adapter: webDappAdapterId(),
86
+ target: path.resolve(target),
87
+ status: "fail",
88
+ exitCode: 2,
89
+ error: {
90
+ code: "UNSUPPORTED_ADAPTER",
91
+ message: `the ${process.env[POLICY_ENV] ? webDappPolicy().checkout.name : "web dapp"} is a website; there is no simulator or build to provision.`,
92
+ userAction: options.rerunCommand || `mm-harness launch --adapter ${webDappAdapterId()}`
93
+ }
94
+ };
95
+ }
96
+ },
97
+ devServer: {
98
+ label: "browser",
99
+ describe: () => `${webDappAdapterId()} slot browser and wallet host (the Next.js dev server belongs to the slot)`,
100
+ stop(target) {
101
+ const result = stopWebDappBrowserSync(target);
102
+ return { kind: "stopped", status: result.status, summary: result.summary, output: result.output };
103
+ }
104
+ },
105
+ logSources(target) {
106
+ return [
107
+ { label: "app-console", path: webDappRuntimeFile(target, WEB_DAPP_CONSOLE_FILES.app) },
108
+ ...webDappSignerFor(target) === "extension" ? [{ label: "extension-console", path: webDappRuntimeFile(target, WEB_DAPP_CONSOLE_FILES.extension) }] : [],
109
+ { label: "wallet-requests", path: webDappRuntimeFile(target, "wallet-requests.jsonl") },
110
+ { label: "wallet-host", path: webDappRuntimeFile(target, "wallet-host.log") },
111
+ { label: "browser", path: webDappRuntimeFile(target, "browser.log") },
112
+ { label: "next", path: path.join(target, "temp/farmslot/next-dev.log") }
113
+ ];
114
+ },
115
+ // The app page's console and uncaught exceptions, as the Extension's
116
+ // diagnostics read the extension console: errors make a run REVIEW.
117
+ appLogSource(target) {
118
+ return { label: "app-console", path: webDappRuntimeFile(target, WEB_DAPP_CONSOLE_FILES.app) };
119
+ },
120
+ flags: { launch: ["freshProfile", "headless", "headful"] },
121
+ hints: {
122
+ // Bare web-dapp has no venue: every hint names the adapter that extends it.
123
+ launch: "pass --adapter <the adapter that extends web-dapp>, e.g. --adapter terminal, with its recipe library on RECIPE_LIBRARY_PATH",
124
+ relaunch: "pass --adapter <the adapter that extends web-dapp>, e.g. --adapter terminal, with --fresh-profile",
125
+ runtimeProbeRecovery: () => "pass --adapter <the adapter that extends web-dapp>, e.g. --adapter terminal, with its recipe library on RECIPE_LIBRARY_PATH"
126
+ },
127
+ launch: async (context) => (await import("./launch.js")).launchWebDapp(context),
128
+ actions: webDappActions,
129
+ harness: webDappHarness,
130
+ runtimeContext: webDappRuntimeContext,
131
+ readiness: webDappReadiness,
132
+ fixtures: { operations: ["init", "sync", "set"] },
133
+ debug: {
134
+ args: ({ devMenu }) => [
135
+ ...process.env.WATCHER_PORT ? ["--port", process.env.WATCHER_PORT] : [],
136
+ "--action",
137
+ devMenu ? "dev-menu" : "debug"
138
+ ]
139
+ },
140
+ diagnostics: webDappDiagnostics,
141
+ observation: webDappObservation
142
+ });
143
+ adapterRegistry.register(webDappSurface);
144
+ export {
145
+ bindWebDappPolicy,
146
+ webDappAppPort,
147
+ webDappCdpPort,
148
+ webDappSurface
149
+ };
package/dist/adapters.js CHANGED
@@ -1,14 +1,13 @@
1
1
  import http from "node:http";
2
2
  import { createHash } from "node:crypto";
3
3
  import { compatibilityMode, fixtureSummary, repoShape } from "./doctor.js";
4
- import { runLiveAdapterScript } from "@farmslot/recipe-cli/harness";
4
+ import { liveAdapterProcessTimeoutMs, runLiveAdapterScript, runNetworkCaptureAction } from "@farmslot/recipe-cli/harness";
5
5
  import { trustedMutationContextExtras } from "./funding-execution-context.js";
6
6
  import { LIBRARY_ACTION_NAMESPACE } from "./paths.js";
7
7
  import { liveTsxCandidates } from "./live-tsx.js";
8
8
  import { resolveWalletImportCredentials, validateWalletImportOptions } from "../library/actions/shared/wallet/import-source.mjs";
9
9
  import { isRecord, traceText } from "./action-values.js";
10
10
  import { getAdapterSurface } from "./adapters/surface.js";
11
- import { handleRunNetworkAction } from "./network-observation.js";
12
11
  import { handleRunPerformanceAction } from "./performance-observation.js";
13
12
  function simpleAdapter(action, executor) {
14
13
  return {
@@ -100,6 +99,12 @@ function liveRuntimeConfigured(platform, node) {
100
99
  if (!getAdapterSurface(platform).actions.liveNeedsCdpPort) return true;
101
100
  return Boolean(node.cdp_port ?? process.env.CDP_PORT ?? process.env.RECIPE_CDP_PORT);
102
101
  }
102
+ const IMMEDIATE_CLOSE_ALLOWANCE_MS = 18e4;
103
+ function withImmediateCloseBudget(action, node) {
104
+ if (action !== `${LIBRARY_ACTION_NAMESPACE}.perps.place_order`) return node;
105
+ if (node.close_immediately !== true && node.closeImmediately !== true) return node;
106
+ return { ...node, live_adapter_timeout_ms: liveAdapterProcessTimeoutMs(node) + IMMEDIATE_CLOSE_ALLOWANCE_MS };
107
+ }
103
108
  async function runLiveFirst(platform, action, node, context, forceLive = false, preparedLiveAdapters) {
104
109
  if (node.allow_static_placeholder === true) {
105
110
  throw new Error(
@@ -111,7 +116,7 @@ async function runLiveFirst(platform, action, node, context, forceLive = false,
111
116
  platform,
112
117
  action,
113
118
  namespace: LIBRARY_ACTION_NAMESPACE,
114
- node,
119
+ node: withImmediateCloseBudget(action, node),
115
120
  context,
116
121
  prepared: preparedLiveAdapters?.get(action),
117
122
  contextExtras: trustedMutationContextExtras(context),
@@ -131,6 +136,15 @@ function liveAdapterPathHint(platform, action) {
131
136
  }
132
137
  return `library/actions/${platform}/${action.replaceAll(".", "/")}.mjs`;
133
138
  }
139
+ async function handleRunNetworkAction(platform, action, node, context) {
140
+ if (!getAdapterSurface(platform).observation?.network?.actions) return null;
141
+ if (action === "app.network_assert") {
142
+ const assertion = await import("../library/actions/shared/app/network-assert.mjs");
143
+ return { output: await assertion.assertNetwork({ action, node, context }) };
144
+ }
145
+ if (action !== "app.network_capture") return null;
146
+ return runNetworkCaptureAction(platform, node, context);
147
+ }
134
148
  async function semanticResult(platform, action, node, context, forceLive = false, preparedLiveAdapters) {
135
149
  const performanceAction = await handleRunPerformanceAction(
136
150
  platform,
@@ -368,7 +382,9 @@ function createMetaMaskAdapters(adapter, declaredCustomActions = [], preparedLiv
368
382
  return adapters;
369
383
  }
370
384
  export {
385
+ IMMEDIATE_CLOSE_ALLOWANCE_MS,
371
386
  createMetaMaskAdapters,
372
387
  createMetaMaskUiTransport,
373
- normalizeUiWaitNode
388
+ normalizeUiWaitNode,
389
+ withImmediateCloseBudget
374
390
  };
package/dist/cli.js CHANGED
@@ -1,18 +1,25 @@
1
1
  import "./host.js";
2
2
  import "./adapters/surface.js";
3
+ import fs from "node:fs";
3
4
  import {
4
5
  detectAdapter,
6
+ handleActions,
7
+ handleCall,
5
8
  handleHarness,
6
9
  handleLast,
7
10
  handleLaunch,
8
11
  handleReload,
12
+ handleRun,
9
13
  handleStop,
10
14
  parseArgs,
11
15
  targetPath
12
16
  } from "@farmslot/recipe-cli/harness";
13
17
  import { stopProjectCollector } from "../library/actions/shared/analytics/collector.mjs";
18
+ import { loadSelectedAdapter } from "./adapters/plugin-surface.js";
14
19
  import { adapterRegistry } from "./adapters/registry.js";
15
- import { handleActions } from "./commands/manifest.js";
20
+ import { applyDeviceTargeting } from "./commands/device-target.js";
21
+ import { metaMaskEngine } from "./engine.js";
22
+ import { walletFixturePath } from "./paths.js";
16
23
  import { handleDoctor } from "./commands/doctor.js";
17
24
  import { handleProvision } from "./commands/provision.js";
18
25
  import { handleRuntimeHealth } from "./commands/runtime-health.js";
@@ -20,9 +27,7 @@ import { handleRuntimeLaunch } from "./commands/runtime-launch.js";
20
27
  import { handleRuntimeDecision } from "./commands/runtime-decision.js";
21
28
  import { handleResolveExtension } from "./commands/resolve-extension.js";
22
29
  import { handleEnsureReady } from "./commands/ensure-ready.js";
23
- import { handleRun } from "./commands/run.js";
24
30
  import { handleSelfTest } from "./commands/self-test.js";
25
- import { handleCall } from "./commands/call.js";
26
31
  import { handleCompletionCandidates, invalidateCompletionCache } from "./commands/completion-candidates.js";
27
32
  import { handleLogs } from "./commands/logs.js";
28
33
  import { handleDebug } from "./commands/debug.js";
@@ -45,15 +50,35 @@ const stopOptions = {
45
50
  return collector.stopped && collector.pid !== null ? [{ label: "analytics collector", pid: collector.pid }] : [];
46
51
  }
47
52
  };
53
+ const targetDevice = (command, adapter, options) => applyDeviceTargeting(command, adapter, options, { gate: true, rerun: "" });
54
+ const runOptions = {
55
+ engine: metaMaskEngine,
56
+ targetDevice,
57
+ plan: {
58
+ // The wallet fixture `fixtures set` seeds.
59
+ steps: (target) => [{
60
+ step: "fixture.file",
61
+ confidence: "static",
62
+ status: "ok",
63
+ detail: fs.existsSync(walletFixturePath(target)) ? `present: ${walletFixturePath(target)}` : `absent: ${walletFixturePath(target)} (fixtures set to seed)`
64
+ }],
65
+ launchDetail: "would launch/attach the app + heal transport (Metro/Chrome/CDP) before executing"
66
+ }
67
+ };
68
+ const callOptions = {
69
+ engine: metaMaskEngine,
70
+ targetDevice,
71
+ // A wallet action makes the most useful first example.
72
+ exampleAction: (names) => names.find((name) => /wallet|unlock/u.test(name))
73
+ };
48
74
  const COMMANDS = {
49
- actions: handleActions,
75
+ actions: (parsed) => handleActions(parsed, { catalog: metaMaskEngine }),
50
76
  doctor: handleDoctor,
51
77
  "runtime-health": handleRuntimeHealth,
52
78
  "runtime-decision": handleRuntimeDecision,
53
79
  "runtime-launch": handleRuntimeLaunch,
54
80
  "resolve-extension": handleResolveExtension,
55
81
  "ensure-ready": handleEnsureReady,
56
- run: handleRun,
57
82
  "self-test": handleSelfTest
58
83
  };
59
84
  const OVERLAY_COMMANDS = ["install", "verify", "cleanup"];
@@ -111,10 +136,13 @@ async function main(argv) {
111
136
  usage();
112
137
  return command ? 0 : 2;
113
138
  }
139
+ const refused = await loadSelectedAdapter(command, argv.slice(1));
140
+ if (refused !== void 0) return refused;
114
141
  if (command === "status" || command === "health" || command === "home") {
115
142
  return handleStatus(parseArgs(argv.slice(1)));
116
143
  }
117
- if (command === "call") return handleCall(argv.slice(1));
144
+ if (command === "run") return handleRun(argv.slice(1), runOptions);
145
+ if (command === "call") return handleCall(argv.slice(1), callOptions);
118
146
  if (command === "completion-candidates") return handleCompletionCandidates(argv.slice(1));
119
147
  if (command === "provision") return handleProvision(parseArgs(argv.slice(1)));
120
148
  if (OVERLAY_COMMANDS.includes(command)) {
@@ -1,4 +1,5 @@
1
- import { closest } from "@farmslot/recipe-cli/harness";
1
+ import { adapterChoices, closest } from "@farmslot/recipe-cli/harness";
2
+ import { configuredLibrarySources, operatorLibraryEnv } from "./adapters/plugin-surface.js";
2
3
  const bool = () => ({ kind: "boolean" });
3
4
  const value = (choices) => ({ kind: "value", ...choices ? { choices } : {} });
4
5
  const optionalValue = (choices) => ({ kind: "optional-value", ...choices ? { choices } : {} });
@@ -8,9 +9,18 @@ const JSON = { "--json": bool() };
8
9
  const NO_OPEN = { "--no-open": bool() };
9
10
  const JSON_STREAM = { "--json-stream": bool() };
10
11
  const TARGET = { "--target": value() };
11
- const ADAPTER = { "--adapter": value(["mobile", "extension", "core", "terminal"]) };
12
+ const adapterIds = (tokens) => adapterChoices(optionValues(tokens, "--library"), {
13
+ configured: configuredLibrarySources(),
14
+ env: operatorLibraryEnv()
15
+ });
16
+ const ADAPTER = { "--adapter": { kind: "value", choices: adapterIds } };
12
17
  const MOBILE_PLATFORM = { "--platform": value(["ios", "android"]) };
13
- const ADAPTER_OR_MOBILE_PLATFORM = { "--platform": value(["mobile", "extension", "core", "terminal", "ios", "android"]) };
18
+ const ADAPTER_OR_MOBILE_PLATFORM = {
19
+ "--platform": {
20
+ kind: "value",
21
+ choices: (tokens) => [...adapterIds(tokens), "ios", "android"]
22
+ }
23
+ };
14
24
  const DEVICE = { "--device": value() };
15
25
  const RUNTIME_PORTS = {
16
26
  "--cdp-port": value(),
@@ -152,7 +162,7 @@ const PUBLIC_COMMAND_CONTRACTS = {
152
162
  prepare: {
153
163
  usage: "mm-harness prepare [--target <checkout>] [--platform mobile|extension|core] [--mobile-platform ios|android] [--clear-metro] [--artifacts-dir <dir>] [--json]",
154
164
  options: options(HELP, JSON, TARGET, {
155
- "--platform": value(["mobile", "extension", "core", "terminal"]),
165
+ "--platform": { kind: "value", choices: adapterIds },
156
166
  "--mobile-platform": value(["ios", "android"]),
157
167
  "--clear-metro": bool(),
158
168
  "--artifacts-dir": value(),
@@ -420,6 +430,16 @@ const PUBLIC_COMMAND_CONTRACTS = {
420
430
  minimumPositionals: 0
421
431
  }
422
432
  };
433
+ function optionValues(args, name) {
434
+ const values = [];
435
+ for (let index = 0; index < args.length; index += 1) {
436
+ const arg = args[index];
437
+ if (arg === "--") break;
438
+ if (arg === name && args[index + 1] !== void 0) values.push(args[index += 1]);
439
+ else if (arg.startsWith(`${name}=`)) values.push(arg.slice(name.length + 1));
440
+ }
441
+ return values;
442
+ }
423
443
  function validatePublicInvocation(argv, examples = {}) {
424
444
  const token = argv[0];
425
445
  if (!token || token === "--help" || token === "-h" || token === "--version" || token === "-v") return null;
@@ -487,12 +507,13 @@ function validatePublicInvocation(argv, examples = {}) {
487
507
  return usageFailure("CLI_MISSING_OPTION_VALUE", name, `${optionName} requires a value.`, contract, examples);
488
508
  }
489
509
  if (inlineValue === void 0) index += 1;
490
- if (spec.choices && !spec.choices.includes(optionValue)) {
491
- const suggestion = closest(optionValue, spec.choices);
510
+ const choices = typeof spec.choices === "function" ? spec.choices(tokens) : spec.choices;
511
+ if (choices && !choices.includes(optionValue)) {
512
+ const suggestion = closest(optionValue, choices);
492
513
  return {
493
514
  code: "CLI_INVALID_OPTION_VALUE",
494
515
  command: name,
495
- message: `${optionName} must be ${formatChoiceList(spec.choices)}; received '${optionValue}'.`,
516
+ message: `${optionName} must be ${formatChoiceList(choices)}; received '${optionValue}'.`,
496
517
  userAction: actionFor(optionValue, suggestion, exampleFor(name, examples))
497
518
  };
498
519
  }
@@ -701,6 +722,7 @@ function exampleFor(command, examples) {
701
722
  }
702
723
  export {
703
724
  PUBLIC_COMMAND_CONTRACTS,
725
+ optionValues,
704
726
  publicCommandNames,
705
727
  publicCommandTokens,
706
728
  validatePublicInvocation
@@ -3,11 +3,12 @@ import { spawnSync } from "node:child_process";
3
3
  import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import "../adapters/surface.js";
6
- const USAGE = "mm-harness check diff [--fix] [--target <repo>] [--adapter mobile|extension|core] [--base <ref>] [--profile fast|full] [--artifacts-dir <dir>] [--json]";
6
+ const USAGE = "mm-harness check diff [--fix] [--target <repo>] [--adapter <adapter>] [--base <ref>] [--profile fast|full] [--artifacts-dir <dir>] [--json]";
7
7
  const JS_TS_EXT_RE = /\.(?:c|m)?jsx?$|\.tsx?$/u;
8
8
  const FORMAT_EXT_RE = /\.(?:c|m)?jsx?$|\.tsx?$|\.json$|\.md$|\.css$|\.scss$|\.ya?ml$/u;
9
9
  const TEST_FILE_RE = /(?:^|[./_-])(?:test|spec)\.(?:c|m)?jsx?$|(?:^|[./_-])(?:test|spec)\.tsx?$/u;
10
10
  const ESLINT_SUPPRESSION_RE = /^\s*eslint-disable(?:-line|-next-line)?\b/u;
11
+ const EXTENSION_LOCALE_RE = /^app\/_locales\//u;
11
12
  async function handleCheck(argv) {
12
13
  const { positional, options } = parseArgs(argv);
13
14
  const action = positional[0];
@@ -20,7 +21,7 @@ async function handleCheck(argv) {
20
21
  const target = path.resolve(optionString(options, "target") ?? process.cwd());
21
22
  const explicitAdapter = optionString(options, "adapter") ?? optionString(options, "platform");
22
23
  const detected = explicitAdapter ?? detectAdapter(target);
23
- if (detected && !["mobile", "extension", "core", "terminal"].includes(detected)) {
24
+ if (detected === "ios" || detected === "android") {
24
25
  return usageOut(json, "check", `unsupported adapter '${detected}'`, USAGE);
25
26
  }
26
27
  const adapter = detected;
@@ -52,10 +53,12 @@ async function handleCheck(argv) {
52
53
  const mergeBase = resolveMergeBase(target, baseRef);
53
54
  const changedFiles = listChangedFiles(target, mergeBase, artifactsDir);
54
55
  const existingChangedFiles = changedFiles.filter((file) => fs.existsSync(path.join(target, file)));
56
+ const untracked = untrackedFiles(target);
55
57
  const suppressionCheck = checkNewEslintSuppressions({
56
58
  target,
57
59
  artifactsDir,
58
60
  mergeBase,
61
+ untracked,
59
62
  files: existingChangedFiles.filter((file) => JS_TS_EXT_RE.test(file))
60
63
  });
61
64
  const policyFailed = suppressionCheck.status === "fail";
@@ -97,6 +100,9 @@ async function handleCheck(argv) {
97
100
  files: existingChangedFiles.filter((file) => TEST_FILE_RE.test(file))
98
101
  })
99
102
  );
103
+ if (adapter === "extension") {
104
+ checks.push(...runExtensionDiffChecks({ target, artifactsDir, packageJson, files: changedFiles }));
105
+ }
100
106
  if (profile === "full") {
101
107
  checks.push(runTypecheck({ target, artifactsDir, packageJson }));
102
108
  } else {
@@ -187,10 +193,10 @@ function listChangedFiles(target, mergeBase, artifactsDir) {
187
193
  }
188
194
  };
189
195
  if (mergeBase) {
190
- addLines(runCommand("git", ["diff", "--name-only", "--diff-filter=ACMR", `${mergeBase}...HEAD`], target).stdout);
196
+ addLines(runCommand("git", ["diff", "--name-only", "--no-renames", "--diff-filter=ACMRD", `${mergeBase}...HEAD`], target).stdout);
191
197
  }
192
- addLines(runCommand("git", ["diff", "--name-only", "--diff-filter=ACMR", "HEAD"], target).stdout);
193
- addLines(runCommand("git", ["diff", "--cached", "--name-only", "--diff-filter=ACMR"], target).stdout);
198
+ addLines(runCommand("git", ["diff", "--name-only", "--no-renames", "--diff-filter=ACMRD", "HEAD"], target).stdout);
199
+ addLines(runCommand("git", ["diff", "--cached", "--name-only", "--no-renames", "--diff-filter=ACMRD"], target).stdout);
194
200
  addLines(runCommand("git", ["ls-files", "--others", "--exclude-standard"], target).stdout);
195
201
  return [...files].filter((file) => !isUnderArtifactsDir(target, artifactsDir, file)).sort();
196
202
  }
@@ -209,9 +215,7 @@ function checkNewEslintSuppressions(input) {
209
215
  if (input.files.length === 0) {
210
216
  return { id, label, status: "skip", reason: "no changed JS/TS files" };
211
217
  }
212
- const untracked = new Set(
213
- runCommand("git", ["ls-files", "--others", "--exclude-standard"], input.target).stdout.split("\n").map((file) => normalizeRelativePath(file.trim())).filter(Boolean)
214
- );
218
+ const { untracked } = input;
215
219
  const findings = [];
216
220
  const trackedFiles = input.files.filter((file) => !untracked.has(normalizeRelativePath(file)));
217
221
  if (trackedFiles.length > 0) {
@@ -240,6 +244,26 @@ function checkNewEslintSuppressions(input) {
240
244
  reason: `${findings.length} newly added eslint-disable directive(s)`
241
245
  } : { id, label, status: "pass", logPath };
242
246
  }
247
+ function untrackedFiles(target) {
248
+ return new Set(
249
+ runCommand("git", ["ls-files", "--others", "--exclude-standard"], target).stdout.split("\n").map((file) => normalizeRelativePath(file.trim())).filter(Boolean)
250
+ );
251
+ }
252
+ function runExtensionDiffChecks(input) {
253
+ const scripts = input.packageJson?.scripts && typeof input.packageJson.scripts === "object" ? input.packageJson.scripts : {};
254
+ const results = [];
255
+ if (typeof scripts["verify-locales"] === "string") {
256
+ results.push(
257
+ input.files.some((file) => EXTENSION_LOCALE_RE.test(normalizeRelativePath(file))) ? runCheckCommand("verify-locales", "Locale strings", input.target, input.artifactsDir, "yarn", ["verify-locales", "--quiet"]) : { id: "verify-locales", label: "Locale strings", status: "skip", reason: "no app/_locales changes" }
258
+ );
259
+ }
260
+ if (typeof scripts["circular-deps:check"] === "string") {
261
+ results.push(
262
+ input.files.some((file) => JS_TS_EXT_RE.test(file)) ? runCheckCommand("circular-deps", "Circular dependencies", input.target, input.artifactsDir, "yarn", ["circular-deps:check"]) : { id: "circular-deps", label: "Circular dependencies", status: "skip", reason: "no JS/TS changes" }
263
+ );
264
+ }
265
+ return results;
266
+ }
243
267
  function findAddedSuppressions(diff) {
244
268
  const findings = [];
245
269
  let file = "";
@@ -1,4 +1,4 @@
1
- import { EXIT, parseArgs, optionFlag, optionString, optionStrings, resolveAdapter, targetPath } from "@farmslot/recipe-cli/harness";
1
+ import { EXIT, listRunnableRecipes, parseArgs, optionFlag, optionString, optionStrings, resolveAdapter, resolveLibrarySources, targetPath } from "@farmslot/recipe-cli/harness";
2
2
  import { resolveActionManifest } from "../manifest.js";
3
3
  import {
4
4
  invalidateCompletionCache,
@@ -6,7 +6,7 @@ import {
6
6
  writeCompletionCandidates
7
7
  } from "../completions-cache.js";
8
8
  import { importRecipeProtocol } from "../paths.js";
9
- import { listRunnableRecipes, resolveMetaMaskLibrarySources } from "./run-engine.js";
9
+ import { metaMaskEngine } from "../engine.js";
10
10
  async function handleCompletionCandidates(argv) {
11
11
  const kind = argv[0];
12
12
  const { options } = parseArgs(argv.slice(1));
@@ -20,7 +20,7 @@ async function handleCompletionCandidates(argv) {
20
20
  let candidates;
21
21
  if (kind === "actions") {
22
22
  adapter = resolveAdapter(options).adapter;
23
- const sources = await resolveMetaMaskLibrarySources(optionStrings(options, "library"));
23
+ const sources = await resolveLibrarySources(metaMaskEngine, optionStrings(options, "library"));
24
24
  const { manifest } = await resolveActionManifest(adapter, optionString(options, "actionManifest"), sources);
25
25
  const { getRecipeActionManifestActionNames } = await importRecipeProtocol();
26
26
  candidates = getRecipeActionManifestActionNames(manifest);
@@ -30,8 +30,8 @@ async function handleCompletionCandidates(argv) {
30
30
  const cacheKey = `recipes:${adapter}`;
31
31
  candidates = readFreshCandidates(target, cacheKey);
32
32
  if (!candidates) {
33
- const sources = await resolveMetaMaskLibrarySources(optionStrings(options, "library")).catch(() => void 0);
34
- candidates = sources ? (await listRunnableRecipes(adapter, sources)).map((recipe) => recipe.name) : [];
33
+ const sources = await resolveLibrarySources(metaMaskEngine, optionStrings(options, "library")).catch(() => void 0);
34
+ candidates = sources ? (await listRunnableRecipes(metaMaskEngine, adapter, sources)).map((recipe) => recipe.name) : [];
35
35
  writeCompletionCandidates(target, cacheKey, candidates);
36
36
  }
37
37
  }
@@ -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 { 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, detectAdapter, 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";
@@ -158,7 +158,10 @@ async function handleDoctor({ options }) {
158
158
  const checks = [
159
159
  ...result.checks,
160
160
  runtimeCheck,
161
- ...await readiness.liveChecks?.(target) ?? []
161
+ ...await readiness.liveChecks?.(target) ?? [],
162
+ ...await platformDoctorChecks(adapter, target),
163
+ // Which library plugin code this process loaded: library, module and digest.
164
+ ...adapterPluginChecks()
162
165
  ];
163
166
  const requiredChecks = requiredDoctorCheckSummary(checks);
164
167
  const doctorResult = {
@@ -244,6 +247,20 @@ async function handleDoctor({ options }) {
244
247
  }
245
248
  return doctorResult.status === "pass" ? 0 : 1;
246
249
  }
250
+ async function platformDoctorChecks(adapter, target) {
251
+ try {
252
+ return await getAdapterSurface(adapter).doctor?.(target) ?? [];
253
+ } catch (error) {
254
+ return [{
255
+ id: `adapter-plugin:${adapter}:doctor`,
256
+ status: "fail",
257
+ required: true,
258
+ message: `${adapter}'s doctor checks failed to run.`,
259
+ detail: error instanceof Error ? error.message : String(error),
260
+ userAction: `fix the doctor() of the ${adapter} adapter, then rerun mm-harness doctor --adapter ${adapter}`
261
+ }];
262
+ }
263
+ }
247
264
  function doctorRuntimeCheck(runtime, error, expectLive) {
248
265
  if (error) {
249
266
  return {
@@ -11,6 +11,7 @@ function farmslotReadyIndicator(adapter, devices) {
11
11
  return "OK";
12
12
  case "core":
13
13
  return "ready";
14
+ case "web-dapp":
14
15
  case "terminal":
15
16
  return "OK";
16
17
  case "mobile": {
@@ -5,7 +5,7 @@ import { handleHelpReview } from "./review.js";
5
5
  import "../adapters/surface.js";
6
6
  const UNSCOPED_NEXT = "cd into a MetaMask checkout or pass --adapter <mobile|extension|core|terminal>";
7
7
  function validHelpLine(value) {
8
- 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", "terminal"].includes(adapter)));
8
+ 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
9
  }
10
10
  function scopedSections(topic, adapter) {
11
11
  const select = (lines) => lines.flatMap((line) => typeof line === "string" ? [line] : line.adapters.includes(adapter) ? [line.text] : []);
@@ -9,15 +9,15 @@ import {
9
9
  parseRemoteFlagSpec,
10
10
  remoteFlagRequestFor
11
11
  } from "../../feature-flags.js";
12
- import { terminalOnlyLaunchFlags } from "../../adapters/terminal/launch.js";
12
+ import { webDappOnlyLaunchFlags } from "../../adapters/web-dapp/launch.js";
13
13
  import { shellQuote } from "./quote.js";
14
14
  async function runDeviceLaunch(context, profile) {
15
15
  const { adapter, options, target, heal, jsonOutput, machine, stream, usage } = context;
16
- const terminalOnly = terminalOnlyLaunchFlags(options);
17
- if (terminalOnly.length > 0) {
16
+ const webDappOnly = webDappOnlyLaunchFlags(options);
17
+ if (webDappOnly.length > 0) {
18
18
  return usage(
19
- `${terminalOnly.join(", ")} apply only to --adapter terminal; ${adapter} does not read them.`,
20
- "drop the terminal flags, or pass --adapter terminal"
19
+ `${webDappOnly.join(", ")} apply only to adapters that extend web-dapp (e.g. --adapter terminal); ${adapter} does not read them.`,
20
+ "drop those flags, or pass --adapter terminal"
21
21
  );
22
22
  }
23
23
  const wantBuild = flag(options, "build");
@@ -1,10 +1,10 @@
1
- import { optionFlag, optionString, requiredOption } from "@farmslot/recipe-cli/harness";
1
+ import { activateRecipeRuntimeEnvironment, optionFlag, optionString, requiredOption, runRecipe } from "@farmslot/recipe-cli/harness";
2
2
  import { mkdtemp } from "node:fs/promises";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { loadActionManifest, validateManifest } from "../manifest.js";
6
6
  import { recipePath, runnerDir } from "../paths.js";
7
- import { runRecipe } from "./run-engine.js";
7
+ import { metaMaskEngine } from "../engine.js";
8
8
  async function handleSelfTest({ options }) {
9
9
  const result = await runSelfTest(options);
10
10
  if (optionFlag(options, "json")) console.log(JSON.stringify(result, null, 2));
@@ -23,9 +23,14 @@ async function runSelfTest(options) {
23
23
  const manifestValidation = await validateManifest(manifest);
24
24
  const smokeRecipe = recipePath(`${adapter}/runner/smoke.recipe.json`);
25
25
  const artifactsDir = path.join(root, adapter);
26
- const result = await runRecipe(adapter, smokeRecipe, artifactsDir, runnerDir, void 0, {
27
- platform: { skipExtensionRuntimePrepare: true }
28
- });
26
+ const runtimeOptions = { platform: { skipExtensionRuntimePrepare: true } };
27
+ const restoreRuntimeEnvironment = activateRecipeRuntimeEnvironment(adapter, runnerDir, runtimeOptions);
28
+ let result;
29
+ try {
30
+ result = await runRecipe(metaMaskEngine, adapter, smokeRecipe, artifactsDir, runnerDir, void 0, runtimeOptions);
31
+ } finally {
32
+ restoreRuntimeEnvironment();
33
+ }
29
34
  runs.push({ adapter, manifestValidation: manifestValidation.summary, artifactsDir, result });
30
35
  }
31
36
  } finally {