@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.
- package/CHANGELOG.md +25 -0
- package/adapters/extension/console-tail.mjs +38 -6
- package/adapters/manifest.json +128 -0
- package/adapters/terminal/cleanup.mjs +68 -0
- package/adapters/terminal/inject.mjs +74 -0
- package/adapters/terminal/launch.mjs +628 -0
- package/adapters/terminal/lib/cdp-client.mjs +84 -0
- package/adapters/terminal/lib/display.mjs +115 -0
- package/adapters/terminal/lib/hud.mjs +148 -0
- package/adapters/terminal/lib/mainnet-guard.mjs +177 -0
- package/adapters/terminal/lib/origin.mjs +30 -0
- package/adapters/terminal/lib/page-script.mjs +246 -0
- package/adapters/terminal/lib/processes.mjs +187 -0
- package/adapters/terminal/lib/readiness.mjs +274 -0
- package/adapters/terminal/lib/strict-wallet.mjs +138 -0
- package/adapters/terminal/stop.mjs +37 -0
- package/adapters/terminal/verify.mjs +48 -0
- package/adapters/terminal/wallet-host.mjs +703 -0
- package/dist/adapters/console-capture.js +122 -0
- package/dist/adapters/extension/console-capture.js +22 -85
- package/dist/adapters/mobile/verify.js +8 -3
- package/dist/adapters/surface.js +3 -1
- package/dist/adapters/terminal/console-capture.js +46 -0
- package/dist/adapters/terminal/stop.js +19 -0
- package/dist/adapters/terminal/surface.js +104 -0
- package/dist/adapters.js +27 -4
- package/dist/cli.js +2 -2
- package/dist/command-contract.js +12 -4
- package/dist/commands/check.js +1 -1
- package/dist/commands/config.js +1 -1
- package/dist/commands/doctor.js +24 -1
- package/dist/commands/domain.js +1 -1
- package/dist/commands/farmslot-ready.js +2 -0
- package/dist/commands/help.js +2 -2
- package/dist/commands/launch/index.js +97 -2
- package/dist/commands/manifest.js +3 -3
- package/dist/commands/recipe-advice.js +1 -1
- package/dist/commands/review.js +1 -1
- package/dist/commands/run-engine.js +1 -1
- package/dist/commands/shared.js +1 -1
- package/dist/doctor.js +3 -0
- package/dist/harness.js +17 -11
- package/dist/manifest.js +5 -0
- package/dist/mm-harness-cli.js +34 -20
- package/dist/paths.js +2 -2
- package/dist/recipe-security.js +12 -3
- package/dist/review/knowledge.js +1 -1
- package/dist/run-diagnostics.js +150 -33
- package/dist/runner.js +1 -1
- package/dist/runtime-context.js +3 -3
- package/docs/CONTRIBUTING.md +1 -1
- package/docs/cli-contract.md +163 -0
- package/library/actions/shared/console-allowlist-add.mjs +137 -0
- package/library/actions/shared/console-collector.mjs +230 -0
- package/library/actions/shared/console-findings.mjs +511 -0
- package/library/actions/terminal/app/assert_no_console_errors.mjs +119 -0
- package/library/actions/terminal/app/launch.mjs +45 -0
- package/library/actions/terminal/perps/_venue.mjs +242 -0
- package/library/actions/terminal/perps/assert_orders.mjs +54 -0
- package/library/actions/terminal/perps/assert_positions.mjs +40 -0
- package/library/actions/terminal/perps/read_orders.mjs +26 -0
- package/library/actions/terminal/perps/read_positions.mjs +25 -0
- package/library/actions/terminal/perps/read_snapshot.mjs +40 -0
- package/library/actions/terminal/perps/revoke_agent.mjs +58 -0
- package/library/actions/terminal/perps/teardown_state.mjs +104 -0
- package/library/actions/terminal/platform/hud.mjs +71 -0
- package/library/actions/terminal/platform/page.mjs +247 -0
- package/library/actions/terminal/platform/runtime.mjs +200 -0
- package/library/actions/terminal/ui/navigate.mjs +31 -0
- package/library/actions/terminal/wallet/_signature-log.mjs +177 -0
- package/library/actions/terminal/wallet/assert_signatures.mjs +43 -0
- package/library/actions/terminal/wallet/list_accounts.mjs +27 -0
- package/library/actions/terminal/wallet/read_signatures.mjs +37 -0
- package/library/manifests/terminal.action-manifest.json +1786 -0
- 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 {
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
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 =
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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":
|
|
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,
|
|
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,
|
package/dist/adapters/surface.js
CHANGED
|
@@ -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 {
|
package/dist/command-contract.js
CHANGED
|
@@ -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
|
},
|
package/dist/commands/check.js
CHANGED
|
@@ -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;
|
package/dist/commands/config.js
CHANGED
|
@@ -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(".");
|
package/dist/commands/doctor.js
CHANGED
|
@@ -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 = [
|
|
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 {
|
package/dist/commands/domain.js
CHANGED
|
@@ -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 });
|