@deeeed/metamask-harness 0.18.0 → 0.19.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 (79) hide show
  1. package/CHANGELOG.md +187 -0
  2. package/README.md +4 -7
  3. package/adapters/core/inject.sh +1 -6
  4. package/adapters/extension/inject.mjs +1 -4
  5. package/adapters/extension/verify.sh +1 -1
  6. package/adapters/mobile/inject.sh +5 -4
  7. package/adapters/mobile/verify.sh +15 -19
  8. package/adapters/shared/harness-source-fingerprint.mjs +14 -12
  9. package/dist/adapters/harness-freshness.js +49 -0
  10. package/dist/cli.js +0 -4
  11. package/dist/command-contract.js +51 -40
  12. package/dist/command-journal.js +50 -12
  13. package/dist/commands/call.js +95 -20
  14. package/dist/commands/check.js +1 -1
  15. package/dist/commands/completion-candidates.js +7 -19
  16. package/dist/commands/fixtures.js +50 -1
  17. package/dist/commands/last.js +9 -1
  18. package/dist/commands/launch/extension.js +2 -2
  19. package/dist/commands/launch/mobile.js +2 -0
  20. package/dist/commands/list-executables.js +56 -20
  21. package/dist/commands/manifest.js +28 -12
  22. package/dist/commands/parse-args.js +55 -3
  23. package/dist/commands/provision.js +0 -1
  24. package/dist/commands/run-engine.js +514 -336
  25. package/dist/commands/run.js +25 -37
  26. package/dist/commands/shared.js +1 -1
  27. package/dist/heal-bounds.js +5 -0
  28. package/dist/mm-harness-cli.js +22 -30
  29. package/dist/run-diagnostics.js +1 -1
  30. package/dist/run-recording.js +1 -1
  31. package/dist/runner.js +24 -2
  32. package/docs/CONTRIBUTING.md +2 -3
  33. package/docs/QA.md +0 -1
  34. package/docs/RECIPES.md +52 -91
  35. package/library/README.md +5 -5
  36. package/library/actions/core/perps/_controller.mjs +33 -1
  37. package/library/actions/core/perps/assert_orders.mjs +6 -7
  38. package/library/actions/core/perps/assert_positions.mjs +6 -7
  39. package/library/actions/core/perps/close_orders.mjs +2 -0
  40. package/library/actions/core/perps/close_positions.mjs +2 -0
  41. package/library/actions/core/perps/ensure_orders.mjs +4 -2
  42. package/library/actions/core/perps/ensure_positions.mjs +4 -2
  43. package/library/actions/core/perps/place_order.mjs +7 -3
  44. package/library/actions/extension/perps/assert_orders.mjs +2 -1
  45. package/library/actions/extension/perps/assert_positions.mjs +2 -1
  46. package/library/actions/extension/perps/perps.mjs +43 -14
  47. package/library/actions/mobile/perps/assert_orders.mjs +2 -1
  48. package/library/actions/mobile/perps/assert_positions.mjs +2 -1
  49. package/library/actions/mobile/perps/perps.mjs +40 -12
  50. package/library/library.json +1 -1
  51. package/library/manifests/core.action-manifest.json +1170 -413
  52. package/library/manifests/extension.action-manifest.json +1495 -641
  53. package/library/manifests/mobile.action-manifest.json +1704 -769
  54. package/library/recipes/app/lifecycle.android-smoke.mobile.recipe.json +63 -81
  55. package/library/recipes/perps/clean-market-testnet.core.recipe.json +44 -0
  56. package/library/recipes/perps/clean-market-testnet.recipe.json +49 -0
  57. package/library/recipes/perps/lifecycle.recipe.json +136 -180
  58. package/library/recipes/perps/order-lifecycle.core.recipe.json +71 -67
  59. package/library/recipes/perps/performance.background-resume.mobile.recipe.json +51 -67
  60. package/library/recipes/perps/performance.cold-start.mobile.recipe.json +51 -67
  61. package/library/recipes/perps/performance.mobile.recipe.json +37 -51
  62. package/library/recipes/perps/performance.warm-start.mobile.recipe.json +44 -59
  63. package/library/recipes/perps/read-markets.core.recipe.json +29 -31
  64. package/library/recipes/perps/smoke.core.recipe.json +29 -32
  65. package/library/recipes/perps/smoke.extension.recipe.json +41 -44
  66. package/library/recipes/perps/smoke.mobile.recipe.json +42 -44
  67. package/library/recipes/perps/trading-lifecycle.core.recipe.json +69 -65
  68. package/library/recipes/runner/action-validation.extension.recipe.json +312 -405
  69. package/library/recipes/runner/action-validation.mobile.recipe.json +316 -409
  70. package/library/recipes/runner/smoke.core.recipe.json +18 -20
  71. package/library/recipes/runner/smoke.extension.recipe.json +23 -24
  72. package/library/recipes/runner/smoke.mobile.recipe.json +23 -24
  73. package/library/recipes/wallet/smoke.extension.recipe.json +33 -35
  74. package/library/recipes/wallet/smoke.mobile.recipe.json +33 -35
  75. package/package.json +3 -3
  76. package/scripts/completions.sh +1 -4
  77. package/dist/adapters/extension/harness-freshness.js +0 -39
  78. package/dist/commands/flows.js +0 -91
  79. package/library/flows/perps.flows.json +0 -64
@@ -18,11 +18,14 @@ const JOURNALED_COMMANDS = /* @__PURE__ */ new Set([
18
18
  "stop",
19
19
  "verify"
20
20
  ]);
21
- const SENSITIVE_KEY = /(?:auth|credential|mnemonic|pass(?:word)?|private[-_]?key|secret|seed|srp|token)/iu;
21
+ const SENSITIVE_KEY = /(?:api[-_]?key|auth|credential|mnemonic|pass(?:word)?|private[-_]?key|secret|seed|srp|token|vault)/iu;
22
22
  const EVIDENCE_OPTIONS = /* @__PURE__ */ new Set(["--artifacts-dir", "--out", "--output"]);
23
+ let activeCommandJournal;
23
24
  async function withCommandJournal(command, argv, execute) {
24
25
  if (!JOURNALED_COMMANDS.has(command)) return execute();
25
26
  const handle = tryBeginCommandJournal(command, argv);
27
+ const previous = activeCommandJournal;
28
+ activeCommandJournal = handle;
26
29
  try {
27
30
  const exitCode = await execute();
28
31
  tryFinishCommandJournal(handle, exitCode);
@@ -30,6 +33,23 @@ async function withCommandJournal(command, argv, execute) {
30
33
  } catch (error) {
31
34
  tryFinishCommandJournal(handle, 1);
32
35
  throw error;
36
+ } finally {
37
+ activeCommandJournal = previous;
38
+ }
39
+ }
40
+ function recordCommandEvidence(...evidence) {
41
+ const handle = activeCommandJournal;
42
+ if (!handle || evidence.length === 0) return;
43
+ try {
44
+ const paths = new Set(handle.record.evidencePaths);
45
+ for (const entry of evidence) paths.add(path.resolve(entry));
46
+ handle.record = { ...handle.record, evidencePaths: [...paths] };
47
+ writeAtomic(handle.file, handle.record);
48
+ } catch (error) {
49
+ process.stderr.write(
50
+ `mm-harness: resumability journal could not record evidence: ${errorMessage(error)}
51
+ `
52
+ );
33
53
  }
34
54
  }
35
55
  function commandJournalPath(target, runtimeDir) {
@@ -48,7 +68,9 @@ function readCommandJournal(target, runtimeDir) {
48
68
  if (!descriptorStat.isFile() || !pathStat.isFile() || pathStat.isSymbolicLink() || pathStat.dev === 0n && pathStat.ino === 0n || descriptorStat.dev !== pathStat.dev || descriptorStat.ino !== pathStat.ino) {
49
69
  return { file, record: null };
50
70
  }
51
- const parsed = JSON.parse(fs.readFileSync(descriptor, "utf8"));
71
+ const parsed = JSON.parse(
72
+ fs.readFileSync(descriptor, "utf8")
73
+ );
52
74
  return isCommandJournalRecord(parsed) ? { file, record: parsed } : { file, record: null };
53
75
  } catch {
54
76
  return { file, record: null };
@@ -64,7 +86,9 @@ function redactCommandArgs(argv) {
64
86
  if (token.startsWith("--") && equals !== -1) {
65
87
  const option = token.slice(0, equals);
66
88
  const value = token.slice(equals + 1);
67
- redacted.push(`${option}=${option === "--arg" ? redactAssignment(value) : SENSITIVE_KEY.test(option) ? "<redacted>" : redactUrl(value)}`);
89
+ redacted.push(
90
+ `${option}=${option === "--arg" ? redactAssignment(value) : SENSITIVE_KEY.test(option) ? "<redacted>" : redactUrl(value)}`
91
+ );
68
92
  continue;
69
93
  }
70
94
  if (token.startsWith("--") && SENSITIVE_KEY.test(token)) {
@@ -110,8 +134,10 @@ function tryBeginCommandJournal(command, argv) {
110
134
  writeAtomic(file, record);
111
135
  return { file, record };
112
136
  } catch (error) {
113
- process.stderr.write(`mm-harness: resumability journal unavailable: ${errorMessage(error)}
114
- `);
137
+ process.stderr.write(
138
+ `mm-harness: resumability journal unavailable: ${errorMessage(error)}
139
+ `
140
+ );
115
141
  return void 0;
116
142
  }
117
143
  }
@@ -125,8 +151,10 @@ function tryFinishCommandJournal(handle, exitCode) {
125
151
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
126
152
  });
127
153
  } catch (error) {
128
- process.stderr.write(`mm-harness: resumability journal could not finalize: ${errorMessage(error)}
129
- `);
154
+ process.stderr.write(
155
+ `mm-harness: resumability journal could not finalize: ${errorMessage(error)}
156
+ `
157
+ );
130
158
  }
131
159
  }
132
160
  function writeAtomic(file, record) {
@@ -147,7 +175,9 @@ function writeAtomic(file, record) {
147
175
  }
148
176
  }
149
177
  function invocationTarget(argv) {
150
- return path.resolve(optionValue(argv, "--target") ?? optionValue(argv, "--project-root") ?? process.cwd());
178
+ return path.resolve(
179
+ optionValue(argv, "--target") ?? process.cwd()
180
+ );
151
181
  }
152
182
  function evidencePaths(argv) {
153
183
  const values = /* @__PURE__ */ new Set();
@@ -172,7 +202,7 @@ function optionValue(argv, option) {
172
202
  }
173
203
  function redactAssignment(token) {
174
204
  const equals = token.indexOf("=");
175
- if (equals <= 0) return token;
205
+ if (equals <= 0) return redactValue(token);
176
206
  const key = token.slice(0, equals);
177
207
  const value = token.slice(equals + 1);
178
208
  return `${key}=${SENSITIVE_KEY.test(key) ? "<redacted>" : redactValue(value)}`;
@@ -185,18 +215,21 @@ function redactValue(value) {
185
215
  return JSON.stringify(redactStructuredValue(JSON.parse(value)));
186
216
  } catch {
187
217
  return value.replace(
188
- /((?:auth|credential|mnemonic|pass(?:word)?|private[-_]?key|secret|seed|srp|token)=)[^\s]+/giu,
218
+ /((?:api[-_]?key|auth|credential|mnemonic|pass(?:word)?|private[-_]?key|secret|seed|srp|token|vault)=)[^\s]+/giu,
189
219
  "$1<redacted>"
190
220
  );
191
221
  }
192
222
  }
223
+ function isSensitiveKey(key) {
224
+ return SENSITIVE_KEY.test(key);
225
+ }
193
226
  function redactStructuredValue(value) {
194
227
  if (Array.isArray(value)) return value.map(redactStructuredValue);
195
228
  if (!value || typeof value !== "object") return value;
196
229
  return Object.fromEntries(
197
230
  Object.entries(value).map(([key, entry]) => [
198
231
  key,
199
- SENSITIVE_KEY.test(key) ? "<redacted>" : redactStructuredValue(entry)
232
+ isSensitiveKey(key) ? "<redacted>" : redactStructuredValue(entry)
200
233
  ])
201
234
  );
202
235
  }
@@ -205,7 +238,9 @@ function assertRelativeRuntimeDir(value) {
205
238
  throw new Error(`--runtime-dir must be a safe relative path: ${value}`);
206
239
  }
207
240
  if (value.split("/").some((part) => !part || part === "." || part === "..")) {
208
- throw new Error(`--runtime-dir contains an unsafe path component: ${value}`);
241
+ throw new Error(
242
+ `--runtime-dir contains an unsafe path component: ${value}`
243
+ );
209
244
  }
210
245
  }
211
246
  function isCommandJournalRecord(value) {
@@ -219,7 +254,10 @@ function errorMessage(error) {
219
254
  export {
220
255
  COMMAND_JOURNAL_FILE,
221
256
  commandJournalPath,
257
+ isSensitiveKey,
222
258
  readCommandJournal,
259
+ recordCommandEvidence,
223
260
  redactCommandArgs,
261
+ redactStructuredValue,
224
262
  withCommandJournal
225
263
  };
@@ -3,15 +3,16 @@ import { createHash } from "node:crypto";
3
3
  import path from "node:path";
4
4
  import { resolveActionManifest } from "../manifest.js";
5
5
  import { importRecipeProtocol } from "../paths.js";
6
- import { recipeRunning } from "../heal-bounds.js";
6
+ import { conciseFailureForHuman, recipeRunning } from "../heal-bounds.js";
7
7
  import { color } from "../cli-color.js";
8
8
  import { checkoutBusyOut, EXIT, usageOut } from "./shared.js";
9
- import { describeManifestActions, fuzzyResolveActions } from "./manifest.js";
9
+ import { actionExampleCommand, describeManifestActions, fuzzyResolveActions } from "./manifest.js";
10
10
  import {
11
11
  parseArgs,
12
12
  isRecord,
13
13
  optionFlag,
14
14
  optionString,
15
+ optionStrings,
15
16
  resolveAdapter,
16
17
  runtimeOptionsFromCli,
17
18
  shellQuote,
@@ -35,6 +36,8 @@ import {
35
36
  import { acquireCheckoutLock } from "../checkout-lock.js";
36
37
  import { formatRunDiagnosticsForHuman, readRunDiagnosticsDocument } from "../run-diagnostics.js";
37
38
  import { recipeTrustFailure } from "../recipe-security.js";
39
+ import { isSensitiveKey, recordCommandEvidence, redactStructuredValue } from "../command-journal.js";
40
+ import { closest } from "../command-contract.js";
38
41
  async function handleCall(argv) {
39
42
  if (argv.includes("--list")) {
40
43
  const { options: options2 } = parseArgs(argv, "call");
@@ -53,7 +56,7 @@ async function handleCall(argv) {
53
56
  let discovery = "mm-harness actions";
54
57
  try {
55
58
  const { adapter: adapter2 } = resolveAdapter(options);
56
- const librarySources2 = await resolveMetaMaskLibrarySources(optionString(options, "library"));
59
+ const librarySources2 = await resolveMetaMaskLibrarySources(optionStrings(options, "library"));
57
60
  const { manifest: manifest2 } = await resolveActionManifest(adapter2, optionString(options, "actionManifest"), librarySources2);
58
61
  const { getRecipeActionManifestActionNames: getRecipeActionManifestActionNames2 } = await importRecipeProtocol();
59
62
  const exampleAction = pickCallExampleAction(getRecipeActionManifestActionNames2(manifest2));
@@ -103,8 +106,8 @@ async function handleCall(argv) {
103
106
  return EXIT.bounded;
104
107
  }
105
108
  const actionManifestOverride = optionString(options, "actionManifest");
106
- const librarySources = await resolveMetaMaskLibrarySources(optionString(options, "library"));
107
- const { manifest } = await resolveActionManifest(adapter, actionManifestOverride, librarySources);
109
+ const librarySources = await resolveMetaMaskLibrarySources(optionStrings(options, "library"));
110
+ const { manifest, actionSources } = await resolveActionManifest(adapter, actionManifestOverride, librarySources);
108
111
  const { getRecipeActionManifestActionNames } = await importRecipeProtocol();
109
112
  const names = getRecipeActionManifestActionNames(manifest);
110
113
  const resolution = resolveActionName(shortName, names);
@@ -125,6 +128,8 @@ async function handleCall(argv) {
125
128
  return EXIT.usage;
126
129
  }
127
130
  const resolvedAction = resolution.resolved;
131
+ const describedAction = describeManifestActions(manifest, actionSources).find((entry) => entry.name === resolvedAction);
132
+ const defaultsUsed = actionDefaultsUsed(describedAction, args);
128
133
  const depsBlock = adapter === "core" && resolvedAction.startsWith("metamask.perps.") ? coreDependencyBlock(target) : null;
129
134
  if (depsBlock) {
130
135
  if (json) {
@@ -136,24 +141,39 @@ async function handleCall(argv) {
136
141
  return EXIT.usage;
137
142
  }
138
143
  const recipe = synthesizeOneNodeRecipe(resolvedAction, args);
139
- const validation = await validateRecipeAdapterAware(recipe, manifest);
144
+ const validation = await validateRecipeAdapterAware(adapter, recipe, manifest, librarySources);
140
145
  if (validation.status === "invalid") {
141
146
  const message = `call ${resolvedAction}: recipe validation failed`;
142
- const userAction = `mm-harness actions --action ${resolvedAction} --adapter ${adapter} --json`;
143
- if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "call", adapter, action: shortName, resolvedAction, args: redactCallValue(args), findings: validation.findings, error: { code: "RECIPE_VALIDATION_FAILED", message, userAction } }, null, 2));
147
+ const parameterHelp = parameterValidationHelp(describedAction, validation.findings, args);
148
+ const userAction = describedAction ? actionExampleCommand(
149
+ describedAction,
150
+ adapter,
151
+ target,
152
+ process.env.MM_HARNESS_INVOKED_AS ?? process.env.MM_HARNESS_EXECUTABLE ?? "mm-harness"
153
+ ) ?? `mm-harness actions --action ${resolvedAction} --adapter ${adapter}` : `mm-harness actions --action ${resolvedAction} --adapter ${adapter}`;
154
+ if (json) console.log(JSON.stringify({ schemaVersion: 1, command: "call", adapter, action: shortName, resolvedAction, args: redactCallValue(args), findings: validation.findings, ...parameterHelp.length > 0 ? { parameterHelp } : {}, error: { code: "RECIPE_VALIDATION_FAILED", message, userAction } }, null, 2));
144
155
  else {
145
156
  console.error(`\u2717 ${message}`);
146
- for (const finding of validation.findings) console.error(` ${finding.code} ${finding.path} \u2014 ${finding.message}`);
157
+ const taught = new Set(parameterHelp.map((entry) => `${entry.issue}:${entry.name}`));
158
+ for (const entry of parameterHelp) console.error(renderParameterValidationHelp(entry));
159
+ for (const finding of validation.findings) {
160
+ const name = finding.path.split(".").at(-1);
161
+ const issue = parameterIssue(finding.code);
162
+ if (issue && name && taught.has(`${issue}:${name}`)) continue;
163
+ console.error(` ${finding.code} ${finding.path} \u2014 ${finding.message}`);
164
+ }
147
165
  console.error(` Next: ${userAction}`);
148
166
  }
149
167
  return EXIT.validation;
150
168
  }
151
169
  const artifactsDir = optionString(options, "artifactsDir") ?? defaultCallArtifactsDir(target, resolvedAction, args);
170
+ recordCommandEvidence(artifactsDir);
152
171
  const requestedRuntimeOptions = runtimeOptionsFromCli(options);
153
172
  const callRuntimeOptions = {
154
173
  ...requestedRuntimeOptions,
155
174
  ...librarySources ? { librarySources } : {},
156
175
  autoHud: false,
176
+ suppressLibraryResolutionLogs: true,
157
177
  stdoutIsMachineContract: json,
158
178
  source: requestedRuntimeOptions.source ?? {
159
179
  kind: "operator",
@@ -233,7 +253,17 @@ async function handleCall(argv) {
233
253
  state,
234
254
  () => recoverRunInfra(adapter, target, json)
235
255
  );
236
- if (violation !== null) return emitHealViolation(json, "call", result, violation, state, adapter);
256
+ if (violation !== null) {
257
+ const conciseFailure = violation.originalError ? conciseFailureForHuman(violation.originalError) : "";
258
+ const example = describedAction ? actionExampleCommand(
259
+ describedAction,
260
+ adapter,
261
+ target,
262
+ process.env.MM_HARNESS_INVOKED_AS ?? process.env.MM_HARNESS_EXECUTABLE ?? "mm-harness"
263
+ ) : void 0;
264
+ const taughtViolation = violation.code === "APP_LOGIC_FAILURE" && conciseFailure.includes(" requires ") && example ? { ...violation, userAction: example } : violation;
265
+ return emitHealViolation(json, "call", result, taughtViolation, state, adapter);
266
+ }
237
267
  const callOutput = readCallOutput(result.tracePath);
238
268
  const safeArgs = redactCallValue(args);
239
269
  const safeCallOutput = redactCallValue(callOutput);
@@ -248,6 +278,7 @@ async function handleCall(argv) {
248
278
  action: shortName,
249
279
  resolvedAction,
250
280
  args: safeArgs,
281
+ ...Object.keys(defaultsUsed).length > 0 ? { defaultsUsed } : {},
251
282
  status: result.status,
252
283
  summaryPath: result.summaryPath,
253
284
  tracePath: result.tracePath,
@@ -277,7 +308,8 @@ ${diagnostics.map((line) => ` ${line}`).join("\n")}
277
308
  console.log(
278
309
  `${out("label", "call")} ${out("cmd", resolvedAction)}: ${out(result.status === "pass" ? "ok" : "err", result.status)}${renderedInputs ? `
279
310
  ${out("label", "Inputs:")}
280
- ${renderedInputs}` : ""}${callOutput !== void 0 ? `
311
+ ${renderedInputs}` : ""}${Object.keys(defaultsUsed).length > 0 ? `
312
+ ${renderDefaultsUsed(defaultsUsed, process.stdout)}` : ""}${callOutput !== void 0 ? `
281
313
  ${out("label", "Result:")}
282
314
  ${formatCallOutput(safeCallOutput)}` : ""}
283
315
  ` + renderedDiagnostics + `${out("label", "Artifacts:")} ${out("path", result.artifactManifestPath)}` + (result.status === "fail" ? `
@@ -289,6 +321,54 @@ ${formatCallOutput(safeCallOutput)}` : ""}
289
321
  lock.release();
290
322
  }
291
323
  }
324
+ function parameterValidationHelp(action, findings, args) {
325
+ const schema = isRecord(action?.schema) ? action.schema : {};
326
+ const properties = isRecord(schema.properties) ? schema.properties : {};
327
+ return findings.flatMap((finding) => {
328
+ const issue = parameterIssue(finding.code);
329
+ if (!issue) return [];
330
+ const name = finding.path.split(".").at(-1);
331
+ const property = name && isRecord(properties[name]) ? properties[name] : void 0;
332
+ if (!name || !property) return [];
333
+ const validValues = Array.isArray(property.enum) ? property.enum : void 0;
334
+ const received = Object.hasOwn(args, name) ? args[name] : void 0;
335
+ const suggestion = issue === "invalid" && received !== void 0 && validValues ? closest(String(received), validValues.map(String)) : void 0;
336
+ return [{
337
+ issue,
338
+ name,
339
+ ...typeof property.type === "string" ? { type: property.type } : {},
340
+ ...validValues ? { validValues } : {},
341
+ ...typeof property.description === "string" ? { description: property.description } : {},
342
+ ...issue === "invalid" && received !== void 0 ? { received } : {},
343
+ ...suggestion ? { suggestion } : {}
344
+ }];
345
+ });
346
+ }
347
+ function parameterIssue(code) {
348
+ if (code === "recipe.missing_param") return "missing";
349
+ if (code === "recipe.invalid_param_value_enum") return "invalid";
350
+ return void 0;
351
+ }
352
+ function renderParameterValidationHelp(parameter) {
353
+ const out = (style, text) => color(style, text, { stream: process.stderr });
354
+ const heading = parameter.issue === "missing" ? `${out("cmd", parameter.name)}${parameter.type ? ` (${parameter.type}, required)` : " (required)"}` : `${out("cmd", parameter.name)} received ${out("err", JSON.stringify(parameter.received) ?? String(parameter.received))}`;
355
+ const values = parameter.validValues?.length ? ` Valid values: ${parameter.validValues.map((value) => out("accent", String(value))).join(", ")}.` : "";
356
+ const suggestion = parameter.suggestion ? ` Did you mean ${out("accent", JSON.stringify(parameter.suggestion))}?` : "";
357
+ const description = parameter.description ? ` ${parameter.description}` : "";
358
+ return ` ${heading}.${values}${suggestion}${description}`;
359
+ }
360
+ function actionDefaultsUsed(action, args) {
361
+ const schema = isRecord(action?.schema) ? action.schema : {};
362
+ const properties = isRecord(schema.properties) ? schema.properties : {};
363
+ return Object.fromEntries(
364
+ Object.entries(properties).filter(([name, entry]) => !Object.hasOwn(args, name) && isRecord(entry) && Object.hasOwn(entry, "default")).map(([name, entry]) => [name, entry.default])
365
+ );
366
+ }
367
+ function renderDefaultsUsed(defaults, stream) {
368
+ const out = (style, text) => color(style, text, { stream });
369
+ const values = Object.entries(defaults).map(([name, value]) => `${out("cmd", name)}=${out("accent", JSON.stringify(value) ?? String(value))}`).join(", ");
370
+ return `${out("label", "Defaults used:")} ${values}`;
371
+ }
292
372
  function defaultCallArtifactsDir(target, action, args) {
293
373
  const actionStem = action.replace(/[^a-zA-Z0-9._-]/gu, "_");
294
374
  const digest = createHash("sha256").update(JSON.stringify({ action, args })).digest("hex").slice(0, 12);
@@ -310,12 +390,7 @@ function formatCallOutput(value) {
310
390
  return JSON.stringify(value, null, 2);
311
391
  }
312
392
  function redactCallValue(value, key = "") {
313
- if (/(?:password|secret|mnemonic|seed|srp|vault|private[_-]?key)/iu.test(key)) return "<redacted>";
314
- if (Array.isArray(value)) return value.map((entry) => redactCallValue(entry));
315
- if (isRecord(value)) {
316
- return Object.fromEntries(Object.entries(value).map(([entryKey, entry]) => [entryKey, redactCallValue(entry, entryKey)]));
317
- }
318
- return value;
393
+ return key && isSensitiveKey(key) ? "<redacted>" : redactStructuredValue(value);
319
394
  }
320
395
  async function handleCallHelp(argv, genericHelp) {
321
396
  const { action: shortName, rest } = parseCallArgs(argv.filter((arg) => arg !== "--help" && arg !== "-h"));
@@ -325,7 +400,7 @@ async function handleCallHelp(argv, genericHelp) {
325
400
  let actionSources = /* @__PURE__ */ new Map();
326
401
  try {
327
402
  ({ adapter } = resolveAdapter(options));
328
- const librarySources = await resolveMetaMaskLibrarySources(optionString(options, "library"));
403
+ const librarySources = await resolveMetaMaskLibrarySources(optionStrings(options, "library"));
329
404
  const resolution = await resolveActionManifest(adapter, optionString(options, "actionManifest"), librarySources);
330
405
  manifest = resolution.manifest;
331
406
  actionSources = resolution.actionSources;
@@ -375,7 +450,8 @@ function renderCallActionHelp(entry) {
375
450
  const req = required.has(name) ? " (required)" : "";
376
451
  const desc = typeof prop.description === "string" ? ` \u2014 ${prop.description}` : "";
377
452
  const enumVals = Array.isArray(prop.enum) ? ` [one of: ${prop.enum.join(", ")}]` : "";
378
- lines.push(` ${name.padEnd(width)} ${type}${req}${desc}${enumVals}`);
453
+ const defaultValue = Object.hasOwn(prop, "default") ? ` [default: ${JSON.stringify(prop.default)}]` : "";
454
+ lines.push(` ${name.padEnd(width)} ${type}${req}${defaultValue}${desc}${enumVals}`);
379
455
  }
380
456
  }
381
457
  const examples = renderCallExamples(short, entry.examples);
@@ -462,7 +538,6 @@ const VALUE_TAKING_CALL_FLAGS = /* @__PURE__ */ new Set([
462
538
  "library",
463
539
  "metro-port",
464
540
  "platform",
465
- "project-root",
466
541
  "slot",
467
542
  "source-digest",
468
543
  "source-kind",
@@ -18,7 +18,7 @@ async function handleCheck(argv) {
18
18
  const message = action ? `unknown action '${action}'` : "missing action";
19
19
  return usageOut(json, "check", message, USAGE);
20
20
  }
21
- const target = path.resolve(optionString(options, "target") ?? optionString(options, "projectRoot") ?? process.cwd());
21
+ const target = path.resolve(optionString(options, "target") ?? process.cwd());
22
22
  const explicitAdapter = optionString(options, "adapter") ?? optionString(options, "platform");
23
23
  const detected = explicitAdapter ?? detectAdapter(target);
24
24
  if (detected && !["mobile", "extension", "core"].includes(detected)) {
@@ -4,12 +4,13 @@ import {
4
4
  readFreshCandidates,
5
5
  writeCompletionCandidates
6
6
  } from "../completions-cache.js";
7
- import { importRecipeHarness, importRecipeProtocol } from "../paths.js";
7
+ import { importRecipeProtocol } from "../paths.js";
8
8
  import { EXIT } from "./shared.js";
9
9
  import {
10
10
  parseArgs,
11
11
  optionFlag,
12
12
  optionString,
13
+ optionStrings,
13
14
  resolveAdapter,
14
15
  targetPath
15
16
  } from "./parse-args.js";
@@ -18,8 +19,8 @@ async function handleCompletionCandidates(argv) {
18
19
  const kind = argv[0];
19
20
  const { options } = parseArgs(argv.slice(1), "completion-candidates");
20
21
  const json = optionFlag(options, "json");
21
- if (kind !== "actions" && kind !== "flows" && kind !== "recipes") {
22
- console.error("completion-candidates requires <actions|flows|recipes>.");
22
+ if (kind !== "actions" && kind !== "recipes") {
23
+ console.error("completion-candidates requires <actions|recipes>.");
23
24
  return EXIT.usage;
24
25
  }
25
26
  const target = targetPath(options);
@@ -27,31 +28,18 @@ async function handleCompletionCandidates(argv) {
27
28
  let candidates;
28
29
  if (kind === "actions") {
29
30
  adapter = resolveAdapter(options).adapter;
30
- const sources = await resolveMetaMaskLibrarySources(optionString(options, "library"));
31
+ const sources = await resolveMetaMaskLibrarySources(optionStrings(options, "library"));
31
32
  const { manifest } = await resolveActionManifest(adapter, optionString(options, "actionManifest"), sources);
32
33
  const { getRecipeActionManifestActionNames } = await importRecipeProtocol();
33
34
  candidates = getRecipeActionManifestActionNames(manifest);
34
35
  writeCompletionCandidates(target, "actions", candidates);
35
- } else if (kind === "flows") {
36
- candidates = readFreshCandidates(target, "flows");
37
- if (!candidates) {
38
- const sources = await resolveMetaMaskLibrarySources(optionString(options, "library")).catch(() => void 0);
39
- if (!sources) {
40
- candidates = [];
41
- } else {
42
- const harness = await importRecipeHarness();
43
- const resolution = await harness.loadRecipeLibraries(sources);
44
- candidates = [...resolution.flows.keys()].sort();
45
- writeCompletionCandidates(target, "flows", candidates);
46
- }
47
- }
48
36
  } else {
49
37
  adapter = resolveAdapter(options).adapter;
50
38
  const cacheKey = `recipes:${adapter}`;
51
39
  candidates = readFreshCandidates(target, cacheKey);
52
40
  if (!candidates) {
53
- const sources = await resolveMetaMaskLibrarySources(optionString(options, "library")).catch(() => void 0);
54
- candidates = sources ? listRunnableRecipes(adapter, sources).map((recipe) => recipe.name) : [];
41
+ const sources = await resolveMetaMaskLibrarySources(optionStrings(options, "library")).catch(() => void 0);
42
+ candidates = sources ? (await listRunnableRecipes(adapter, sources)).map((recipe) => recipe.name) : [];
55
43
  writeCompletionCandidates(target, cacheKey, candidates);
56
44
  }
57
45
  }
@@ -11,7 +11,8 @@ import { ADAPTER_DETECT_NEXT, checkoutBusyOut, EXIT, flag, parseFlags, resolveAd
11
11
  const FIXTURES_BOOLEANS = /* @__PURE__ */ new Set(["dev", "force", "json"]);
12
12
  const FIXTURE_STREAM_TIMEOUT_MS = 12e4;
13
13
  async function handleFixtures(argv) {
14
- const { options } = parseFlags(argv, FIXTURES_BOOLEANS);
14
+ const { positional, options } = parseFlags(argv, FIXTURES_BOOLEANS);
15
+ if (!positional[0]) return fixturesStatus(options);
15
16
  const json = flag(options, "json");
16
17
  const target = targetOf(options);
17
18
  if (!fs.existsSync(target)) return handleFixturesLocked(argv);
@@ -25,6 +26,54 @@ async function handleFixtures(argv) {
25
26
  lock.release();
26
27
  }
27
28
  }
29
+ function fixturesStatus(options) {
30
+ const json = flag(options, "json");
31
+ const statusOptions = /* @__PURE__ */ new Set(["json", "target", "adapter", "platform", "device"]);
32
+ const actionOption = Object.keys(options).find((key) => !statusOptions.has(key));
33
+ if (actionOption) {
34
+ return usageOut(
35
+ json,
36
+ "fixtures",
37
+ `--${actionOption.replace(/[A-Z]/gu, (letter) => `-${letter.toLowerCase()}`)} requires a fixture action.`,
38
+ "mm-harness fixtures --help"
39
+ );
40
+ }
41
+ const target = targetOf(options);
42
+ const adapter = resolveAdapter(options, target);
43
+ if (!adapter) {
44
+ return usageOut(json, "fixtures", `could not detect the MetaMask repo type for ${target}`, ADAPTER_DETECT_NEXT);
45
+ }
46
+ const canonical = walletFixturePath(target);
47
+ const summary = fixtureSummary(target, adapter);
48
+ const fixture = { ...summary, path: canonical };
49
+ const ready = summary.status === "ready";
50
+ const nextCommand = ready ? "mm-harness fixtures set" : "mm-harness fixtures init --from <path> # or: mm-harness fixtures init --dev";
51
+ const operations = adapter === "extension" ? ["init", "sync", "set", "generate", "finalize"] : ["init", "sync", "set"];
52
+ if (json) {
53
+ console.log(JSON.stringify({
54
+ schemaVersion: 1,
55
+ command: "fixtures",
56
+ action: "status",
57
+ adapter,
58
+ target,
59
+ status: "pass",
60
+ fixture,
61
+ operations,
62
+ nextCommand,
63
+ exitCode: EXIT.ok
64
+ }, null, 2));
65
+ return EXIT.ok;
66
+ }
67
+ console.log(`wallet fixture (${adapter}): ${String(summary.status).toUpperCase()}`);
68
+ console.log(`path: ${canonical}`);
69
+ if (typeof summary.accountCount === "number") {
70
+ const password = summary.hasPassword === true ? "present" : "missing";
71
+ console.log(`accounts: ${summary.accountCount} \xB7 password: ${password}`);
72
+ }
73
+ console.log(`operations: ${operations.join(", ")}`);
74
+ console.log(`Next: ${nextCommand}`);
75
+ return EXIT.ok;
76
+ }
28
77
  function ensureFixturePublicationParent(target) {
29
78
  const repoRoot = fs.realpathSync(target);
30
79
  const parent = path.dirname(walletFixturePath(repoRoot));
@@ -3,6 +3,7 @@ import { EXIT } from "./shared.js";
3
3
  import {
4
4
  optionFlag,
5
5
  optionString,
6
+ shellQuote,
6
7
  targetPath
7
8
  } from "./parse-args.js";
8
9
  async function handleLast({ options }) {
@@ -41,9 +42,16 @@ async function handleLast({ options }) {
41
42
  } else {
42
43
  const end = record.finishedAt ?? "still running or interrupted";
43
44
  console.log(`${record.verdict.toUpperCase()} ${record.command} (exit ${record.exitCode ?? "pending"})`);
45
+ console.log(`command: mm-harness ${record.command}${record.args.length > 0 ? ` ${record.args.map(shellQuote).join(" ")}` : ""}`);
44
46
  console.log(`started: ${record.startedAt}`);
45
47
  console.log(`finished: ${end}`);
46
- if (record.evidencePaths.length > 0) console.log(`evidence: ${record.evidencePaths.join(", ")}`);
48
+ if (record.evidencePaths.length > 0) {
49
+ console.log("evidence:");
50
+ for (const evidence of record.evidencePaths) console.log(` ${evidence}`);
51
+ } else {
52
+ console.log("evidence: none recorded");
53
+ }
54
+ console.log(`journal: ${file}`);
47
55
  }
48
56
  return EXIT.ok;
49
57
  }
@@ -9,7 +9,7 @@ import { extensionIdFromKey } from "../../adapters/extension/extension-id.js";
9
9
  import { extensionProductConfigBlock } from "../../adapters/extension/product-config.js";
10
10
  import { isExtensionDistStale } from "../../adapters/extension/runtime-decision.js";
11
11
  import { checkExtensionRuntimeHealth } from "../../adapters/extension/runtime.js";
12
- import { ensureExtensionHarnessFresh } from "../../adapters/extension/harness-freshness.js";
12
+ import { ensureHarnessFresh } from "../../adapters/harness-freshness.js";
13
13
  import { stopExtensionWatcher } from "../../adapters/slot-ports.js";
14
14
  import { EXIT, spawnScriptStreaming } from "../shared.js";
15
15
  function extensionDepsBlock(target) {
@@ -46,7 +46,7 @@ Next: ${block.userAction}`
46
46
  };
47
47
  }
48
48
  }
49
- await ensureExtensionHarnessFresh(target);
49
+ await ensureHarnessFresh(target, "extension");
50
50
  if (wantWatch) {
51
51
  const startWatchSh = path.join(runnerDir, "adapters/extension/start-watch.sh");
52
52
  const watchArgs = ["--target", target];
@@ -1,5 +1,7 @@
1
1
  import { prepareMobile } from "../../adapters/mobile/prepare.js";
2
+ import { ensureHarnessFresh } from "../../adapters/harness-freshness.js";
2
3
  async function launchMobile(target, mobileTarget, tier, json) {
4
+ await ensureHarnessFresh(target, "mobile");
3
5
  const platform = mobileTarget ?? "ios";
4
6
  const watcherPort = process.env.WATCHER_PORT ? parseInt(process.env.WATCHER_PORT, 10) : void 0;
5
7
  const preflightMode = tier === "build" ? "auto" : "fast";