@deeeed/metamask-harness 0.27.0 → 0.29.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 +26 -0
- package/README.md +41 -0
- package/adapters/extension/build-lavamoat.sh +2 -1
- package/adapters/extension/ensure-browser.sh +82 -9
- package/adapters/extension/inject.mjs +1 -0
- package/adapters/extension/launch-browser.cjs +83 -1
- package/adapters/extension/lib/chrome-args.cjs +325 -1
- package/adapters/extension/lib/playwright-cdp.cjs +34 -0
- package/adapters/extension/lib/slot-title.cjs +2 -4
- package/adapters/extension/lib/validation-launch-supervisor.cjs +292 -0
- package/adapters/extension/lib/validation-process-ownership.cjs +69 -0
- package/adapters/extension/reattach.sh +2 -1
- package/adapters/extension/sidepanel-toggle.sh +14 -96
- package/adapters/extension/wallet-fixture-state.cjs +8 -31
- package/adapters/manifest.json +16 -0
- package/adapters/shared/private-atomic-write.cjs +47 -0
- package/adapters/shared/setup-base.sh +864 -0
- package/dist/adapters/extension/runtime.js +367 -24
- package/dist/adapters/extension/validation-process-ownership.js +10 -0
- package/dist/adapters.js +0 -11
- package/dist/cli-commands.js +2 -1
- package/dist/command-contract.js +13 -0
- package/dist/commands/call.js +38 -1
- package/dist/commands/launch/extension.js +130 -19
- package/dist/commands/manifest.js +201 -6
- package/dist/commands/parse-args.js +1 -0
- package/dist/commands/run.js +74 -8
- package/dist/commands/setup-base.js +24 -0
- package/dist/mm-harness-cli.js +30 -2
- package/dist/recipe-security.js +1 -0
- package/library/actions/extension/analytics/consent.mjs +203 -0
- package/library/actions/extension/analytics/set_consent.mjs +19 -143
- package/library/actions/extension/perps/perps.mjs +2 -16
- package/library/actions/extension/perps/state.mjs +20 -0
- package/library/actions/extension/ui/locators.mjs +13 -0
- package/library/actions/extension/wallet/list_accounts.mjs +3 -25
- package/library/actions/extension/wallet/read_state.mjs +3 -23
- package/library/actions/extension/wallet/select_account.mjs +6 -33
- package/library/actions/extension/wallet/setup.mjs +2 -20
- package/library/actions/extension/wallet/state.mjs +111 -0
- package/library/actions/mobile/platform/observe-ui.mjs +21 -0
- package/library/actions/mobile/ui/locators.mjs +17 -0
- package/library/actions/shared/ui/locators.mjs +160 -0
- package/library/manifests/extension.action-manifest.json +18 -0
- package/library/manifests/mobile.action-manifest.json +18 -0
- package/library/recipes/runner/action-validation.extension.recipe.json +1 -1
- package/library/recipes/runner/action-validation.mobile.recipe.json +1 -1
- package/package.json +7 -4
- package/scripts/site-contrast.mjs +538 -0
- package/site/architecture.html +415 -0
- package/site/assets/progress.mjs +272 -0
- package/site/assets/style.css +808 -0
- package/site/cheatsheet.html +305 -0
- package/site/index.html +643 -0
- package/site/recipes.html +396 -0
- package/site/reviewers.html +374 -0
- package/site/tutorials/index.html +180 -0
- package/site/tutorials/v1.html +211 -0
- package/site/tutorials/v2.html +207 -0
- package/site/tutorials/v3.html +214 -0
- package/site/tutorials/v4.html +195 -0
- package/site/tutorials/v5.html +163 -0
- package/site/tutorials/v6.html +165 -0
- package/site/tutorials/v7.html +184 -0
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import http from "node:http";
|
|
3
3
|
import fs from "node:fs";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
import {
|
|
6
7
|
depsCheck,
|
|
7
8
|
recordDepsBaseline
|
|
8
9
|
} from "@farmslot/recipe-harness/runtime/deps-readiness";
|
|
9
10
|
import { colorHumanMessage } from "../../cli-color.js";
|
|
10
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
importRecipeHarnessRuntimeCdp,
|
|
13
|
+
recipeHarnessPath,
|
|
14
|
+
recipeRuntimeDir,
|
|
15
|
+
runnerDir
|
|
16
|
+
} from "../../paths.js";
|
|
11
17
|
import { extensionIdFromKey } from "../../adapters/extension/extension-id.js";
|
|
12
18
|
import { extensionProductConfigBlock } from "../../adapters/extension/product-config.js";
|
|
13
19
|
import { isExtensionDistStale } from "../../adapters/extension/runtime-decision.js";
|
|
@@ -15,6 +21,10 @@ import { checkExtensionRuntimeHealth } from "../../adapters/extension/runtime.js
|
|
|
15
21
|
import { ensureHarnessFresh } from "../../adapters/harness-freshness.js";
|
|
16
22
|
import { stopExtensionWatcher } from "../../adapters/slot-ports.js";
|
|
17
23
|
import { EXIT, spawnScriptStreaming } from "../shared.js";
|
|
24
|
+
const { CdpSession } = await importRecipeHarnessRuntimeCdp();
|
|
25
|
+
const { RUNTIME_IDENTITY_FILENAME, RUNTIME_NONCE_PREFIX } = createRequire(import.meta.url)(
|
|
26
|
+
path.join(runnerDir, "adapters/extension/lib/chrome-args.cjs")
|
|
27
|
+
);
|
|
18
28
|
function extensionDepsBlock(target) {
|
|
19
29
|
if (!fs.existsSync(path.join(target, "package.json"))) return null;
|
|
20
30
|
const deps = depsCheck(target);
|
|
@@ -73,7 +83,8 @@ async function extensionRuntimeReusable(target) {
|
|
|
73
83
|
if (isExtensionDistStale(target)) return false;
|
|
74
84
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
75
85
|
const reachable = await cdpVersionReachable(cdpPort);
|
|
76
|
-
const
|
|
86
|
+
const nonceOwned = reachable && await cdpOwnedByRuntimeNonce(cdpPort, target);
|
|
87
|
+
const owned = nonceOwned || reachable && cdpOwnedByExpectedRuntime(cdpPort, target);
|
|
77
88
|
const targetPresent = owned && await cdpHasExpectedExtensionTarget(cdpPort, target);
|
|
78
89
|
const healthy = targetPresent && await cdpRuntimeHealthy(cdpPort, target);
|
|
79
90
|
if (healthy) return true;
|
|
@@ -88,17 +99,20 @@ function expectedChromeProfile(target) {
|
|
|
88
99
|
function expectedRuntimeDist(target) {
|
|
89
100
|
return path.resolve(path.join(target, recipeRuntimeDir(), process.env.RECIPE_RUNTIME_DIST_DIR || "runtime-dist"));
|
|
90
101
|
}
|
|
91
|
-
function
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
return false;
|
|
102
|
+
function commandHasExactFlagValue(command, flag, value) {
|
|
103
|
+
const expected = `${flag}=${value}`;
|
|
104
|
+
let offset = command.indexOf(expected);
|
|
105
|
+
while (offset !== -1) {
|
|
106
|
+
const before = offset === 0 ? "" : command[offset - 1];
|
|
107
|
+
const afterOffset = offset + expected.length;
|
|
108
|
+
const after = afterOffset === command.length ? "" : command[afterOffset];
|
|
109
|
+
if ((!before || /\s/u.test(before)) && (!after || /\s/u.test(after))) return true;
|
|
110
|
+
offset = command.indexOf(expected, offset + 1);
|
|
101
111
|
}
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
function cdpOwnedByExpectedRuntime(port, target) {
|
|
115
|
+
const pids = cdpListenerPids(port);
|
|
102
116
|
const profile = expectedChromeProfile(target);
|
|
103
117
|
const runtimeDist = expectedRuntimeDist(target);
|
|
104
118
|
for (const pid of pids) {
|
|
@@ -107,7 +121,7 @@ function cdpOwnedByExpectedRuntime(port, target) {
|
|
|
107
121
|
encoding: "utf8",
|
|
108
122
|
stdio: ["ignore", "pipe", "ignore"]
|
|
109
123
|
});
|
|
110
|
-
if (command
|
|
124
|
+
if (commandHasExactFlagValue(command, "--user-data-dir", profile) && (commandHasExactFlagValue(command, "--load-extension", runtimeDist) || commandHasExactFlagValue(command, "--disable-extensions-except", runtimeDist))) {
|
|
111
125
|
return true;
|
|
112
126
|
}
|
|
113
127
|
} catch {
|
|
@@ -115,21 +129,118 @@ function cdpOwnedByExpectedRuntime(port, target) {
|
|
|
115
129
|
}
|
|
116
130
|
return false;
|
|
117
131
|
}
|
|
118
|
-
function
|
|
132
|
+
async function cdpOwnedByRuntimeNonce(port, target) {
|
|
133
|
+
const identity = readRuntimeIdentity(port, target);
|
|
134
|
+
if (!identity) return false;
|
|
135
|
+
const version = await cdpVersion(port);
|
|
136
|
+
const webSocketDebuggerUrl = version?.webSocketDebuggerUrl;
|
|
137
|
+
if (typeof webSocketDebuggerUrl !== "string" || !webSocketDebuggerUrl) return false;
|
|
138
|
+
let session;
|
|
139
|
+
try {
|
|
140
|
+
session = await CdpSession.connect(webSocketDebuggerUrl, { timeoutMs: 2e3 });
|
|
141
|
+
const result = await boundedBrowserCdpCall(
|
|
142
|
+
session,
|
|
143
|
+
"Browser.getBrowserCommandLine",
|
|
144
|
+
{},
|
|
145
|
+
2e3
|
|
146
|
+
);
|
|
147
|
+
if (!Array.isArray(result.arguments)) return false;
|
|
148
|
+
const nonceArgs = result.arguments.filter(
|
|
149
|
+
(argument) => typeof argument === "string" && argument.startsWith(RUNTIME_NONCE_PREFIX)
|
|
150
|
+
);
|
|
151
|
+
return nonceArgs.length === 1 && nonceArgs[0] === `${RUNTIME_NONCE_PREFIX}${identity.nonce}`;
|
|
152
|
+
} catch {
|
|
153
|
+
return false;
|
|
154
|
+
} finally {
|
|
155
|
+
session?.close();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
async function boundedBrowserCdpCall(session, method, params, timeoutMs) {
|
|
159
|
+
let timer;
|
|
160
|
+
try {
|
|
161
|
+
return await Promise.race([
|
|
162
|
+
session.call(method, params),
|
|
163
|
+
new Promise((_resolve, reject) => {
|
|
164
|
+
timer = setTimeout(
|
|
165
|
+
() => reject(new Error(`${method} timed out after ${timeoutMs}ms.`)),
|
|
166
|
+
timeoutMs
|
|
167
|
+
);
|
|
168
|
+
})
|
|
169
|
+
]);
|
|
170
|
+
} finally {
|
|
171
|
+
if (timer) clearTimeout(timer);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function readRuntimeIdentity(port, target) {
|
|
175
|
+
const expectedPort = Number(port);
|
|
176
|
+
if (!Number.isInteger(expectedPort) || expectedPort <= 0 || expectedPort > 65535) return null;
|
|
177
|
+
const identityPath = path.join(target, recipeRuntimeDir(), RUNTIME_IDENTITY_FILENAME);
|
|
178
|
+
let descriptor;
|
|
179
|
+
try {
|
|
180
|
+
descriptor = fs.openSync(
|
|
181
|
+
identityPath,
|
|
182
|
+
fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW
|
|
183
|
+
);
|
|
184
|
+
const stat = fs.fstatSync(descriptor);
|
|
185
|
+
if (!stat.isFile() || stat.size <= 0 || stat.size > 4096 || (stat.mode & 63) !== 0) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
const parsed = JSON.parse(fs.readFileSync(descriptor, "utf8"));
|
|
189
|
+
if (parsed.port !== expectedPort || !Number.isInteger(parsed.pid) || Number(parsed.pid) <= 0 || !Number.isInteger(parsed.startedAt) || Number(parsed.startedAt) <= 0 || Number(parsed.startedAt) > Date.now() + 6e4 || typeof parsed.nonce !== "string" || !/^[a-f0-9]{64}$/u.test(parsed.nonce)) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
if (!cdpListenerPids(port).includes(String(parsed.pid))) return null;
|
|
193
|
+
return parsed;
|
|
194
|
+
} catch {
|
|
195
|
+
return null;
|
|
196
|
+
} finally {
|
|
197
|
+
if (descriptor !== void 0) fs.closeSync(descriptor);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function cdpListenerPids(port) {
|
|
201
|
+
try {
|
|
202
|
+
const out = execFileSync("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN", "-t"], {
|
|
203
|
+
encoding: "utf8",
|
|
204
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
205
|
+
});
|
|
206
|
+
return out.split(/\s+/u).filter((value) => /^\d+$/u.test(value));
|
|
207
|
+
} catch {
|
|
208
|
+
return [];
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
async function cdpVersionReachable(port) {
|
|
212
|
+
return Boolean(await cdpVersion(port));
|
|
213
|
+
}
|
|
214
|
+
function cdpVersion(port) {
|
|
119
215
|
return new Promise((resolve) => {
|
|
120
216
|
const request = http.get(
|
|
121
217
|
{ host: "127.0.0.1", port: Number(port), path: "/json/version", timeout: 2e3 },
|
|
122
218
|
(response) => {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
219
|
+
let body = "";
|
|
220
|
+
response.setEncoding("utf8");
|
|
221
|
+
response.on("data", (chunk) => {
|
|
222
|
+
body += chunk;
|
|
223
|
+
});
|
|
224
|
+
response.on("end", () => {
|
|
225
|
+
const code = response.statusCode ?? 0;
|
|
226
|
+
if (code < 200 || code >= 300) {
|
|
227
|
+
resolve(null);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
try {
|
|
231
|
+
const parsed = JSON.parse(body);
|
|
232
|
+
resolve(parsed && typeof parsed === "object" ? parsed : null);
|
|
233
|
+
} catch {
|
|
234
|
+
resolve(null);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
126
237
|
}
|
|
127
238
|
);
|
|
128
239
|
request.on("timeout", () => {
|
|
129
240
|
request.destroy();
|
|
130
|
-
resolve(
|
|
241
|
+
resolve(null);
|
|
131
242
|
});
|
|
132
|
-
request.on("error", () => resolve(
|
|
243
|
+
request.on("error", () => resolve(null));
|
|
133
244
|
});
|
|
134
245
|
}
|
|
135
246
|
function expectedExtensionId(target) {
|
|
@@ -18,22 +18,33 @@ import {
|
|
|
18
18
|
import { metaMaskActionExecutionCapabilities } from "../recipe-security.js";
|
|
19
19
|
import { closest } from "../command-contract.js";
|
|
20
20
|
const OFFICIAL_ACTIONS = new Set(OFFICIAL_RECIPE_ACTIONS);
|
|
21
|
+
const ACTION_MATRIX_ADAPTERS = ["mobile", "extension", "core"];
|
|
22
|
+
function actionLibraryContextArgs(librarySources) {
|
|
23
|
+
return (librarySources ?? []).filter((source) => source.name !== "metamask").map((source) => {
|
|
24
|
+
const entry = source.name ? `${source.name}=${source.root}` : source.root;
|
|
25
|
+
return ` --library ${shellQuoteArg(entry)}`;
|
|
26
|
+
}).join("");
|
|
27
|
+
}
|
|
21
28
|
async function handleActions({ options, positional }) {
|
|
22
|
-
const { adapter, target } = resolveAdapter(options);
|
|
23
29
|
const librarySources = await resolveMetaMaskLibrarySources(optionStrings(options, "library"));
|
|
30
|
+
const json = optionFlag(options, "json");
|
|
31
|
+
const action = optionString(options, "action");
|
|
32
|
+
const query = positional[0]?.trim();
|
|
33
|
+
const category = optionString(options, "category")?.toLowerCase();
|
|
34
|
+
if (optionFlag(options, "matrix")) {
|
|
35
|
+
return handleActionMatrix(options, { json, action, query, category, librarySources });
|
|
36
|
+
}
|
|
37
|
+
const { adapter, target } = resolveAdapter(options);
|
|
38
|
+
const actionManifestOverride = optionString(options, "actionManifest");
|
|
24
39
|
const { manifest, actionSources } = await resolveActionManifest(
|
|
25
40
|
adapter,
|
|
26
|
-
|
|
41
|
+
actionManifestOverride,
|
|
27
42
|
librarySources
|
|
28
43
|
);
|
|
29
44
|
if (optionFlag(options, "raw")) {
|
|
30
45
|
console.log(JSON.stringify(manifest, null, 2));
|
|
31
46
|
return EXIT.ok;
|
|
32
47
|
}
|
|
33
|
-
const json = optionFlag(options, "json");
|
|
34
|
-
const action = optionString(options, "action");
|
|
35
|
-
const query = positional[0]?.trim();
|
|
36
|
-
const category = optionString(options, "category")?.toLowerCase();
|
|
37
48
|
const categoriesOnly = optionFlag(options, "categories");
|
|
38
49
|
const all = describeManifestActions(manifest, actionSources);
|
|
39
50
|
const categories = summarizeActionCategories(all);
|
|
@@ -67,6 +78,36 @@ async function handleActions({ options, positional }) {
|
|
|
67
78
|
}
|
|
68
79
|
const actions = action ? fuzzyResolveActions(categoryActions, action) : query ? searchActions(categoryActions, query) : categoryActions;
|
|
69
80
|
if (action && actions.length === 0) {
|
|
81
|
+
const refusal = actionManifestOverride ? void 0 : resolveActionCapabilityRefusal(
|
|
82
|
+
action,
|
|
83
|
+
adapter,
|
|
84
|
+
await resolveActionCapabilityMatrix(librarySources)
|
|
85
|
+
);
|
|
86
|
+
if (refusal) {
|
|
87
|
+
const satisfying = refusal.satisfyingAdapters.join(", ");
|
|
88
|
+
const message2 = `missing action capability "${refusal.capability}" for the ${adapter} adapter.`;
|
|
89
|
+
const userAction2 = `Satisfying adapters for "${refusal.capability}": ${satisfying}. Inspect: mm-harness actions --matrix --action ${shellQuoteArg(refusal.capability)}${actionLibraryContextArgs(librarySources)} --json`;
|
|
90
|
+
if (json) {
|
|
91
|
+
console.log(JSON.stringify({
|
|
92
|
+
schemaVersion: 1,
|
|
93
|
+
command: "actions",
|
|
94
|
+
adapter,
|
|
95
|
+
action,
|
|
96
|
+
category,
|
|
97
|
+
error: {
|
|
98
|
+
code: "ACTION_CAPABILITY_UNAVAILABLE",
|
|
99
|
+
message: message2,
|
|
100
|
+
capability: refusal.capability,
|
|
101
|
+
satisfyingAdapters: refusal.satisfyingAdapters,
|
|
102
|
+
userAction: userAction2
|
|
103
|
+
}
|
|
104
|
+
}, null, 2));
|
|
105
|
+
} else {
|
|
106
|
+
console.error(`\u2717 mm-harness actions: ${message2}
|
|
107
|
+
Next: ${userAction2}`);
|
|
108
|
+
}
|
|
109
|
+
return EXIT.usage;
|
|
110
|
+
}
|
|
70
111
|
const message = `no action matches "${action}" for the ${adapter} adapter.`;
|
|
71
112
|
const userAction = `mm-harness actions --adapter ${adapter}`;
|
|
72
113
|
if (json) {
|
|
@@ -126,6 +167,154 @@ ${color("label", "Related:", { stream: process.stdout })} ${relatedActions.join(
|
|
|
126
167
|
}
|
|
127
168
|
return 0;
|
|
128
169
|
}
|
|
170
|
+
async function resolveActionCapabilityMatrix(librarySources) {
|
|
171
|
+
const catalogs = await Promise.all(
|
|
172
|
+
ACTION_MATRIX_ADAPTERS.map(async (adapter) => {
|
|
173
|
+
const { manifest, actionSources } = await resolveActionManifest(
|
|
174
|
+
adapter,
|
|
175
|
+
void 0,
|
|
176
|
+
librarySources
|
|
177
|
+
);
|
|
178
|
+
return { adapter, actions: describeManifestActions(manifest, actionSources) };
|
|
179
|
+
})
|
|
180
|
+
);
|
|
181
|
+
const actions = /* @__PURE__ */ new Map();
|
|
182
|
+
for (const catalog of catalogs) {
|
|
183
|
+
for (const action of catalog.actions) {
|
|
184
|
+
const entry = actions.get(action.name) ?? { entries: [], adapters: /* @__PURE__ */ new Set() };
|
|
185
|
+
entry.entries.push(action);
|
|
186
|
+
entry.adapters.add(catalog.adapter);
|
|
187
|
+
actions.set(action.name, entry);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return [...actions.entries()].map(([name, entry]) => {
|
|
191
|
+
const representative = entry.entries[0];
|
|
192
|
+
const satisfyingAdapters = ACTION_MATRIX_ADAPTERS.filter(
|
|
193
|
+
(adapter) => entry.adapters.has(adapter)
|
|
194
|
+
);
|
|
195
|
+
return {
|
|
196
|
+
name,
|
|
197
|
+
category: representative.category,
|
|
198
|
+
description: entry.entries.map((action) => action.description).find(Boolean) ?? "",
|
|
199
|
+
fields: [...new Set(entry.entries.flatMap((action) => action.fields))].sort(),
|
|
200
|
+
support: Object.fromEntries(
|
|
201
|
+
ACTION_MATRIX_ADAPTERS.map((adapter) => [
|
|
202
|
+
adapter,
|
|
203
|
+
entry.adapters.has(adapter) ? "available" : "unavailable"
|
|
204
|
+
])
|
|
205
|
+
),
|
|
206
|
+
satisfyingAdapters
|
|
207
|
+
};
|
|
208
|
+
}).sort((left, right) => left.name.localeCompare(right.name));
|
|
209
|
+
}
|
|
210
|
+
function resolveActionCapabilityRefusal(action, adapter, matrix) {
|
|
211
|
+
const matches = fuzzyResolveActions(matrix, action);
|
|
212
|
+
if (matches.length !== 1 || matches[0].satisfyingAdapters.includes(adapter)) {
|
|
213
|
+
return void 0;
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
capability: matches[0].name,
|
|
217
|
+
satisfyingAdapters: matches[0].satisfyingAdapters
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
function missingActionCapabilities(adapter, actionNames, matrix) {
|
|
221
|
+
return [...new Set(actionNames)].flatMap((action) => {
|
|
222
|
+
const row = matrix.find((entry) => entry.name === action);
|
|
223
|
+
if (!row || row.satisfyingAdapters.includes(adapter)) return [];
|
|
224
|
+
return [{ capability: row.name, satisfyingAdapters: row.satisfyingAdapters }];
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
function handleActionMatrixError(json, code, message, userAction, details = {}) {
|
|
228
|
+
if (json) {
|
|
229
|
+
console.log(JSON.stringify({
|
|
230
|
+
schemaVersion: 1,
|
|
231
|
+
command: "actions",
|
|
232
|
+
view: "matrix",
|
|
233
|
+
...details,
|
|
234
|
+
error: { code, message, userAction }
|
|
235
|
+
}, null, 2));
|
|
236
|
+
} else {
|
|
237
|
+
console.error(`\u2717 mm-harness actions: ${message}
|
|
238
|
+
Next: ${userAction}`);
|
|
239
|
+
}
|
|
240
|
+
return EXIT.usage;
|
|
241
|
+
}
|
|
242
|
+
async function handleActionMatrix(options, input) {
|
|
243
|
+
const inspectCommand = `mm-harness actions --matrix${actionLibraryContextArgs(input.librarySources)} --json`;
|
|
244
|
+
const conflicts = [
|
|
245
|
+
optionFlag(options, "raw") ? "--raw" : void 0,
|
|
246
|
+
optionFlag(options, "categories") ? "--categories" : void 0,
|
|
247
|
+
optionString(options, "adapter") ? "--adapter" : void 0,
|
|
248
|
+
optionString(options, "platform") ? "--platform" : void 0,
|
|
249
|
+
optionString(options, "actionManifest") ? "--action-manifest" : void 0
|
|
250
|
+
].filter((value) => Boolean(value));
|
|
251
|
+
if (conflicts.length > 0) {
|
|
252
|
+
return handleActionMatrixError(
|
|
253
|
+
input.json,
|
|
254
|
+
"ACTION_MATRIX_CONFLICT",
|
|
255
|
+
`--matrix cannot be combined with ${conflicts.join(", ")}.`,
|
|
256
|
+
inspectCommand
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
const matrix = await resolveActionCapabilityMatrix(input.librarySources);
|
|
260
|
+
const categories = summarizeActionCategories(matrix);
|
|
261
|
+
const categoryActions = input.category ? matrix.filter((entry) => entry.category === input.category) : matrix;
|
|
262
|
+
if (input.category && categoryActions.length === 0) {
|
|
263
|
+
return handleActionMatrixError(
|
|
264
|
+
input.json,
|
|
265
|
+
"ACTION_CATEGORY_UNKNOWN",
|
|
266
|
+
`no action category matches "${input.category}" across the adapter matrix.`,
|
|
267
|
+
inspectCommand,
|
|
268
|
+
{ category: input.category, availableCategories: categories }
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
const actions = input.action ? fuzzyResolveActions(categoryActions, input.action) : input.query ? searchActions(categoryActions, input.query) : categoryActions;
|
|
272
|
+
if (input.action && actions.length === 0) {
|
|
273
|
+
return handleActionMatrixError(
|
|
274
|
+
input.json,
|
|
275
|
+
"ACTION_UNKNOWN",
|
|
276
|
+
`no action capability matches "${input.action}" across Mobile, Extension, or Core.`,
|
|
277
|
+
inspectCommand,
|
|
278
|
+
{ action: input.action, category: input.category }
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
if (input.query && actions.length === 0) {
|
|
282
|
+
return handleActionMatrixError(
|
|
283
|
+
input.json,
|
|
284
|
+
"ACTION_SEARCH_EMPTY",
|
|
285
|
+
`no action capability matches search "${input.query}" across Mobile, Extension, or Core.`,
|
|
286
|
+
inspectCommand,
|
|
287
|
+
{ query: input.query, category: input.category }
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
if (input.json) {
|
|
291
|
+
console.log(JSON.stringify({
|
|
292
|
+
schemaVersion: 1,
|
|
293
|
+
command: "actions",
|
|
294
|
+
view: "matrix",
|
|
295
|
+
adapters: ACTION_MATRIX_ADAPTERS,
|
|
296
|
+
...input.query ? { query: input.query } : {},
|
|
297
|
+
...input.category ? { category: input.category } : {},
|
|
298
|
+
actions
|
|
299
|
+
}, null, 2));
|
|
300
|
+
} else {
|
|
301
|
+
console.log(renderHumanActionMatrix(actions));
|
|
302
|
+
}
|
|
303
|
+
return EXIT.ok;
|
|
304
|
+
}
|
|
305
|
+
function renderHumanActionMatrix(actions) {
|
|
306
|
+
const out = (style, text) => color(style, text, { stream: process.stdout });
|
|
307
|
+
const width = Math.max("action capability".length, ...actions.map((action) => action.name.length));
|
|
308
|
+
const cell = (status, width2) => status === "available" ? out("accent", "yes".padEnd(width2)) : out("dim", "\u2014".padEnd(width2));
|
|
309
|
+
return [
|
|
310
|
+
out("bold", "action capability matrix"),
|
|
311
|
+
out("comment", "Inspect one: mm-harness actions --matrix --action <name> --json"),
|
|
312
|
+
`${"action capability".padEnd(width)} mobile extension core`,
|
|
313
|
+
...actions.map(
|
|
314
|
+
(action) => `${action.name.padEnd(width)} ${cell(action.support.mobile, 6)} ${cell(action.support.extension, 9)} ${cell(action.support.core, 4)}`
|
|
315
|
+
)
|
|
316
|
+
].join("\n");
|
|
317
|
+
}
|
|
129
318
|
function fuzzyResolveActions(entries, query) {
|
|
130
319
|
const exactFull = entries.filter((e) => e.name === query);
|
|
131
320
|
if (exactFull.length > 0) return exactFull;
|
|
@@ -393,13 +582,19 @@ function levenshtein(left, right) {
|
|
|
393
582
|
return prior[right.length] ?? right.length;
|
|
394
583
|
}
|
|
395
584
|
export {
|
|
585
|
+
ACTION_MATRIX_ADAPTERS,
|
|
396
586
|
actionExampleCommand,
|
|
587
|
+
actionLibraryContextArgs,
|
|
397
588
|
describeManifestActions,
|
|
398
589
|
findRelatedActions,
|
|
399
590
|
fuzzyResolveActions,
|
|
400
591
|
handleActions,
|
|
592
|
+
missingActionCapabilities,
|
|
401
593
|
renderHumanActionCatalog,
|
|
402
594
|
renderHumanActionExample,
|
|
595
|
+
renderHumanActionMatrix,
|
|
596
|
+
resolveActionCapabilityMatrix,
|
|
597
|
+
resolveActionCapabilityRefusal,
|
|
403
598
|
searchActions,
|
|
404
599
|
summarizeActionCategories,
|
|
405
600
|
summarizeDescribedAction
|
package/dist/commands/run.js
CHANGED
|
@@ -35,6 +35,30 @@ import { JsonStreamWriter } from "../json-stream.js";
|
|
|
35
35
|
import { formatRunDiagnosticsForHuman, readRunDiagnosticsDocument } from "../run-diagnostics.js";
|
|
36
36
|
import { recipeTrustFailure } from "../recipe-security.js";
|
|
37
37
|
import { recordCommandEvidence, redactStructuredValue } from "../command-journal.js";
|
|
38
|
+
import {
|
|
39
|
+
actionLibraryContextArgs,
|
|
40
|
+
missingActionCapabilities,
|
|
41
|
+
resolveActionCapabilityMatrix
|
|
42
|
+
} from "./manifest.js";
|
|
43
|
+
async function validationCapabilityRefusals(adapter, findings, librarySources) {
|
|
44
|
+
const actionNames = findings.flatMap((finding) => {
|
|
45
|
+
if (finding.code !== "recipe.action_not_declared_by_manifest") return [];
|
|
46
|
+
const match = /^Recipe action ([^ ]+) is not declared by the runner action manifest[.]?$/u.exec(finding.message);
|
|
47
|
+
return match?.[1] ? [match[1]] : [];
|
|
48
|
+
});
|
|
49
|
+
if (actionNames.length === 0) return [];
|
|
50
|
+
return missingActionCapabilities(
|
|
51
|
+
adapter,
|
|
52
|
+
actionNames,
|
|
53
|
+
await resolveActionCapabilityMatrix(librarySources)
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
function capabilityValidationUserAction(adapter, refusals, libraryContextArgs) {
|
|
57
|
+
const capabilities = refusals.map(
|
|
58
|
+
(refusal) => `"${refusal.capability}" [${refusal.satisfyingAdapters.join(", ")}]`
|
|
59
|
+
).join("; ");
|
|
60
|
+
return `Missing action capabilities for ${adapter}, with satisfying adapters: ${capabilities}. Inspect: mm-harness actions --matrix${libraryContextArgs} --json; then rerun from a checkout matching a satisfying adapter, splitting the recipe if no single adapter satisfies every capability`;
|
|
61
|
+
}
|
|
38
62
|
async function handleRun(parsed) {
|
|
39
63
|
const stream = new JsonStreamWriter("run", optionFlag(parsed.options, "jsonStream"));
|
|
40
64
|
const restoreStdout = stream.isolateStdout();
|
|
@@ -165,7 +189,21 @@ async function handleRunInner({ positional, options }, stream) {
|
|
|
165
189
|
);
|
|
166
190
|
}
|
|
167
191
|
if (validated.errorCount > 0) {
|
|
168
|
-
|
|
192
|
+
const capabilityRefusals = optionString(options, "actionManifest") ? [] : await validationCapabilityRefusals(
|
|
193
|
+
adapter,
|
|
194
|
+
validated.findings,
|
|
195
|
+
validated.librarySources
|
|
196
|
+
);
|
|
197
|
+
return emitRunValidationError(
|
|
198
|
+
jsonOutput,
|
|
199
|
+
stream,
|
|
200
|
+
adapter,
|
|
201
|
+
validated.recipeFile,
|
|
202
|
+
validated.findings,
|
|
203
|
+
validated.errorCount,
|
|
204
|
+
capabilityRefusals,
|
|
205
|
+
actionLibraryContextArgs(validated.librarySources)
|
|
206
|
+
);
|
|
169
207
|
}
|
|
170
208
|
const depsBlock = adapter === "core" && await recipeUsesCoreController(validated.recipe, validated.librarySources) ? coreDependencyBlock(target) : null;
|
|
171
209
|
if (depsBlock) {
|
|
@@ -447,8 +485,23 @@ async function handleRunPlan(recipeArg, params, options, stream) {
|
|
|
447
485
|
userAction
|
|
448
486
|
);
|
|
449
487
|
}
|
|
450
|
-
const {
|
|
488
|
+
const {
|
|
489
|
+
recipe,
|
|
490
|
+
recipeFile,
|
|
491
|
+
findings,
|
|
492
|
+
errorCount,
|
|
493
|
+
manifestOk,
|
|
494
|
+
schemaValid,
|
|
495
|
+
effectiveParams,
|
|
496
|
+
librarySources
|
|
497
|
+
} = validated;
|
|
451
498
|
const status = errorCount === 0 ? "pass" : "fail";
|
|
499
|
+
const capabilityRefusals = status === "fail" && !optionString(options, "actionManifest") ? await validationCapabilityRefusals(adapter, findings, librarySources) : [];
|
|
500
|
+
const failureUserAction = capabilityRefusals.length > 0 ? capabilityValidationUserAction(
|
|
501
|
+
adapter,
|
|
502
|
+
capabilityRefusals,
|
|
503
|
+
actionLibraryContextArgs(librarySources)
|
|
504
|
+
) : runPlanProbe(adapter, recipeFile);
|
|
452
505
|
const nodeCount = countRecipeNodes(recipe);
|
|
453
506
|
const artifactsDir = optionString(options, "artifactsDir");
|
|
454
507
|
const plan = [
|
|
@@ -514,7 +567,8 @@ async function handleRunPlan(recipeArg, params, options, stream) {
|
|
|
514
567
|
payload.error = {
|
|
515
568
|
code: "RECIPE_VALIDATION_FAILED",
|
|
516
569
|
message: `recipe validation found ${errorCount} error(s)`,
|
|
517
|
-
|
|
570
|
+
...capabilityRefusals.length > 0 ? { missingCapabilities: capabilityRefusals } : {},
|
|
571
|
+
userAction: failureUserAction
|
|
518
572
|
};
|
|
519
573
|
}
|
|
520
574
|
if (stream.enabled) {
|
|
@@ -534,7 +588,7 @@ async function handleRunPlan(recipeArg, params, options, stream) {
|
|
|
534
588
|
console.log(` ${finding.severity === "error" ? "\u2717" : "\u26A0"} ${finding.code} ${finding.path} \u2014 ${finding.message}`);
|
|
535
589
|
}
|
|
536
590
|
}
|
|
537
|
-
if (status === "fail") console.error(` Next: ${
|
|
591
|
+
if (status === "fail") console.error(` Next: ${failureUserAction}`);
|
|
538
592
|
}
|
|
539
593
|
return status === "pass" ? EXIT.ok : EXIT.validation;
|
|
540
594
|
}
|
|
@@ -582,10 +636,17 @@ function emitRunUsageError(json, stream, adapter, recipeFile, code, message, use
|
|
|
582
636
|
}
|
|
583
637
|
return EXIT.usage;
|
|
584
638
|
}
|
|
585
|
-
function emitRunValidationError(json, stream, adapter, recipeFile, findings, errorCount) {
|
|
639
|
+
function emitRunValidationError(json, stream, adapter, recipeFile, findings, errorCount, capabilityRefusals, libraryContextArgs) {
|
|
586
640
|
const message = `recipe validation found ${errorCount} error(s)`;
|
|
587
|
-
const userAction = runPlanProbe(adapter, recipeFile);
|
|
588
|
-
|
|
641
|
+
const userAction = capabilityRefusals.length > 0 ? capabilityValidationUserAction(adapter, capabilityRefusals, libraryContextArgs) : runPlanProbe(adapter, recipeFile);
|
|
642
|
+
const capabilityDetails = capabilityRefusals.length > 0 ? { missingCapabilities: capabilityRefusals } : {};
|
|
643
|
+
stream.error({
|
|
644
|
+
code: "RECIPE_VALIDATION_FAILED",
|
|
645
|
+
message,
|
|
646
|
+
userAction,
|
|
647
|
+
findings,
|
|
648
|
+
...capabilityDetails
|
|
649
|
+
});
|
|
589
650
|
if (json) {
|
|
590
651
|
console.log(
|
|
591
652
|
JSON.stringify(
|
|
@@ -599,7 +660,12 @@ function emitRunValidationError(json, stream, adapter, recipeFile, findings, err
|
|
|
599
660
|
mutations: [],
|
|
600
661
|
recipe: recipeFile,
|
|
601
662
|
findings,
|
|
602
|
-
error: {
|
|
663
|
+
error: {
|
|
664
|
+
code: "RECIPE_VALIDATION_FAILED",
|
|
665
|
+
message,
|
|
666
|
+
userAction,
|
|
667
|
+
...capabilityDetails
|
|
668
|
+
}
|
|
603
669
|
},
|
|
604
670
|
null,
|
|
605
671
|
2
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { runnerDir } from "../paths.js";
|
|
4
|
+
function handleSetupBase(argv) {
|
|
5
|
+
const script = path.join(runnerDir, "adapters", "shared", "setup-base.sh");
|
|
6
|
+
const result = spawnSync("bash", [script, ...argv], {
|
|
7
|
+
cwd: process.cwd(),
|
|
8
|
+
env: { ...process.env, MM_HARNESS_NODE: process.execPath },
|
|
9
|
+
stdio: "inherit"
|
|
10
|
+
});
|
|
11
|
+
if (result.error) {
|
|
12
|
+
const code = result.error.code ?? "ESPAWN";
|
|
13
|
+
process.stderr.write(
|
|
14
|
+
`setup-base could not start (${code}).
|
|
15
|
+
Next: npm i -g @deeeed/metamask-harness@latest
|
|
16
|
+
`
|
|
17
|
+
);
|
|
18
|
+
return 3;
|
|
19
|
+
}
|
|
20
|
+
return result.status ?? 1;
|
|
21
|
+
}
|
|
22
|
+
export {
|
|
23
|
+
handleSetupBase
|
|
24
|
+
};
|