@deeeed/metamask-harness 0.70.1 → 0.72.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 (75) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/adapters/extension/console-tail.mjs +38 -6
  3. package/adapters/manifest.json +128 -0
  4. package/adapters/terminal/cleanup.mjs +68 -0
  5. package/adapters/terminal/inject.mjs +74 -0
  6. package/adapters/terminal/launch.mjs +628 -0
  7. package/adapters/terminal/lib/cdp-client.mjs +84 -0
  8. package/adapters/terminal/lib/display.mjs +115 -0
  9. package/adapters/terminal/lib/hud.mjs +148 -0
  10. package/adapters/terminal/lib/mainnet-guard.mjs +177 -0
  11. package/adapters/terminal/lib/origin.mjs +30 -0
  12. package/adapters/terminal/lib/page-script.mjs +246 -0
  13. package/adapters/terminal/lib/processes.mjs +187 -0
  14. package/adapters/terminal/lib/readiness.mjs +274 -0
  15. package/adapters/terminal/lib/strict-wallet.mjs +138 -0
  16. package/adapters/terminal/stop.mjs +37 -0
  17. package/adapters/terminal/verify.mjs +48 -0
  18. package/adapters/terminal/wallet-host.mjs +703 -0
  19. package/dist/adapters/console-capture.js +122 -0
  20. package/dist/adapters/extension/console-capture.js +22 -85
  21. package/dist/adapters/mobile/verify.js +8 -3
  22. package/dist/adapters/surface.js +3 -1
  23. package/dist/adapters/terminal/console-capture.js +46 -0
  24. package/dist/adapters/terminal/stop.js +19 -0
  25. package/dist/adapters/terminal/surface.js +104 -0
  26. package/dist/adapters.js +27 -4
  27. package/dist/cli.js +2 -2
  28. package/dist/command-contract.js +12 -4
  29. package/dist/commands/check.js +1 -1
  30. package/dist/commands/config.js +1 -1
  31. package/dist/commands/doctor.js +24 -1
  32. package/dist/commands/domain.js +1 -1
  33. package/dist/commands/farmslot-ready.js +2 -0
  34. package/dist/commands/help.js +2 -2
  35. package/dist/commands/launch/index.js +97 -2
  36. package/dist/commands/manifest.js +3 -3
  37. package/dist/commands/recipe-advice.js +1 -1
  38. package/dist/commands/review.js +1 -1
  39. package/dist/commands/run-engine.js +1 -1
  40. package/dist/commands/shared.js +1 -1
  41. package/dist/doctor.js +3 -0
  42. package/dist/harness.js +17 -11
  43. package/dist/manifest.js +5 -0
  44. package/dist/mm-harness-cli.js +34 -20
  45. package/dist/paths.js +2 -2
  46. package/dist/recipe-security.js +12 -3
  47. package/dist/review/knowledge.js +1 -1
  48. package/dist/run-diagnostics.js +150 -33
  49. package/dist/runner.js +1 -1
  50. package/dist/runtime-context.js +3 -3
  51. package/docs/CONTRIBUTING.md +1 -1
  52. package/docs/cli-contract.md +163 -0
  53. package/library/actions/shared/console-allowlist-add.mjs +137 -0
  54. package/library/actions/shared/console-collector.mjs +230 -0
  55. package/library/actions/shared/console-findings.mjs +511 -0
  56. package/library/actions/terminal/app/assert_no_console_errors.mjs +119 -0
  57. package/library/actions/terminal/app/launch.mjs +45 -0
  58. package/library/actions/terminal/perps/_venue.mjs +242 -0
  59. package/library/actions/terminal/perps/assert_orders.mjs +54 -0
  60. package/library/actions/terminal/perps/assert_positions.mjs +40 -0
  61. package/library/actions/terminal/perps/read_orders.mjs +26 -0
  62. package/library/actions/terminal/perps/read_positions.mjs +25 -0
  63. package/library/actions/terminal/perps/read_snapshot.mjs +40 -0
  64. package/library/actions/terminal/perps/revoke_agent.mjs +58 -0
  65. package/library/actions/terminal/perps/teardown_state.mjs +104 -0
  66. package/library/actions/terminal/platform/hud.mjs +71 -0
  67. package/library/actions/terminal/platform/page.mjs +247 -0
  68. package/library/actions/terminal/platform/runtime.mjs +200 -0
  69. package/library/actions/terminal/ui/navigate.mjs +31 -0
  70. package/library/actions/terminal/wallet/_signature-log.mjs +177 -0
  71. package/library/actions/terminal/wallet/assert_signatures.mjs +43 -0
  72. package/library/actions/terminal/wallet/list_accounts.mjs +27 -0
  73. package/library/actions/terminal/wallet/read_signatures.mjs +37 -0
  74. package/library/manifests/terminal.action-manifest.json +1786 -0
  75. package/package.json +2 -1
@@ -0,0 +1,122 @@
1
+ import { spawn, spawnSync } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import {
5
+ collectorMatches,
6
+ collectorOwnerMarker,
7
+ isLegacyCollector,
8
+ isOwnedCollector,
9
+ isPriorInstallCollector,
10
+ processArgs,
11
+ processCommand,
12
+ readPid
13
+ } from "../../library/actions/shared/console-collector.mjs";
14
+ import { runnerDir } from "../paths.js";
15
+ const COLLECTOR_SCRIPT = path.join(runnerDir, "adapters", "extension", "console-tail.mjs");
16
+ async function ensureConsoleCollector(options) {
17
+ const { cdpPort, extensionLog, pageLog, collectorLog, pidFile } = options;
18
+ fs.mkdirSync(path.dirname(extensionLog), { recursive: true });
19
+ const marker = collectorOwnerMarker(path.dirname(pidFile));
20
+ const identity = { script: COLLECTOR_SCRIPT, cdpPort, extensionLog, pageLog, marker };
21
+ const existingPid = readPid(pidFile);
22
+ const existing = existingPid ? processArgs(existingPid) : null;
23
+ if (existingPid && existing && isOwnedCollector(existing, identity)) {
24
+ if (collectorMatches(existing, identity) && collectorIsCurrent(existingPid, COLLECTOR_SCRIPT)) return { pid: existingPid, reused: true };
25
+ await stopProcess(existingPid);
26
+ } else if (existingPid && existing && isLegacyCollector(existing, identity)) {
27
+ await stopProcess(existingPid);
28
+ } else if (existingPid && existing && isPriorInstallCollector(existing, identity)) {
29
+ await stopProcess(existingPid);
30
+ }
31
+ for (const log of [extensionLog, pageLog]) {
32
+ rotateLargeLog(log);
33
+ fs.closeSync(fs.openSync(log, "a"));
34
+ }
35
+ const output = fs.openSync(collectorLog, "a");
36
+ let child;
37
+ try {
38
+ child = spawn(process.execPath, [
39
+ COLLECTOR_SCRIPT,
40
+ "--cdp-port",
41
+ cdpPort,
42
+ "--extension-log",
43
+ extensionLog,
44
+ "--dapp-log",
45
+ pageLog,
46
+ marker
47
+ ], {
48
+ cwd: options.cwd,
49
+ detached: true,
50
+ stdio: ["ignore", output, output]
51
+ });
52
+ } finally {
53
+ fs.closeSync(output);
54
+ }
55
+ if (!child.pid) throw new Error("Console collector did not start.");
56
+ child.unref();
57
+ fs.writeFileSync(pidFile, `${child.pid}
58
+ `);
59
+ const attachmentFrom = { [extensionLog]: fileSize(extensionLog), [pageLog]: fileSize(pageLog) };
60
+ for (let attempt = 0; attempt < 100; attempt += 1) {
61
+ const proc = processArgs(child.pid);
62
+ if (!proc) break;
63
+ if (collectorMatches(proc, identity)) {
64
+ if (!options.waitForAttachment) return { pid: child.pid, reused: false };
65
+ if (containsAttachment(attachmentFrom)) return { pid: child.pid, reused: false };
66
+ }
67
+ await new Promise((resolve) => setTimeout(resolve, 100));
68
+ }
69
+ throw new Error(
70
+ options.waitForAttachment ? `Console collector ${child.pid} did not attach to CDP ${cdpPort}.` : `Console collector ${child.pid} exited right after starting (see ${collectorLog}).`
71
+ );
72
+ }
73
+ async function stopProcess(pid) {
74
+ try {
75
+ process.kill(pid, "SIGTERM");
76
+ } catch {
77
+ return;
78
+ }
79
+ for (let attempt = 0; attempt < 30 && processCommand(pid); attempt += 1) {
80
+ await new Promise((resolve) => setTimeout(resolve, 100));
81
+ }
82
+ }
83
+ const ROTATE_LOG_BYTES = 32 * 1024 * 1024;
84
+ function rotateLargeLog(file) {
85
+ if (fileSize(file) < ROTATE_LOG_BYTES) return;
86
+ try {
87
+ fs.renameSync(file, `${file}.1`);
88
+ } catch {
89
+ }
90
+ }
91
+ function fileSize(file) {
92
+ try {
93
+ return fs.statSync(file).size;
94
+ } catch {
95
+ return 0;
96
+ }
97
+ }
98
+ function collectorIsCurrent(pid, script) {
99
+ const result = spawnSync("ps", ["-p", String(pid), "-o", "lstart="], { encoding: "utf8", env: { ...process.env, LC_ALL: "C" } });
100
+ const startedAt = Date.parse(result.stdout.trim());
101
+ if (!Number.isFinite(startedAt)) return false;
102
+ try {
103
+ return startedAt >= fs.statSync(script).mtimeMs - 1e3;
104
+ } catch {
105
+ return false;
106
+ }
107
+ }
108
+ function containsAttachment(from) {
109
+ return Object.entries(from).some(([log, offset]) => {
110
+ try {
111
+ const text = fs.readFileSync(log);
112
+ return text.subarray(Math.min(offset, text.length)).includes("[attached]");
113
+ } catch {
114
+ return false;
115
+ }
116
+ });
117
+ }
118
+ export {
119
+ COLLECTOR_SCRIPT,
120
+ ROTATE_LOG_BYTES,
121
+ ensureConsoleCollector
122
+ };
@@ -1,95 +1,32 @@
1
- import { spawn, spawnSync } from "node:child_process";
2
- import fs from "node:fs";
3
- import path from "node:path";
4
- import { recipeRuntimePath, runnerDir } from "../../paths.js";
1
+ import { recipeRuntimePath } from "../../paths.js";
2
+ import { ensureConsoleCollector } from "../console-capture.js";
3
+ function extensionCaptureFiles(target) {
4
+ return {
5
+ extensionLog: recipeRuntimePath(target, "extension-console.log"),
6
+ pageLog: recipeRuntimePath(target, "dapp-console.log"),
7
+ collectorLog: recipeRuntimePath(target, "console-tail.log"),
8
+ pidFile: recipeRuntimePath(target, "console-tail.pid")
9
+ };
10
+ }
11
+ function extensionCdpPort() {
12
+ return process.env.CDP_PORT ?? process.env.RECIPE_CDP_PORT;
13
+ }
5
14
  async function ensureExtensionConsoleCapture(target) {
6
- const cdpPort = process.env.CDP_PORT ?? process.env.RECIPE_CDP_PORT;
15
+ const cdpPort = extensionCdpPort();
7
16
  if (!cdpPort) throw new Error("Extension console capture requires a resolved CDP port.");
8
17
  const response = await fetch(`http://127.0.0.1:${cdpPort}/json/version`, {
9
18
  signal: AbortSignal.timeout(3e3)
10
19
  });
11
20
  if (!response.ok) throw new Error(`Extension CDP ${cdpPort} is not ready for console capture.`);
12
- const extensionLog = recipeRuntimePath(target, "extension-console.log");
13
- const dappLog = recipeRuntimePath(target, "dapp-console.log");
14
- const collectorLog = recipeRuntimePath(target, "console-tail.log");
15
- const pidFile = recipeRuntimePath(target, "console-tail.pid");
16
- fs.mkdirSync(path.dirname(extensionLog), { recursive: true });
17
- const existingPid = readPid(pidFile);
18
- if (existingPid && collectorOwnsLog(existingPid, extensionLog)) return;
19
- if (existingPid) {
20
- try {
21
- process.kill(existingPid, "SIGTERM");
22
- } catch {
23
- }
24
- }
25
- fs.rmSync(pidFile, { force: true });
26
- fs.writeFileSync(extensionLog, "");
27
- fs.writeFileSync(dappLog, "");
28
- const script = path.join(runnerDir, "adapters", "extension", "console-tail.mjs");
29
- const output = fs.openSync(collectorLog, "a");
30
- let child;
31
- try {
32
- child = spawn(process.execPath, [
33
- script,
34
- "--cdp-port",
35
- cdpPort,
36
- "--extension-log",
37
- extensionLog,
38
- "--dapp-log",
39
- dappLog
40
- ], {
41
- cwd: target,
42
- detached: true,
43
- stdio: ["ignore", output, output]
44
- });
45
- } finally {
46
- fs.closeSync(output);
47
- }
48
- if (!child.pid) throw new Error("Extension console collector did not start.");
49
- child.unref();
50
- fs.writeFileSync(pidFile, `${child.pid}
51
- `);
52
- for (let attempt = 0; attempt < 100; attempt += 1) {
53
- if (!processAlive(child.pid)) break;
54
- if (collectorOwnsLog(child.pid, extensionLog) && containsAttachment(extensionLog, dappLog)) return;
55
- await new Promise((resolve) => setTimeout(resolve, 100));
56
- }
57
- throw new Error(`Extension console collector ${child.pid} did not attach to CDP ${cdpPort}.`);
58
- }
59
- function processAlive(pid) {
60
- try {
61
- process.kill(pid, 0);
62
- return true;
63
- } catch {
64
- return false;
65
- }
66
- }
67
- function readPid(pidFile) {
68
- try {
69
- const pid = Number.parseInt(fs.readFileSync(pidFile, "utf8").trim(), 10);
70
- return Number.isInteger(pid) && pid > 0 ? pid : null;
71
- } catch {
72
- return null;
73
- }
74
- }
75
- function collectorOwnsLog(pid, extensionLog) {
76
- try {
77
- process.kill(pid, 0);
78
- } catch {
79
- return false;
80
- }
81
- const result = spawnSync("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf8" });
82
- return result.status === 0 && result.stdout.includes("console-tail.mjs") && result.stdout.includes(extensionLog);
83
- }
84
- function containsAttachment(...logs) {
85
- return logs.some((log) => {
86
- try {
87
- return fs.readFileSync(log, "utf8").includes("[attached]");
88
- } catch {
89
- return false;
90
- }
21
+ await ensureConsoleCollector({
22
+ cdpPort,
23
+ ...extensionCaptureFiles(target),
24
+ waitForAttachment: true,
25
+ cwd: target
91
26
  });
92
27
  }
93
28
  export {
94
- ensureExtensionConsoleCapture
29
+ ensureExtensionConsoleCapture,
30
+ extensionCaptureFiles,
31
+ extensionCdpPort
95
32
  };
@@ -12,7 +12,11 @@ const AUTO_START_TRUE = ["1", "true", "TRUE", "True", "yes", "YES", "Yes", "on",
12
12
  const AUTO_START_FALSE = ["0", "false", "FALSE", "False", "no", "NO", "No", "off", "OFF", "Off", ""];
13
13
  const AUTO_START_REFUSAL = "Mobile auto-start is not allowed from product-local scripts. Start or prepare the app through the runner/slot runtime, then rerun verify with --no-auto-start.";
14
14
  const EVM_READINESS_EXPRESSION = "(async function(){try{var engine=globalThis.Engine;var controller=engine&&engine.context&&engine.context.NetworkController;if(!controller||typeof controller.getSelectedNetworkClient!=='function')throw new Error('NetworkController selected client is unavailable');var client=controller.getSelectedNetworkClient();if(!client||!client.provider||typeof client.provider.request!=='function')throw new Error('selected EVM provider is unavailable');var result=await client.provider.request({method:'eth_blockNumber'});if(typeof result!=='string'||!/^0x[0-9a-f]+$/i.test(result))throw new Error('eth_blockNumber returned an invalid result');return {ok:true,result:result};}catch(error){return {ok:false,error:error&&(error.message||String(error))};}})()";
15
- const LIVE_SMOKE_RECIPE = `{
15
+ function liveSmokeProbeTimeoutMs(env = process.env) {
16
+ const value = Number(env.MM_HARNESS_MOBILE_VERIFY_PROBE_TIMEOUT_MS);
17
+ return Number.isInteger(value) && value > 0 ? value : 6e4;
18
+ }
19
+ const liveSmokeRecipeText = (timeoutMs) => `{
16
20
  "$schema": "https://farmslot.io/schemas/recipe-v1.schema.json",
17
21
  "title": "Mobile v1 runner live bridge smoke",
18
22
  "description": "Verifies the installed MetaMask runner can read the React Native debug bridge without mutating wallet state.",
@@ -20,7 +24,7 @@ const LIVE_SMOKE_RECIPE = `{
20
24
  "entry": "status",
21
25
  "nodes": {
22
26
  "status": { "action": "app.status", "intent": "Read Mobile app status through the v1 runner", "next": "cdp-probe" },
23
- "cdp-probe": { "action": "cdp.target", "intent": "Verify the React Native debug bridge target is reachable", "require_reachable": true, "timeout_ms": 60000, "cdp_timeout_ms": 60000, "next": "done" },
27
+ "cdp-probe": { "action": "cdp.target", "intent": "Verify the React Native debug bridge target is reachable", "require_reachable": true, "timeout_ms": ${timeoutMs}, "cdp_timeout_ms": ${timeoutMs}, "next": "done" },
24
28
  "done": { "action": "end", "status": "pass" }
25
29
  }
26
30
  }
@@ -386,7 +390,7 @@ async function runMobileVerify(argv, io = defaultIo) {
386
390
  }
387
391
  }
388
392
  const liveSmokeRecipe = path.join(artifacts, "mobile-v1-live-smoke.recipe.json");
389
- fs.writeFileSync(liveSmokeRecipe, LIVE_SMOKE_RECIPE);
393
+ fs.writeFileSync(liveSmokeRecipe, liveSmokeRecipeText(liveSmokeProbeTimeoutMs()));
390
394
  const iosSimulatorResolved = resolveJsEnvValue(target, "IOS_SIMULATOR");
391
395
  const adbSerialResolved = resolveJsEnvValue(target, "ADB_SERIAL");
392
396
  let liveSmokeCode = 1;
@@ -495,6 +499,7 @@ export {
495
499
  buildMobileVerifySummary,
496
500
  classifyMobileRuntimeOwner,
497
501
  isNumericWatcherPort,
502
+ liveSmokeProbeTimeoutMs,
498
503
  parseMobileVerifyArgs,
499
504
  resolveJsEnvValue,
500
505
  resolveWatcherPort,
@@ -1,10 +1,12 @@
1
1
  import { coreSurface } from "./core/surface.js";
2
2
  import { extensionSurface } from "./extension/surface.js";
3
3
  import { mobileSurface } from "./mobile/surface.js";
4
+ import { terminalSurface } from "./terminal/surface.js";
4
5
  const SURFACES = {
5
6
  mobile: mobileSurface,
6
7
  extension: extensionSurface,
7
- core: coreSurface
8
+ core: coreSurface,
9
+ terminal: terminalSurface
8
10
  };
9
11
  function getAdapterSurface(adapter) {
10
12
  return SURFACES[adapter];
@@ -0,0 +1,46 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { recipeRuntimePath } from "../../paths.js";
4
+ import { ensureConsoleCollector } from "../console-capture.js";
5
+ const TERMINAL_CONSOLE_FILES = Object.freeze({
6
+ app: "app-console.log",
7
+ extension: "extension-console.log",
8
+ collectorLog: "console-tail.log",
9
+ pidFile: "console-tail.pid"
10
+ });
11
+ function terminalRuntimeFile(target, name) {
12
+ let root = path.resolve(target);
13
+ try {
14
+ root = fs.realpathSync(root);
15
+ } catch {
16
+ }
17
+ return recipeRuntimePath(root, "terminal", name);
18
+ }
19
+ function terminalCaptureFiles(target) {
20
+ return {
21
+ extensionLog: terminalRuntimeFile(target, TERMINAL_CONSOLE_FILES.extension),
22
+ pageLog: terminalRuntimeFile(target, TERMINAL_CONSOLE_FILES.app),
23
+ collectorLog: terminalRuntimeFile(target, TERMINAL_CONSOLE_FILES.collectorLog),
24
+ pidFile: terminalRuntimeFile(target, TERMINAL_CONSOLE_FILES.pidFile)
25
+ };
26
+ }
27
+ function terminalCdpPort() {
28
+ return process.env.RECIPE_CDP_PORT ?? process.env.CDP_PORT;
29
+ }
30
+ async function ensureTerminalConsoleCapture(target) {
31
+ const cdpPort = terminalCdpPort();
32
+ if (!cdpPort) throw new Error("Terminal console capture requires the slot CDP port.");
33
+ await ensureConsoleCollector({
34
+ cdpPort,
35
+ ...terminalCaptureFiles(target),
36
+ waitForAttachment: false,
37
+ cwd: target
38
+ });
39
+ }
40
+ export {
41
+ TERMINAL_CONSOLE_FILES,
42
+ ensureTerminalConsoleCapture,
43
+ terminalCaptureFiles,
44
+ terminalCdpPort,
45
+ terminalRuntimeFile
46
+ };
@@ -0,0 +1,19 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import path from "node:path";
3
+ import { runnerDir } from "../../paths.js";
4
+ function stopTerminalBrowserSync(target) {
5
+ const args = [path.join(runnerDir, "adapters", "terminal", "stop.mjs"), "--target", path.resolve(target)];
6
+ const cdpPort = process.env.RECIPE_CDP_PORT ?? process.env.CDP_PORT;
7
+ if (cdpPort) args.push("--cdp-port", cdpPort);
8
+ const result = spawnSync(process.execPath, args, { encoding: "utf8" });
9
+ const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
10
+ const status = result.status ?? 1;
11
+ return {
12
+ status,
13
+ summary: status === 0 ? "terminal browser and wallet host stopped" : "terminal browser stop failed",
14
+ output
15
+ };
16
+ }
17
+ export {
18
+ stopTerminalBrowserSync
19
+ };
@@ -0,0 +1,104 @@
1
+ import path from "node:path";
2
+ import { shellQuote } from "../../commands/parse-args.js";
3
+ import fs from "node:fs";
4
+ import { TERMINAL_CONSOLE_FILES, terminalRuntimeFile } from "./console-capture.js";
5
+ import { stopTerminalBrowserSync } from "./stop.js";
6
+ function portFromEnv(...names) {
7
+ for (const name of names) {
8
+ const raw = process.env[name];
9
+ if (raw && /^\d+$/u.test(raw)) return Number(raw);
10
+ }
11
+ return void 0;
12
+ }
13
+ function terminalAppPort() {
14
+ return portFromEnv("TERMINAL_APP_PORT", "RECIPE_WATCHER_PORT", "WATCHER_PORT");
15
+ }
16
+ function terminalCdpPort() {
17
+ return portFromEnv("RECIPE_CDP_PORT", "CDP_PORT");
18
+ }
19
+ function terminalSignerFor(target) {
20
+ try {
21
+ const state = JSON.parse(fs.readFileSync(terminalRuntimeFile(target, "browser.json"), "utf8"));
22
+ if (typeof state.signer === "string") return state.signer;
23
+ } catch {
24
+ }
25
+ return process.env.TERMINAL_SIGNER || "extension";
26
+ }
27
+ const terminalSurface = {
28
+ adapter: "terminal",
29
+ headless: false,
30
+ resolveSlotPorts() {
31
+ },
32
+ async runtimeStatus(target) {
33
+ const { terminalReadiness } = await import("../../../adapters/terminal/lib/readiness.mjs");
34
+ const report = await terminalReadiness({
35
+ target: path.resolve(target),
36
+ appPort: terminalAppPort(),
37
+ cdpPort: terminalCdpPort(),
38
+ signer: process.env.TERMINAL_SIGNER || "extension"
39
+ });
40
+ const blocking = report.checks.filter((check) => check.required && check.status === "fail");
41
+ const devServer = report.checks.find((check) => check.id === "app-dev-server");
42
+ return {
43
+ decision: blocking.length === 0 ? "ready" : "blocked",
44
+ reasonCode: blocking[0]?.id,
45
+ reasons: (blocking.length ? blocking : report.checks).map((check) => `${check.id}: ${check.detail}`),
46
+ 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 terminal --target ${shellQuote(path.resolve(target))} --json`,
47
+ deps: report.checks.find((check) => check.id === "dependencies")?.status === "pass" ? "current" : "missing",
48
+ devServer: {
49
+ label: "next",
50
+ status: devServer?.status === "pass" ? "up" : "down"
51
+ }
52
+ };
53
+ },
54
+ runwayProvision: {
55
+ async run(target, options) {
56
+ return {
57
+ schemaVersion: 1,
58
+ command: "provision",
59
+ adapter: "terminal",
60
+ target: path.resolve(target),
61
+ status: "fail",
62
+ exitCode: 2,
63
+ error: {
64
+ code: "UNSUPPORTED_ADAPTER",
65
+ message: "the Web Terminal is a website; there is no simulator or build to provision.",
66
+ userAction: options.rerunCommand || "mm-harness launch --adapter terminal"
67
+ }
68
+ };
69
+ }
70
+ },
71
+ devServer: {
72
+ label: "browser",
73
+ describe: () => "terminal slot browser and wallet host (the Next.js dev server belongs to the slot)",
74
+ stop(target) {
75
+ const result = stopTerminalBrowserSync(target);
76
+ return { kind: "stopped", status: result.status, summary: result.summary, output: result.output };
77
+ }
78
+ },
79
+ logSources(target) {
80
+ return [
81
+ { label: "app-console", path: terminalRuntimeFile(target, TERMINAL_CONSOLE_FILES.app) },
82
+ ...terminalSignerFor(target) === "extension" ? [{ label: "extension-console", path: terminalRuntimeFile(target, TERMINAL_CONSOLE_FILES.extension) }] : [],
83
+ { label: "wallet-requests", path: terminalRuntimeFile(target, "wallet-requests.jsonl") },
84
+ { label: "wallet-host", path: terminalRuntimeFile(target, "wallet-host.log") },
85
+ { label: "browser", path: terminalRuntimeFile(target, "browser.log") },
86
+ { label: "next", path: path.join(target, "temp/farmslot/next-dev.log") }
87
+ ];
88
+ },
89
+ // The terminal page's console and uncaught exceptions, as the Extension's
90
+ // diagnostics read the extension console: errors make a run REVIEW.
91
+ appLogSource(target) {
92
+ return { label: "app-console", path: terminalRuntimeFile(target, TERMINAL_CONSOLE_FILES.app) };
93
+ },
94
+ hints: {
95
+ launch: "mm-harness launch --adapter terminal --cdp-port <port> --watcher-port <app port>",
96
+ relaunch: "mm-harness launch --adapter terminal --fresh-profile",
97
+ runtimeProbeRecovery: (target) => `mm-harness doctor --adapter terminal --target ${shellQuote(path.resolve(target))}`
98
+ }
99
+ };
100
+ export {
101
+ terminalAppPort,
102
+ terminalCdpPort,
103
+ terminalSurface
104
+ };
package/dist/adapters.js CHANGED
@@ -121,7 +121,7 @@ function requiresLiveAdapter(platform, action) {
121
121
  return LIVE_ONLY_ACTIONS.has(action) || platform === "core" && CORE_ONLY_PERPS_ACTIONS.has(action) || LIVE_ONLY_WALLET_ACTIONS.has(action) || LIVE_ONLY_FEATURE_FLAG_ACTIONS.has(action) || LIVE_ONLY_APP_ACTIONS.has(action);
122
122
  }
123
123
  function liveRuntimeConfigured(platform, node) {
124
- if (platform === "mobile" || platform === "core") return true;
124
+ if (platform === "mobile" || platform === "core" || platform === "terminal") return true;
125
125
  return Boolean(node.cdp_port ?? process.env.CDP_PORT ?? process.env.RECIPE_CDP_PORT);
126
126
  }
127
127
  async function runLiveFirst(platform, action, node, context, forceLive = false, preparedLiveAdapters) {
@@ -895,7 +895,7 @@ function mobileHudStatusLabel(status) {
895
895
  if (status === "pass") return "pass";
896
896
  return "run";
897
897
  }
898
- function createMetaMaskUiTransport(platform, harness, preparedLiveAdapters) {
898
+ function createMetaMaskUiTransport(platform, harness, preparedLiveAdapters, { hudPolicy = "auto" } = {}) {
899
899
  if (platform === "core") {
900
900
  return {
901
901
  async execute(action) {
@@ -905,7 +905,23 @@ function createMetaMaskUiTransport(platform, harness, preparedLiveAdapters) {
905
905
  }
906
906
  };
907
907
  }
908
- const base = platform === "mobile" ? harness.createReactNativeBridgeUiTransport({ bridge: createMetaMaskMobileBridge() }) : harness.createCdpWebUiTransport({
908
+ const base = platform === "mobile" ? harness.createReactNativeBridgeUiTransport({ bridge: createMetaMaskMobileBridge() }) : platform === "terminal" ? harness.createCdpWebUiTransport({
909
+ async withPage(input, callback) {
910
+ const { withTerminalPage } = await import("../library/actions/terminal/platform/page.mjs");
911
+ return withTerminalPage(
912
+ {
913
+ action: input.action,
914
+ node: input.node,
915
+ context: {
916
+ nodeId: input.context.nodeId,
917
+ projectRoot: input.context.projectRoot,
918
+ artifactsDir: input.context.artifactsDir
919
+ }
920
+ },
921
+ callback
922
+ );
923
+ }
924
+ }) : harness.createCdpWebUiTransport({
909
925
  async withPage(input, callback) {
910
926
  return withExtensionPage(
911
927
  {
@@ -924,6 +940,13 @@ function createMetaMaskUiTransport(platform, harness, preparedLiveAdapters) {
924
940
  return {
925
941
  ...base.observe ? { observe: base.observe.bind(base) } : {},
926
942
  async execute(action, node, context) {
943
+ if (platform === "terminal" && action === "app.hud") {
944
+ const { showTerminalHud } = await import("../library/actions/terminal/platform/hud.mjs");
945
+ return showTerminalHud(
946
+ { action, node, context: { nodeId: context.nodeId, projectRoot: context.projectRoot } },
947
+ { hudPolicy }
948
+ );
949
+ }
927
950
  if (action === "ui.navigate") {
928
951
  const live = await runLiveFirst(
929
952
  platform,
@@ -1110,7 +1133,7 @@ function createMetaMaskAdapters(adapter, declaredCustomActions = [], preparedLiv
1110
1133
  return {
1111
1134
  output: {
1112
1135
  platform,
1113
- transport: platform === "mobile" ? "react-native-debug-bridge" : "chrome-extension-cdp",
1136
+ transport: platform === "mobile" ? "react-native-debug-bridge" : platform === "terminal" ? "chrome-cdp" : "chrome-extension-cdp",
1114
1137
  cdpPort,
1115
1138
  metroPort,
1116
1139
  probeUrl: url,
package/dist/cli.js CHANGED
@@ -44,7 +44,7 @@ const COMMANDS = {
44
44
  const OVERLAY_COMMANDS = ["install", "verify", "cleanup"];
45
45
  function usage() {
46
46
  console.error(`mm-harness \u2014 the MetaMask recipe harness: launch the app, prove behavior, manage the runtime overlay.
47
- Run it from inside a MetaMask checkout; the platform (mobile | extension | core) is auto-detected.
47
+ Run it from inside a MetaMask checkout; the platform (mobile | extension | core | terminal) is auto-detected.
48
48
  Grammar: mm-harness <command> [target] [flags] (target is a positional: ios | android | extension; flags add agent depth)
49
49
 
50
50
  DAILY LOOP \u2014 what a teammate runs many times a day:
@@ -108,7 +108,7 @@ async function main(argv) {
108
108
  const { options } = parseArgs(argv.slice(1), command);
109
109
  const target = targetPath(options);
110
110
  const adapterRaw = (typeof options["adapter"] === "string" ? options["adapter"] : void 0) ?? (typeof options["platform"] === "string" ? options["platform"] : void 0) ?? detectAdapter(target);
111
- if (adapterRaw === "mobile" || adapterRaw === "extension" || adapterRaw === "core") {
111
+ if (adapterRaw === "mobile" || adapterRaw === "extension" || adapterRaw === "core" || adapterRaw === "terminal") {
112
112
  await handleHarness(["install", "--platform", adapterRaw, "--target", target, "--quiet"]);
113
113
  }
114
114
  } catch {
@@ -7,9 +7,9 @@ const JSON = { "--json": bool() };
7
7
  const NO_OPEN = { "--no-open": bool() };
8
8
  const JSON_STREAM = { "--json-stream": bool() };
9
9
  const TARGET = { "--target": value() };
10
- const ADAPTER = { "--adapter": value(["mobile", "extension", "core"]) };
10
+ const ADAPTER = { "--adapter": value(["mobile", "extension", "core", "terminal"]) };
11
11
  const MOBILE_PLATFORM = { "--platform": value(["ios", "android"]) };
12
- const ADAPTER_OR_MOBILE_PLATFORM = { "--platform": value(["mobile", "extension", "core", "ios", "android"]) };
12
+ const ADAPTER_OR_MOBILE_PLATFORM = { "--platform": value(["mobile", "extension", "core", "terminal", "ios", "android"]) };
13
13
  const DEVICE = { "--device": value() };
14
14
  const RUNTIME_PORTS = {
15
15
  "--cdp-port": value(),
@@ -151,7 +151,7 @@ const PUBLIC_COMMAND_CONTRACTS = {
151
151
  prepare: {
152
152
  usage: "mm-harness prepare [--target <checkout>] [--platform mobile|extension|core] [--mobile-platform ios|android] [--clear-metro] [--artifacts-dir <dir>] [--json]",
153
153
  options: options(HELP, JSON, TARGET, {
154
- "--platform": value(["mobile", "extension", "core"]),
154
+ "--platform": value(["mobile", "extension", "core", "terminal"]),
155
155
  "--mobile-platform": value(["ios", "android"]),
156
156
  "--clear-metro": bool(),
157
157
  "--artifacts-dir": value(),
@@ -347,7 +347,15 @@ const PUBLIC_COMMAND_CONTRACTS = {
347
347
  "--fullscreen": bool(),
348
348
  "--url": value(),
349
349
  "--heal": value(["off", "infra-only", "auto"]),
350
- "--remote-flag": value()
350
+ "--remote-flag": value(),
351
+ "--signer": value(["extension", "injected"]),
352
+ "--account": value(),
353
+ "--fresh-profile": bool(),
354
+ "--headless": bool(),
355
+ "--headful": bool(),
356
+ "--slow-mo": value(),
357
+ "--network": value(["testnet", "mainnet"]),
358
+ "--mainnet-confirmation": value()
351
359
  }),
352
360
  positionals: [{ label: "platform", choices: ["ios", "android"] }]
353
361
  },
@@ -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"].includes(detected)) {
24
+ if (detected && !["mobile", "extension", "core", "terminal"].includes(detected)) {
25
25
  return usageOut(json, "check", `unsupported adapter '${detected}'`, USAGE);
26
26
  }
27
27
  const adapter = detected;
@@ -7,7 +7,7 @@ import {
7
7
  writeConfig
8
8
  } from "../review/knowledge.js";
9
9
  import { optionFlag, parseArgs, usageError } from "./parse-args.js";
10
- const USAGE = "Usage: mm-harness config <list|path|get <key>|set <key> <value>|unset <key>> [--json]\n keys: libraries.<name>, references.<mobile|extension|core>, advisor.<enabled|provider|model|stages>";
10
+ const USAGE = "Usage: mm-harness config <list|path|get <key>|set <key> <value>|unset <key>> [--json]\n keys: libraries.<name>, references.<mobile|extension|core|terminal>, advisor.<enabled|provider|model|stages>";
11
11
  function parseKey(raw) {
12
12
  if (!raw) throw usageError(USAGE);
13
13
  const dot = raw.indexOf(".");
@@ -7,6 +7,7 @@ import { featureFlagReport } from "../feature-flags.js";
7
7
  import { color } from "../cli-color.js";
8
8
  import { collectCheckoutView, renderCheckoutView } from "../checkout-view.js";
9
9
  import { createDoctorReport, fixtureSummary, requiredDoctorCheckSummary } from "../doctor.js";
10
+ import { terminalAppPort, terminalCdpPort } from "../adapters/terminal/surface.js";
10
11
  import { detectAdapter, resolveRuntimeContextPath } from "../harness.js";
11
12
  import { assertAdapter, runnerDir } from "../paths.js";
12
13
  import { getAdapterSurface } from "../adapters/surface.js";
@@ -179,7 +180,11 @@ async function handleDoctor({ options }) {
179
180
  runtimeProbeError = error instanceof Error ? error.message : String(error);
180
181
  }
181
182
  const runtimeCheck = doctorRuntimeCheck(runtime, runtimeProbeError, expectLive);
182
- const checks = [...result.checks, runtimeCheck];
183
+ const checks = [
184
+ ...result.checks,
185
+ runtimeCheck,
186
+ ...adapter === "terminal" ? await terminalDoctorChecks(target) : []
187
+ ];
183
188
  const requiredChecks = requiredDoctorCheckSummary(checks);
184
189
  const doctorResult = {
185
190
  ...result,
@@ -268,6 +273,24 @@ async function handleDoctor({ options }) {
268
273
  }
269
274
  return doctorResult.status === "pass" ? 0 : 1;
270
275
  }
276
+ async function terminalDoctorChecks(target) {
277
+ const { terminalReadiness } = await import("../../adapters/terminal/lib/readiness.mjs");
278
+ const { resolveBrowser } = await import("../../adapters/terminal/launch.mjs");
279
+ const report = await terminalReadiness({
280
+ target,
281
+ appPort: terminalAppPort(),
282
+ cdpPort: terminalCdpPort(),
283
+ signer: process.env.TERMINAL_SIGNER || "extension",
284
+ account: process.env.TERMINAL_ACCOUNT || "dev1",
285
+ probeBrowser: resolveBrowser
286
+ });
287
+ return report.checks.map((check) => ({
288
+ id: `terminal-${check.id}`,
289
+ status: check.status === "pass" ? "pass" : "fail",
290
+ required: check.required,
291
+ message: check.detail
292
+ }));
293
+ }
271
294
  function doctorRuntimeCheck(runtime, error, expectLive) {
272
295
  if (error) {
273
296
  return {
@@ -19,7 +19,7 @@ async function handleDomain(argv) {
19
19
  return 0;
20
20
  }
21
21
  if (!adapter) {
22
- throw usageError("No MetaMask checkout detected: pass --adapter <mobile|extension|core> or --domain <name>.");
22
+ throw usageError("No MetaMask checkout detected: pass --adapter <mobile|extension|core|terminal> or --domain <name>.");
23
23
  }
24
24
  const config = readConfig(configPath());
25
25
  const libraries = discoverLibraries({ target, config });