@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.
- package/CHANGELOG.md +187 -0
- package/README.md +4 -7
- package/adapters/core/inject.sh +1 -6
- package/adapters/extension/inject.mjs +1 -4
- package/adapters/extension/verify.sh +1 -1
- package/adapters/mobile/inject.sh +5 -4
- package/adapters/mobile/verify.sh +15 -19
- package/adapters/shared/harness-source-fingerprint.mjs +14 -12
- package/dist/adapters/harness-freshness.js +49 -0
- package/dist/cli.js +0 -4
- package/dist/command-contract.js +51 -40
- package/dist/command-journal.js +50 -12
- package/dist/commands/call.js +95 -20
- package/dist/commands/check.js +1 -1
- package/dist/commands/completion-candidates.js +7 -19
- package/dist/commands/fixtures.js +50 -1
- package/dist/commands/last.js +9 -1
- package/dist/commands/launch/extension.js +2 -2
- package/dist/commands/launch/mobile.js +2 -0
- package/dist/commands/list-executables.js +56 -20
- package/dist/commands/manifest.js +28 -12
- package/dist/commands/parse-args.js +55 -3
- package/dist/commands/provision.js +0 -1
- package/dist/commands/run-engine.js +514 -336
- package/dist/commands/run.js +25 -37
- package/dist/commands/shared.js +1 -1
- package/dist/heal-bounds.js +5 -0
- package/dist/mm-harness-cli.js +22 -30
- package/dist/run-diagnostics.js +1 -1
- package/dist/run-recording.js +1 -1
- package/dist/runner.js +24 -2
- package/docs/CONTRIBUTING.md +2 -3
- package/docs/QA.md +0 -1
- package/docs/RECIPES.md +52 -91
- package/library/README.md +5 -5
- package/library/actions/core/perps/_controller.mjs +33 -1
- package/library/actions/core/perps/assert_orders.mjs +6 -7
- package/library/actions/core/perps/assert_positions.mjs +6 -7
- package/library/actions/core/perps/close_orders.mjs +2 -0
- package/library/actions/core/perps/close_positions.mjs +2 -0
- package/library/actions/core/perps/ensure_orders.mjs +4 -2
- package/library/actions/core/perps/ensure_positions.mjs +4 -2
- package/library/actions/core/perps/place_order.mjs +7 -3
- package/library/actions/extension/perps/assert_orders.mjs +2 -1
- package/library/actions/extension/perps/assert_positions.mjs +2 -1
- package/library/actions/extension/perps/perps.mjs +43 -14
- package/library/actions/mobile/perps/assert_orders.mjs +2 -1
- package/library/actions/mobile/perps/assert_positions.mjs +2 -1
- package/library/actions/mobile/perps/perps.mjs +40 -12
- package/library/library.json +1 -1
- package/library/manifests/core.action-manifest.json +1170 -413
- package/library/manifests/extension.action-manifest.json +1495 -641
- package/library/manifests/mobile.action-manifest.json +1704 -769
- package/library/recipes/app/lifecycle.android-smoke.mobile.recipe.json +63 -81
- package/library/recipes/perps/clean-market-testnet.core.recipe.json +44 -0
- package/library/recipes/perps/clean-market-testnet.recipe.json +49 -0
- package/library/recipes/perps/lifecycle.recipe.json +136 -180
- package/library/recipes/perps/order-lifecycle.core.recipe.json +71 -67
- package/library/recipes/perps/performance.background-resume.mobile.recipe.json +51 -67
- package/library/recipes/perps/performance.cold-start.mobile.recipe.json +51 -67
- package/library/recipes/perps/performance.mobile.recipe.json +37 -51
- package/library/recipes/perps/performance.warm-start.mobile.recipe.json +44 -59
- package/library/recipes/perps/read-markets.core.recipe.json +29 -31
- package/library/recipes/perps/smoke.core.recipe.json +29 -32
- package/library/recipes/perps/smoke.extension.recipe.json +41 -44
- package/library/recipes/perps/smoke.mobile.recipe.json +42 -44
- package/library/recipes/perps/trading-lifecycle.core.recipe.json +69 -65
- package/library/recipes/runner/action-validation.extension.recipe.json +312 -405
- package/library/recipes/runner/action-validation.mobile.recipe.json +316 -409
- package/library/recipes/runner/smoke.core.recipe.json +18 -20
- package/library/recipes/runner/smoke.extension.recipe.json +23 -24
- package/library/recipes/runner/smoke.mobile.recipe.json +23 -24
- package/library/recipes/wallet/smoke.extension.recipe.json +33 -35
- package/library/recipes/wallet/smoke.mobile.recipe.json +33 -35
- package/package.json +3 -3
- package/scripts/completions.sh +1 -4
- package/dist/adapters/extension/harness-freshness.js +0 -39
- package/dist/commands/flows.js +0 -91
- package/library/flows/perps.flows.json +0 -64
package/dist/command-journal.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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
|
};
|
package/dist/commands/call.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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
|
|
143
|
-
|
|
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
|
-
|
|
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)
|
|
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}` : ""}${
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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",
|
package/dist/commands/check.js
CHANGED
|
@@ -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") ??
|
|
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 {
|
|
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 !== "
|
|
22
|
-
console.error("completion-candidates requires <actions|
|
|
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(
|
|
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(
|
|
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));
|
package/dist/commands/last.js
CHANGED
|
@@ -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)
|
|
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 {
|
|
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
|
|
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";
|