@deeeed/metamask-harness 0.17.4 → 0.18.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 +775 -0
- package/README.md +65 -71
- package/adapters/extension/ensure-browser.sh +12 -1
- package/adapters/extension/inject.mjs +7 -0
- package/adapters/extension/launch-browser.cjs +10 -3
- package/adapters/extension/lib/chrome-args.cjs +31 -0
- package/adapters/extension/lib/macos-focus.cjs +32 -0
- package/adapters/extension/live.sh +10 -20
- package/adapters/manifest.json +8 -0
- package/adapters/mobile/open-device.sh +45 -7
- package/adapters/mobile/verify.sh +15 -3
- package/adapters/shared/harness-source-fingerprint.mjs +49 -0
- package/adapters/shared/install-repo-deps.sh +1 -5
- package/adapters/shared/open-debug.mjs +184 -103
- package/adapters/shared/resolve-slot-ports-core.mjs +23 -6
- package/adapters/shared/resolve-slot-ports.sh +22 -5
- package/bin/mm-harness +15 -3
- package/dist/adapters/core/surface.js +4 -1
- package/dist/adapters/extension/console-capture.js +3 -1
- package/dist/adapters/extension/harness-freshness.js +39 -0
- package/dist/adapters/extension/product-config.js +110 -0
- package/dist/adapters/extension/runtime-decision.js +20 -71
- package/dist/adapters/extension/surface.js +19 -1
- package/dist/adapters/mobile/prepare.js +17 -0
- package/dist/adapters/mobile/source-freshness.js +26 -41
- package/dist/adapters/mobile/surface.js +4 -1
- package/dist/adapters/resolve-slot-ports.js +2 -0
- package/dist/adapters/slot-ports.js +13 -32
- package/dist/adapters.js +50 -17
- package/dist/checkout-lock.js +27 -2
- package/dist/cli-color.js +19 -0
- package/dist/cli-commands.js +1 -1
- package/dist/cli.js +2 -3
- package/dist/command-contract.js +13 -3
- package/dist/commands/call.js +115 -29
- package/dist/commands/checklist.js +4 -1
- package/dist/commands/completion-candidates.js +20 -13
- package/dist/commands/debug.js +31 -38
- package/dist/commands/doctor.js +33 -6
- package/dist/commands/fixtures.js +65 -17
- package/dist/commands/flows.js +39 -10
- package/dist/commands/launch/extension.js +40 -15
- package/dist/commands/launch/index.js +41 -5
- package/dist/commands/list-executables.js +151 -29
- package/dist/commands/manifest.js +127 -18
- package/dist/commands/parse-args.js +11 -1
- package/dist/commands/run-engine.js +384 -56
- package/dist/commands/run.js +112 -17
- package/dist/commands/shared.js +22 -2
- package/dist/commands/status-probe.js +3 -0
- package/dist/commands/status.js +1 -0
- package/dist/completions-cache.js +1 -1
- package/dist/doctor.js +56 -6
- package/dist/harness.js +6 -5
- package/dist/heal-bounds.js +1 -1
- package/dist/live-adapter-contract.js +132 -12
- package/dist/manifest.js +161 -1
- package/dist/mm-harness-cli.js +13 -7
- package/dist/recipe-security.js +178 -0
- package/dist/runner.js +87 -13
- package/dist/runtime-context.js +8 -26
- package/docs/CONTRIBUTING.md +137 -0
- package/docs/QA.md +185 -0
- package/docs/RECIPES.md +161 -0
- package/docs/SECURITY.md +88 -0
- package/library/actions/core/perps/read_account.mjs +2 -2
- package/library/actions/core/perps/read_orders.mjs +2 -1
- package/library/actions/core/perps/read_positions.mjs +2 -1
- package/library/actions/core/wallet/list_accounts.mjs +95 -0
- package/library/actions/extension/platform/cdp.mjs +1 -0
- package/library/actions/extension/wallet/list_accounts.mjs +41 -0
- package/library/actions/mobile/platform/bridge.mjs +1 -5
- package/library/actions/mobile/wallet/list_accounts.mjs +37 -0
- package/library/manifests/core.action-manifest.json +61 -0
- package/library/manifests/extension.action-manifest.json +53 -0
- package/library/manifests/mobile.action-manifest.json +53 -0
- package/library/recipes/runner/action-validation.extension.recipe.json +8 -1
- package/library/recipes/runner/action-validation.mobile.recipe.json +8 -1
- package/package.json +10 -6
- package/scripts/completions.sh +7 -7
- package/docs/ADAPTER-SURFACE.md +0 -26
- package/docs/CHEATSHEET.md +0 -48
- package/docs/CLI-ERGONOMICS-AUDIT.md +0 -32
- package/docs/CLI-ERGONOMICS-HUMAN-QA.md +0 -64
- package/docs/CODE-MAP.md +0 -62
- package/docs/UX-PRINCIPLES.md +0 -66
- package/docs/VIDEO-DEMO-VALIDATION.md +0 -74
- package/docs/architecture.md +0 -88
- package/docs/live-adapter-contract.md +0 -190
- package/docs/package-boundaries.md +0 -47
- package/docs/perps-flow-catalog.md +0 -235
- package/docs/recipe-libraries.md +0 -71
- package/docs/runtime-file-conventions.md +0 -36
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { color } from "../../cli-color.js";
|
|
4
|
+
import { color, colorHumanMessage } from "../../cli-color.js";
|
|
5
5
|
import { handleHarness } from "../../harness.js";
|
|
6
6
|
import { recipeRuntimePath, runnerDir } from "../../paths.js";
|
|
7
7
|
import { getAdapterSurface } from "../../adapters/surface.js";
|
|
@@ -36,6 +36,7 @@ import { applyDeviceTargeting } from "../device-target.js";
|
|
|
36
36
|
import { acquireCheckoutLock } from "../../checkout-lock.js";
|
|
37
37
|
import { JsonStreamWriter } from "../../json-stream.js";
|
|
38
38
|
import { ensureExtensionConsoleCapture } from "../../adapters/extension/console-capture.js";
|
|
39
|
+
import { extensionProductConfigBlock } from "../../adapters/extension/product-config.js";
|
|
39
40
|
const LAUNCH_BOOLEANS = /* @__PURE__ */ new Set([
|
|
40
41
|
"build",
|
|
41
42
|
"watch",
|
|
@@ -150,9 +151,9 @@ async function handleLaunchLocked(argv, stream) {
|
|
|
150
151
|
if (!machine && adapter === "extension") {
|
|
151
152
|
const modeNote = displayMode === "sidepanel" ? `sidepanel \xB7 dapp ${process.env.EXTENSION_START_URL ?? DEFAULT_EXTENSION_DAPP_URL}` : "fullscreen";
|
|
152
153
|
const workNote = wantWatch ? "watch only" : tier === "build" ? "clean build" : "quick reuse probe";
|
|
153
|
-
console.error(
|
|
154
|
+
console.error(colorHumanMessage(
|
|
154
155
|
`\u2192 extension launch \u2014 ${modeNote} \xB7 CDP :${process.env.CDP_PORT ?? "default"} \xB7 ${workNote}`
|
|
155
|
-
);
|
|
156
|
+
));
|
|
156
157
|
}
|
|
157
158
|
if (adapter === "mobile") {
|
|
158
159
|
const device = str(options, "device") ?? (mobileTarget === "android" ? process.env.ADB_SERIAL ?? process.env.ANDROID_SERIAL : process.env.IOS_SIMULATOR ?? process.env.SIM_UDID) ?? "configured device";
|
|
@@ -185,6 +186,16 @@ async function handleLaunchLocked(argv, stream) {
|
|
|
185
186
|
if (adapter === "extension") {
|
|
186
187
|
const willBuild = wantWatch || tier === "build" || !await extensionRuntimeReusable(target);
|
|
187
188
|
if (willBuild) {
|
|
189
|
+
const productConfigBlock = extensionProductConfigBlock(target);
|
|
190
|
+
if (productConfigBlock) {
|
|
191
|
+
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
192
|
+
code: "EXTENSION_PRODUCT_CONFIG_REQUIRED",
|
|
193
|
+
message: productConfigBlock.message,
|
|
194
|
+
recoverable: false,
|
|
195
|
+
userAction: productConfigBlock.userAction,
|
|
196
|
+
exitCode: EXIT.infra
|
|
197
|
+
});
|
|
198
|
+
}
|
|
188
199
|
const depsBlock = extensionDepsBlock(target);
|
|
189
200
|
if (depsBlock) {
|
|
190
201
|
if (heal === "off") {
|
|
@@ -214,6 +225,25 @@ async function handleLaunchLocked(argv, stream) {
|
|
|
214
225
|
if (attempt.status === 0) {
|
|
215
226
|
return await finishLaunch(jsonOutput, machine, stream, adapter, mobileTarget, tier, displayMode, target, state, wantWatch, wantVerify);
|
|
216
227
|
}
|
|
228
|
+
if (adapter === "extension" && extensionProductConfigMissing(attempt.output)) {
|
|
229
|
+
const block = extensionProductConfigBlock(target);
|
|
230
|
+
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
231
|
+
code: "EXTENSION_PRODUCT_CONFIG_REQUIRED",
|
|
232
|
+
message: block?.message ?? "Extension product configuration was not ready when the build started.",
|
|
233
|
+
recoverable: false,
|
|
234
|
+
userAction: block?.userAction ?? `mm-harness launch --adapter extension --target ${shellQuote(target)}`,
|
|
235
|
+
exitCode: EXIT.infra
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
if (adapter === "extension" && extensionBrowserMissing(attempt.output)) {
|
|
239
|
+
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
240
|
+
code: "EXTENSION_BROWSER_REQUIRED",
|
|
241
|
+
message: "the checkout Playwright browser is not installed; system Chrome is not a compatible unpacked-extension fallback. Installing the browser cache requires user approval.",
|
|
242
|
+
recoverable: false,
|
|
243
|
+
userAction: `ask the user for approval, then run: cd ${shellQuote(target)} && yarn playwright install chromium`,
|
|
244
|
+
exitCode: EXIT.infra
|
|
245
|
+
});
|
|
246
|
+
}
|
|
217
247
|
if (heal === "off") {
|
|
218
248
|
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
219
249
|
code: "LAUNCH_FAILED",
|
|
@@ -282,6 +312,12 @@ function extensionQuickReattachFailed(output) {
|
|
|
282
312
|
function extensionRuntimeBlocked(output) {
|
|
283
313
|
return /ERR_BLOCKED_BY_CLIENT|has been blocked by Chromium/u.test(output);
|
|
284
314
|
}
|
|
315
|
+
function extensionBrowserMissing(output) {
|
|
316
|
+
return /Playwright Chromium is not installed|No compatible Chromium binary selected/u.test(output);
|
|
317
|
+
}
|
|
318
|
+
function extensionProductConfigMissing(output) {
|
|
319
|
+
return /EXTENSION_PRODUCT_CONFIG_REQUIRED:/u.test(output);
|
|
320
|
+
}
|
|
285
321
|
function mobileProvisioningBlocked(output) {
|
|
286
322
|
return /open-device: configured iOS simulator '.+' does not exist|open-device: no MetaMask bundle found|fast mode requires an installed (?:iOS dev client|Android dev client)/u.test(output);
|
|
287
323
|
}
|
|
@@ -561,13 +597,13 @@ function launchFail(json, stream, adapter, mobileTarget, tier, state, target, fa
|
|
|
561
597
|
)
|
|
562
598
|
);
|
|
563
599
|
} else {
|
|
564
|
-
console.error(
|
|
600
|
+
console.error(colorHumanMessage(
|
|
565
601
|
`\u2717 launch ${adapter}${mobileTarget ? ` ${mobileTarget}` : ""} failed
|
|
566
602
|
${failure.message}` + (failure.originalError ? `
|
|
567
603
|
--- original failure ---
|
|
568
604
|
${failure.originalError}` : "") + `
|
|
569
605
|
Next: ${userAction}`
|
|
570
|
-
);
|
|
606
|
+
));
|
|
571
607
|
}
|
|
572
608
|
return failure.exitCode;
|
|
573
609
|
}
|
|
@@ -1,48 +1,170 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { resolveActionManifest } from "../manifest.js";
|
|
3
|
+
import { importRecipeProtocol } from "../paths.js";
|
|
4
|
+
import { color } from "../cli-color.js";
|
|
3
5
|
import { EXIT } from "./shared.js";
|
|
4
|
-
import { optionFlag, optionString, resolveAdapter } from "./parse-args.js";
|
|
5
|
-
import {
|
|
6
|
+
import { optionFlag, optionString, resolveAdapter, shellQuoteArg } from "./parse-args.js";
|
|
7
|
+
import { describeManifestActions, renderHumanActionCatalog } from "./manifest.js";
|
|
8
|
+
import {
|
|
9
|
+
describeRunnableRecipe,
|
|
10
|
+
listRunnableRecipes,
|
|
11
|
+
resolveMetaMaskLibrarySources
|
|
12
|
+
} from "./run-engine.js";
|
|
6
13
|
const finalSegment = (name) => name.split(".").pop() ?? name;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const harness = await importRecipeHarness();
|
|
12
|
-
const resolution = await harness.loadRecipeLibraries(sources);
|
|
13
|
-
return [...resolution.flows.keys()].sort();
|
|
14
|
-
} catch {
|
|
15
|
-
return [];
|
|
16
|
-
}
|
|
14
|
+
function catalogDescription(description) {
|
|
15
|
+
if (!description) return "";
|
|
16
|
+
const firstSentence = description.match(/^.*?[.!?](?:\s|$)/u)?.[0]?.trim() ?? description;
|
|
17
|
+
return firstSentence.length <= 180 ? firstSentence : `${firstSentence.slice(0, 177).trimEnd()}\u2026`;
|
|
17
18
|
}
|
|
18
19
|
async function handleListExecutables(command, options) {
|
|
19
20
|
const json = optionFlag(options, "json");
|
|
20
21
|
const { adapter } = resolveAdapter(options);
|
|
21
|
-
const
|
|
22
|
+
const librarySources = await resolveMetaMaskLibrarySources(optionString(options, "library"));
|
|
23
|
+
if (command === "run") {
|
|
24
|
+
const recipes = listRunnableRecipes(adapter, librarySources);
|
|
25
|
+
if (json) {
|
|
26
|
+
console.log(JSON.stringify({ schemaVersion: 1, command, action: "list", adapter, recipes }, null, 2));
|
|
27
|
+
return EXIT.ok;
|
|
28
|
+
}
|
|
29
|
+
const out = (style, text) => color(style, text, { stream: process.stdout });
|
|
30
|
+
console.log(out("bold", `runnable recipes (${adapter})`));
|
|
31
|
+
console.log(out("comment", "Inspect: mm-harness run <recipe> --describe"));
|
|
32
|
+
for (const recipe of recipes) {
|
|
33
|
+
const shadowed = recipe.shadows.length > 0 ? ` shadows=${recipe.shadows.join(",")}` : "";
|
|
34
|
+
const summary = catalogDescription(recipe.description);
|
|
35
|
+
const description = summary ? ` \u2014 ${summary}` : "";
|
|
36
|
+
console.log(` ${out("cmd", recipe.name)} ${out("dim", `[${recipe.source}]${shadowed}`)}${description}`);
|
|
37
|
+
}
|
|
38
|
+
return EXIT.ok;
|
|
39
|
+
}
|
|
40
|
+
const { manifest, actionSources } = await resolveActionManifest(
|
|
41
|
+
adapter,
|
|
42
|
+
optionString(options, "actionManifest"),
|
|
43
|
+
librarySources
|
|
44
|
+
);
|
|
22
45
|
const { getRecipeActionManifestActionNames } = await importRecipeProtocol();
|
|
23
|
-
const
|
|
46
|
+
const protocolNames = new Set(getRecipeActionManifestActionNames(manifest));
|
|
47
|
+
const described = describeManifestActions(manifest, actionSources).filter((entry) => protocolNames.has(entry.name)).sort((left, right) => left.name.localeCompare(right.name));
|
|
24
48
|
const shortCounts = /* @__PURE__ */ new Map();
|
|
25
|
-
for (const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
49
|
+
for (const entry of described) {
|
|
50
|
+
const short = finalSegment(entry.name);
|
|
51
|
+
shortCounts.set(short, (shortCounts.get(short) ?? 0) + 1);
|
|
52
|
+
}
|
|
53
|
+
const shortByName = /* @__PURE__ */ new Map();
|
|
54
|
+
const actions = described.map((entry) => {
|
|
55
|
+
const short = finalSegment(entry.name);
|
|
56
|
+
const usableShort = shortCounts.get(short) === 1 ? short : null;
|
|
57
|
+
shortByName.set(entry.name, usableShort);
|
|
58
|
+
return {
|
|
59
|
+
name: entry.name,
|
|
60
|
+
short: usableShort,
|
|
61
|
+
description: entry.description,
|
|
62
|
+
fields: entry.fields,
|
|
63
|
+
source: entry.source,
|
|
64
|
+
sourceTier: entry.sourceTier,
|
|
65
|
+
sourceManifest: entry.sourceManifest,
|
|
66
|
+
shadows: entry.shadows
|
|
67
|
+
};
|
|
29
68
|
});
|
|
30
|
-
const flows = await listFlowIds();
|
|
31
69
|
if (json) {
|
|
32
|
-
console.log(JSON.stringify({ schemaVersion: 1, command, action: "list", adapter, actions
|
|
70
|
+
console.log(JSON.stringify({ schemaVersion: 1, command, action: "list", adapter, actions }, null, 2));
|
|
33
71
|
return EXIT.ok;
|
|
34
72
|
}
|
|
35
|
-
console.log(
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
73
|
+
console.log(renderHumanActionCatalog(described, {
|
|
74
|
+
title: `invocable actions (${adapter})`,
|
|
75
|
+
guidance: "Use: mm-harness call <name> \xB7 short names shown first where unambiguous",
|
|
76
|
+
displayName: (entry) => {
|
|
77
|
+
const short = shortByName.get(entry.name);
|
|
78
|
+
return short ? `${short} (${entry.name})` : `${entry.name} (full name only \u2014 ambiguous short)`;
|
|
79
|
+
}
|
|
80
|
+
}));
|
|
81
|
+
return EXIT.ok;
|
|
82
|
+
}
|
|
83
|
+
async function handleDescribeRecipe(recipeArg, options) {
|
|
84
|
+
const json = optionFlag(options, "json");
|
|
85
|
+
const { adapter } = resolveAdapter(options);
|
|
86
|
+
const libraryEntry = optionString(options, "library");
|
|
87
|
+
const librarySources = await resolveMetaMaskLibrarySources(libraryEntry);
|
|
88
|
+
const result = describeRunnableRecipe(recipeArg, adapter, librarySources);
|
|
89
|
+
const targetEntry = optionString(options, "target") ?? optionString(options, "projectRoot");
|
|
90
|
+
const replayTarget = targetEntry ? path.resolve(targetEntry) : void 0;
|
|
91
|
+
const contextFlags = [
|
|
92
|
+
`--adapter ${adapter}`,
|
|
93
|
+
...libraryEntry ? [`--library ${shellQuoteArg(libraryEntry)}`] : [],
|
|
94
|
+
...replayTarget ? [`--target ${shellQuoteArg(replayTarget)}`] : []
|
|
95
|
+
].join(" ");
|
|
96
|
+
const discoveryCommand = `mm-harness run --list ${contextFlags}`;
|
|
97
|
+
if ("notFound" in result || "unreadable" in result) {
|
|
98
|
+
const code = "notFound" in result ? "RECIPE_NOT_FOUND" : "RECIPE_UNPARSEABLE";
|
|
99
|
+
const message = "notFound" in result ? result.notFound : result.unreadable;
|
|
100
|
+
const userAction = "notFound" in result ? discoveryCommand : `fix ${shellQuoteArg(recipeArg)}, then retry: mm-harness run ${shellQuoteArg(recipeArg)} --describe ${contextFlags}`;
|
|
101
|
+
if (json) {
|
|
102
|
+
console.log(JSON.stringify({
|
|
103
|
+
schemaVersion: 1,
|
|
104
|
+
command: "run",
|
|
105
|
+
action: "describe",
|
|
106
|
+
status: "fail",
|
|
107
|
+
error: { code, message, userAction },
|
|
108
|
+
exitCode: EXIT.usage
|
|
109
|
+
}, null, 2));
|
|
110
|
+
} else {
|
|
111
|
+
console.error(`\u2717 run: ${message}
|
|
112
|
+
Next: ${userAction}`);
|
|
113
|
+
}
|
|
114
|
+
return EXIT.usage;
|
|
39
115
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
116
|
+
const recipe = result.recipe;
|
|
117
|
+
const nextCommand = `mm-harness run ${shellQuoteArg(recipeArg)} --plan ${contextFlags}`;
|
|
118
|
+
if (json) {
|
|
119
|
+
console.log(JSON.stringify({
|
|
120
|
+
schemaVersion: 1,
|
|
121
|
+
command: "run",
|
|
122
|
+
action: "describe",
|
|
123
|
+
status: "pass",
|
|
124
|
+
recipe,
|
|
125
|
+
nextCommand
|
|
126
|
+
}, null, 2));
|
|
127
|
+
return EXIT.ok;
|
|
128
|
+
}
|
|
129
|
+
const out = (style, text) => color(style, text, { stream: process.stdout });
|
|
130
|
+
console.log(out("bold", `recipe ${recipe.name}`));
|
|
131
|
+
if (recipe.title) console.log(` ${out("label", "title:")} ${recipe.title}`);
|
|
132
|
+
if (recipe.description) console.log(` ${out("label", "description:")} ${recipe.description}`);
|
|
133
|
+
console.log(` ${out("label", "adapter:")} ${recipe.adapter}`);
|
|
134
|
+
console.log(` ${out("label", "source:")} ${recipe.source} \xB7 ${recipe.file}`);
|
|
135
|
+
console.log(` ${out("label", "path:")} ${out("path", recipe.path)}`);
|
|
136
|
+
const inputEntries = Object.entries(recipe.inputs);
|
|
137
|
+
if (inputEntries.length === 0) {
|
|
138
|
+
console.log(` ${out("label", "declared inputs:")} none`);
|
|
139
|
+
} else {
|
|
140
|
+
console.log(` ${out("label", "input overrides:")} unavailable \u2014 declared inputs are recipe-owned`);
|
|
141
|
+
console.log(` ${out("label", `declared inputs (${inputEntries.length})`)}`);
|
|
142
|
+
for (const [name, declaration] of inputEntries) {
|
|
143
|
+
console.log(` ${out("cmd", name)} = ${compactValue(declaration)}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
renderComposition("actions", recipe.actions, out);
|
|
147
|
+
if (!recipe.actionsComplete) {
|
|
148
|
+
console.log(` ${out("comment", "Note: actions inside external library flows are not expanded.")}`);
|
|
43
149
|
}
|
|
150
|
+
renderComposition("flows", recipe.flows, out);
|
|
151
|
+
console.log(` ${out("comment", `Next: ${nextCommand}`)}`);
|
|
44
152
|
return EXIT.ok;
|
|
45
153
|
}
|
|
154
|
+
function compactValue(value) {
|
|
155
|
+
const rendered = JSON.stringify(value);
|
|
156
|
+
if (rendered === void 0) return String(value);
|
|
157
|
+
return rendered.length <= 180 ? rendered : `${rendered.slice(0, 177)}\u2026`;
|
|
158
|
+
}
|
|
159
|
+
function renderComposition(label, values, out) {
|
|
160
|
+
if (values.length === 0) {
|
|
161
|
+
console.log(` ${out("label", `${label}:`)} none`);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
console.log(` ${out("label", `${label} (${values.length})`)}`);
|
|
165
|
+
for (const value of values) console.log(` ${out("cmd", value)}`);
|
|
166
|
+
}
|
|
46
167
|
export {
|
|
168
|
+
handleDescribeRecipe,
|
|
47
169
|
handleListExecutables
|
|
48
170
|
};
|
|
@@ -1,10 +1,26 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolveActionManifest } from "../manifest.js";
|
|
2
|
+
import { color } from "../cli-color.js";
|
|
2
3
|
import { EXIT } from "./shared.js";
|
|
3
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
isRecord,
|
|
6
|
+
optionFlag,
|
|
7
|
+
optionString,
|
|
8
|
+
resolveAdapter,
|
|
9
|
+
shellQuoteArg
|
|
10
|
+
} from "./parse-args.js";
|
|
11
|
+
import { resolveMetaMaskLibrarySources } from "./run-engine.js";
|
|
12
|
+
import {
|
|
13
|
+
officialRecipeActionCapabilities
|
|
14
|
+
} from "@farmslot/protocol";
|
|
15
|
+
import { metaMaskActionExecutionCapabilities } from "../recipe-security.js";
|
|
4
16
|
async function handleActions({ options, positional }) {
|
|
5
|
-
const { adapter } = resolveAdapter(options);
|
|
6
|
-
const
|
|
7
|
-
await
|
|
17
|
+
const { adapter, target } = resolveAdapter(options);
|
|
18
|
+
const librarySources = await resolveMetaMaskLibrarySources(optionString(options, "library"));
|
|
19
|
+
const { manifest, actionSources } = await resolveActionManifest(
|
|
20
|
+
adapter,
|
|
21
|
+
optionString(options, "actionManifest"),
|
|
22
|
+
librarySources
|
|
23
|
+
);
|
|
8
24
|
if (optionFlag(options, "raw")) {
|
|
9
25
|
console.log(JSON.stringify(manifest, null, 2));
|
|
10
26
|
return EXIT.ok;
|
|
@@ -14,7 +30,7 @@ async function handleActions({ options, positional }) {
|
|
|
14
30
|
const query = positional[0]?.trim();
|
|
15
31
|
const category = optionString(options, "category")?.toLowerCase();
|
|
16
32
|
const categoriesOnly = optionFlag(options, "categories");
|
|
17
|
-
const all = describeManifestActions(manifest);
|
|
33
|
+
const all = describeManifestActions(manifest, actionSources);
|
|
18
34
|
const categories = summarizeActionCategories(all);
|
|
19
35
|
if (categoriesOnly && (action || category)) {
|
|
20
36
|
const message = "--categories cannot be combined with --action or --category.";
|
|
@@ -83,14 +99,23 @@ async function handleActions({ options, positional }) {
|
|
|
83
99
|
...query ? { query } : {},
|
|
84
100
|
category,
|
|
85
101
|
actions,
|
|
86
|
-
...relatedActions ? { relatedActions } : {}
|
|
102
|
+
...relatedActions?.length ? { relatedActions } : {}
|
|
87
103
|
}, null, 2));
|
|
88
104
|
} else {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
|
|
105
|
+
console.log(renderHumanActionCatalog(actions, {
|
|
106
|
+
title: `actions (${adapter})`,
|
|
107
|
+
guidance: "Inspect: mm-harness actions --action <name> \xB7 Run: mm-harness call <name>"
|
|
108
|
+
}));
|
|
109
|
+
const example = action && actions.length === 1 ? renderHumanActionExample(
|
|
110
|
+
actions[0],
|
|
111
|
+
adapter,
|
|
112
|
+
target,
|
|
113
|
+
process.env.MM_HARNESS_INVOKED_AS ?? process.env.MM_HARNESS_EXECUTABLE ?? "mm-harness"
|
|
114
|
+
) : void 0;
|
|
115
|
+
if (example) console.log(`
|
|
116
|
+
${example}`);
|
|
117
|
+
if (relatedActions?.length) console.log(`
|
|
118
|
+
${color("label", "Related:", { stream: process.stdout })} ${relatedActions.join(", ")}`);
|
|
94
119
|
}
|
|
95
120
|
return 0;
|
|
96
121
|
}
|
|
@@ -120,12 +145,84 @@ function findRelatedActions(entries, selected, limit = 5) {
|
|
|
120
145
|
}).filter(({ score }) => score > 0).sort((left, right) => right.score - left.score || left.name.localeCompare(right.name)).slice(0, limit).map(({ name }) => name);
|
|
121
146
|
}
|
|
122
147
|
const GENERIC_OPERATION_TERMS = /* @__PURE__ */ new Set(["assert", "call", "close", "ensure", "place", "read", "start", "teardown"]);
|
|
148
|
+
function renderHumanActionCatalog(actions, options) {
|
|
149
|
+
const out = (style, text) => color(style, text, { stream: process.stdout });
|
|
150
|
+
const groups = /* @__PURE__ */ new Map();
|
|
151
|
+
for (const entry of actions) {
|
|
152
|
+
const group = actionDomainGroup(entry);
|
|
153
|
+
groups.set(group, [...groups.get(group) ?? [], entry]);
|
|
154
|
+
}
|
|
155
|
+
const lines = [out("bold", options.title), out("comment", options.guidance)];
|
|
156
|
+
for (const [group, entries] of groups) {
|
|
157
|
+
lines.push("", `${out(group === "official" ? "label" : "accent", group)} ${out("dim", `(${entries.length})`)}`);
|
|
158
|
+
for (const entry of entries) {
|
|
159
|
+
const fieldNames = humanFieldNames(entry);
|
|
160
|
+
const fields = fieldNames.length ? ` ${out("dim", `fields=${fieldNames.join(",")}`)}` : "";
|
|
161
|
+
const description = entry.description ? ` ${out("comment", `\u2014 ${entry.description}`)}` : "";
|
|
162
|
+
const source = ` ${out("dim", `[${entry.source}]`)}`;
|
|
163
|
+
const risk = entry.capabilities.length ? ` ${out("dim", `risk=${entry.capabilities.join(",")}`)}` : "";
|
|
164
|
+
lines.push(` ${out("cmd", options.displayName?.(entry) ?? entry.name)}${source}${fields}${risk}${description}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return lines.join("\n");
|
|
168
|
+
}
|
|
169
|
+
function renderHumanActionExample(entry, adapter, target, executable) {
|
|
170
|
+
const node = authoredExampleNode(entry.examples);
|
|
171
|
+
if (!node) return void 0;
|
|
172
|
+
const out = (style, text) => color(style, text, { stream: process.stdout });
|
|
173
|
+
const args = entry.fields.flatMap(
|
|
174
|
+
(field) => Object.hasOwn(node, field) ? [shellQuoteArg(`${field}=${actionCallValue(node[field])}`)] : []
|
|
175
|
+
);
|
|
176
|
+
const command = [
|
|
177
|
+
shellQuoteArg(executable),
|
|
178
|
+
"call",
|
|
179
|
+
shellQuoteArg(entry.name),
|
|
180
|
+
...args,
|
|
181
|
+
"--adapter",
|
|
182
|
+
adapter,
|
|
183
|
+
"--target",
|
|
184
|
+
shellQuoteArg(target)
|
|
185
|
+
].join(" ");
|
|
186
|
+
const lines = [
|
|
187
|
+
out("label", "Example call:"),
|
|
188
|
+
` ${out("cmd", command)}`,
|
|
189
|
+
out("label", "Recipe node:"),
|
|
190
|
+
...JSON.stringify(node, null, 2).split("\n").map((line) => ` ${line}`)
|
|
191
|
+
];
|
|
192
|
+
return lines.join("\n");
|
|
193
|
+
}
|
|
194
|
+
function authoredExampleNode(examples) {
|
|
195
|
+
if (!Array.isArray(examples)) return void 0;
|
|
196
|
+
for (const example of examples) {
|
|
197
|
+
if (isRecord(example) && isRecord(example.node)) return example.node;
|
|
198
|
+
}
|
|
199
|
+
return void 0;
|
|
200
|
+
}
|
|
201
|
+
function actionCallValue(value) {
|
|
202
|
+
return typeof value === "string" ? value : JSON.stringify(value);
|
|
203
|
+
}
|
|
204
|
+
function humanFieldNames(entry) {
|
|
205
|
+
const schema = isRecord(entry.schema) ? entry.schema : void 0;
|
|
206
|
+
const properties = schema && isRecord(schema.properties) ? schema.properties : void 0;
|
|
207
|
+
if (!properties) return entry.fields;
|
|
208
|
+
return entry.fields.map((field) => {
|
|
209
|
+
const property = properties[field];
|
|
210
|
+
if (!isRecord(property) || !Array.isArray(property.enum)) return field;
|
|
211
|
+
const values = property.enum.filter((value) => typeof value === "string" || typeof value === "number" || typeof value === "boolean");
|
|
212
|
+
return values.length > 0 ? `${field}=${values.join("|")}` : field;
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
function actionDomainGroup(entry) {
|
|
216
|
+
if (entry.kind === "official") return "official";
|
|
217
|
+
const match = /^metamask[.]([^.]+)[.]/u.exec(entry.name);
|
|
218
|
+
return match ? `metamask \xB7 ${match[1]}` : "custom";
|
|
219
|
+
}
|
|
123
220
|
function summarizeActionCategories(actions) {
|
|
124
221
|
const counts = /* @__PURE__ */ new Map();
|
|
125
222
|
for (const action of actions) counts.set(action.category, (counts.get(action.category) ?? 0) + 1);
|
|
126
223
|
return [...counts.entries()].map(([name, count]) => ({ name, count })).sort((left, right) => left.name.localeCompare(right.name));
|
|
127
224
|
}
|
|
128
|
-
function describeManifestActions(manifest) {
|
|
225
|
+
function describeManifestActions(manifest, actionSources = /* @__PURE__ */ new Map()) {
|
|
129
226
|
const manifestRecord = isRecord(manifest) ? manifest : {};
|
|
130
227
|
const metadata = isRecord(manifestRecord.action_metadata) ? manifestRecord.action_metadata : {};
|
|
131
228
|
const official = Array.isArray(manifestRecord.supported_official_actions) ? manifestRecord.supported_official_actions.filter((value) => typeof value === "string") : [];
|
|
@@ -140,15 +237,15 @@ function describeManifestActions(manifest) {
|
|
|
140
237
|
return [];
|
|
141
238
|
}) : [];
|
|
142
239
|
return [
|
|
143
|
-
...official.map((name) => describeManifestAction(name, "official", metadata[name])),
|
|
144
|
-
...custom.map((entry) => describeManifestAction(entry.name, "custom", entry.metadata))
|
|
240
|
+
...official.map((name) => describeManifestAction(name, "official", metadata[name], actionSources.get(name))),
|
|
241
|
+
...custom.map((entry) => describeManifestAction(entry.name, "custom", entry.metadata, actionSources.get(entry.name)))
|
|
145
242
|
];
|
|
146
243
|
}
|
|
147
|
-
function describeManifestAction(name, kind, metadata) {
|
|
244
|
+
function describeManifestAction(name, kind, metadata, source) {
|
|
148
245
|
const record = isRecord(metadata) ? metadata : {};
|
|
149
246
|
const schema = record.schema;
|
|
150
247
|
const schemaRecord = isRecord(schema) ? schema : {};
|
|
151
|
-
const properties = isRecord(schemaRecord.properties) ? Object.keys(schemaRecord.properties).sort() : [];
|
|
248
|
+
const properties = isRecord(schemaRecord.properties) ? Object.keys(schemaRecord.properties).filter((field) => field !== "action" && field !== "next").sort() : [];
|
|
152
249
|
return {
|
|
153
250
|
name,
|
|
154
251
|
kind,
|
|
@@ -156,7 +253,17 @@ function describeManifestAction(name, kind, metadata) {
|
|
|
156
253
|
description: typeof record.description === "string" ? record.description : "",
|
|
157
254
|
fields: properties,
|
|
158
255
|
schema,
|
|
159
|
-
examples: record.examples
|
|
256
|
+
examples: record.examples,
|
|
257
|
+
source: source?.name ?? (kind === "official" ? "official" : "metamask"),
|
|
258
|
+
sourceTier: source?.tier ?? (kind === "official" ? "official" : "canonical"),
|
|
259
|
+
sourceManifest: source?.manifestPath ?? "",
|
|
260
|
+
shadows: source?.shadows ?? [],
|
|
261
|
+
capabilities: [.../* @__PURE__ */ new Set([
|
|
262
|
+
...Array.isArray(record.execution_capabilities) ? record.execution_capabilities.filter(
|
|
263
|
+
(value) => typeof value === "string"
|
|
264
|
+
) : [],
|
|
265
|
+
...kind === "official" ? officialRecipeActionCapabilities(name) : metaMaskActionExecutionCapabilities(name)
|
|
266
|
+
])]
|
|
160
267
|
};
|
|
161
268
|
}
|
|
162
269
|
function actionCategory(name, configured) {
|
|
@@ -215,6 +322,8 @@ export {
|
|
|
215
322
|
findRelatedActions,
|
|
216
323
|
fuzzyResolveActions,
|
|
217
324
|
handleActions,
|
|
325
|
+
renderHumanActionCatalog,
|
|
326
|
+
renderHumanActionExample,
|
|
218
327
|
searchActions,
|
|
219
328
|
summarizeActionCategories
|
|
220
329
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { detectAdapter } from "../harness.js";
|
|
3
3
|
import { assertAdapter, manifestPath } from "../paths.js";
|
|
4
|
+
import { explicitRecipeTrustOptions } from "../recipe-security.js";
|
|
4
5
|
import { ADAPTER_DETECT_NEXT } from "./shared.js";
|
|
5
6
|
import { EXIT } from "./shared.js";
|
|
6
7
|
class CliError extends Error {
|
|
@@ -25,6 +26,7 @@ function parseArgs(argv, command) {
|
|
|
25
26
|
"record",
|
|
26
27
|
"plan",
|
|
27
28
|
"list",
|
|
29
|
+
"describe",
|
|
28
30
|
"raw",
|
|
29
31
|
"categories",
|
|
30
32
|
"fix",
|
|
@@ -142,13 +144,21 @@ function parsePort(value, errorMessage) {
|
|
|
142
144
|
}
|
|
143
145
|
function runtimeOptionsFromCli(options) {
|
|
144
146
|
const recordVideo = options.recordVideo;
|
|
147
|
+
const trust = explicitRecipeTrustOptions({
|
|
148
|
+
sourceTrust: optionString(options, "sourceTrust"),
|
|
149
|
+
sourceKind: optionString(options, "sourceKind"),
|
|
150
|
+
sourceName: optionString(options, "sourceName"),
|
|
151
|
+
sourceDigest: optionString(options, "sourceDigest"),
|
|
152
|
+
approvalDigest: optionString(options, "approvePlan")
|
|
153
|
+
});
|
|
145
154
|
return {
|
|
146
155
|
cdpPort: optionString(options, "cdpPort"),
|
|
147
156
|
watcherPort: optionString(options, "watcherPort") ?? optionString(options, "metroPort"),
|
|
148
157
|
launchExistingDist: optionFlag(options, "launchExistingDist"),
|
|
149
158
|
slot: optionString(options, "slot"),
|
|
150
159
|
validationRuntimeDir: optionString(options, "validationRuntimeDir"),
|
|
151
|
-
recordVideo: recordVideo === "full-run" ? "full-run" : false
|
|
160
|
+
recordVideo: recordVideo === "full-run" ? "full-run" : false,
|
|
161
|
+
...trust
|
|
152
162
|
};
|
|
153
163
|
}
|
|
154
164
|
function resolveAdapter(options) {
|