@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.
Files changed (93) hide show
  1. package/CHANGELOG.md +775 -0
  2. package/README.md +65 -71
  3. package/adapters/extension/ensure-browser.sh +12 -1
  4. package/adapters/extension/inject.mjs +7 -0
  5. package/adapters/extension/launch-browser.cjs +10 -3
  6. package/adapters/extension/lib/chrome-args.cjs +31 -0
  7. package/adapters/extension/lib/macos-focus.cjs +32 -0
  8. package/adapters/extension/live.sh +10 -20
  9. package/adapters/manifest.json +8 -0
  10. package/adapters/mobile/open-device.sh +45 -7
  11. package/adapters/mobile/verify.sh +15 -3
  12. package/adapters/shared/harness-source-fingerprint.mjs +49 -0
  13. package/adapters/shared/install-repo-deps.sh +1 -5
  14. package/adapters/shared/open-debug.mjs +184 -103
  15. package/adapters/shared/resolve-slot-ports-core.mjs +23 -6
  16. package/adapters/shared/resolve-slot-ports.sh +22 -5
  17. package/bin/mm-harness +15 -3
  18. package/dist/adapters/core/surface.js +4 -1
  19. package/dist/adapters/extension/console-capture.js +3 -1
  20. package/dist/adapters/extension/harness-freshness.js +39 -0
  21. package/dist/adapters/extension/product-config.js +110 -0
  22. package/dist/adapters/extension/runtime-decision.js +20 -71
  23. package/dist/adapters/extension/surface.js +19 -1
  24. package/dist/adapters/mobile/prepare.js +17 -0
  25. package/dist/adapters/mobile/source-freshness.js +26 -41
  26. package/dist/adapters/mobile/surface.js +4 -1
  27. package/dist/adapters/resolve-slot-ports.js +2 -0
  28. package/dist/adapters/slot-ports.js +13 -32
  29. package/dist/adapters.js +50 -17
  30. package/dist/checkout-lock.js +27 -2
  31. package/dist/cli-color.js +19 -0
  32. package/dist/cli-commands.js +1 -1
  33. package/dist/cli.js +2 -3
  34. package/dist/command-contract.js +13 -3
  35. package/dist/commands/call.js +115 -29
  36. package/dist/commands/checklist.js +4 -1
  37. package/dist/commands/completion-candidates.js +20 -13
  38. package/dist/commands/debug.js +31 -38
  39. package/dist/commands/doctor.js +33 -6
  40. package/dist/commands/fixtures.js +65 -17
  41. package/dist/commands/flows.js +39 -10
  42. package/dist/commands/launch/extension.js +40 -15
  43. package/dist/commands/launch/index.js +41 -5
  44. package/dist/commands/list-executables.js +151 -29
  45. package/dist/commands/manifest.js +127 -18
  46. package/dist/commands/parse-args.js +11 -1
  47. package/dist/commands/run-engine.js +384 -56
  48. package/dist/commands/run.js +112 -17
  49. package/dist/commands/shared.js +22 -2
  50. package/dist/commands/status-probe.js +3 -0
  51. package/dist/commands/status.js +1 -0
  52. package/dist/completions-cache.js +1 -1
  53. package/dist/doctor.js +56 -6
  54. package/dist/harness.js +6 -5
  55. package/dist/heal-bounds.js +1 -1
  56. package/dist/live-adapter-contract.js +132 -12
  57. package/dist/manifest.js +161 -1
  58. package/dist/mm-harness-cli.js +13 -7
  59. package/dist/recipe-security.js +178 -0
  60. package/dist/runner.js +87 -13
  61. package/dist/runtime-context.js +8 -26
  62. package/docs/CONTRIBUTING.md +137 -0
  63. package/docs/QA.md +185 -0
  64. package/docs/RECIPES.md +161 -0
  65. package/docs/SECURITY.md +88 -0
  66. package/library/actions/core/perps/read_account.mjs +2 -2
  67. package/library/actions/core/perps/read_orders.mjs +2 -1
  68. package/library/actions/core/perps/read_positions.mjs +2 -1
  69. package/library/actions/core/wallet/list_accounts.mjs +95 -0
  70. package/library/actions/extension/platform/cdp.mjs +1 -0
  71. package/library/actions/extension/wallet/list_accounts.mjs +41 -0
  72. package/library/actions/mobile/platform/bridge.mjs +1 -5
  73. package/library/actions/mobile/wallet/list_accounts.mjs +37 -0
  74. package/library/manifests/core.action-manifest.json +61 -0
  75. package/library/manifests/extension.action-manifest.json +53 -0
  76. package/library/manifests/mobile.action-manifest.json +53 -0
  77. package/library/recipes/runner/action-validation.extension.recipe.json +8 -1
  78. package/library/recipes/runner/action-validation.mobile.recipe.json +8 -1
  79. package/package.json +10 -6
  80. package/scripts/completions.sh +7 -7
  81. package/docs/ADAPTER-SURFACE.md +0 -26
  82. package/docs/CHEATSHEET.md +0 -48
  83. package/docs/CLI-ERGONOMICS-AUDIT.md +0 -32
  84. package/docs/CLI-ERGONOMICS-HUMAN-QA.md +0 -64
  85. package/docs/CODE-MAP.md +0 -62
  86. package/docs/UX-PRINCIPLES.md +0 -66
  87. package/docs/VIDEO-DEMO-VALIDATION.md +0 -74
  88. package/docs/architecture.md +0 -88
  89. package/docs/live-adapter-contract.md +0 -190
  90. package/docs/package-boundaries.md +0 -47
  91. package/docs/perps-flow-catalog.md +0 -235
  92. package/docs/recipe-libraries.md +0 -71
  93. 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 { loadActionManifest } from "../manifest.js";
2
- import { importRecipeHarness, importRecipeProtocol } from "../paths.js";
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 { resolveMetaMaskLibrarySources } from "./run-engine.js";
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
- async function listFlowIds() {
8
- try {
9
- const sources = await resolveMetaMaskLibrarySources(void 0);
10
- if (!sources || sources.length === 0) return [];
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 manifest = loadActionManifest(adapter, optionString(options, "actionManifest"));
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 names = getRecipeActionManifestActionNames(manifest).slice().sort();
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 name of names) shortCounts.set(finalSegment(name), (shortCounts.get(finalSegment(name)) ?? 0) + 1);
26
- const actions = names.map((name) => {
27
- const short = finalSegment(name);
28
- return { name, short: shortCounts.get(short) === 1 ? short : null };
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, flows }, null, 2));
70
+ console.log(JSON.stringify({ schemaVersion: 1, command, action: "list", adapter, actions }, null, 2));
33
71
  return EXIT.ok;
34
72
  }
35
- console.log(`invocable for ${adapter}:`);
36
- console.log(` actions (mm-harness call <name> \u2014 short or full):`);
37
- for (const entry of actions) {
38
- console.log(entry.short ? ` ${entry.short} (${entry.name})` : ` ${entry.name} (full name only \u2014 ambiguous short)`);
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
- if (flows.length > 0) {
41
- console.log(` flows (mm-harness run <flow>):`);
42
- for (const flow of flows) console.log(` ${flow}`);
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 { loadActionManifest, validateManifest } from "../manifest.js";
1
+ import { resolveActionManifest } from "../manifest.js";
2
+ import { color } from "../cli-color.js";
2
3
  import { EXIT } from "./shared.js";
3
- import { isRecord, optionFlag, optionString, resolveAdapter } from "./parse-args.js";
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 manifest = loadActionManifest(adapter, optionString(options, "actionManifest"));
7
- await validateManifest(manifest);
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
- for (const entry of actions) {
90
- const fields = entry.fields.length ? ` fields=${entry.fields.join(",")}` : "";
91
- console.log(`${entry.name} (${entry.kind})${fields}${entry.description ? ` \u2014 ${entry.description}` : ""}`);
92
- }
93
- if (relatedActions) console.log(`Related: ${relatedActions.join(", ")}`);
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) {