@deeeed/metamask-harness 0.81.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 (71) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/adapters/manifest.json +51 -51
  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 +23 -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 +15 -25
  13. package/adapters/{terminal → web-dapp}/stop.mjs +5 -4
  14. package/adapters/{terminal → web-dapp}/verify.mjs +11 -15
  15. package/adapters/{terminal → web-dapp}/wallet-host.mjs +10 -11
  16. package/dist/adapters/extension/run.js +5 -28
  17. package/dist/adapters/plugin-surface.js +98 -0
  18. package/dist/adapters/surface.js +1 -1
  19. package/dist/adapters/{terminal → web-dapp}/actions.js +7 -7
  20. package/dist/adapters/web-dapp/console-capture.js +63 -0
  21. package/dist/adapters/web-dapp/doctor-checks.js +33 -0
  22. package/dist/adapters/{terminal → web-dapp}/launch.js +21 -22
  23. package/dist/adapters/web-dapp/overlay.js +21 -0
  24. package/dist/adapters/web-dapp/readiness.js +10 -0
  25. package/dist/adapters/{terminal → web-dapp}/run.js +15 -15
  26. package/dist/adapters/{terminal → web-dapp}/stop.js +5 -4
  27. package/dist/adapters/web-dapp/surface.js +149 -0
  28. package/dist/cli.js +3 -0
  29. package/dist/command-contract.js +29 -7
  30. package/dist/commands/check.js +2 -2
  31. package/dist/commands/doctor.js +19 -2
  32. package/dist/commands/farmslot-ready.js +1 -0
  33. package/dist/commands/help.js +1 -1
  34. package/dist/commands/launch/device.js +5 -5
  35. package/dist/commands/self-test.js +9 -5
  36. package/dist/engine.js +6 -16
  37. package/dist/manifest.js +48 -24
  38. package/dist/mm-harness-cli.js +17 -0
  39. package/dist/performance-observation.js +7 -13
  40. package/dist/runner.js +26 -7
  41. package/docs/cli-contract.md +5 -3
  42. package/library/actions/shared/console-collector.mjs +1 -1
  43. package/library/actions/{terminal → web-dapp}/app/assert_no_console_errors.mjs +4 -4
  44. package/library/actions/{terminal → web-dapp}/app/launch.mjs +7 -7
  45. package/library/actions/{terminal → web-dapp}/platform/hud.mjs +5 -5
  46. package/library/actions/{terminal → web-dapp}/platform/page.mjs +11 -11
  47. package/library/actions/{terminal → web-dapp}/platform/runtime.mjs +16 -12
  48. package/library/actions/web-dapp/ui/navigate.mjs +24 -0
  49. package/library/actions/web-dapp/wallet/_signature-log.mjs +32 -0
  50. package/library/actions/{terminal → web-dapp}/wallet/assert_signatures.mjs +4 -4
  51. package/library/actions/{terminal → web-dapp}/wallet/list_accounts.mjs +2 -2
  52. package/library/actions/{terminal → web-dapp}/wallet/read_signatures.mjs +6 -6
  53. package/library/manifests/{terminal.action-manifest.json → web-dapp.action-manifest.json} +18 -702
  54. package/package.json +5 -5
  55. package/adapters/terminal/lib/mainnet-guard.mjs +0 -186
  56. package/adapters/terminal/policy.mjs +0 -35
  57. package/dist/adapters/terminal/console-capture.js +0 -59
  58. package/dist/adapters/terminal/doctor-checks.js +0 -24
  59. package/dist/adapters/terminal/overlay.js +0 -25
  60. package/dist/adapters/terminal/readiness.js +0 -11
  61. package/dist/adapters/terminal/surface.js +0 -135
  62. package/library/actions/terminal/perps/_venue.mjs +0 -242
  63. package/library/actions/terminal/perps/assert_orders.mjs +0 -54
  64. package/library/actions/terminal/perps/assert_positions.mjs +0 -40
  65. package/library/actions/terminal/perps/read_orders.mjs +0 -26
  66. package/library/actions/terminal/perps/read_positions.mjs +0 -25
  67. package/library/actions/terminal/perps/read_snapshot.mjs +0 -40
  68. package/library/actions/terminal/perps/revoke_agent.mjs +0 -58
  69. package/library/actions/terminal/perps/teardown_state.mjs +0 -104
  70. package/library/actions/terminal/ui/navigate.mjs +0 -31
  71. package/library/actions/terminal/wallet/_signature-log.mjs +0 -56
@@ -2,7 +2,8 @@ import { flag, str } from "@farmslot/recipe-cli/harness";
2
2
  import { execFileSync } from "node:child_process";
3
3
  import path from "node:path";
4
4
  import { runnerDir } from "../../paths.js";
5
- const TERMINAL_ONLY_LAUNCH_OPTIONS = Object.freeze({
5
+ import { webDappAdapterId } from "../../../adapters/web-dapp/lib/policy.mjs";
6
+ const WEB_DAPP_ONLY_LAUNCH_OPTIONS = Object.freeze({
6
7
  signer: "--signer",
7
8
  account: "--account",
8
9
  freshProfile: "--fresh-profile",
@@ -12,12 +13,12 @@ const TERMINAL_ONLY_LAUNCH_OPTIONS = Object.freeze({
12
13
  network: "--network",
13
14
  mainnetConfirmation: "--mainnet-confirmation"
14
15
  });
15
- function terminalOnlyLaunchFlags(options) {
16
- return Object.entries(TERMINAL_ONLY_LAUNCH_OPTIONS).filter(([key]) => options[key] !== void 0 && options[key] !== false).map(([, name]) => name);
16
+ function webDappOnlyLaunchFlags(options) {
17
+ return Object.entries(WEB_DAPP_ONLY_LAUNCH_OPTIONS).filter(([key]) => options[key] !== void 0 && options[key] !== false).map(([, name]) => name);
17
18
  }
18
- function parseTerminalLauncherOutput(output) {
19
+ function parseWebDappLauncherOutput(output) {
19
20
  const text = output.trim();
20
- if (!text) return { status: "fail", error: "the terminal launcher printed no summary" };
21
+ if (!text) return { status: "fail", error: `the ${webDappAdapterId()} launcher printed no summary` };
21
22
  try {
22
23
  return JSON.parse(text);
23
24
  } catch {
@@ -25,30 +26,28 @@ function parseTerminalLauncherOutput(output) {
25
26
  try {
26
27
  return JSON.parse(last);
27
28
  } catch {
28
- return { status: "fail", error: `the terminal launcher printed no JSON summary: ${last.slice(0, 200)}` };
29
+ return { status: "fail", error: `the ${webDappAdapterId()} launcher printed no JSON summary: ${last.slice(0, 200)}` };
29
30
  }
30
31
  }
31
32
  }
32
- async function launchTerminal({ options, target, jsonOutput, stream, usage }) {
33
+ async function launchWebDapp({ options, target, jsonOutput, stream, usage }) {
34
+ const id = webDappAdapterId();
33
35
  const cdpPort = str(options, "cdpPort") ?? process.env.RECIPE_CDP_PORT ?? process.env.CDP_PORT;
34
36
  const appPort = str(options, "watcherPort") ?? process.env.TERMINAL_APP_PORT ?? process.env.RECIPE_WATCHER_PORT ?? process.env.WATCHER_PORT;
35
37
  if (!cdpPort || !appPort) {
36
38
  return usage(
37
- "terminal launch needs the slot browser CDP port and the slot dev-server port.",
38
- "mm-harness launch --adapter terminal --cdp-port <port> --watcher-port <app port>"
39
+ `${id} launch needs the slot browser CDP port and the slot dev-server port.`,
40
+ `mm-harness launch --adapter ${id} --cdp-port <port> --watcher-port <app port>`
39
41
  );
40
42
  }
41
43
  const args = [
42
- path.join(runnerDir, "adapters", "terminal", "launch.mjs"),
44
+ path.join(runnerDir, "adapters", "web-dapp", "launch.mjs"),
43
45
  "--target",
44
46
  target,
45
47
  "--cdp-port",
46
48
  cdpPort,
47
49
  "--app-port",
48
50
  appPort,
49
- // The Terminal's venue policy (Hyperliquid hosts, refusal, start page).
50
- "--policy-module",
51
- path.join(runnerDir, "adapters", "terminal", "policy.mjs"),
52
51
  "--signer",
53
52
  str(options, "signer") ?? process.env.TERMINAL_SIGNER ?? "extension",
54
53
  "--account",
@@ -64,7 +63,7 @@ async function launchTerminal({ options, target, jsonOutput, stream, usage }) {
64
63
  if (network) args.push("--network", network);
65
64
  const mainnetConfirmation = str(options, "mainnetConfirmation");
66
65
  if (mainnetConfirmation) args.push("--mainnet-confirmation", mainnetConfirmation);
67
- stream.phase("launch", { adapter: "terminal", cdpPort, appPort });
66
+ stream.phase("launch", { adapter: id, cdpPort, appPort });
68
67
  let output = "";
69
68
  let status = 0;
70
69
  try {
@@ -73,16 +72,16 @@ async function launchTerminal({ options, target, jsonOutput, stream, usage }) {
73
72
  status = 1;
74
73
  output = String(error.stdout ?? "");
75
74
  }
76
- const result = parseTerminalLauncherOutput(output);
77
- if (jsonOutput) console.log(JSON.stringify({ schemaVersion: 1, command: "launch", adapter: "terminal", target, ...result }, null, 2));
78
- else if (result.status === "pass") console.log(`\u2713 terminal browser ${result.reused ? "reused" : "started"} on CDP ${result.cdpPort} (${result.signer}, ${result.account?.name})`);
79
- else console.error(`\u2717 terminal launch failed: ${result.error ?? "unknown error"}`);
75
+ const result = parseWebDappLauncherOutput(output);
76
+ if (jsonOutput) console.log(JSON.stringify({ schemaVersion: 1, command: "launch", adapter: id, target, ...result }, null, 2));
77
+ else if (result.status === "pass") console.log(`\u2713 ${id} browser ${result.reused ? "reused" : "started"} on CDP ${result.cdpPort} (${result.signer}, ${result.account?.name})`);
78
+ else console.error(`\u2717 ${id} launch failed: ${result.error ?? "unknown error"}`);
80
79
  stream.complete(result.status === "pass" ? "pass" : "fail", status);
81
80
  return result.status === "pass" ? 0 : 1;
82
81
  }
83
82
  export {
84
- TERMINAL_ONLY_LAUNCH_OPTIONS,
85
- launchTerminal,
86
- parseTerminalLauncherOutput,
87
- terminalOnlyLaunchFlags
83
+ WEB_DAPP_ONLY_LAUNCH_OPTIONS,
84
+ launchWebDapp,
85
+ parseWebDappLauncherOutput,
86
+ webDappOnlyLaunchFlags
88
87
  };
@@ -0,0 +1,21 @@
1
+ import path from "node:path";
2
+ import { runnerDir } from "../../paths.js";
3
+ const webDappHarness = {
4
+ install: { entry: "adapters/web-dapp/inject.mjs", fallback: "adapters/web-dapp/inject.mjs", node: true },
5
+ cleanup: { entry: "adapters/web-dapp/cleanup.mjs", fallback: "adapters/web-dapp/cleanup.mjs", node: true },
6
+ verify: () => ({
7
+ command: process.execPath,
8
+ prefixArgs: [path.join(runnerDir, "adapters", "web-dapp", "verify.mjs")]
9
+ }),
10
+ restart: "mm-harness launch"
11
+ };
12
+ const webDappRuntimeContext = {
13
+ forbiddenFields: ["metroPort", "simulator", "simulatorUdid", "adbSerial", "extensionId"],
14
+ resources: ({ existing, defaults, envPort }) => ({
15
+ cdpPort: envPort("RECIPE_CDP_PORT", "CDP_PORT") ?? existing.cdpPort ?? defaults.cdpPort
16
+ })
17
+ };
18
+ export {
19
+ webDappHarness,
20
+ webDappRuntimeContext
21
+ };
@@ -0,0 +1,10 @@
1
+ import { POLICY_ENV, webDappPolicy } from "../../../adapters/web-dapp/lib/policy.mjs";
2
+ const webDappReadiness = {
3
+ compatibilityMode: (target) => process.env[POLICY_ENV] && webDappPolicy().checkout.matches(target) ? "slot browser over CDP (website; no product bridge)" : "unsupported/no bridge",
4
+ liveChecks: async (target) => (await import("./doctor-checks.js")).webDappDoctorChecks(target),
5
+ // Screenshots come from the slot browser over CDP.
6
+ captureProviders: ["cdp"]
7
+ };
8
+ export {
9
+ webDappReadiness
10
+ };
@@ -2,21 +2,21 @@ import { isRecord } from "@farmslot/recipe-cli/harness";
2
2
  import { verifyOwnedCollectorControl } from "../console-capture.js";
3
3
  import { devicePlatformFromEnv } from "../performance/device-platform.js";
4
4
  import {
5
- ensureTerminalConsoleCapture,
6
- terminalCaptureFiles,
7
- terminalCdpPort,
8
- terminalRuntimeFile,
9
- verifyTerminalConsoleControl
5
+ ensureWebDappConsoleCapture,
6
+ webDappCaptureFiles,
7
+ webDappCdpPort,
8
+ webDappRuntimeFile,
9
+ verifyWebDappConsoleControl
10
10
  } from "./console-capture.js";
11
- const terminalDiagnostics = {
11
+ const webDappDiagnostics = {
12
12
  console: {
13
- start: (projectRoot) => ensureTerminalConsoleCapture(projectRoot),
14
- files: (projectRoot) => terminalCaptureFiles(projectRoot),
15
- cdpPort: () => terminalCdpPort(),
16
- verifyControl: (capture) => verifyOwnedCollectorControl(capture, () => verifyTerminalConsoleControl(capture))
13
+ start: (projectRoot) => ensureWebDappConsoleCapture(projectRoot),
14
+ files: (projectRoot) => webDappCaptureFiles(projectRoot),
15
+ cdpPort: () => webDappCdpPort(),
16
+ verifyControl: (capture) => verifyOwnedCollectorControl(capture, () => verifyWebDappConsoleControl(capture))
17
17
  },
18
18
  requestLog: {
19
- path: (projectRoot) => terminalRuntimeFile(projectRoot, "wallet-requests.jsonl"),
19
+ path: (projectRoot) => webDappRuntimeFile(projectRoot, "wallet-requests.jsonl"),
20
20
  findings: (lines) => walletRequestFindings(lines)
21
21
  }
22
22
  };
@@ -38,7 +38,7 @@ function walletRequestFindings(lines) {
38
38
  }
39
39
  return findings;
40
40
  }
41
- const terminalObservation = {
41
+ const webDappObservation = {
42
42
  network: {
43
43
  backend: async (_target, env, artifactsDir) => (await import("../extension/network-observer.js")).createExtensionNetworkBackend(env, artifactsDir)
44
44
  },
@@ -46,13 +46,13 @@ const terminalObservation = {
46
46
  start: async (context) => {
47
47
  const observation = await import("../../performance-observation.js");
48
48
  return observation.startRunPerformanceObservation({
49
- backend: (target, env, ports) => observation.createExtensionPerformanceObserver(target, env, ports),
49
+ backend: (target, env) => observation.createExtensionPerformanceObserver(target, env),
50
50
  platform: (env) => devicePlatformFromEnv(env)
51
51
  }, context);
52
52
  }
53
53
  }
54
54
  };
55
55
  export {
56
- terminalDiagnostics,
57
- terminalObservation
56
+ webDappDiagnostics,
57
+ webDappObservation
58
58
  };
@@ -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/cli.js CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  targetPath
16
16
  } from "@farmslot/recipe-cli/harness";
17
17
  import { stopProjectCollector } from "../library/actions/shared/analytics/collector.mjs";
18
+ import { loadSelectedAdapter } from "./adapters/plugin-surface.js";
18
19
  import { adapterRegistry } from "./adapters/registry.js";
19
20
  import { applyDeviceTargeting } from "./commands/device-target.js";
20
21
  import { metaMaskEngine } from "./engine.js";
@@ -135,6 +136,8 @@ async function main(argv) {
135
136
  usage();
136
137
  return command ? 0 : 2;
137
138
  }
139
+ const refused = await loadSelectedAdapter(command, argv.slice(1));
140
+ if (refused !== void 0) return refused;
138
141
  if (command === "status" || command === "health" || command === "home") {
139
142
  return handleStatus(parseArgs(argv.slice(1)));
140
143
  }
@@ -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,7 +3,7 @@ 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;
@@ -21,7 +21,7 @@ async function handleCheck(argv) {
21
21
  const target = path.resolve(optionString(options, "target") ?? process.cwd());
22
22
  const explicitAdapter = optionString(options, "adapter") ?? optionString(options, "platform");
23
23
  const detected = explicitAdapter ?? detectAdapter(target);
24
- if (detected && !["mobile", "extension", "core", "terminal"].includes(detected)) {
24
+ if (detected === "ios" || detected === "android") {
25
25
  return usageOut(json, "check", `unsupported adapter '${detected}'`, USAGE);
26
26
  }
27
27
  const adapter = detected;
@@ -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,4 +1,4 @@
1
- import { optionFlag, optionString, requiredOption, runRecipe } 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";
@@ -23,10 +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(metaMaskEngine, adapter, smokeRecipe, artifactsDir, runnerDir, void 0, {
27
- cli: { adapter },
28
- platform: { skipExtensionRuntimePrepare: true }
29
- });
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
+ }
30
34
  runs.push({ adapter, manifestValidation: manifestValidation.summary, artifactsDir, result });
31
35
  }
32
36
  } finally {
package/dist/engine.js CHANGED
@@ -1,5 +1,4 @@
1
1
  import path from "node:path";
2
- import { resolveAdapter } from "@farmslot/recipe-cli/harness";
3
2
  import { getAdapterSurface } from "./adapters/surface.js";
4
3
  import { consoleClassifier } from "./console-classifier.js";
5
4
  import {
@@ -21,20 +20,21 @@ const metaMaskEngine = {
21
20
  resolveActionManifest,
22
21
  validateManifest,
23
22
  actionCapabilities: metaMaskActionExecutionCapabilities,
24
- createRunner: async (adapter, manifest, { trustedMutation, ...options }) => {
23
+ createRunner: async (adapter, manifest, options) => {
25
24
  const { createMetaMaskRunner } = await import("./runner.js");
26
- return trustedMutation === NO_PLAN_MUTATION ? createMetaMaskRunner(adapter, manifest, { ...options, suppressLibraryResolutionLogs: true }) : createMetaMaskRunner(adapter, manifest, { ...options, trustedMutation });
25
+ return createMetaMaskRunner(adapter, manifest, options);
27
26
  },
28
27
  // Funding files from the run's command line, bound to the run's plan. Without
29
28
  // them, a platform whose actions take a trusted mutation (the Extension) runs a
30
29
  // plan with external mutations in the fixture-policy lane, where each action
31
30
  // applies the shared order policy; any other plan runs without one.
32
31
  trustedMutation: {
33
- load: async ({ cli, ...run }) => await loadTrustedMutationBase({ ...fundingOptionsFromCli(cli), ...run }) ?? (takesTrustedMutation(cli) ? fixturePolicyMutationBase({ artifactsDir: run.artifactsDir, rootRecipe: run.rootRecipe }) : void 0),
32
+ load: async ({ cli, adapter, ...run }) => await loadTrustedMutationBase({ ...fundingOptionsFromCli(cli), ...run }) ?? (getAdapterSurface(adapter).actions.trustedMutation === true ? fixturePolicyMutationBase({ artifactsDir: run.artifactsDir, rootRecipe: run.rootRecipe }) : void 0),
33
+ // Nothing to bind (undefined): recipe-cli keeps the runner it planned with.
34
34
  authorize: async (base, plan) => {
35
- if (!base || base === NO_PLAN_MUTATION) return void 0;
35
+ if (!base) return void 0;
36
36
  if (isFixturePolicyBase(base) && !plan.nodes.some((node) => node.capabilities.includes("external-mutation"))) {
37
- return NO_PLAN_MUTATION;
37
+ return void 0;
38
38
  }
39
39
  return authorizeTrustedMutationPlan(base, plan);
40
40
  }
@@ -42,16 +42,6 @@ const metaMaskEngine = {
42
42
  console: consoleClassifier,
43
43
  runnerIncludes: ["src", "adapters", "library", "bin", "dist", "package.json"]
44
44
  };
45
- const NO_PLAN_MUTATION = /* @__PURE__ */ Symbol("no plan mutation");
46
- function takesTrustedMutation(cli) {
47
- let adapter;
48
- try {
49
- ({ adapter } = resolveAdapter(cli));
50
- } catch {
51
- return false;
52
- }
53
- return getAdapterSurface(adapter).actions.trustedMutation === true;
54
- }
55
45
  export {
56
46
  metaMaskEngine
57
47
  };