@deeeed/metamask-harness 0.17.5 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +872 -0
- package/README.md +94 -140
- package/adapters/extension/console-tail.mjs +55 -24
- package/adapters/extension/ensure-browser.sh +6 -1
- package/adapters/extension/inject.mjs +6 -9
- package/adapters/extension/launch-browser.cjs +7 -1
- package/adapters/extension/launch-webpack.cjs +83 -0
- package/adapters/extension/launch.sh +8 -11
- package/adapters/extension/lib/chrome-args.cjs +8 -1
- package/adapters/extension/lib/macos-focus.cjs +32 -0
- package/adapters/extension/live.sh +12 -21
- package/adapters/extension/reattach.sh +1 -0
- package/adapters/extension/seed-fixture.sh +4 -12
- package/adapters/extension/sidepanel-toggle.sh +4 -1
- package/adapters/extension/stamp-runtime-title.cjs +58 -0
- package/adapters/extension/start-watch.sh +17 -4
- package/adapters/extension/stop-viewers.sh +1 -1
- package/adapters/extension/sync-webpack-dist.cjs +107 -0
- package/adapters/extension/verify.sh +1 -1
- package/adapters/manifest.json +53 -37
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +16 -0
- package/adapters/mobile/bridge-runtime/console-forwarder.cjs +1 -1
- package/adapters/mobile/bridge-runtime/lib/match-bridge-target.cjs +3 -1
- package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +2 -3
- package/adapters/mobile/cleanup.sh +16 -176
- package/adapters/mobile/inject.sh +90 -671
- package/adapters/mobile/launch-console-forwarder.cjs +39 -0
- package/adapters/mobile/open-device.sh +45 -7
- package/adapters/mobile/start-console-forwarder.sh +70 -0
- package/adapters/mobile/start-metro.sh +0 -41
- package/adapters/mobile/verify.sh +19 -64
- package/adapters/mobile/wait-for-bridge.sh +22 -8
- package/adapters/shared/resolve-slot-ports-core.mjs +17 -6
- package/adapters/shared/resolve-slot-ports.sh +17 -5
- package/adapters/shared/update-check-worker.mjs +43 -0
- package/bin/mm-harness +21 -2
- package/dist/adapters/core/surface.js +4 -1
- package/dist/adapters/extension/console-capture.js +95 -0
- package/dist/adapters/extension/product-config.js +110 -0
- package/dist/adapters/extension/runtime-decision.js +20 -2
- package/dist/adapters/extension/surface.js +22 -3
- package/dist/adapters/mobile/perps-env.js +43 -3
- package/dist/adapters/mobile/prepare.js +39 -7
- package/dist/adapters/mobile/surface.js +5 -2
- package/dist/adapters/resolve-slot-ports.js +2 -2
- package/dist/adapters/slot-ports.js +13 -16
- package/dist/adapters.js +56 -16
- package/dist/checkout-lock.js +27 -2
- package/dist/cli-color.js +19 -0
- package/dist/cli-commands.js +1 -1
- package/dist/cli.js +7 -14
- package/dist/command-contract.js +451 -0
- package/dist/command-journal.js +225 -0
- package/dist/commands/call.js +170 -50
- package/dist/commands/check.js +9 -3
- package/dist/commands/completion-candidates.js +20 -13
- package/dist/commands/device-target.js +27 -12
- package/dist/commands/doctor.js +106 -25
- package/dist/commands/fixtures.js +92 -34
- package/dist/commands/flows.js +39 -10
- package/dist/commands/last.js +52 -0
- package/dist/commands/launch/extension.js +38 -15
- package/dist/commands/launch/index.js +207 -63
- package/dist/commands/list-executables.js +151 -29
- package/dist/commands/logs.js +8 -6
- package/dist/commands/manifest.js +270 -35
- package/dist/commands/parse-args.js +13 -1
- package/dist/commands/provision.js +10 -3
- package/dist/commands/run-engine.js +435 -105
- package/dist/commands/run-report.js +12 -3
- package/dist/commands/run.js +355 -50
- package/dist/commands/shared.js +75 -3
- package/dist/commands/status-probe.js +4 -1
- package/dist/commands/status.js +2 -1
- package/dist/commands/stop.js +7 -2
- package/dist/commands/update.js +16 -28
- package/dist/completions-cache.js +1 -1
- package/dist/doctor.js +57 -10
- package/dist/harness.js +47 -137
- package/dist/heal-bounds.js +2 -2
- package/dist/json-stream.js +57 -0
- package/dist/live-adapter-contract.js +138 -32
- package/dist/manifest.js +161 -1
- package/dist/mm-harness-cli.js +135 -54
- package/dist/paths.js +2 -5
- package/dist/recipe-security.js +178 -0
- package/dist/run-diagnostics.js +261 -0
- package/dist/runner.js +117 -8
- package/docs/CONTRIBUTING.md +137 -0
- package/docs/QA.md +185 -0
- package/docs/RECIPES.md +161 -0
- package/docs/SECURITY.md +88 -0
- package/library/README.md +4 -0
- package/library/actions/core/perps/_controller.mjs +10 -55
- package/library/actions/core/perps/read_account.mjs +2 -2
- package/library/actions/core/perps/read_orders.mjs +2 -1
- package/library/actions/core/perps/read_positions.mjs +2 -1
- package/library/actions/core/wallet/list_accounts.mjs +95 -0
- package/library/actions/extension/platform/cdp.mjs +1 -0
- package/library/actions/extension/wallet/list_accounts.mjs +41 -0
- package/library/actions/mobile/wallet/list_accounts.mjs +37 -0
- package/library/manifests/core.action-manifest.json +68 -7
- package/library/manifests/extension.action-manifest.json +53 -0
- package/library/manifests/mobile.action-manifest.json +81 -3
- package/library/recipes/runner/action-validation.extension.recipe.json +8 -1
- package/library/recipes/runner/action-validation.mobile.recipe.json +8 -1
- package/library/recipes/runner/smoke.core.recipe.json +27 -0
- package/library/recipes/wallet/smoke.extension.recipe.json +42 -0
- package/library/recipes/wallet/smoke.mobile.recipe.json +42 -0
- package/package.json +12 -5
- package/scripts/completions.sh +7 -7
- package/scripts/validate-human-outcomes.mjs +169 -0
- package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +0 -185
- package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +0 -1662
- package/adapters/mobile/overlay/compat/README.md +0 -28
- package/adapters/mobile/overlay/compat/rn81-message-event-source.patch +0 -42
- package/adapters/shared/resolve-farmslot-ports-core.mjs +0 -3
- package/adapters/shared/resolve-farmslot-ports.mjs +0 -5
- package/adapters/shared/resolve-farmslot-ports.sh +0 -7
- package/docs/ADAPTER-SURFACE.md +0 -119
- package/docs/CHEATSHEET.md +0 -61
- package/docs/CLI-SPEC.md +0 -1098
- package/docs/CODE-MAP.md +0 -62
- package/docs/DEBUG-HANDOVER.md +0 -36
- package/docs/MENTAL-MODEL.md +0 -295
- package/docs/UX-PRINCIPLES.md +0 -64
- package/docs/architecture.md +0 -398
- package/docs/live-adapter-contract.md +0 -188
- package/docs/package-boundaries.md +0 -47
- package/docs/perps-flow-catalog.md +0 -235
- package/docs/recipe-libraries.md +0 -301
- package/docs/runtime-file-conventions.md +0 -36
|
@@ -9,8 +9,9 @@ import {
|
|
|
9
9
|
recipeRunning,
|
|
10
10
|
checkHealBounds
|
|
11
11
|
} from "../heal-bounds.js";
|
|
12
|
-
import {
|
|
12
|
+
import { resolveActionManifest, validateManifest } from "../manifest.js";
|
|
13
13
|
import { getAdapterSurface } from "../adapters/surface.js";
|
|
14
|
+
import { extensionProductConfigBlock } from "../adapters/extension/product-config.js";
|
|
14
15
|
import {
|
|
15
16
|
importRecipeHarness,
|
|
16
17
|
importRecipeProtocol,
|
|
@@ -20,56 +21,51 @@ import {
|
|
|
20
21
|
import { captureHelperSupportsRecordSessionSnapshots } from "../recording-target.js";
|
|
21
22
|
import { listRecipeFiles } from "../recipe-files.js";
|
|
22
23
|
import { startRecipeRecording, stopRecipeRecording } from "../run-recording.js";
|
|
24
|
+
import {
|
|
25
|
+
beginRunDiagnostics,
|
|
26
|
+
finishRunDiagnostics
|
|
27
|
+
} from "../run-diagnostics.js";
|
|
23
28
|
import { EXIT, spawnScriptStreaming } from "./shared.js";
|
|
24
29
|
import {
|
|
25
30
|
actionManifestPathOption,
|
|
26
31
|
optionString,
|
|
27
32
|
isRecord,
|
|
28
33
|
shellQuoteArg,
|
|
34
|
+
targetPath,
|
|
29
35
|
usageError
|
|
30
36
|
} from "./parse-args.js";
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const previousExtensionAutolaunch = process.env.METAMASK_RECIPE_EXTENSION_AUTOLAUNCH;
|
|
37
|
-
getAdapterSurface(adapter).resolveSlotPorts(projectRoot);
|
|
38
|
-
if (runtimeOptions.cdpPort) {
|
|
39
|
-
process.env.CDP_PORT = runtimeOptions.cdpPort;
|
|
40
|
-
process.env.RECIPE_CDP_PORT = runtimeOptions.cdpPort;
|
|
41
|
-
}
|
|
42
|
-
if (runtimeOptions.watcherPort) {
|
|
43
|
-
process.env.WATCHER_PORT = runtimeOptions.watcherPort;
|
|
44
|
-
process.env.METRO_PORT = runtimeOptions.watcherPort;
|
|
45
|
-
}
|
|
46
|
-
if (runtimeOptions.launchExistingDist) {
|
|
47
|
-
process.env.METAMASK_RECIPE_EXTENSION_AUTOLAUNCH = "1";
|
|
37
|
+
function requireRecipeTrustResolver(harness) {
|
|
38
|
+
if (typeof harness.resolveRecipeTrustInput !== "function") {
|
|
39
|
+
throw usageError(
|
|
40
|
+
"Recipe trust enforcement requires a newer @farmslot/recipe-harness than the installed package. Next: mm-harness update"
|
|
41
|
+
);
|
|
48
42
|
}
|
|
43
|
+
return harness.resolveRecipeTrustInput;
|
|
44
|
+
}
|
|
45
|
+
async function runRecipe(adapter, recipe, artifactsDir, projectRoot, actionManifestPath, runtimeOptions = {}, preflightedExecution) {
|
|
46
|
+
const restoreRuntimeEnvironment = activateRecipeRuntimeEnvironment(
|
|
47
|
+
adapter,
|
|
48
|
+
projectRoot,
|
|
49
|
+
runtimeOptions
|
|
50
|
+
);
|
|
49
51
|
try {
|
|
52
|
+
const execution = preflightedExecution ?? await resolveRecipeExecution(
|
|
53
|
+
adapter,
|
|
54
|
+
recipe,
|
|
55
|
+
artifactsDir,
|
|
56
|
+
projectRoot,
|
|
57
|
+
actionManifestPath,
|
|
58
|
+
runtimeOptions
|
|
59
|
+
);
|
|
60
|
+
const { runner, runRequest, absoluteArtifactsDir, useFramedExtensionRecording } = execution;
|
|
61
|
+
await runner.preflight(runRequest);
|
|
50
62
|
await prepareRuntimeIfNeeded(adapter, projectRoot, runtimeOptions);
|
|
51
|
-
const
|
|
52
|
-
const recordVideo = runtimeOptions.recordVideo ?? false;
|
|
53
|
-
const useFramedExtensionRecording = adapter === "extension" && recordVideo === "full-run" && captureHelperSupportsRecordSessionSnapshots(projectRoot);
|
|
63
|
+
const diagnosticBaseline = await beginRunDiagnostics(adapter, projectRoot);
|
|
54
64
|
const recording = useFramedExtensionRecording ? await startRecipeRecording(adapter, projectRoot, absoluteArtifactsDir, {
|
|
55
65
|
record: true,
|
|
56
66
|
cdpPort: runtimeOptions.cdpPort
|
|
57
67
|
}) : void 0;
|
|
58
68
|
try {
|
|
59
|
-
const manifest = loadActionManifest(adapter, actionManifestPath);
|
|
60
|
-
await validateManifest(manifest);
|
|
61
|
-
const { createMetaMaskRunner } = await import("../runner.js");
|
|
62
|
-
const runner = await createMetaMaskRunner(adapter, manifest, {
|
|
63
|
-
quietStdout: runtimeOptions.stdoutIsMachineContract === true
|
|
64
|
-
});
|
|
65
|
-
const runRequest = {
|
|
66
|
-
recipePath: path.resolve(recipe),
|
|
67
|
-
artifactsDir: absoluteArtifactsDir,
|
|
68
|
-
projectRoot,
|
|
69
|
-
env: recipeRunEnv(adapter, runtimeOptions),
|
|
70
|
-
recordVideo: useFramedExtensionRecording ? false : recordVideo,
|
|
71
|
-
...runtimeOptions.librarySources ? { librarySources: runtimeOptions.librarySources } : {}
|
|
72
|
-
};
|
|
73
69
|
let result;
|
|
74
70
|
try {
|
|
75
71
|
result = await runner.run(runRequest);
|
|
@@ -80,17 +76,141 @@ async function runRecipe(adapter, recipe, artifactsDir, projectRoot, actionManif
|
|
|
80
76
|
}
|
|
81
77
|
}
|
|
82
78
|
await stopRecipeRecording(recording, result);
|
|
83
|
-
return result;
|
|
79
|
+
return finishRunDiagnostics(diagnosticBaseline, result);
|
|
84
80
|
} finally {
|
|
85
81
|
await stopRecipeRecording(recording);
|
|
86
82
|
}
|
|
87
83
|
} finally {
|
|
84
|
+
restoreRuntimeEnvironment();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
async function preflightRecipe(adapter, recipe, artifactsDir, projectRoot, actionManifestPath, runtimeOptions = {}) {
|
|
88
|
+
const restoreRuntimeEnvironment = activateRecipeRuntimeEnvironment(
|
|
89
|
+
adapter,
|
|
90
|
+
projectRoot,
|
|
91
|
+
runtimeOptions
|
|
92
|
+
);
|
|
93
|
+
try {
|
|
94
|
+
const execution = await resolveRecipeExecution(
|
|
95
|
+
adapter,
|
|
96
|
+
recipe,
|
|
97
|
+
artifactsDir,
|
|
98
|
+
projectRoot,
|
|
99
|
+
actionManifestPath,
|
|
100
|
+
runtimeOptions
|
|
101
|
+
);
|
|
102
|
+
await execution.runner.preflight(execution.runRequest);
|
|
103
|
+
return execution;
|
|
104
|
+
} finally {
|
|
105
|
+
restoreRuntimeEnvironment();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async function resolveRecipeExecution(adapter, recipe, artifactsDir, projectRoot, actionManifestPath, runtimeOptions) {
|
|
109
|
+
const absoluteArtifactsDir = path.resolve(artifactsDir);
|
|
110
|
+
const absoluteRecipePath = typeof recipe === "string" ? path.resolve(recipe) : void 0;
|
|
111
|
+
const recordVideo = runtimeOptions.recordVideo ?? false;
|
|
112
|
+
const resolveRecipeTrustInput = requireRecipeTrustResolver(await importRecipeHarness());
|
|
113
|
+
const trust = resolveRecipeTrustInput({
|
|
114
|
+
sourceTrust: runtimeOptions.source?.trust,
|
|
115
|
+
sourceKind: runtimeOptions.source?.kind,
|
|
116
|
+
sourceName: runtimeOptions.source?.name,
|
|
117
|
+
sourceDigest: runtimeOptions.source?.digest,
|
|
118
|
+
approvalDigest: runtimeOptions.approval?.planDigest
|
|
119
|
+
});
|
|
120
|
+
const librarySources = executionLibrarySources(
|
|
121
|
+
runtimeOptions.librarySources,
|
|
122
|
+
trust.source?.trust ?? "trusted"
|
|
123
|
+
);
|
|
124
|
+
const { manifest, actionSources } = await resolveActionManifest(
|
|
125
|
+
adapter,
|
|
126
|
+
actionManifestPath,
|
|
127
|
+
librarySources,
|
|
128
|
+
process.env.METAMASK_RECIPE_LIVE_ADAPTER_DIR
|
|
129
|
+
);
|
|
130
|
+
const { createMetaMaskRunner } = await import("../runner.js");
|
|
131
|
+
const runner = await createMetaMaskRunner(adapter, manifest, {
|
|
132
|
+
quietStdout: runtimeOptions.stdoutIsMachineContract === true,
|
|
133
|
+
autoHud: trust.source && trust.source.trust !== "trusted" ? false : runtimeOptions.autoHud,
|
|
134
|
+
onActionEvent: runtimeOptions.onActionEvent,
|
|
135
|
+
actionSources,
|
|
136
|
+
// A direct engineer invocation is the explicit trust boundary for a
|
|
137
|
+
// task-local adapter. Inherited gateway provenance remains authoritative,
|
|
138
|
+
// so an untrusted worker cannot promote itself with CLI flags.
|
|
139
|
+
trustTaskActions: (trust.source?.trust ?? "trusted") === "trusted"
|
|
140
|
+
});
|
|
141
|
+
const useFramedExtensionRecording = adapter === "extension" && recordVideo === "full-run" && trust.source?.trust !== "untrusted" && trust.source?.trust !== "unknown" && captureHelperSupportsRecordSessionSnapshots(projectRoot);
|
|
142
|
+
return {
|
|
143
|
+
runner,
|
|
144
|
+
absoluteArtifactsDir,
|
|
145
|
+
useFramedExtensionRecording,
|
|
146
|
+
runRequest: {
|
|
147
|
+
...absoluteRecipePath ? { recipePath: absoluteRecipePath } : { recipeDocument: recipe },
|
|
148
|
+
artifactsDir: absoluteArtifactsDir,
|
|
149
|
+
projectRoot,
|
|
150
|
+
inheritProcessEnv: false,
|
|
151
|
+
env: {
|
|
152
|
+
...recipeProcessEnvironment(),
|
|
153
|
+
...recipeRunEnv(adapter, runtimeOptions),
|
|
154
|
+
METAMASK_RECIPE_ACTION_SOURCE_MAP: actionSourceMap(actionSources)
|
|
155
|
+
},
|
|
156
|
+
recordVideo: useFramedExtensionRecording ? false : recordVideo,
|
|
157
|
+
...trust,
|
|
158
|
+
...librarySources ? { librarySources } : {}
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function recipeProcessEnvironment() {
|
|
163
|
+
const env = { ...process.env };
|
|
164
|
+
delete env.MM_HARNESS_CHECKOUT_LOCK_TOKEN;
|
|
165
|
+
return env;
|
|
166
|
+
}
|
|
167
|
+
function executionLibrarySources(sources, invocationTrust) {
|
|
168
|
+
return sources?.map((source) => {
|
|
169
|
+
if (source.name === "metamask" || source.provenance?.trust === "trusted") return source;
|
|
170
|
+
return {
|
|
171
|
+
...source,
|
|
172
|
+
provenance: {
|
|
173
|
+
...source.provenance,
|
|
174
|
+
kind: source.provenance?.kind ?? "library",
|
|
175
|
+
trust: invocationTrust === "trusted" ? "trusted" : "unknown",
|
|
176
|
+
name: source.provenance?.name ?? source.name ?? path.basename(source.root)
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
function activateRecipeRuntimeEnvironment(adapter, projectRoot, runtimeOptions) {
|
|
182
|
+
const previousCdpPort = process.env.CDP_PORT;
|
|
183
|
+
const previousRecipeCdpPort = process.env.RECIPE_CDP_PORT;
|
|
184
|
+
const previousWatcherPort = process.env.WATCHER_PORT;
|
|
185
|
+
const previousMetroPort = process.env.METRO_PORT;
|
|
186
|
+
const previousExtensionAutolaunch = process.env.METAMASK_RECIPE_EXTENSION_AUTOLAUNCH;
|
|
187
|
+
getAdapterSurface(adapter).resolveSlotPorts(projectRoot);
|
|
188
|
+
if (runtimeOptions.cdpPort) {
|
|
189
|
+
process.env.CDP_PORT = runtimeOptions.cdpPort;
|
|
190
|
+
process.env.RECIPE_CDP_PORT = runtimeOptions.cdpPort;
|
|
191
|
+
}
|
|
192
|
+
if (runtimeOptions.watcherPort) {
|
|
193
|
+
process.env.WATCHER_PORT = runtimeOptions.watcherPort;
|
|
194
|
+
process.env.METRO_PORT = runtimeOptions.watcherPort;
|
|
195
|
+
}
|
|
196
|
+
if (runtimeOptions.launchExistingDist) {
|
|
197
|
+
process.env.METAMASK_RECIPE_EXTENSION_AUTOLAUNCH = "1";
|
|
198
|
+
}
|
|
199
|
+
return () => {
|
|
88
200
|
restoreEnv("CDP_PORT", previousCdpPort);
|
|
89
201
|
restoreEnv("RECIPE_CDP_PORT", previousRecipeCdpPort);
|
|
90
202
|
restoreEnv("WATCHER_PORT", previousWatcherPort);
|
|
91
203
|
restoreEnv("METRO_PORT", previousMetroPort);
|
|
92
204
|
restoreEnv("METAMASK_RECIPE_EXTENSION_AUTOLAUNCH", previousExtensionAutolaunch);
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
function actionSourceMap(sources) {
|
|
208
|
+
const roots = {};
|
|
209
|
+
for (const [action, source] of sources) {
|
|
210
|
+
if (source.tier === "official") continue;
|
|
211
|
+
roots[action] = source.implementationRoot ?? (source.tier === "task" ? path.join(path.dirname(source.manifestPath), "actions") : path.join(path.dirname(path.dirname(source.manifestPath)), "actions"));
|
|
93
212
|
}
|
|
213
|
+
return JSON.stringify(roots);
|
|
94
214
|
}
|
|
95
215
|
function recipeRunEnv(adapter, runtimeOptions = {}) {
|
|
96
216
|
const base = {
|
|
@@ -162,12 +282,16 @@ function resolveRunRecipeArg(recipeArg, adapter, librarySources) {
|
|
|
162
282
|
const file = path.join(source.root, "recipes", candidate);
|
|
163
283
|
if (isRecipeFile(file)) return { recipeFile: file };
|
|
164
284
|
}
|
|
285
|
+
const listedFile = recipeFileByPublicName(path.join(source.root, "recipes"), recipeArg, adapter);
|
|
286
|
+
if (listedFile) return { recipeFile: listedFile };
|
|
165
287
|
}
|
|
166
288
|
} else {
|
|
167
289
|
for (const candidate of candidates) {
|
|
168
290
|
const file = recipePath(candidate);
|
|
169
291
|
if (isRecipeFile(file)) return { recipeFile: file };
|
|
170
292
|
}
|
|
293
|
+
const listedFile = recipeFileByPublicName(recipePath(""), recipeArg, adapter);
|
|
294
|
+
if (listedFile) return { recipeFile: listedFile };
|
|
171
295
|
}
|
|
172
296
|
}
|
|
173
297
|
const packagedNames = libraryRecipeNames(adapter);
|
|
@@ -176,6 +300,155 @@ function resolveRunRecipeArg(recipeArg, adapter, librarySources) {
|
|
|
176
300
|
const suffix = packagedNames.length > 0 ? ` Library recipes for ${adapter}: ${packagedNames.join(", ")} (mm-harness run <name>).` : ` The packaged library has no recipes for ${adapter}.`;
|
|
177
301
|
return { notFound: notFoundCore + suffix };
|
|
178
302
|
}
|
|
303
|
+
function readRecipeMetadata(file) {
|
|
304
|
+
try {
|
|
305
|
+
const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
306
|
+
return {
|
|
307
|
+
...typeof parsed.title === "string" && parsed.title.trim() ? { title: parsed.title.trim() } : {},
|
|
308
|
+
...typeof parsed.description === "string" && parsed.description.trim() ? { description: parsed.description.trim() } : {}
|
|
309
|
+
};
|
|
310
|
+
} catch {
|
|
311
|
+
return {};
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
function recipeNameFromFile(file, adapter) {
|
|
315
|
+
if (!file.endsWith(".recipe.json")) return void 0;
|
|
316
|
+
let name = file.slice(0, -".recipe.json".length).replace(/[\\/]/gu, ".");
|
|
317
|
+
const scope = name.split(".").pop() ?? "";
|
|
318
|
+
if (["mobile", "extension", "core"].includes(scope) && scope !== adapter) return void 0;
|
|
319
|
+
const scoped = scope === adapter;
|
|
320
|
+
if (scoped) name = name.slice(0, -(adapter.length + 1));
|
|
321
|
+
return { name, scoped };
|
|
322
|
+
}
|
|
323
|
+
function recipeFileByPublicName(recipesRoot, recipeName, adapter) {
|
|
324
|
+
let match;
|
|
325
|
+
let files;
|
|
326
|
+
try {
|
|
327
|
+
files = listRecipeFiles(recipesRoot);
|
|
328
|
+
} catch {
|
|
329
|
+
return void 0;
|
|
330
|
+
}
|
|
331
|
+
for (const file of files) {
|
|
332
|
+
const candidate = recipeNameFromFile(file, adapter);
|
|
333
|
+
if (!candidate || candidate.name !== recipeName) continue;
|
|
334
|
+
if (!match || candidate.scoped && !match.scoped) match = { file, scoped: candidate.scoped };
|
|
335
|
+
}
|
|
336
|
+
return match ? path.join(recipesRoot, match.file) : void 0;
|
|
337
|
+
}
|
|
338
|
+
function listRunnableRecipes(adapter, librarySources) {
|
|
339
|
+
const sources = librarySources && librarySources.length > 0 ? librarySources : [{ name: "metamask", root: recipePath("") }];
|
|
340
|
+
const resolved = /* @__PURE__ */ new Map();
|
|
341
|
+
for (const source of sources) {
|
|
342
|
+
const recipesRoot = librarySources && librarySources.length > 0 ? path.join(source.root, "recipes") : source.root;
|
|
343
|
+
let files;
|
|
344
|
+
try {
|
|
345
|
+
files = listRecipeFiles(recipesRoot);
|
|
346
|
+
} catch {
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
const sourceName = source.name ?? path.basename(source.root);
|
|
350
|
+
const withinSource = /* @__PURE__ */ new Map();
|
|
351
|
+
for (const file of files) {
|
|
352
|
+
const candidate = recipeNameFromFile(file, adapter);
|
|
353
|
+
if (!candidate) continue;
|
|
354
|
+
const existing = withinSource.get(candidate.name);
|
|
355
|
+
if (!existing || candidate.scoped && !existing.scoped) {
|
|
356
|
+
withinSource.set(candidate.name, { file, scoped: candidate.scoped });
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
for (const [name, candidate] of withinSource) {
|
|
360
|
+
const winner = resolved.get(name);
|
|
361
|
+
if (winner) winner.shadows.push(sourceName);
|
|
362
|
+
else resolved.set(name, {
|
|
363
|
+
name,
|
|
364
|
+
source: sourceName,
|
|
365
|
+
file: path.join("recipes", candidate.file),
|
|
366
|
+
shadows: [],
|
|
367
|
+
...readRecipeMetadata(path.join(recipesRoot, candidate.file))
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return [...resolved.values()].sort((left, right) => left.name.localeCompare(right.name));
|
|
372
|
+
}
|
|
373
|
+
function describeRunnableRecipe(recipeArg, adapter, librarySources) {
|
|
374
|
+
const resolved = resolveRunRecipeArg(recipeArg, adapter, librarySources);
|
|
375
|
+
if ("notFound" in resolved) return resolved;
|
|
376
|
+
let document;
|
|
377
|
+
try {
|
|
378
|
+
document = JSON.parse(fs.readFileSync(resolved.recipeFile, "utf8"));
|
|
379
|
+
} catch (error) {
|
|
380
|
+
return {
|
|
381
|
+
unreadable: `could not read recipe ${resolved.recipeFile}: ${error instanceof Error ? error.message : String(error)}`
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
if (!isRecord(document)) {
|
|
385
|
+
return { unreadable: `recipe ${resolved.recipeFile} must contain a JSON object.` };
|
|
386
|
+
}
|
|
387
|
+
const listed = listRunnableRecipes(adapter, librarySources).find((candidate) => candidate.name === recipeArg);
|
|
388
|
+
const composition = recipeComposition(document);
|
|
389
|
+
return {
|
|
390
|
+
recipe: {
|
|
391
|
+
name: listed?.name ?? recipeArg,
|
|
392
|
+
source: listed?.source ?? "file",
|
|
393
|
+
file: listed?.file ?? resolved.recipeFile,
|
|
394
|
+
shadows: listed?.shadows ?? [],
|
|
395
|
+
...typeof document.title === "string" && document.title.trim() ? { title: document.title.trim() } : {},
|
|
396
|
+
...typeof document.description === "string" && document.description.trim() ? { description: document.description.trim() } : {},
|
|
397
|
+
adapter,
|
|
398
|
+
path: resolved.recipeFile,
|
|
399
|
+
...Object.prototype.hasOwnProperty.call(document, "schema_version") ? { schemaVersion: document.schema_version } : {},
|
|
400
|
+
inputs: isRecord(document.inputs) ? redactRecipeInputs(document.inputs) : {},
|
|
401
|
+
inputOverridesSupported: false,
|
|
402
|
+
actions: composition.actions,
|
|
403
|
+
actionsComplete: composition.actionsComplete,
|
|
404
|
+
flows: composition.flows
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
function recipeComposition(recipe) {
|
|
409
|
+
const actions = /* @__PURE__ */ new Set();
|
|
410
|
+
const flows = /* @__PURE__ */ new Set();
|
|
411
|
+
const visitNode = (value) => {
|
|
412
|
+
if (!isRecord(value) || typeof value.action !== "string") return;
|
|
413
|
+
actions.add(value.action);
|
|
414
|
+
if (value.action === "call" && typeof value.ref === "string") flows.add(value.ref);
|
|
415
|
+
};
|
|
416
|
+
const visitWorkflow = (value) => {
|
|
417
|
+
if (!isRecord(value)) return;
|
|
418
|
+
for (const phase of ["setup", "teardown"]) {
|
|
419
|
+
if (Array.isArray(value[phase])) value[phase].forEach(visitNode);
|
|
420
|
+
}
|
|
421
|
+
if (isRecord(value.nodes)) Object.values(value.nodes).forEach(visitNode);
|
|
422
|
+
};
|
|
423
|
+
visitNode(recipe.startState);
|
|
424
|
+
if (isRecord(recipe.validate)) visitWorkflow(recipe.validate.workflow);
|
|
425
|
+
const inlineFlows = isRecord(recipe.flows) ? recipe.flows : {};
|
|
426
|
+
const visitedFlows = /* @__PURE__ */ new Set();
|
|
427
|
+
let actionsComplete = true;
|
|
428
|
+
for (const ref of flows) {
|
|
429
|
+
if (visitedFlows.has(ref)) continue;
|
|
430
|
+
visitedFlows.add(ref);
|
|
431
|
+
const flow = inlineFlows[ref];
|
|
432
|
+
if (flow !== void 0) {
|
|
433
|
+
visitWorkflow(isRecord(flow) && isRecord(flow.workflow) ? flow.workflow : flow);
|
|
434
|
+
} else {
|
|
435
|
+
actionsComplete = false;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return {
|
|
439
|
+
actions: [...actions].sort(),
|
|
440
|
+
actionsComplete,
|
|
441
|
+
flows: [...flows].sort()
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
function redactRecipeInputs(inputs) {
|
|
445
|
+
return Object.fromEntries(
|
|
446
|
+
Object.entries(inputs).map(([key, value]) => [
|
|
447
|
+
key,
|
|
448
|
+
/(?:password|passphrase|secret|token|credential|mnemonic|seed|srp|vault|api[_-]?key|private[_-]?key)/iu.test(key) ? "<redacted>" : isRecord(value) ? redactRecipeInputs(value) : Array.isArray(value) ? value.map((entry) => isRecord(entry) ? redactRecipeInputs(entry) : entry) : value
|
|
449
|
+
])
|
|
450
|
+
);
|
|
451
|
+
}
|
|
179
452
|
function libraryRecipeNames(adapter) {
|
|
180
453
|
let entries;
|
|
181
454
|
try {
|
|
@@ -183,20 +456,11 @@ function libraryRecipeNames(adapter) {
|
|
|
183
456
|
} catch {
|
|
184
457
|
return [];
|
|
185
458
|
}
|
|
186
|
-
const
|
|
187
|
-
const names = entries.filter((f) => f.endsWith(".recipe.json")).map((f) => f.slice(0, -".recipe.json".length).replace(/[\\/]/gu, ".")).filter((n) => {
|
|
188
|
-
const scope = n.split(".").pop() ?? "";
|
|
189
|
-
return !scopes.includes(scope) || scope === adapter;
|
|
190
|
-
}).map((n) => n.endsWith(`.${adapter}`) ? n.slice(0, -(adapter.length + 1)) : n);
|
|
459
|
+
const names = entries.map((file) => recipeNameFromFile(file, adapter)?.name).filter((name) => Boolean(name));
|
|
191
460
|
return [...new Set(names)].sort();
|
|
192
461
|
}
|
|
193
462
|
function recipeNameCandidates(recipeArg, adapter) {
|
|
194
|
-
|
|
195
|
-
const candidates = [];
|
|
196
|
-
for (const name of names) {
|
|
197
|
-
candidates.push(...singleRecipeNameCandidates(name, adapter));
|
|
198
|
-
}
|
|
199
|
-
return [...new Set(candidates)];
|
|
463
|
+
return singleRecipeNameCandidates(recipeArg, adapter);
|
|
200
464
|
}
|
|
201
465
|
function singleRecipeNameCandidates(name, adapter) {
|
|
202
466
|
if (name.endsWith(".recipe.json")) {
|
|
@@ -222,32 +486,6 @@ function domainPathForRecipeName(name) {
|
|
|
222
486
|
function uniqueStrings(values) {
|
|
223
487
|
return [...new Set(values)];
|
|
224
488
|
}
|
|
225
|
-
const LEGACY_RECIPE_ALIASES = {
|
|
226
|
-
"action-validation": ["runner.action-validation"],
|
|
227
|
-
"action-validation.extension.recipe.json": ["runner.action-validation.extension.recipe.json"],
|
|
228
|
-
"action-validation.mobile.recipe.json": ["runner.action-validation.mobile.recipe.json"],
|
|
229
|
-
"app-lifecycle-android-smoke": ["app.lifecycle.android-smoke"],
|
|
230
|
-
"app-lifecycle-android-smoke.mobile.recipe.json": ["app.lifecycle.android-smoke.mobile.recipe.json"],
|
|
231
|
-
"order-lifecycle": ["perps.order-lifecycle"],
|
|
232
|
-
"order-lifecycle.core.recipe.json": ["perps.order-lifecycle.core.recipe.json"],
|
|
233
|
-
"perps-lifecycle": ["perps.lifecycle"],
|
|
234
|
-
"perps-lifecycle.recipe.json": ["perps.lifecycle.recipe.json"],
|
|
235
|
-
"perps-performance": ["perps.performance"],
|
|
236
|
-
"perps-performance-background-resume": ["perps.performance.background-resume"],
|
|
237
|
-
"perps-performance-background-resume.mobile.recipe.json": ["perps.performance.background-resume.mobile.recipe.json"],
|
|
238
|
-
"perps-performance-cold-start": ["perps.performance.cold-start"],
|
|
239
|
-
"perps-performance-cold-start.mobile.recipe.json": ["perps.performance.cold-start.mobile.recipe.json"],
|
|
240
|
-
"perps-performance-warm-start": ["perps.performance.warm-start"],
|
|
241
|
-
"perps-performance-warm-start.mobile.recipe.json": ["perps.performance.warm-start.mobile.recipe.json"],
|
|
242
|
-
"perps-performance.mobile.recipe.json": ["perps.performance.mobile.recipe.json"],
|
|
243
|
-
"read-markets": ["perps.read-markets"],
|
|
244
|
-
"read-markets.core.recipe.json": ["perps.read-markets.core.recipe.json"],
|
|
245
|
-
"smoke": ["runner.smoke"],
|
|
246
|
-
"smoke.extension.recipe.json": ["runner.smoke.extension.recipe.json"],
|
|
247
|
-
"smoke.mobile.recipe.json": ["runner.smoke.mobile.recipe.json"],
|
|
248
|
-
"trading-lifecycle": ["perps.trading-lifecycle"],
|
|
249
|
-
"trading-lifecycle.core.recipe.json": ["perps.trading-lifecycle.core.recipe.json"]
|
|
250
|
-
};
|
|
251
489
|
async function validateRunRecipeStatic(recipeArg, adapter, options) {
|
|
252
490
|
const librarySources = await resolveMetaMaskLibrarySources(optionString(options, "library"));
|
|
253
491
|
const resolved = resolveRunRecipeArg(recipeArg, adapter, librarySources);
|
|
@@ -283,7 +521,11 @@ async function validateRunRecipeStatic(recipeArg, adapter, options) {
|
|
|
283
521
|
};
|
|
284
522
|
}
|
|
285
523
|
const findings = [];
|
|
286
|
-
const manifest =
|
|
524
|
+
const { manifest } = await resolveActionManifest(
|
|
525
|
+
adapter,
|
|
526
|
+
optionString(options, "actionManifest"),
|
|
527
|
+
librarySources
|
|
528
|
+
);
|
|
287
529
|
let manifestOk = true;
|
|
288
530
|
try {
|
|
289
531
|
await validateManifest(manifest);
|
|
@@ -333,7 +575,12 @@ async function validateRunRecipeStatic(recipeArg, adapter, options) {
|
|
|
333
575
|
async function actionInsteadOfRecipeHint(recipeArg, adapter, options) {
|
|
334
576
|
if (recipeArg.includes("/") || recipeArg.includes(path.sep)) return "";
|
|
335
577
|
try {
|
|
336
|
-
const
|
|
578
|
+
const librarySources = await resolveMetaMaskLibrarySources(optionString(options, "library"));
|
|
579
|
+
const { manifest } = await resolveActionManifest(
|
|
580
|
+
adapter,
|
|
581
|
+
optionString(options, "actionManifest"),
|
|
582
|
+
librarySources
|
|
583
|
+
);
|
|
337
584
|
const { getRecipeActionManifestActionNames } = await importRecipeProtocol();
|
|
338
585
|
const actions = getRecipeActionManifestActionNames(manifest);
|
|
339
586
|
const matches = actions.includes(recipeArg) ? [recipeArg] : actions.filter((name) => name.split(".").pop() === recipeArg);
|
|
@@ -341,6 +588,7 @@ async function actionInsteadOfRecipeHint(recipeArg, adapter, options) {
|
|
|
341
588
|
const device = optionString(options, "device");
|
|
342
589
|
const actionManifest = optionString(options, "actionManifest");
|
|
343
590
|
const parts = ["mm-harness", "call", matches[0]];
|
|
591
|
+
parts.push("--adapter", adapter, "--target", shellQuoteArg(targetPath(options)));
|
|
344
592
|
if (device) parts.push("--device", shellQuoteArg(device));
|
|
345
593
|
if (actionManifest) parts.push("--action-manifest", shellQuoteArg(actionManifest));
|
|
346
594
|
return ` This is an action, not a recipe. Use: ${parts.join(" ")}.`;
|
|
@@ -353,16 +601,33 @@ async function resolveMetaMaskLibrarySources(libraryEntry) {
|
|
|
353
601
|
if (typeof harness.resolveRecipeLibrarySources !== "function") {
|
|
354
602
|
if (libraryEntry) {
|
|
355
603
|
throw usageError(
|
|
356
|
-
"--library requires @farmslot/recipe-harness
|
|
604
|
+
"--library requires a current @farmslot/recipe-harness installation that exports recipe-library support. Next: mm-harness update"
|
|
357
605
|
);
|
|
358
606
|
}
|
|
359
607
|
return void 0;
|
|
360
608
|
}
|
|
609
|
+
const cliEntries = typeof libraryEntry === "string" ? [libraryEntry] : libraryEntry ? [...libraryEntry] : [];
|
|
361
610
|
const sources = await harness.resolveRecipeLibrarySources(
|
|
362
|
-
|
|
611
|
+
cliEntries.length > 0 ? { cliEntries } : void 0
|
|
363
612
|
);
|
|
364
|
-
|
|
365
|
-
|
|
613
|
+
const canonicalRoot = path.resolve(runnerDir, "library");
|
|
614
|
+
const configuredCanonical = sources.find((source) => source.name === "metamask");
|
|
615
|
+
if (configuredCanonical && path.resolve(configuredCanonical.root) !== canonicalRoot) {
|
|
616
|
+
throw usageError(
|
|
617
|
+
`Recipe library source metamask must resolve to ${canonicalRoot}; got ${path.resolve(configuredCanonical.root)}.`
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
if (!sources.some((source) => path.resolve(source.root) === canonicalRoot)) {
|
|
621
|
+
sources.push({ name: "metamask", root: canonicalRoot });
|
|
622
|
+
}
|
|
623
|
+
return sources.map((source) => ({
|
|
624
|
+
...source,
|
|
625
|
+
provenance: source.provenance ?? {
|
|
626
|
+
kind: source.name === "metamask" ? "bundled" : "library",
|
|
627
|
+
trust: source.name === "metamask" ? "trusted" : "unknown",
|
|
628
|
+
name: source.name ?? path.basename(source.root)
|
|
629
|
+
}
|
|
630
|
+
}));
|
|
366
631
|
}
|
|
367
632
|
function synthesizeOneNodeRecipe(action, args) {
|
|
368
633
|
return {
|
|
@@ -373,7 +638,12 @@ function synthesizeOneNodeRecipe(action, args) {
|
|
|
373
638
|
workflow: {
|
|
374
639
|
entry: "call",
|
|
375
640
|
nodes: {
|
|
376
|
-
call: {
|
|
641
|
+
call: {
|
|
642
|
+
...args,
|
|
643
|
+
action,
|
|
644
|
+
next: "done",
|
|
645
|
+
intent: typeof args.intent === "string" ? args.intent : `Call ${action} in isolation`
|
|
646
|
+
},
|
|
377
647
|
done: { action: "end", status: "pass" }
|
|
378
648
|
}
|
|
379
649
|
}
|
|
@@ -381,7 +651,8 @@ function synthesizeOneNodeRecipe(action, args) {
|
|
|
381
651
|
};
|
|
382
652
|
}
|
|
383
653
|
async function runOneNode(adapter, action, args, target, actionManifest) {
|
|
384
|
-
const
|
|
654
|
+
const librarySources = await resolveMetaMaskLibrarySources(void 0);
|
|
655
|
+
const { manifest } = await resolveActionManifest(adapter, actionManifest, librarySources);
|
|
385
656
|
const recipe = synthesizeOneNodeRecipe(action, args);
|
|
386
657
|
const validation = await validateRecipeAdapterAware(recipe, manifest);
|
|
387
658
|
if (validation.status === "invalid") return { status: "fail" };
|
|
@@ -390,7 +661,7 @@ async function runOneNode(adapter, action, args, target, actionManifest) {
|
|
|
390
661
|
fs.writeFileSync(recipeFile, `${JSON.stringify(recipe, null, 2)}
|
|
391
662
|
`);
|
|
392
663
|
const result = await runRecipe(adapter, recipeFile, path.join(scratch, "artifacts"), target, actionManifest, {
|
|
393
|
-
librarySources
|
|
664
|
+
librarySources,
|
|
394
665
|
stdoutIsMachineContract: true
|
|
395
666
|
});
|
|
396
667
|
return { status: result.status === "pass" ? "pass" : "fail" };
|
|
@@ -398,10 +669,12 @@ async function runOneNode(adapter, action, args, target, actionManifest) {
|
|
|
398
669
|
async function prepareHeal(adapter, target, options, json, prepareOptions = {}) {
|
|
399
670
|
if (recipeRunning(target)) {
|
|
400
671
|
const msg = "a recipe is currently running \u2014 refusing to start while another recipe executes.";
|
|
672
|
+
const userAction = `inspect the checkout state with: mm-harness status --target ${shellQuoteArg(target)} --json; retry after the active recipe finishes`;
|
|
401
673
|
if (json) {
|
|
402
|
-
console.log(JSON.stringify({ schemaVersion: 1, status: "fail", recoverable: false, error: { code: "RECIPE_RUNNING", message: msg } }, null, 2));
|
|
674
|
+
console.log(JSON.stringify({ schemaVersion: 1, status: "fail", recoverable: false, error: { code: "RECIPE_RUNNING", message: msg, userAction } }, null, 2));
|
|
403
675
|
} else {
|
|
404
|
-
console.error(`\u2717 mm-harness: ${msg}
|
|
676
|
+
console.error(`\u2717 mm-harness: ${msg}
|
|
677
|
+
Next: ${userAction}`);
|
|
405
678
|
}
|
|
406
679
|
return EXIT.bounded;
|
|
407
680
|
}
|
|
@@ -414,21 +687,25 @@ async function prepareHeal(adapter, target, options, json, prepareOptions = {})
|
|
|
414
687
|
return EXIT.usage;
|
|
415
688
|
}
|
|
416
689
|
const state = newHealState();
|
|
690
|
+
prepareOptions.onPhase?.("install");
|
|
417
691
|
const ensured = await ensureOverlay(adapter, target, heal, state, json);
|
|
418
692
|
if (!ensured.ok) {
|
|
693
|
+
const userAction = `mm-harness install --adapter ${adapter} --target ${shellQuoteArg(target)}`;
|
|
419
694
|
if (json) {
|
|
420
695
|
console.log(
|
|
421
696
|
JSON.stringify(
|
|
422
|
-
{ schemaVersion: 1, status: "fail", recoverable: false, mutations: state.mutations, error: { code: "OVERLAY_INSTALL_FAILED", message: ensured.error } },
|
|
697
|
+
{ schemaVersion: 1, status: "fail", recoverable: false, mutations: state.mutations, error: { code: "OVERLAY_INSTALL_FAILED", message: ensured.error, userAction } },
|
|
423
698
|
null,
|
|
424
699
|
2
|
|
425
700
|
)
|
|
426
701
|
);
|
|
427
702
|
} else {
|
|
428
|
-
console.error(`\u2717 overlay auto-ensure failed: ${ensured.error}
|
|
703
|
+
console.error(`\u2717 overlay auto-ensure failed: ${ensured.error}
|
|
704
|
+
Next: ${userAction}`);
|
|
429
705
|
}
|
|
430
706
|
return EXIT.infra;
|
|
431
707
|
}
|
|
708
|
+
prepareOptions.onPhase?.("healthcheck");
|
|
432
709
|
if (adapter === "extension") {
|
|
433
710
|
const previousCdpPort = process.env.CDP_PORT;
|
|
434
711
|
const previousRecipeCdpPort = process.env.RECIPE_CDP_PORT;
|
|
@@ -441,7 +718,9 @@ async function prepareHeal(adapter, target, options, json, prepareOptions = {})
|
|
|
441
718
|
}
|
|
442
719
|
if (explicitWatcherPort) process.env.WATCHER_PORT = explicitWatcherPort;
|
|
443
720
|
try {
|
|
444
|
-
const current = await ensureExtensionProofRuntime(target, heal, state, json
|
|
721
|
+
const current = await ensureExtensionProofRuntime(target, heal, state, json, {
|
|
722
|
+
onRecovery: (code) => prepareOptions.onPhase?.("recover", { code })
|
|
723
|
+
});
|
|
445
724
|
if (current !== null) return current;
|
|
446
725
|
} finally {
|
|
447
726
|
restoreEnv("CDP_PORT", previousCdpPort);
|
|
@@ -450,7 +729,9 @@ async function prepareHeal(adapter, target, options, json, prepareOptions = {})
|
|
|
450
729
|
}
|
|
451
730
|
}
|
|
452
731
|
if (adapter === "mobile" && prepareOptions.skipMobileSourceFreshness !== true) {
|
|
453
|
-
const current = await ensureMobileProofRuntime(target, heal, state, json
|
|
732
|
+
const current = await ensureMobileProofRuntime(target, heal, state, json, {
|
|
733
|
+
onRecovery: (code) => prepareOptions.onPhase?.("recover", { code })
|
|
734
|
+
});
|
|
454
735
|
if (current !== null) return current;
|
|
455
736
|
}
|
|
456
737
|
return { state, heal };
|
|
@@ -460,6 +741,20 @@ async function ensureMobileProofRuntime(target, heal, state, json, deps = {}) {
|
|
|
460
741
|
const check = deps.check ?? (() => source.mobileSourceCheck(target));
|
|
461
742
|
const record = deps.record ?? ((fingerprint) => source.recordMobileSourceBaseline(target, fingerprint));
|
|
462
743
|
const initial = check();
|
|
744
|
+
if (initial.status === "unavailable") {
|
|
745
|
+
if (json) {
|
|
746
|
+
console.log(JSON.stringify({
|
|
747
|
+
schemaVersion: 1,
|
|
748
|
+
status: "fail",
|
|
749
|
+
recoverable: false,
|
|
750
|
+
error: initial.error
|
|
751
|
+
}, null, 2));
|
|
752
|
+
} else {
|
|
753
|
+
console.error(`\u2717 ${initial.error.message}`);
|
|
754
|
+
console.error(` Next: ${initial.error.userAction}`);
|
|
755
|
+
}
|
|
756
|
+
return EXIT.usage;
|
|
757
|
+
}
|
|
463
758
|
if (initial.status === "current") return null;
|
|
464
759
|
const targetArg = JSON.stringify(path.resolve(target));
|
|
465
760
|
const userAction = `mm-harness call app.lifecycle --arg command=restart --adapter mobile --target ${targetArg}`;
|
|
@@ -481,6 +776,7 @@ async function ensureMobileProofRuntime(target, heal, state, json, deps = {}) {
|
|
|
481
776
|
}
|
|
482
777
|
const recoveryCode = "mobile.source_reloaded";
|
|
483
778
|
state.attemptedRecoveries.push(recoveryCode);
|
|
779
|
+
deps.onRecovery?.(recoveryCode);
|
|
484
780
|
if (!json) console.error(`\u2192 Mobile proof preflight: source ${initial.status}; restarting the app before execution`);
|
|
485
781
|
const restart = deps.restart ?? (async () => {
|
|
486
782
|
const result2 = await runOneNode(
|
|
@@ -521,6 +817,12 @@ async function ensureMobileProofRuntime(target, heal, state, json, deps = {}) {
|
|
|
521
817
|
state.mutations.push({ type: "runtime", action: "restarted", reasonCode: `source-${initial.status}` });
|
|
522
818
|
return null;
|
|
523
819
|
}
|
|
820
|
+
function resolveRunMobilePlatform() {
|
|
821
|
+
if (process.env.PLATFORM === "android" || process.env.PLATFORM === "ios") {
|
|
822
|
+
return process.env.PLATFORM;
|
|
823
|
+
}
|
|
824
|
+
return process.env.ADB_SERIAL || process.env.ANDROID_SERIAL ? "android" : "ios";
|
|
825
|
+
}
|
|
524
826
|
async function ensureExtensionProofRuntime(target, heal, state, json, deps = {}) {
|
|
525
827
|
const decide = deps.decide ?? (async () => {
|
|
526
828
|
const { decideExtensionReadiness } = await import("../adapters/extension/runtime-decision.js");
|
|
@@ -532,6 +834,22 @@ async function ensureExtensionProofRuntime(target, heal, state, json, deps = {})
|
|
|
532
834
|
});
|
|
533
835
|
const initial = await decide();
|
|
534
836
|
if (initial.decision === "ready") return null;
|
|
837
|
+
if (initial.reasonCode === "runtime-dist-check-failed") {
|
|
838
|
+
const message = `Extension proof runtime freshness could not be checked: ${initial.reasons[0] ?? initial.decision}`;
|
|
839
|
+
const userAction2 = `mm-harness doctor --adapter extension --target ${JSON.stringify(path.resolve(target))}`;
|
|
840
|
+
if (json) {
|
|
841
|
+
console.log(JSON.stringify({
|
|
842
|
+
schemaVersion: 1,
|
|
843
|
+
status: "fail",
|
|
844
|
+
recoverable: false,
|
|
845
|
+
error: { code: "EXTENSION_RUNTIME_CHECK_FAILED", message, userAction: userAction2 }
|
|
846
|
+
}, null, 2));
|
|
847
|
+
} else {
|
|
848
|
+
console.error(`\u2717 ${message}`);
|
|
849
|
+
console.error(` Next: ${userAction2}`);
|
|
850
|
+
}
|
|
851
|
+
return EXIT.infra;
|
|
852
|
+
}
|
|
535
853
|
if (!["dist-stale", "dist-missing", "runtime-dist-stale", "runtime-dist-missing"].includes(initial.reasonCode)) {
|
|
536
854
|
return null;
|
|
537
855
|
}
|
|
@@ -556,20 +874,33 @@ async function ensureExtensionProofRuntime(target, heal, state, json, deps = {})
|
|
|
556
874
|
return EXIT.infra;
|
|
557
875
|
}
|
|
558
876
|
const recoveryCode = tier === "build" ? "extension.runtime_rebuilt" : "extension.runtime_reloaded";
|
|
877
|
+
const productConfigBlock = tier === "build" ? extensionProductConfigBlock(target) : null;
|
|
878
|
+
if (productConfigBlock) {
|
|
879
|
+
const message = productConfigBlock.message;
|
|
880
|
+
const userAction2 = productConfigBlock.userAction;
|
|
881
|
+
if (json) {
|
|
882
|
+
console.log(JSON.stringify({
|
|
883
|
+
schemaVersion: 1,
|
|
884
|
+
status: "fail",
|
|
885
|
+
recoverable: false,
|
|
886
|
+
mutations: state.mutations,
|
|
887
|
+
error: { code: "EXTENSION_PRODUCT_CONFIG_REQUIRED", message, userAction: userAction2 }
|
|
888
|
+
}, null, 2));
|
|
889
|
+
} else {
|
|
890
|
+
console.error(`\u2717 ${message}`);
|
|
891
|
+
console.error(` Next: ${userAction2}`);
|
|
892
|
+
}
|
|
893
|
+
return EXIT.infra;
|
|
894
|
+
}
|
|
559
895
|
state.attemptedRecoveries.push(recoveryCode);
|
|
896
|
+
deps.onRecovery?.(recoveryCode);
|
|
560
897
|
if (!json) console.error(`\u2192 Extension proof preflight: ${initial.reasonCode}; ${tier === "build" ? "rebuilding" : "reloading"} before execution`);
|
|
561
898
|
const launch = deps.launch ?? (async (requestedTier) => {
|
|
562
899
|
const { launchExtension } = await import("./launch/extension.js");
|
|
563
900
|
return launchExtension(target, requestedTier, false);
|
|
564
901
|
});
|
|
565
902
|
const result = await launch(tier);
|
|
566
|
-
|
|
567
|
-
if (result.status === 0 && verified.decision !== "ready") {
|
|
568
|
-
for (let attempt = 0; attempt < 8 && verified.decision !== "ready"; attempt += 1) {
|
|
569
|
-
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
570
|
-
verified = await decide();
|
|
571
|
-
}
|
|
572
|
-
}
|
|
903
|
+
const verified = result.status === 0 ? await decide() : initial;
|
|
573
904
|
if (result.status !== 0 || verified.decision !== "ready") {
|
|
574
905
|
const message = `Extension runtime recovery did not produce a current healthy runtime (${verified.reasonCode}).`;
|
|
575
906
|
if (json) {
|
|
@@ -603,17 +934,11 @@ async function recoverRunInfra(adapter, target, json) {
|
|
|
603
934
|
}
|
|
604
935
|
if (adapter === "mobile") {
|
|
605
936
|
const { launchMobile } = await import("./launch/mobile.js");
|
|
606
|
-
const platform =
|
|
937
|
+
const platform = process.env.PLATFORM === "android" ? "android" : "ios";
|
|
607
938
|
return launchMobile(target, platform, "quick", json);
|
|
608
939
|
}
|
|
609
940
|
return { status: 0, output: "headless run retry" };
|
|
610
941
|
}
|
|
611
|
-
function resolveRunMobilePlatform() {
|
|
612
|
-
if (process.env.PLATFORM === "android" || process.env.PLATFORM === "ios") {
|
|
613
|
-
return process.env.PLATFORM;
|
|
614
|
-
}
|
|
615
|
-
return process.env.ADB_SERIAL || process.env.ANDROID_SERIAL ? "android" : "ios";
|
|
616
|
-
}
|
|
617
942
|
function readRunFailureText(result) {
|
|
618
943
|
try {
|
|
619
944
|
const trace = JSON.parse(fs.readFileSync(result.tracePath, "utf8"));
|
|
@@ -623,7 +948,7 @@ function readRunFailureText(result) {
|
|
|
623
948
|
return "";
|
|
624
949
|
}
|
|
625
950
|
}
|
|
626
|
-
async function executeWithHealBounds(exec, adapter, target, heal, state, recover = () => recoverRunInfra(adapter, target, false)) {
|
|
951
|
+
async function executeWithHealBounds(exec, adapter, target, heal, state, recover = () => recoverRunInfra(adapter, target, false), onRecovery = () => void 0) {
|
|
627
952
|
let result = await exec();
|
|
628
953
|
for (; ; ) {
|
|
629
954
|
if (result.status === "pass") return { result, violation: null };
|
|
@@ -632,6 +957,7 @@ async function executeWithHealBounds(exec, adapter, target, heal, state, recover
|
|
|
632
957
|
if (heal === "off") return { result, violation: null };
|
|
633
958
|
const recoveryCode = RUN_RECOVERY_CODE[adapter];
|
|
634
959
|
state.attemptedRecoveries.push(recoveryCode);
|
|
960
|
+
onRecovery(recoveryCode);
|
|
635
961
|
await recover();
|
|
636
962
|
result = await exec();
|
|
637
963
|
if (result.status === "pass") {
|
|
@@ -641,7 +967,7 @@ async function executeWithHealBounds(exec, adapter, target, heal, state, recover
|
|
|
641
967
|
}
|
|
642
968
|
}
|
|
643
969
|
function emitHealViolation(json, command, result, violation, state, adapter) {
|
|
644
|
-
const userAction = adapter === "core" && violation.code === "WALLET_STATE_REQUIRED" ? '
|
|
970
|
+
const userAction = adapter === "core" && violation.code === "WALLET_STATE_REQUIRED" ? 'create temp/recipe/runtime/wallet-fixture.json, or add "account": "<0x\u2026>" to the node block in the recipe' : violation.userAction ?? `inspect ${shellQuoteArg(result.summaryPath)} and ${shellQuoteArg(result.tracePath)}; fix the application or recipe failure before retrying`;
|
|
645
971
|
if (json) {
|
|
646
972
|
console.log(
|
|
647
973
|
JSON.stringify(
|
|
@@ -661,7 +987,7 @@ function emitHealViolation(json, command, result, violation, state, adapter) {
|
|
|
661
987
|
code: violation.code,
|
|
662
988
|
message: violation.message,
|
|
663
989
|
retryable: false,
|
|
664
|
-
userAction
|
|
990
|
+
userAction,
|
|
665
991
|
originalError: violation.originalError ?? null
|
|
666
992
|
}
|
|
667
993
|
},
|
|
@@ -688,12 +1014,16 @@ function countRecipeNodes(recipe) {
|
|
|
688
1014
|
}
|
|
689
1015
|
export {
|
|
690
1016
|
countRecipeNodes,
|
|
1017
|
+
describeRunnableRecipe,
|
|
691
1018
|
emitHealViolation,
|
|
692
1019
|
ensureExtensionProofRuntime,
|
|
693
1020
|
ensureMobileProofRuntime,
|
|
694
1021
|
executeWithHealBounds,
|
|
1022
|
+
listRunnableRecipes,
|
|
1023
|
+
preflightRecipe,
|
|
695
1024
|
prepareHeal,
|
|
696
1025
|
recoverRunInfra,
|
|
1026
|
+
requireRecipeTrustResolver,
|
|
697
1027
|
resolveMetaMaskLibrarySources,
|
|
698
1028
|
resolveRunRecipeArg,
|
|
699
1029
|
runOneNode,
|