@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deeeed/metamask-harness",
3
- "version": "0.81.0",
3
+ "version": "0.82.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mm-harness": "bin/mm-harness"
@@ -21,14 +21,14 @@
21
21
  "check:syntax": "find . -name '*.mjs' -print0 | xargs -0 -n1 node --check"
22
22
  },
23
23
  "dependencies": {
24
- "@farmslot/adapter-rn": "0.17.0",
25
- "@farmslot/adapter-sdk": "0.4.0",
24
+ "@farmslot/adapter-rn": "0.17.1",
25
+ "@farmslot/adapter-sdk": "0.5.0",
26
26
  "@farmslot/adapter-web": "0.3.1",
27
27
  "@farmslot/agent-runtime": "0.17.0",
28
28
  "@farmslot/handoff": "^0.3.1",
29
29
  "@farmslot/protocol": "0.34.0",
30
- "@farmslot/recipe-cli": "0.5.0",
31
- "@farmslot/recipe-runner": "0.24.0",
30
+ "@farmslot/recipe-cli": "0.6.0",
31
+ "@farmslot/recipe-runner": "0.25.0",
32
32
  "agent-device": "0.19.3",
33
33
  "commander": "^12.0.0",
34
34
  "esbuild": "0.28.1",
@@ -1,186 +0,0 @@
1
- // Testnet enforcement for the terminal slot browser. In testnet mode nothing
2
- // the browser runs may reach a mainnet venue (Hyperliquid, or Lighter when the
3
- // controller carries it), whatever the dev server happens to serve:
4
- // - the network layer: --host-resolver-rules makes every mainnet venue host
5
- // unresolvable for the whole browser (pages, workers, WebSockets), and
6
- // the wallet host fails and logs each attempt it sees (blocked-mainnet);
7
- // - the wallet layer: a typed-data request for a mainnet action is refused
8
- // before MetaMask or the injected signer sees it (refused-mainnet).
9
- // The venue hosts are the endpoints the venue SDK, the perps controller and
10
- // the app's source in the checkout connect to.
11
-
12
- import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
13
- import { createRequire } from 'node:module';
14
- import path from 'node:path';
15
-
16
- const SDK_PACKAGE = '@nktkas/hyperliquid';
17
- const CONTROLLER_PACKAGE = '@metamask/perps-controller';
18
- // Venue API hosts of the perps providers the terminal can configure: the
19
- // Hyperliquid API/RPC hosts and Lighter's zkLighter API.
20
- const VENUE_HOST = /^(?:[a-z0-9-]+\.)*(?:hyperliquid(?:-testnet)?\.xyz|zklighter\.elliot\.ai)$/u;
21
- // A URL in source text, any case; it is then parsed (canonicalUrl).
22
- const URL_CANDIDATE = /\b(?:https?|wss?):\/\/[^\s'"`<>(){}\\]+/giu;
23
- // Any mention of a venue host, URL or not: the cross-check of the SDK scan.
24
- const VENUE_HOST_MENTION = /(?<![\w.-])(?:[a-z0-9-]+\.)*(?:hyperliquid(?:-testnet)?\.xyz|zklighter\.elliot\.ai)(?![\w-])/giu;
25
- // Request paths of the venue APIs (info, exchange, WebSocket, EVM RPC,
26
- // Lighter's stream and REST API).
27
- const API_PATH = /^\/(?:ws|info|exchange|evm|stream|api)(?:\/|$)/u;
28
- const SOURCE_FILE = /\.(?:m?js|cjs|tsx?)$/u;
29
- // The venues' web hosts: the Hyperliquid app, which serves the coin icons the
30
- // terminal shows and the explorer pages it links to. Never an API endpoint,
31
- // on either network, so never blocked and never proof of the served network.
32
- export const LINK_HOSTS = new Set(['app.hyperliquid.xyz', 'app.hyperliquid-testnet.xyz']);
33
-
34
- function packageRoot(checkout, name) {
35
- try {
36
- const require = createRequire(path.join(checkout, 'package.json'));
37
- return path.dirname(require.resolve(`${name}/package.json`));
38
- } catch {
39
- const direct = path.join(checkout, 'node_modules', name);
40
- return existsSync(path.join(direct, 'package.json')) ? direct : null;
41
- }
42
- }
43
-
44
- function* sourceFiles(dir, depth = 0) {
45
- if (depth > 12) return;
46
- for (const entry of readdirSync(dir)) {
47
- if (entry === 'node_modules') continue;
48
- const file = path.join(dir, entry);
49
- const stat = statSync(file);
50
- if (stat.isDirectory()) yield* sourceFiles(file, depth + 1);
51
- else if (SOURCE_FILE.test(entry)) yield file;
52
- }
53
- }
54
-
55
- export function isTestnetHost(host) {
56
- return /(?:^|[.-])testnet(?:[.-]|$)/u.test(host);
57
- }
58
-
59
- // Venue URLs in source text, by kind: hosts used with an API path, hosts
60
- // used bare (a base URL whose paths are appended elsewhere), and hosts with
61
- // any other path (/rpc, /health, /coins/BTC.svg). Link hosts are left out,
62
- // except that a link host with an API path is reported (linkApi).
63
- export function venueUrlHosts(text) {
64
- const found = { api: new Set(), bare: new Set(), other: new Set(), linkApi: new Set() };
65
- for (const [candidate] of text.matchAll(URL_CANDIDATE)) {
66
- const url = canonicalUrl(candidate);
67
- if (!url || !VENUE_HOST.test(url.hostname)) continue;
68
- const { hostname: host, pathname } = url;
69
- // The path as a server reads it: decoded, any case.
70
- const apiPath = API_PATH.test(safeDecode(pathname).toLowerCase());
71
- if (LINK_HOSTS.has(host)) {
72
- // A link host used as an API: the exemption would hide an endpoint.
73
- if (apiPath) found.linkApi.add(`${host}${pathname}`);
74
- continue;
75
- }
76
- if (apiPath) found.api.add(host);
77
- else if (pathname === '/') found.bare.add(host);
78
- else found.other.add(host);
79
- }
80
- return found;
81
- }
82
-
83
-
84
- // Endpoint hosts in one text: with anyPath (the venue SDK) every venue host;
85
- // otherwise API URLs and base URLs.
86
- export function venueEndpointHosts(text, { anyPath = false } = {}) {
87
- const { api, bare, other } = venueUrlHosts(text);
88
- return new Set([...api, ...bare, ...(anyPath ? other : [])]);
89
- }
90
-
91
- // The venue hosts the checkout can connect to. { mainnet: [...], testnet: [...] }
92
- // - the venue SDK: every venue host, whatever the path;
93
- // - the perps controller and the app's own source: API URLs and base URLs.
94
- // Link hosts (LINK_HOSTS) are never on either list, and a link host with an
95
- // API path in any scanned source fails the launch. Every mainnet host the SDK
96
- // mentions (as a URL or not) must be on the list, or the launch fails.
97
- export function venueHosts(checkout) {
98
- const sdk = packageRoot(checkout, SDK_PACKAGE);
99
- if (!sdk) throw new Error(`${SDK_PACKAGE} is not installed in ${checkout}; the mainnet block list comes from it.`);
100
- const sources = [
101
- { root: sdk, anyPath: true },
102
- { root: packageRoot(checkout, CONTROLLER_PACKAGE), anyPath: false },
103
- { root: path.join(checkout, 'src'), anyPath: false },
104
- ].filter(({ root }) => root && existsSync(root));
105
- const hosts = new Set();
106
- const sdkMentions = new Set();
107
- for (const { root, anyPath } of sources) {
108
- for (const file of sourceFiles(root)) {
109
- const text = readFileSync(file, 'utf8');
110
- const { linkApi } = venueUrlHosts(text);
111
- if (linkApi.size > 0) throw new Error(`${file} uses the venue web host as an API (${[...linkApi].join(', ')}); it is exempt from the mainnet block, so refusing to launch.`);
112
- for (const host of venueEndpointHosts(text, { anyPath })) hosts.add(host);
113
- if (anyPath) {
114
- for (const [mention] of text.matchAll(VENUE_HOST_MENTION)) {
115
- const host = mention.toLowerCase();
116
- if (!LINK_HOSTS.has(host)) sdkMentions.add(host);
117
- }
118
- }
119
- }
120
- }
121
- const mainnet = [...hosts].filter((host) => !isTestnetHost(host)).sort();
122
- const testnet = [...hosts].filter((host) => isTestnetHost(host)).sort();
123
- if (mainnet.length === 0) throw new Error(`no mainnet venue host found in ${SDK_PACKAGE} at ${sdk}; refusing to launch without a block list.`);
124
- const missing = [...sdkMentions].filter((host) => !isTestnetHost(host) && !mainnet.includes(host));
125
- if (missing.length > 0) throw new Error(`the mainnet block list misses ${missing.join(', ')}, which ${SDK_PACKAGE} names; refusing to launch.`);
126
- return { mainnet, testnet };
127
- }
128
-
129
- export function mainnetVenueHosts(checkout) {
130
- return venueHosts(checkout).mainnet;
131
- }
132
-
133
- export function hostResolverRules(hosts) {
134
- // The host and its fully qualified form (trailing dot) alike.
135
- return `--host-resolver-rules=${hosts.flatMap((host) => [`MAP ${host} ~NOTFOUND`, `MAP ${host}. ~NOTFOUND`]).join(', ')}`;
136
- }
137
-
138
- // A URL as the network sees it: new URL() lowercases the host, drops a
139
- // default port and resolves dot segments; a trailing dot names the same host.
140
- // { hostname, pathname }, or null.
141
- export function canonicalUrl(value) {
142
- try {
143
- const url = new URL(String(value));
144
- return { hostname: url.hostname.toLowerCase().replace(/\.$/u, ''), pathname: url.pathname };
145
- } catch {
146
- return null;
147
- }
148
- }
149
-
150
- function safeDecode(value) {
151
- try {
152
- return decodeURIComponent(value);
153
- } catch {
154
- return value;
155
- }
156
- }
157
-
158
- export function hostOf(url) {
159
- return canonicalUrl(url)?.hostname ?? null;
160
- }
161
-
162
- export function isMainnetVenueUrl(url, hosts) {
163
- const host = hostOf(url);
164
- return host !== null && hosts.includes(host);
165
- }
166
-
167
- // A Hyperliquid action meant for mainnet: user-signed actions carry
168
- // hyperliquidChain "Mainnet"; L1 actions are signed as a phantom Agent whose
169
- // source is "a" on mainnet ("b" on testnet).
170
- export function mainnetTypedDataReason(data) {
171
- const message = data?.message ?? {};
172
- if (typeof message.hyperliquidChain === 'string' && message.hyperliquidChain.toLowerCase() === 'mainnet') {
173
- return 'hyperliquidChain=Mainnet';
174
- }
175
- if (data?.primaryType === 'Agent' && message.source === 'a') return 'L1 Agent source=a';
176
- return null;
177
- }
178
-
179
- // The wallet refusal for testnet runs, as the @farmslot/adapter-web/dapp page
180
- // script and wallet-request binding take it: mainnetTypedDataReason runs in
181
- // the page too, so it stays self-contained.
182
- export const TESTNET_TYPED_DATA_POLICY = Object.freeze({
183
- reason: mainnetTypedDataReason,
184
- kind: 'refused-mainnet',
185
- message: 'Refused: this test run is held to Hyperliquid testnet and the request signs a mainnet action.',
186
- });
@@ -1,35 +0,0 @@
1
- // The Web Terminal's venue policy: what the generic web-dapp launch, wallet
2
- // host and readiness need to know about Hyperliquid. They take it as an
3
- // argument (the leaf scripts import it from --policy-module, reading its
4
- // `policy` export), so none of them names the venue. It becomes the terminal
5
- // plugin's `policy` once recipe libraries can declare adapters; the Terminal's
6
- // detection and its signature-log rules (_signature-log.mjs) move with it.
7
-
8
- import { fileURLToPath } from 'node:url';
9
-
10
- import { LINK_HOSTS, TESTNET_TYPED_DATA_POLICY, venueHosts } from './lib/mainnet-guard.mjs';
11
-
12
- export const policy = Object.freeze({
13
- // network=testnet: the hosts the browser must never reach, and the hosts
14
- // whose traffic proves the app serves testnet.
15
- venueHosts(checkout) {
16
- const { mainnet, testnet } = venueHosts(checkout);
17
- return { blocked: mainnet, served: testnet };
18
- },
19
- // Venue web hosts (pages, coin icons): never blocked, never proof.
20
- linkHosts: Object.freeze([...LINK_HOSTS]),
21
- // The launch's proof that the block holds: requests to this blocked host
22
- // (preferred; else the first blocked host) must fail.
23
- probe: Object.freeze({ host: 'api.hyperliquid.xyz', httpPath: '/info', httpBody: '{"type":"meta"}', wsPath: '/ws' }),
24
- // The chain the injected wallet starts on (Arbitrum One).
25
- startChain: 42161,
26
- // Typed data a testnet run never signs.
27
- refuseTypedData: TESTNET_TYPED_DATA_POLICY,
28
- // The app's first page: the launch opens it, readiness probes it.
29
- startPath: '/order/BTC',
30
- dependencies: Object.freeze(['next', '@nktkas/hyperliquid', '@metamask/perps-controller']),
31
- // The dev-server environment variable that forces testnet (advisory check).
32
- testnetVariable: 'NEXT_PUBLIC_HYPERLIQUID_FORCE_TESTNET',
33
- // This module, for the wallet host (a separate process).
34
- module: fileURLToPath(import.meta.url),
35
- });
@@ -1,59 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { recipeRuntimePath } from "@farmslot/recipe-cli/harness";
4
- import { verifyCapture } from "../../../library/actions/shared/console-collector.mjs";
5
- import { ensureConsoleCollector } from "../console-capture.js";
6
- const TERMINAL_CONSOLE_FILES = Object.freeze({
7
- app: "app-console.log",
8
- extension: "extension-console.log",
9
- collectorLog: "console-tail.log",
10
- pidFile: "console-tail.pid"
11
- });
12
- function terminalRuntimeFile(target, name) {
13
- let root = path.resolve(target);
14
- try {
15
- root = fs.realpathSync(root);
16
- } catch {
17
- }
18
- return recipeRuntimePath(root, "terminal", name);
19
- }
20
- function terminalCaptureFiles(target) {
21
- return {
22
- extensionLog: terminalRuntimeFile(target, TERMINAL_CONSOLE_FILES.extension),
23
- pageLog: terminalRuntimeFile(target, TERMINAL_CONSOLE_FILES.app),
24
- collectorLog: terminalRuntimeFile(target, TERMINAL_CONSOLE_FILES.collectorLog),
25
- pidFile: terminalRuntimeFile(target, TERMINAL_CONSOLE_FILES.pidFile)
26
- };
27
- }
28
- function terminalCdpPort() {
29
- return process.env.RECIPE_CDP_PORT ?? process.env.CDP_PORT;
30
- }
31
- async function ensureTerminalConsoleCapture(target) {
32
- const cdpPort = terminalCdpPort();
33
- if (!cdpPort) throw new Error("Terminal console capture requires the slot CDP port.");
34
- await ensureConsoleCollector({
35
- cdpPort,
36
- ...terminalCaptureFiles(target),
37
- waitForAttachment: false,
38
- cwd: target
39
- });
40
- }
41
- async function verifyTerminalConsoleControl(capture) {
42
- let browser = null;
43
- try {
44
- const value = JSON.parse(fs.readFileSync(terminalRuntimeFile(capture.projectRoot, "browser.json"), "utf8"));
45
- browser = typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
46
- } catch {
47
- browser = null;
48
- }
49
- if (typeof browser?.appOrigin !== "string") return { ok: false, detail: "no slot browser state (browser.json) to verify the page console capture against" };
50
- return verifyCapture({ cdpPort: capture.cdpPort, log: capture.pageLog, target: "page", appOrigin: browser.appOrigin });
51
- }
52
- export {
53
- TERMINAL_CONSOLE_FILES,
54
- ensureTerminalConsoleCapture,
55
- terminalCaptureFiles,
56
- terminalCdpPort,
57
- terminalRuntimeFile,
58
- verifyTerminalConsoleControl
59
- };
@@ -1,24 +0,0 @@
1
- import { terminalAppPort, terminalCdpPort } from "./surface.js";
2
- async function terminalDoctorChecks(target) {
3
- const { terminalReadiness } = await import("../../../adapters/terminal/lib/readiness.mjs");
4
- const { resolveBrowser } = await import("../../../adapters/terminal/launch.mjs");
5
- const { policy } = await import("../../../adapters/terminal/policy.mjs");
6
- const report = await terminalReadiness({
7
- policy,
8
- target,
9
- appPort: terminalAppPort(),
10
- cdpPort: terminalCdpPort(),
11
- signer: process.env.TERMINAL_SIGNER || "extension",
12
- account: process.env.TERMINAL_ACCOUNT || "dev1",
13
- probeBrowser: resolveBrowser
14
- });
15
- return report.checks.map((check) => ({
16
- id: `terminal-${check.id}`,
17
- status: check.status === "pass" ? "pass" : "fail",
18
- required: check.required,
19
- message: check.detail
20
- }));
21
- }
22
- export {
23
- terminalDoctorChecks
24
- };
@@ -1,25 +0,0 @@
1
- import path from "node:path";
2
- import { runnerDir } from "../../paths.js";
3
- const terminalHarness = {
4
- install: { entry: "adapters/terminal/inject.mjs", fallback: "adapters/terminal/inject.mjs", node: true },
5
- cleanup: { entry: "adapters/terminal/cleanup.mjs", fallback: "adapters/terminal/cleanup.mjs", node: true },
6
- verify: () => ({
7
- command: process.execPath,
8
- prefixArgs: [
9
- path.join(runnerDir, "adapters", "terminal", "verify.mjs"),
10
- "--policy-module",
11
- path.join(runnerDir, "adapters", "terminal", "policy.mjs")
12
- ]
13
- }),
14
- restart: "mm-harness launch"
15
- };
16
- const terminalRuntimeContext = {
17
- forbiddenFields: ["metroPort", "simulator", "simulatorUdid", "adbSerial", "extensionId"],
18
- resources: ({ existing, defaults, envPort }) => ({
19
- cdpPort: envPort("RECIPE_CDP_PORT", "CDP_PORT") ?? existing.cdpPort ?? defaults.cdpPort
20
- })
21
- };
22
- export {
23
- terminalHarness,
24
- terminalRuntimeContext
25
- };
@@ -1,11 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- const terminalReadiness = {
4
- compatibilityMode: (target) => fs.existsSync(path.join(target, "src/features/perpetuals")) ? "slot browser over CDP (website; no product bridge)" : "unsupported/no bridge",
5
- liveChecks: async (target) => (await import("./doctor-checks.js")).terminalDoctorChecks(target),
6
- // Screenshots come from the slot browser over CDP.
7
- captureProviders: ["cdp"]
8
- };
9
- export {
10
- terminalReadiness
11
- };
@@ -1,135 +0,0 @@
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 { TERMINAL_CONSOLE_FILES, terminalRuntimeFile } from "./console-capture.js";
6
- import { stopTerminalBrowserSync } from "./stop.js";
7
- import { adapterRegistry } from "../registry.js";
8
- import { terminalActions } from "./actions.js";
9
- import { terminalHarness, terminalRuntimeContext } from "./overlay.js";
10
- import { terminalReadiness } from "./readiness.js";
11
- import { terminalDiagnostics, terminalObservation } from "./run.js";
12
- import { checkoutHas, checkoutHasDir } from "../checkout-files.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 terminalAppPort() {
21
- return portFromEnv("TERMINAL_APP_PORT", "RECIPE_WATCHER_PORT", "WATCHER_PORT");
22
- }
23
- function terminalCdpPort() {
24
- return portFromEnv("RECIPE_CDP_PORT", "CDP_PORT");
25
- }
26
- function terminalSignerFor(target) {
27
- try {
28
- const state = JSON.parse(fs.readFileSync(terminalRuntimeFile(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
- const terminalSurface = defineAdapter({
35
- id: "terminal",
36
- sdkVersion: 1,
37
- headless: false,
38
- resolveSlotPorts() {
39
- },
40
- async runtimeStatus(target) {
41
- const { terminalReadiness: terminalReadiness2 } = await import("../../../adapters/terminal/lib/readiness.mjs");
42
- const { policy } = await import("../../../adapters/terminal/policy.mjs");
43
- const report = await terminalReadiness2({
44
- policy,
45
- target: path.resolve(target),
46
- appPort: terminalAppPort(),
47
- cdpPort: terminalCdpPort(),
48
- signer: process.env.TERMINAL_SIGNER || "extension"
49
- });
50
- const blocking = report.checks.filter((check) => check.required && check.status === "fail");
51
- const devServer = report.checks.find((check) => check.id === "app-dev-server");
52
- return {
53
- decision: blocking.length === 0 ? "ready" : "blocked",
54
- reasonCode: blocking[0]?.id,
55
- reasons: (blocking.length ? blocking : report.checks).map((check) => `${check.id}: ${check.detail}`),
56
- 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`,
57
- deps: report.checks.find((check) => check.id === "dependencies")?.status === "pass" ? "current" : "missing",
58
- devServer: {
59
- label: "next",
60
- status: devServer?.status === "pass" ? "up" : "down"
61
- }
62
- };
63
- },
64
- runwayProvision: {
65
- async run(target, options) {
66
- return {
67
- schemaVersion: 1,
68
- command: "provision",
69
- adapter: "terminal",
70
- target: path.resolve(target),
71
- status: "fail",
72
- exitCode: 2,
73
- error: {
74
- code: "UNSUPPORTED_ADAPTER",
75
- message: "the Web Terminal is a website; there is no simulator or build to provision.",
76
- userAction: options.rerunCommand || "mm-harness launch --adapter terminal"
77
- }
78
- };
79
- }
80
- },
81
- devServer: {
82
- label: "browser",
83
- describe: () => "terminal slot browser and wallet host (the Next.js dev server belongs to the slot)",
84
- stop(target) {
85
- const result = stopTerminalBrowserSync(target);
86
- return { kind: "stopped", status: result.status, summary: result.summary, output: result.output };
87
- }
88
- },
89
- logSources(target) {
90
- return [
91
- { label: "app-console", path: terminalRuntimeFile(target, TERMINAL_CONSOLE_FILES.app) },
92
- ...terminalSignerFor(target) === "extension" ? [{ label: "extension-console", path: terminalRuntimeFile(target, TERMINAL_CONSOLE_FILES.extension) }] : [],
93
- { label: "wallet-requests", path: terminalRuntimeFile(target, "wallet-requests.jsonl") },
94
- { label: "wallet-host", path: terminalRuntimeFile(target, "wallet-host.log") },
95
- { label: "browser", path: terminalRuntimeFile(target, "browser.log") },
96
- { label: "next", path: path.join(target, "temp/farmslot/next-dev.log") }
97
- ];
98
- },
99
- // The terminal page's console and uncaught exceptions, as the Extension's
100
- // diagnostics read the extension console: errors make a run REVIEW.
101
- appLogSource(target) {
102
- return { label: "app-console", path: terminalRuntimeFile(target, TERMINAL_CONSOLE_FILES.app) };
103
- },
104
- detect: {
105
- remote: (url) => url.includes("va-mmcx-terminal"),
106
- files: (target) => checkoutHasDir(target, "src/features/perpetuals") && ["next.config.ts", "next.config.mjs", "next.config.js"].some((file) => checkoutHas(target, file))
107
- },
108
- flags: { launch: ["freshProfile", "headless", "headful"] },
109
- hints: {
110
- launch: "mm-harness launch --adapter terminal --cdp-port <port> --watcher-port <app port>",
111
- relaunch: "mm-harness launch --adapter terminal --fresh-profile",
112
- runtimeProbeRecovery: (target) => `mm-harness doctor --adapter terminal --target ${shellQuote(path.resolve(target))}`
113
- },
114
- launch: async (context) => (await import("./launch.js")).launchTerminal(context),
115
- actions: terminalActions,
116
- harness: terminalHarness,
117
- runtimeContext: terminalRuntimeContext,
118
- readiness: terminalReadiness,
119
- fixtures: { operations: ["init", "sync", "set"] },
120
- debug: {
121
- args: ({ devMenu }) => [
122
- ...process.env.WATCHER_PORT ? ["--port", process.env.WATCHER_PORT] : [],
123
- "--action",
124
- devMenu ? "dev-menu" : "debug"
125
- ]
126
- },
127
- diagnostics: terminalDiagnostics,
128
- observation: terminalObservation
129
- });
130
- adapterRegistry.register(terminalSurface);
131
- export {
132
- terminalAppPort,
133
- terminalCdpPort,
134
- terminalSurface
135
- };