@deeeed/metamask-harness 0.17.5 → 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 +872 -0
- package/README.md +94 -140
- package/adapters/extension/console-tail.mjs +55 -24
- package/adapters/extension/ensure-browser.sh +6 -1
- package/adapters/extension/inject.mjs +6 -9
- package/adapters/extension/launch-browser.cjs +7 -1
- package/adapters/extension/launch-webpack.cjs +83 -0
- package/adapters/extension/launch.sh +8 -11
- package/adapters/extension/lib/chrome-args.cjs +8 -1
- package/adapters/extension/lib/macos-focus.cjs +32 -0
- package/adapters/extension/live.sh +12 -21
- package/adapters/extension/reattach.sh +1 -0
- package/adapters/extension/seed-fixture.sh +4 -12
- package/adapters/extension/sidepanel-toggle.sh +4 -1
- package/adapters/extension/stamp-runtime-title.cjs +58 -0
- package/adapters/extension/start-watch.sh +17 -4
- package/adapters/extension/stop-viewers.sh +1 -1
- package/adapters/extension/sync-webpack-dist.cjs +107 -0
- package/adapters/extension/verify.sh +1 -1
- package/adapters/manifest.json +53 -37
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +16 -0
- package/adapters/mobile/bridge-runtime/console-forwarder.cjs +1 -1
- package/adapters/mobile/bridge-runtime/lib/match-bridge-target.cjs +3 -1
- package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +2 -3
- package/adapters/mobile/cleanup.sh +16 -176
- package/adapters/mobile/inject.sh +90 -671
- package/adapters/mobile/launch-console-forwarder.cjs +39 -0
- package/adapters/mobile/open-device.sh +45 -7
- package/adapters/mobile/start-console-forwarder.sh +70 -0
- package/adapters/mobile/start-metro.sh +0 -41
- package/adapters/mobile/verify.sh +19 -64
- package/adapters/mobile/wait-for-bridge.sh +22 -8
- package/adapters/shared/resolve-slot-ports-core.mjs +17 -6
- package/adapters/shared/resolve-slot-ports.sh +17 -5
- package/adapters/shared/update-check-worker.mjs +43 -0
- package/bin/mm-harness +21 -2
- package/dist/adapters/core/surface.js +4 -1
- package/dist/adapters/extension/console-capture.js +95 -0
- package/dist/adapters/extension/product-config.js +110 -0
- package/dist/adapters/extension/runtime-decision.js +20 -2
- package/dist/adapters/extension/surface.js +22 -3
- package/dist/adapters/mobile/perps-env.js +43 -3
- package/dist/adapters/mobile/prepare.js +39 -7
- package/dist/adapters/mobile/surface.js +5 -2
- package/dist/adapters/resolve-slot-ports.js +2 -2
- package/dist/adapters/slot-ports.js +13 -16
- package/dist/adapters.js +56 -16
- 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 +7 -14
- package/dist/command-contract.js +451 -0
- package/dist/command-journal.js +225 -0
- package/dist/commands/call.js +170 -50
- package/dist/commands/check.js +9 -3
- package/dist/commands/completion-candidates.js +20 -13
- package/dist/commands/device-target.js +27 -12
- package/dist/commands/doctor.js +106 -25
- package/dist/commands/fixtures.js +92 -34
- package/dist/commands/flows.js +39 -10
- package/dist/commands/last.js +52 -0
- package/dist/commands/launch/extension.js +38 -15
- package/dist/commands/launch/index.js +207 -63
- package/dist/commands/list-executables.js +151 -29
- package/dist/commands/logs.js +8 -6
- package/dist/commands/manifest.js +270 -35
- package/dist/commands/parse-args.js +13 -1
- package/dist/commands/provision.js +10 -3
- package/dist/commands/run-engine.js +435 -105
- package/dist/commands/run-report.js +12 -3
- package/dist/commands/run.js +355 -50
- package/dist/commands/shared.js +75 -3
- package/dist/commands/status-probe.js +4 -1
- package/dist/commands/status.js +2 -1
- package/dist/commands/stop.js +7 -2
- package/dist/commands/update.js +16 -28
- package/dist/completions-cache.js +1 -1
- package/dist/doctor.js +57 -10
- package/dist/harness.js +47 -137
- package/dist/heal-bounds.js +2 -2
- package/dist/json-stream.js +57 -0
- package/dist/live-adapter-contract.js +138 -32
- package/dist/manifest.js +161 -1
- package/dist/mm-harness-cli.js +135 -54
- package/dist/paths.js +2 -5
- package/dist/recipe-security.js +178 -0
- package/dist/run-diagnostics.js +261 -0
- package/dist/runner.js +117 -8
- 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/README.md +4 -0
- package/library/actions/core/perps/_controller.mjs +10 -55
- 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/wallet/list_accounts.mjs +37 -0
- package/library/manifests/core.action-manifest.json +68 -7
- package/library/manifests/extension.action-manifest.json +53 -0
- package/library/manifests/mobile.action-manifest.json +81 -3
- package/library/recipes/runner/action-validation.extension.recipe.json +8 -1
- package/library/recipes/runner/action-validation.mobile.recipe.json +8 -1
- package/library/recipes/runner/smoke.core.recipe.json +27 -0
- package/library/recipes/wallet/smoke.extension.recipe.json +42 -0
- package/library/recipes/wallet/smoke.mobile.recipe.json +42 -0
- package/package.json +12 -5
- package/scripts/completions.sh +7 -7
- package/scripts/validate-human-outcomes.mjs +169 -0
- package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +0 -185
- package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +0 -1662
- package/adapters/mobile/overlay/compat/README.md +0 -28
- package/adapters/mobile/overlay/compat/rn81-message-event-source.patch +0 -42
- package/adapters/shared/resolve-farmslot-ports-core.mjs +0 -3
- package/adapters/shared/resolve-farmslot-ports.mjs +0 -5
- package/adapters/shared/resolve-farmslot-ports.sh +0 -7
- package/docs/ADAPTER-SURFACE.md +0 -119
- package/docs/CHEATSHEET.md +0 -61
- package/docs/CLI-SPEC.md +0 -1098
- package/docs/CODE-MAP.md +0 -62
- package/docs/DEBUG-HANDOVER.md +0 -36
- package/docs/MENTAL-MODEL.md +0 -295
- package/docs/UX-PRINCIPLES.md +0 -64
- package/docs/architecture.md +0 -398
- package/docs/live-adapter-contract.md +0 -188
- package/docs/package-boundaries.md +0 -47
- package/docs/perps-flow-catalog.md +0 -235
- package/docs/recipe-libraries.md +0 -301
- package/docs/runtime-file-conventions.md +0 -36
|
@@ -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
|
};
|
package/dist/commands/logs.js
CHANGED
|
@@ -19,9 +19,10 @@ async function handleLogs(argv) {
|
|
|
19
19
|
const logSources = surface.logSources(target);
|
|
20
20
|
const appLogSource = surface.appLogSource(target);
|
|
21
21
|
const sourceLabels = logSources.map((entry) => entry.label);
|
|
22
|
-
const
|
|
22
|
+
const appSourceLabel = appLogSource?.label ?? "app";
|
|
23
|
+
const defaultSource = appLogSource && appSourceLabel !== "app" ? appSourceLabel : sourceLabels[0];
|
|
23
24
|
const source = str(options, "source") ?? defaultSource;
|
|
24
|
-
const validSources = [...sourceLabels, ...appLogSource ? ["app"] : []];
|
|
25
|
+
const validSources = [...sourceLabels, ...appLogSource ? [appSourceLabel] : [], ...appLogSource && appSourceLabel !== "app" ? ["app"] : []];
|
|
25
26
|
if (!validSources.includes(source)) {
|
|
26
27
|
return usageOut(json, "logs", `--source must be one of: ${validSources.join(", ")}.`, `mm-harness logs --source ${defaultSource}`);
|
|
27
28
|
}
|
|
@@ -32,11 +33,12 @@ async function handleLogs(argv) {
|
|
|
32
33
|
}
|
|
33
34
|
process.env.RECIPE_LOG_EVENTS = events;
|
|
34
35
|
}
|
|
35
|
-
const
|
|
36
|
-
const
|
|
36
|
+
const isAppSource = source === "app" || source === appSourceLabel;
|
|
37
|
+
const requested = isAppSource ? appLogSource : logSources.find((entry) => entry.label === source);
|
|
38
|
+
const ordered = isAppSource ? requested ? [requested] : [] : requested ? [requested] : logSources;
|
|
37
39
|
const logFile = ordered.find((entry) => fs.existsSync(entry.path))?.path;
|
|
38
40
|
if (!logFile) {
|
|
39
|
-
if (
|
|
41
|
+
if (isAppSource && appLogSource) {
|
|
40
42
|
const message = `application console unavailable for this checkout (${path.basename(appLogSource.path)} is absent).`;
|
|
41
43
|
const userAction = surface.hints.launch;
|
|
42
44
|
if (json) {
|
|
@@ -55,7 +57,7 @@ async function handleLogs(argv) {
|
|
|
55
57
|
}
|
|
56
58
|
return EXIT.runtime;
|
|
57
59
|
}
|
|
58
|
-
const names =
|
|
60
|
+
const names = isAppSource && appLogSource ? path.basename(appLogSource.path) : logSources.map((entry) => path.basename(entry.path)).join(" / ");
|
|
59
61
|
return usageOut(
|
|
60
62
|
json,
|
|
61
63
|
"logs",
|
|
@@ -1,59 +1,228 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolveActionManifest } from "../manifest.js";
|
|
2
|
+
import { color } from "../cli-color.js";
|
|
2
3
|
import { EXIT } from "./shared.js";
|
|
3
4
|
import {
|
|
4
|
-
actionManifestPathOption,
|
|
5
5
|
isRecord,
|
|
6
6
|
optionFlag,
|
|
7
7
|
optionString,
|
|
8
|
-
resolveAdapter
|
|
8
|
+
resolveAdapter,
|
|
9
|
+
shellQuoteArg
|
|
9
10
|
} from "./parse-args.js";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
11
|
+
import { resolveMetaMaskLibrarySources } from "./run-engine.js";
|
|
12
|
+
import {
|
|
13
|
+
officialRecipeActionCapabilities
|
|
14
|
+
} from "@farmslot/protocol";
|
|
15
|
+
import { metaMaskActionExecutionCapabilities } from "../recipe-security.js";
|
|
16
|
+
async function handleActions({ options, positional }) {
|
|
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
|
+
);
|
|
24
|
+
if (optionFlag(options, "raw")) {
|
|
25
|
+
console.log(JSON.stringify(manifest, null, 2));
|
|
26
|
+
return EXIT.ok;
|
|
27
|
+
}
|
|
23
28
|
const json = optionFlag(options, "json");
|
|
24
29
|
const action = optionString(options, "action");
|
|
25
|
-
const
|
|
26
|
-
const
|
|
30
|
+
const query = positional[0]?.trim();
|
|
31
|
+
const category = optionString(options, "category")?.toLowerCase();
|
|
32
|
+
const categoriesOnly = optionFlag(options, "categories");
|
|
33
|
+
const all = describeManifestActions(manifest, actionSources);
|
|
34
|
+
const categories = summarizeActionCategories(all);
|
|
35
|
+
if (categoriesOnly && (action || category)) {
|
|
36
|
+
const message = "--categories cannot be combined with --action or --category.";
|
|
37
|
+
const userAction = `mm-harness actions --adapter ${adapter} --categories`;
|
|
38
|
+
if (json) {
|
|
39
|
+
console.log(JSON.stringify({ schemaVersion: 1, command: "actions", adapter, error: { code: "ACTION_FILTER_CONFLICT", message, userAction } }, null, 2));
|
|
40
|
+
} else {
|
|
41
|
+
console.error(`\u2717 mm-harness actions: ${message}
|
|
42
|
+
Next: ${userAction}`);
|
|
43
|
+
}
|
|
44
|
+
return EXIT.usage;
|
|
45
|
+
}
|
|
46
|
+
if (categoriesOnly) {
|
|
47
|
+
if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "actions", adapter, categories }, null, 2));
|
|
48
|
+
else for (const entry of categories) console.log(`${entry.name} (${entry.count})`);
|
|
49
|
+
return EXIT.ok;
|
|
50
|
+
}
|
|
51
|
+
const categoryActions = category ? all.filter((entry) => entry.category === category) : all;
|
|
52
|
+
if (category && categoryActions.length === 0) {
|
|
53
|
+
const message = `no action category matches "${category}" for the ${adapter} adapter.`;
|
|
54
|
+
const userAction = `mm-harness actions --adapter ${adapter} --categories`;
|
|
55
|
+
if (json) {
|
|
56
|
+
console.log(JSON.stringify({ schemaVersion: 1, command: "actions", adapter, category, availableCategories: categories, error: { code: "ACTION_CATEGORY_UNKNOWN", message, userAction } }, null, 2));
|
|
57
|
+
} else {
|
|
58
|
+
console.error(`\u2717 mm-harness actions: ${message}
|
|
59
|
+
Next: ${userAction}`);
|
|
60
|
+
}
|
|
61
|
+
return EXIT.usage;
|
|
62
|
+
}
|
|
63
|
+
const actions = action ? fuzzyResolveActions(categoryActions, action) : query ? searchActions(categoryActions, query) : categoryActions;
|
|
27
64
|
if (action && actions.length === 0) {
|
|
28
65
|
const message = `no action matches "${action}" for the ${adapter} adapter.`;
|
|
29
|
-
const userAction = `mm-harness actions --adapter ${adapter}
|
|
66
|
+
const userAction = `mm-harness actions --adapter ${adapter}`;
|
|
67
|
+
if (json) {
|
|
68
|
+
console.log(JSON.stringify({ schemaVersion: 1, command: "actions", adapter, action, category, error: { code: "ACTION_UNKNOWN", message, userAction } }, null, 2));
|
|
69
|
+
} else {
|
|
70
|
+
console.error(`\u2717 mm-harness actions: ${message}
|
|
71
|
+
Next: ${userAction}`);
|
|
72
|
+
}
|
|
73
|
+
return EXIT.usage;
|
|
74
|
+
}
|
|
75
|
+
if (query && actions.length === 0) {
|
|
76
|
+
const message = `no action matches search "${query}" for the ${adapter} adapter.`;
|
|
77
|
+
const userAction = `mm-harness actions --adapter ${adapter} --categories`;
|
|
30
78
|
if (json) {
|
|
31
|
-
console.log(JSON.stringify({
|
|
79
|
+
console.log(JSON.stringify({
|
|
80
|
+
schemaVersion: 1,
|
|
81
|
+
command: "actions",
|
|
82
|
+
adapter,
|
|
83
|
+
query,
|
|
84
|
+
category,
|
|
85
|
+
error: { code: "ACTION_SEARCH_EMPTY", message, userAction }
|
|
86
|
+
}, null, 2));
|
|
32
87
|
} else {
|
|
33
88
|
console.error(`\u2717 mm-harness actions: ${message}
|
|
34
89
|
Next: ${userAction}`);
|
|
35
90
|
}
|
|
36
91
|
return EXIT.usage;
|
|
37
92
|
}
|
|
93
|
+
const relatedActions = action && actions.length === 1 ? findRelatedActions(all, actions[0]) : void 0;
|
|
38
94
|
if (json) {
|
|
39
|
-
console.log(JSON.stringify({
|
|
95
|
+
console.log(JSON.stringify({
|
|
96
|
+
schemaVersion: 1,
|
|
97
|
+
command: "actions",
|
|
98
|
+
adapter,
|
|
99
|
+
...query ? { query } : {},
|
|
100
|
+
category,
|
|
101
|
+
actions,
|
|
102
|
+
...relatedActions?.length ? { relatedActions } : {}
|
|
103
|
+
}, null, 2));
|
|
40
104
|
} else {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
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(", ")}`);
|
|
45
119
|
}
|
|
46
120
|
return 0;
|
|
47
121
|
}
|
|
48
122
|
function fuzzyResolveActions(entries, query) {
|
|
49
123
|
const exactFull = entries.filter((e) => e.name === query);
|
|
50
124
|
if (exactFull.length > 0) return exactFull;
|
|
51
|
-
const
|
|
52
|
-
const exactSegment = entries.filter((e) =>
|
|
125
|
+
const finalSegment2 = (name) => name.split(".").pop() ?? name;
|
|
126
|
+
const exactSegment = entries.filter((e) => finalSegment2(e.name) === query);
|
|
53
127
|
if (exactSegment.length > 0) return exactSegment;
|
|
54
|
-
return entries.filter((e) =>
|
|
128
|
+
return entries.filter((e) => finalSegment2(e.name).includes(query));
|
|
129
|
+
}
|
|
130
|
+
function searchActions(entries, query) {
|
|
131
|
+
const terms = searchTerms(query);
|
|
132
|
+
if (terms.length === 0) return [];
|
|
133
|
+
return entries.map((entry) => {
|
|
134
|
+
const scores = terms.map((term) => actionSearchScore(entry, term));
|
|
135
|
+
return { entry, scores, score: scores.reduce((total, value) => total + value, 0) };
|
|
136
|
+
}).filter(({ score, scores }) => score > 0 && scores.every((value) => value > 0)).sort((left, right) => right.score - left.score || left.entry.name.localeCompare(right.entry.name)).map(({ entry }) => entry);
|
|
137
|
+
}
|
|
138
|
+
function findRelatedActions(entries, selected, limit = 5) {
|
|
139
|
+
const selectedNameTerms = searchTerms(finalSegment(selected.name).replaceAll("_", " "));
|
|
140
|
+
return entries.filter((entry) => entry.name !== selected.name).map((entry) => {
|
|
141
|
+
const nameTerms = new Set(searchTerms(finalSegment(entry.name).replaceAll("_", " ")));
|
|
142
|
+
const sharedScore = selectedNameTerms.filter((term) => nameTerms.has(term)).reduce((score2, term) => score2 + (GENERIC_OPERATION_TERMS.has(term) ? 5 : 30), 0);
|
|
143
|
+
const score = (entry.category === selected.category ? 100 : 0) + sharedScore;
|
|
144
|
+
return { name: entry.name, score };
|
|
145
|
+
}).filter(({ score }) => score > 0).sort((left, right) => right.score - left.score || left.name.localeCompare(right.name)).slice(0, limit).map(({ name }) => name);
|
|
146
|
+
}
|
|
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);
|
|
55
203
|
}
|
|
56
|
-
function
|
|
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
|
+
}
|
|
220
|
+
function summarizeActionCategories(actions) {
|
|
221
|
+
const counts = /* @__PURE__ */ new Map();
|
|
222
|
+
for (const action of actions) counts.set(action.category, (counts.get(action.category) ?? 0) + 1);
|
|
223
|
+
return [...counts.entries()].map(([name, count]) => ({ name, count })).sort((left, right) => left.name.localeCompare(right.name));
|
|
224
|
+
}
|
|
225
|
+
function describeManifestActions(manifest, actionSources = /* @__PURE__ */ new Map()) {
|
|
57
226
|
const manifestRecord = isRecord(manifest) ? manifest : {};
|
|
58
227
|
const metadata = isRecord(manifestRecord.action_metadata) ? manifestRecord.action_metadata : {};
|
|
59
228
|
const official = Array.isArray(manifestRecord.supported_official_actions) ? manifestRecord.supported_official_actions.filter((value) => typeof value === "string") : [];
|
|
@@ -68,27 +237,93 @@ function describeManifestActions(manifest) {
|
|
|
68
237
|
return [];
|
|
69
238
|
}) : [];
|
|
70
239
|
return [
|
|
71
|
-
...official.map((name) => describeManifestAction(name, "official", metadata[name])),
|
|
72
|
-
...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)))
|
|
73
242
|
];
|
|
74
243
|
}
|
|
75
|
-
function describeManifestAction(name, kind, metadata) {
|
|
244
|
+
function describeManifestAction(name, kind, metadata, source) {
|
|
76
245
|
const record = isRecord(metadata) ? metadata : {};
|
|
77
246
|
const schema = record.schema;
|
|
78
247
|
const schemaRecord = isRecord(schema) ? schema : {};
|
|
79
|
-
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() : [];
|
|
80
249
|
return {
|
|
81
250
|
name,
|
|
82
251
|
kind,
|
|
252
|
+
category: actionCategory(name, record.category),
|
|
83
253
|
description: typeof record.description === "string" ? record.description : "",
|
|
84
254
|
fields: properties,
|
|
85
255
|
schema,
|
|
86
|
-
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
|
+
])]
|
|
87
267
|
};
|
|
88
268
|
}
|
|
269
|
+
function actionCategory(name, configured) {
|
|
270
|
+
if (typeof configured === "string" && configured.trim()) return configured.trim().toLowerCase();
|
|
271
|
+
const segments = name.split(".");
|
|
272
|
+
if (segments[0] === "metamask" && segments.length > 2) return segments[1] ?? "metamask";
|
|
273
|
+
if (segments[0] === "app" || segments[0] === "cdp") return "runtime";
|
|
274
|
+
if (segments[0] === "ui") return "ui";
|
|
275
|
+
if (name.startsWith("assert_")) return "assertion";
|
|
276
|
+
if (name === "watch_logs" || name === "index_artifacts") return "evidence";
|
|
277
|
+
if (name === "command" || name === "wait" || name === "call" || name === "end") return "control";
|
|
278
|
+
return segments.length > 1 ? segments[0] || "utility" : "utility";
|
|
279
|
+
}
|
|
280
|
+
function actionSearchScore(entry, term) {
|
|
281
|
+
const name = entry.name.toLowerCase();
|
|
282
|
+
const segment = finalSegment(name);
|
|
283
|
+
const nameTerms = searchTerms(name.replaceAll(".", " ").replaceAll("_", " "));
|
|
284
|
+
const fields = entry.fields.map((field) => field.toLowerCase());
|
|
285
|
+
const description = entry.description.toLowerCase();
|
|
286
|
+
if (name === term) return 120;
|
|
287
|
+
if (segment === term) return 110;
|
|
288
|
+
if (entry.category === term) return 100;
|
|
289
|
+
if (fields.includes(term)) return 90;
|
|
290
|
+
if (nameTerms.includes(term)) return 80;
|
|
291
|
+
if (name.includes(term)) return 70;
|
|
292
|
+
if (fields.some((field) => field.includes(term))) return 60;
|
|
293
|
+
if (description.includes(term)) return 50;
|
|
294
|
+
if ([...nameTerms, ...fields].some((candidate) => fuzzyTermMatch(term, candidate))) return 40;
|
|
295
|
+
return 0;
|
|
296
|
+
}
|
|
297
|
+
function fuzzyTermMatch(term, candidate) {
|
|
298
|
+
if (term.length < 4 || candidate.length < 4) return false;
|
|
299
|
+
const threshold = Math.max(1, Math.floor(Math.max(term.length, candidate.length) / 4));
|
|
300
|
+
return levenshtein(term, candidate) <= threshold;
|
|
301
|
+
}
|
|
302
|
+
function searchTerms(value) {
|
|
303
|
+
return value.toLowerCase().match(/[a-z0-9]+/gu) ?? [];
|
|
304
|
+
}
|
|
305
|
+
function finalSegment(name) {
|
|
306
|
+
return name.split(".").pop() ?? name;
|
|
307
|
+
}
|
|
308
|
+
function levenshtein(left, right) {
|
|
309
|
+
const prior = Array.from({ length: right.length + 1 }, (_, index) => index);
|
|
310
|
+
for (let leftIndex = 1; leftIndex <= left.length; leftIndex += 1) {
|
|
311
|
+
const current = [leftIndex];
|
|
312
|
+
for (let rightIndex = 1; rightIndex <= right.length; rightIndex += 1) {
|
|
313
|
+
const substitution = (prior[rightIndex - 1] ?? 0) + (left[leftIndex - 1] === right[rightIndex - 1] ? 0 : 1);
|
|
314
|
+
current[rightIndex] = Math.min((current[rightIndex - 1] ?? 0) + 1, (prior[rightIndex] ?? 0) + 1, substitution);
|
|
315
|
+
}
|
|
316
|
+
for (let index = 0; index < current.length; index += 1) prior[index] = current[index] ?? 0;
|
|
317
|
+
}
|
|
318
|
+
return prior[right.length] ?? right.length;
|
|
319
|
+
}
|
|
89
320
|
export {
|
|
90
321
|
describeManifestActions,
|
|
322
|
+
findRelatedActions,
|
|
91
323
|
fuzzyResolveActions,
|
|
92
324
|
handleActions,
|
|
93
|
-
|
|
325
|
+
renderHumanActionCatalog,
|
|
326
|
+
renderHumanActionExample,
|
|
327
|
+
searchActions,
|
|
328
|
+
summarizeActionCategories
|
|
94
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 {
|
|
@@ -19,12 +20,15 @@ function parseArgs(argv, command) {
|
|
|
19
20
|
const options = {};
|
|
20
21
|
const booleanOptions = /* @__PURE__ */ new Set([
|
|
21
22
|
"json",
|
|
23
|
+
"jsonStream",
|
|
22
24
|
"launchExistingDist",
|
|
23
25
|
"startWatch",
|
|
24
26
|
"record",
|
|
25
27
|
"plan",
|
|
26
28
|
"list",
|
|
29
|
+
"describe",
|
|
27
30
|
"raw",
|
|
31
|
+
"categories",
|
|
28
32
|
"fix",
|
|
29
33
|
"force",
|
|
30
34
|
"resolveOnly",
|
|
@@ -140,13 +144,21 @@ function parsePort(value, errorMessage) {
|
|
|
140
144
|
}
|
|
141
145
|
function runtimeOptionsFromCli(options) {
|
|
142
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
|
+
});
|
|
143
154
|
return {
|
|
144
155
|
cdpPort: optionString(options, "cdpPort"),
|
|
145
156
|
watcherPort: optionString(options, "watcherPort") ?? optionString(options, "metroPort"),
|
|
146
157
|
launchExistingDist: optionFlag(options, "launchExistingDist"),
|
|
147
158
|
slot: optionString(options, "slot"),
|
|
148
159
|
validationRuntimeDir: optionString(options, "validationRuntimeDir"),
|
|
149
|
-
recordVideo: recordVideo === "full-run" ? "full-run" : false
|
|
160
|
+
recordVideo: recordVideo === "full-run" ? "full-run" : false,
|
|
161
|
+
...trust
|
|
150
162
|
};
|
|
151
163
|
}
|
|
152
164
|
function resolveAdapter(options) {
|