@deeeed/metamask-harness 0.34.4 → 0.36.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 +25 -0
- package/README.md +9 -1
- package/adapters/core/inject.sh +3 -3
- package/adapters/extension/check-infura-readiness.cjs +102 -0
- package/adapters/extension/inject.mjs +4 -4
- package/adapters/extension/live.sh +25 -10
- package/adapters/extension/start-watch.sh +15 -0
- package/adapters/extension/wallet-fixture-state.cjs +3 -1
- package/adapters/manifest.json +16 -0
- package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +73 -42
- package/adapters/mobile/inject.sh +4 -3
- package/adapters/mobile/reset-app-data.sh +154 -0
- package/adapters/mobile/verify.sh +37 -0
- package/dist/adapters/extension/product-config.js +29 -1
- package/dist/adapters/extension/runtime.js +60 -15
- package/dist/adapters/extension/surface.js +6 -1
- package/dist/adapters/mobile/surface.js +6 -1
- package/dist/adapters.js +18 -0
- package/dist/cli-commands.js +1 -1
- package/dist/cli.js +2 -2
- package/dist/command-contract.js +1 -1
- package/dist/commands/device-target.js +5 -0
- package/dist/commands/fixtures.js +106 -31
- package/dist/commands/launch/extension.js +39 -3
- package/dist/commands/launch/index.js +13 -0
- package/dist/doctor.js +30 -1
- package/dist/mm-harness-cli.js +6 -3
- package/dist/recipe-security.js +3 -0
- package/docs/RECIPES.md +29 -0
- package/library/actions/extension/wallet/import.mjs +234 -0
- package/library/actions/extension/wallet/reset.mjs +98 -0
- package/library/actions/extension/wallet/state.mjs +1 -0
- package/library/actions/mobile/analytics/consent-settings.mjs +112 -0
- package/library/actions/mobile/analytics/set_consent.mjs +4 -112
- package/library/actions/mobile/platform/bridge.mjs +8 -0
- package/library/actions/mobile/wallet/import.mjs +259 -0
- package/library/actions/mobile/wallet/reset-helper.mjs +99 -0
- package/library/actions/mobile/wallet/reset.mjs +7 -0
- package/library/actions/shared/wallet/import-source.mjs +101 -0
- package/library/manifests/extension.action-manifest.json +112 -0
- package/library/manifests/mobile.action-manifest.json +108 -0
- package/library/recipes/wallet/import.recipe.json +83 -0
- package/library/recipes/wallet/reset-import.recipe.json +88 -0
- package/package.json +1 -1
- package/scripts/completions.sh +2 -2
- package/scripts/site-contrast.mjs +7 -0
- package/site/architecture.html +14 -7
- package/site/assets/metamask-fox.svg +24 -0
- package/site/assets/progress.mjs +6 -0
- package/site/assets/style.css +88 -29
- package/site/cheatsheet.html +5 -6
- package/site/ecosystem.html +162 -0
- package/site/how-it-works.html +8 -9
- package/site/index.html +17 -12
- package/site/perps.html +103 -45
- package/site/recipes.html +5 -6
- package/site/reviewers.html +5 -6
- package/site/tutorials/index.html +5 -6
- package/site/tutorials/v1.html +5 -6
- package/site/tutorials/v2.html +6 -8
- package/site/tutorials/v3.html +5 -6
- package/site/tutorials/v4.html +5 -6
- package/site/tutorials/v5.html +5 -6
- package/site/tutorials/v6.html +5 -6
- package/site/tutorials/v7.html +5 -6
|
@@ -48,7 +48,7 @@ function fixturesStatus(options) {
|
|
|
48
48
|
const fixture = { ...summary, path: canonical };
|
|
49
49
|
const ready = summary.status === "ready";
|
|
50
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"];
|
|
51
|
+
const operations = adapter === "extension" ? ["init", "sync", "set", "reset", "generate", "finalize"] : adapter === "mobile" ? ["init", "sync", "set", "reset"] : ["init", "sync", "set"];
|
|
52
52
|
if (json) {
|
|
53
53
|
console.log(JSON.stringify({
|
|
54
54
|
schemaVersion: 1,
|
|
@@ -103,8 +103,8 @@ async function handleFixturesLocked(argv) {
|
|
|
103
103
|
const { positional, options } = parseFlags(argv, FIXTURES_BOOLEANS);
|
|
104
104
|
const json = flag(options, "json");
|
|
105
105
|
const sub = positional[0];
|
|
106
|
-
if (sub !== "init" && sub !== "sync" && sub !== "set" && sub !== "generate" && sub !== "finalize") {
|
|
107
|
-
return usageOut(json, "fixtures", "fixtures requires a subcommand: mm-harness fixtures <init|sync|set|generate|finalize>", "mm-harness fixtures init --from <path> or mm-harness fixtures init --dev");
|
|
106
|
+
if (sub !== "init" && sub !== "sync" && sub !== "set" && sub !== "reset" && sub !== "generate" && sub !== "finalize") {
|
|
107
|
+
return usageOut(json, "fixtures", "fixtures requires a subcommand: mm-harness fixtures <init|sync|set|reset|generate|finalize>", "mm-harness fixtures init --from <path> or mm-harness fixtures init --dev");
|
|
108
108
|
}
|
|
109
109
|
if (sub !== "init" && (flag(options, "dev") || flag(options, "force") || str(options, "from"))) {
|
|
110
110
|
return usageOut(
|
|
@@ -121,7 +121,7 @@ async function handleFixturesLocked(argv) {
|
|
|
121
121
|
}
|
|
122
122
|
const surface = getAdapterSurface(adapter);
|
|
123
123
|
surface.resolveSlotPorts(target);
|
|
124
|
-
const dtResult = applyDeviceTargeting("fixtures", adapter, options, { gate:
|
|
124
|
+
const dtResult = applyDeviceTargeting("fixtures", adapter, options, { gate: sub === "reset", rerun: "" });
|
|
125
125
|
if ("code" in dtResult) {
|
|
126
126
|
return usageOut(json, "fixtures", dtResult.message, dtResult.userAction);
|
|
127
127
|
}
|
|
@@ -129,6 +129,9 @@ async function handleFixturesLocked(argv) {
|
|
|
129
129
|
if (sub === "finalize") return fixturesFinalize(adapter, target, options, json);
|
|
130
130
|
const canonicalFixture = walletFixturePath(target);
|
|
131
131
|
if (sub === "init") return fixturesInit(adapter, target, options, json);
|
|
132
|
+
if (sub === "reset" && surface.headless) {
|
|
133
|
+
return usageOut(json, "fixtures", "fixtures reset requires a Mobile or Extension client.", "use fixtures set for the core adapter");
|
|
134
|
+
}
|
|
132
135
|
if (surface.headless && sub === "set") {
|
|
133
136
|
const fixture = fixtureSummary(target, adapter);
|
|
134
137
|
if (fixture.status !== "ready") {
|
|
@@ -139,7 +142,7 @@ async function handleFixturesLocked(argv) {
|
|
|
139
142
|
else console.error(message);
|
|
140
143
|
return EXIT.ok;
|
|
141
144
|
}
|
|
142
|
-
const retryHint = `${surface.hints.relaunch} # relaunch, then retry: mm-harness fixtures
|
|
145
|
+
const retryHint = `${surface.hints.relaunch} # relaunch, then retry: mm-harness fixtures ${sub}`;
|
|
143
146
|
if (sub === "sync") {
|
|
144
147
|
const exitCode = fixturesSync(adapter, target, json);
|
|
145
148
|
if (json) {
|
|
@@ -162,34 +165,105 @@ async function handleFixturesLocked(argv) {
|
|
|
162
165
|
"mm-harness fixtures init --dev && mm-harness fixtures set"
|
|
163
166
|
);
|
|
164
167
|
}
|
|
168
|
+
if (sub === "reset") {
|
|
169
|
+
const selectedFixture = fixtureFileSummary(fixturePath, adapter, fixturePath);
|
|
170
|
+
if (selectedFixture.status !== "ready") {
|
|
171
|
+
return usageOut(
|
|
172
|
+
json,
|
|
173
|
+
"fixtures",
|
|
174
|
+
`wallet fixture is ${String(selectedFixture.status)} at ${fixturePath}; reset was not started.`,
|
|
175
|
+
`mm-harness fixtures init --from <path> --target ${target}`
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
165
179
|
if (adapter === "extension") {
|
|
166
|
-
return applyExtensionFixture(target, fixturePath, canonicalFixture, json);
|
|
180
|
+
return applyExtensionFixture(target, fixturePath, canonicalFixture, json, sub);
|
|
167
181
|
}
|
|
168
|
-
|
|
182
|
+
const reset = sub === "reset";
|
|
183
|
+
process.stderr.write(`\u2192 fixtures ${sub} ${adapter} \u2014 ${reset ? "resetting selected app data, relaunching, then applying" : "connecting bridge + applying"} wallet fixture (can take ~30s)\u2026
|
|
169
184
|
`);
|
|
170
185
|
let status;
|
|
171
186
|
let mobileSetupResult;
|
|
187
|
+
let resetError;
|
|
172
188
|
if (adapter === "mobile") {
|
|
173
189
|
const setupWalletSh = path.join(runnerDir, "adapters/mobile/bridge-runtime/setup-wallet.sh");
|
|
174
190
|
const previousAppRoot = process.env.APP_ROOT;
|
|
175
191
|
process.env.APP_ROOT = target;
|
|
176
192
|
try {
|
|
193
|
+
if (reset) {
|
|
194
|
+
const platform = process.env.PLATFORM === "android" || process.env.ADB_SERIAL || process.env.ANDROID_SERIAL ? "android" : "ios";
|
|
195
|
+
const port = process.env.WATCHER_PORT ?? process.env.METRO_PORT ?? "8081";
|
|
196
|
+
const resetAppData = path.join(runnerDir, "adapters/mobile/reset-app-data.sh");
|
|
197
|
+
const resetArgs = ["--platform", platform, "--target", target];
|
|
198
|
+
if (platform === "ios") {
|
|
199
|
+
resetArgs.push("--simulator", process.env.SIM_UDID || process.env.IOS_SIMULATOR || "booted");
|
|
200
|
+
} else if (process.env.ADB_SERIAL || process.env.ANDROID_SERIAL) {
|
|
201
|
+
resetArgs.push("--adb-serial", process.env.ADB_SERIAL || process.env.ANDROID_SERIAL || "");
|
|
202
|
+
}
|
|
203
|
+
const resetResult = await spawnScriptStreaming(resetAppData, resetArgs, target, {
|
|
204
|
+
timeoutMs: FIXTURE_STREAM_TIMEOUT_MS
|
|
205
|
+
});
|
|
206
|
+
if (resetResult.status !== 0) {
|
|
207
|
+
resetError = new Error("Mobile app data reset failed.");
|
|
208
|
+
} else {
|
|
209
|
+
const prewarmBundle = path.join(runnerDir, "adapters/mobile/prewarm-bundle.sh");
|
|
210
|
+
const prewarmResult = await spawnScriptStreaming(
|
|
211
|
+
prewarmBundle,
|
|
212
|
+
["--platform", platform, "--target", target, "--port", port],
|
|
213
|
+
target,
|
|
214
|
+
{
|
|
215
|
+
timeoutMs: FIXTURE_STREAM_TIMEOUT_MS
|
|
216
|
+
}
|
|
217
|
+
);
|
|
218
|
+
if (prewarmResult.status !== 0) {
|
|
219
|
+
resetError = new Error("Mobile bundle prewarm failed after app data reset.");
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (!resetError) {
|
|
223
|
+
const openDevice = path.join(runnerDir, "adapters/mobile/open-device.sh");
|
|
224
|
+
const openArgs = ["--platform", platform, "--target", target, "--port", port, "--preflight-mode", "fast", "--restart"];
|
|
225
|
+
if (platform === "ios") {
|
|
226
|
+
openArgs.push("--simulator", process.env.SIM_UDID || process.env.IOS_SIMULATOR || "booted");
|
|
227
|
+
} else if (process.env.ADB_SERIAL || process.env.ANDROID_SERIAL) {
|
|
228
|
+
openArgs.push("--adb-serial", process.env.ADB_SERIAL || process.env.ANDROID_SERIAL || "");
|
|
229
|
+
}
|
|
230
|
+
const openResult = await spawnScriptStreaming(openDevice, openArgs, target, {
|
|
231
|
+
timeoutMs: FIXTURE_STREAM_TIMEOUT_MS
|
|
232
|
+
});
|
|
233
|
+
if (openResult.status !== 0) {
|
|
234
|
+
resetError = new Error("Mobile app relaunch after data reset failed.");
|
|
235
|
+
} else {
|
|
236
|
+
const waitForBridge = path.join(runnerDir, "adapters/mobile/wait-for-bridge.sh");
|
|
237
|
+
const waitResult = await spawnScriptStreaming(
|
|
238
|
+
waitForBridge,
|
|
239
|
+
["--target", target, "--port", port, "--platform", platform],
|
|
240
|
+
target,
|
|
241
|
+
{ timeoutMs: FIXTURE_STREAM_TIMEOUT_MS }
|
|
242
|
+
);
|
|
243
|
+
if (waitResult.status !== 0) {
|
|
244
|
+
resetError = new Error("Mobile bridge did not return after app data reset.");
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
177
249
|
const configuredTimeout = Number(process.env.MM_HARNESS_FIXTURES_SET_TIMEOUT_MS ?? FIXTURE_STREAM_TIMEOUT_MS);
|
|
178
250
|
const timeoutMs = Number.isFinite(configuredTimeout) && configuredTimeout > 0 ? configuredTimeout : FIXTURE_STREAM_TIMEOUT_MS;
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
251
|
+
if (!resetError) {
|
|
252
|
+
mobileSetupResult = await spawnScriptStreaming(
|
|
253
|
+
setupWalletSh,
|
|
254
|
+
["--fixture", fixturePath],
|
|
255
|
+
target,
|
|
256
|
+
{
|
|
257
|
+
env: {
|
|
258
|
+
APP_ROOT: target,
|
|
259
|
+
WATCHER_PORT: process.env.WATCHER_PORT ?? "8081",
|
|
260
|
+
METRO_PORT: process.env.METRO_PORT ?? process.env.WATCHER_PORT ?? "8081"
|
|
261
|
+
},
|
|
262
|
+
timeoutMs
|
|
263
|
+
}
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
status = !resetError && mobileSetupResult?.status === 0 ? "pass" : "fail";
|
|
193
267
|
} finally {
|
|
194
268
|
if (previousAppRoot === void 0) delete process.env.APP_ROOT;
|
|
195
269
|
else process.env.APP_ROOT = previousAppRoot;
|
|
@@ -205,16 +279,17 @@ async function handleFixturesLocked(argv) {
|
|
|
205
279
|
{
|
|
206
280
|
schemaVersion: 1,
|
|
207
281
|
command: "fixtures",
|
|
208
|
-
action:
|
|
282
|
+
action: sub,
|
|
209
283
|
adapter,
|
|
210
284
|
fixture: fixturePath,
|
|
211
285
|
canonicalFixture,
|
|
212
286
|
status,
|
|
213
287
|
exitCode: status === "pass" ? EXIT.ok : EXIT.runtime,
|
|
288
|
+
walletReset: reset && status === "pass",
|
|
214
289
|
message: teaching,
|
|
215
290
|
error: status === "fail" ? {
|
|
216
|
-
code: mobileSetupResult?.timedOut ? "SETUP_WALLET_TIMEOUT" : "SETUP_WALLET_FAILED",
|
|
217
|
-
message: mobileSetupResult?.timedOut ? `wallet fixture setup exceeded its ${String(mobileSetupResult.timeoutMs)}ms bound on Metro port ${process.env.WATCHER_PORT ?? "8081"}` : `wallet fixture setup failed on ${process.env.WATCHER_PORT ?? "8081"}`,
|
|
291
|
+
code: resetError ? "MOBILE_WALLET_RESET_FAILED" : mobileSetupResult?.timedOut ? "SETUP_WALLET_TIMEOUT" : "SETUP_WALLET_FAILED",
|
|
292
|
+
message: resetError ? `wallet reset failed: ${resetError instanceof Error ? resetError.message : String(resetError)}` : mobileSetupResult?.timedOut ? `wallet fixture setup exceeded its ${String(mobileSetupResult.timeoutMs)}ms bound on Metro port ${process.env.WATCHER_PORT ?? "8081"}` : `wallet fixture setup failed on ${process.env.WATCHER_PORT ?? "8081"}`,
|
|
218
293
|
userAction: retryHint
|
|
219
294
|
} : null
|
|
220
295
|
},
|
|
@@ -227,7 +302,7 @@ async function handleFixturesLocked(argv) {
|
|
|
227
302
|
}
|
|
228
303
|
return status === "pass" ? EXIT.ok : EXIT.runtime;
|
|
229
304
|
}
|
|
230
|
-
async function applyExtensionFixture(target, fixturePath, canonicalFixture, json) {
|
|
305
|
+
async function applyExtensionFixture(target, fixturePath, canonicalFixture, json, action) {
|
|
231
306
|
const distManifest = path.join(target, "dist/chrome/manifest.json");
|
|
232
307
|
const userAction = `mm-harness launch --build --verify --target ${JSON.stringify(target)}`;
|
|
233
308
|
if (!fs.existsSync(distManifest)) {
|
|
@@ -236,7 +311,7 @@ async function applyExtensionFixture(target, fixturePath, canonicalFixture, json
|
|
|
236
311
|
console.log(JSON.stringify({
|
|
237
312
|
schemaVersion: 1,
|
|
238
313
|
command: "fixtures",
|
|
239
|
-
action
|
|
314
|
+
action,
|
|
240
315
|
adapter: "extension",
|
|
241
316
|
fixture: fixturePath,
|
|
242
317
|
canonicalFixture,
|
|
@@ -245,15 +320,15 @@ async function applyExtensionFixture(target, fixturePath, canonicalFixture, json
|
|
|
245
320
|
error: { code: "EXTENSION_FIXTURE_BUILD_REQUIRED", message: message2, userAction }
|
|
246
321
|
}, null, 2));
|
|
247
322
|
} else {
|
|
248
|
-
console.error(`\u2717 mm-harness fixtures
|
|
323
|
+
console.error(`\u2717 mm-harness fixtures ${action}: ${message2}
|
|
249
324
|
Next: ${userAction}`);
|
|
250
325
|
}
|
|
251
326
|
return EXIT.runtime;
|
|
252
327
|
}
|
|
253
|
-
process.stderr.write(`${colorHumanMessage(
|
|
328
|
+
process.stderr.write(`${colorHumanMessage(`\u2192 fixtures ${action} extension \u2014 resetting and seeding the slot-owned profile from the existing build\u2026`)}
|
|
254
329
|
`);
|
|
255
330
|
const liveScript = path.join(runnerDir, "adapters/extension/live.sh");
|
|
256
|
-
const args = ["--target", target, "--launch-existing-dist"];
|
|
331
|
+
const args = ["--target", target, "--launch-existing-dist", "--launch-only"];
|
|
257
332
|
if (process.env.CDP_PORT) args.push("--cdp-port", process.env.CDP_PORT);
|
|
258
333
|
const result = await spawnScriptStreaming(liveScript, args, target, {
|
|
259
334
|
env: { RECIPE_WALLET_FIXTURE: fixturePath },
|
|
@@ -266,7 +341,7 @@ async function applyExtensionFixture(target, fixturePath, canonicalFixture, json
|
|
|
266
341
|
console.log(JSON.stringify({
|
|
267
342
|
schemaVersion: 1,
|
|
268
343
|
command: "fixtures",
|
|
269
|
-
action
|
|
344
|
+
action,
|
|
270
345
|
adapter: "extension",
|
|
271
346
|
fixture: fixturePath,
|
|
272
347
|
canonicalFixture,
|
|
@@ -279,7 +354,7 @@ async function applyExtensionFixture(target, fixturePath, canonicalFixture, json
|
|
|
279
354
|
} else if (status === "pass") {
|
|
280
355
|
console.error(message);
|
|
281
356
|
} else {
|
|
282
|
-
console.error(`\u2717 mm-harness fixtures
|
|
357
|
+
console.error(`\u2717 mm-harness fixtures ${action}: ${message}
|
|
283
358
|
Next: ${userAction}`);
|
|
284
359
|
}
|
|
285
360
|
return exitCode;
|
|
@@ -15,11 +15,16 @@ import {
|
|
|
15
15
|
runnerDir
|
|
16
16
|
} from "../../paths.js";
|
|
17
17
|
import { extensionIdFromKey } from "../../adapters/extension/extension-id.js";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
EXTENSION_PRODUCT_CONFIG_FINGERPRINT_FILENAME,
|
|
20
|
+
extensionCompiledScriptsMatchProductConfig,
|
|
21
|
+
extensionProductConfigBlock,
|
|
22
|
+
extensionProductConfigFingerprint
|
|
23
|
+
} from "../../adapters/extension/product-config.js";
|
|
19
24
|
import { isExtensionDistStale } from "../../adapters/extension/runtime-decision.js";
|
|
20
25
|
import { checkExtensionRuntimeHealth } from "../../adapters/extension/runtime.js";
|
|
21
26
|
import { ensureHarnessFresh } from "../../adapters/harness-freshness.js";
|
|
22
|
-
import { stopExtensionWatcher } from "../../adapters/slot-ports.js";
|
|
27
|
+
import { isExtensionWatcherLive, stopExtensionWatcher } from "../../adapters/slot-ports.js";
|
|
23
28
|
import { EXIT, spawnScriptStreaming } from "../shared.js";
|
|
24
29
|
const { CdpSession } = await importRecipeHarnessRuntimeCdp();
|
|
25
30
|
const { RUNTIME_IDENTITY_FILENAME, RUNTIME_NONCE_PREFIX } = createRequire(import.meta.url)(
|
|
@@ -75,12 +80,14 @@ Next: ${block.userAction}`
|
|
|
75
80
|
if (reusable) {
|
|
76
81
|
return extensionReattach(target, displayMode);
|
|
77
82
|
}
|
|
78
|
-
return
|
|
83
|
+
return extensionLaunchDevelopment(target);
|
|
79
84
|
}
|
|
80
85
|
async function extensionRuntimeReusable(target) {
|
|
81
86
|
const cdpPort = process.env.CDP_PORT;
|
|
82
87
|
if (!cdpPort) return false;
|
|
83
88
|
if (isExtensionDistStale(target)) return false;
|
|
89
|
+
if (isExtensionWatcherLive(target) && !harnessWatcherMatchesProductConfig(target)) return false;
|
|
90
|
+
if (!extensionCompiledScriptsMatchProductConfig(target, expectedRuntimeDist(target))) return false;
|
|
84
91
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
85
92
|
const reachable = await cdpVersionReachable(cdpPort);
|
|
86
93
|
const nonceOwned = reachable && await cdpOwnedByRuntimeNonce(cdpPort, target);
|
|
@@ -325,6 +332,34 @@ async function extensionReattach(target, displayMode = "fullscreen") {
|
|
|
325
332
|
console.error(colorHumanMessage(`\u2192 extension quick reattach \u2014 reload in place \xB7 CDP :${process.env.CDP_PORT ?? "default"} (no rebuild, no relaunch)`));
|
|
326
333
|
return spawnScriptStreaming(reattachScript, reattachArgs, target);
|
|
327
334
|
}
|
|
335
|
+
async function extensionLaunchDevelopment(target) {
|
|
336
|
+
const liveScript = recipeHarnessPath(target, "extension", "scripts", "live.sh");
|
|
337
|
+
const liveArgs = ["--target", target];
|
|
338
|
+
const existingBuild = isExtensionWatcherLive(target) && harnessWatcherMatchesProductConfig(target) && extensionCompiledScriptsMatchProductConfig(target, path.join(target, "dist/chrome")) && fs.existsSync(path.join(target, "dist/chrome/manifest.json")) && !isExtensionDistStale(target);
|
|
339
|
+
liveArgs.push(existingBuild ? "--launch-existing-dist" : "--start-watch");
|
|
340
|
+
if (process.env.CDP_PORT) liveArgs.push("--cdp-port", process.env.CDP_PORT);
|
|
341
|
+
if (process.env.EXTENSION_START_URL) liveArgs.push("--start-url", process.env.EXTENSION_START_URL);
|
|
342
|
+
console.error(colorHumanMessage(
|
|
343
|
+
existingBuild ? `\u2192 extension quick launch \u2014 existing yarn start build \xB7 CDP :${process.env.CDP_PORT ?? "default"} (output streams below)` : `\u2192 extension quick launch \u2014 yarn start development build \xB7 CDP :${process.env.CDP_PORT ?? "default"} (output streams below)`
|
|
344
|
+
));
|
|
345
|
+
return spawnScriptStreaming(liveScript, liveArgs, target);
|
|
346
|
+
}
|
|
347
|
+
function harnessWatcherMatchesProductConfig(target) {
|
|
348
|
+
const runtimeDir = path.join(target, recipeRuntimeDir());
|
|
349
|
+
const harnessPidFile = path.join(runtimeDir, "recipe-harness-webpack.pid");
|
|
350
|
+
if (!fs.existsSync(harnessPidFile)) return true;
|
|
351
|
+
const expected = extensionProductConfigFingerprint(target);
|
|
352
|
+
if (!expected) return true;
|
|
353
|
+
try {
|
|
354
|
+
const actual = fs.readFileSync(
|
|
355
|
+
path.join(runtimeDir, EXTENSION_PRODUCT_CONFIG_FINGERPRINT_FILENAME),
|
|
356
|
+
"utf8"
|
|
357
|
+
).trim();
|
|
358
|
+
return actual === expected;
|
|
359
|
+
} catch {
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
328
363
|
async function extensionRebuild(target) {
|
|
329
364
|
const runtimeDirRel = recipeRuntimeDir();
|
|
330
365
|
const runtimeAbs = path.join(target, runtimeDirRel);
|
|
@@ -348,6 +383,7 @@ async function extensionRebuild(target) {
|
|
|
348
383
|
}
|
|
349
384
|
export {
|
|
350
385
|
extensionDepsBlock,
|
|
386
|
+
extensionLaunchDevelopment,
|
|
351
387
|
extensionReattach,
|
|
352
388
|
extensionRebuild,
|
|
353
389
|
extensionRuntimeReusable,
|
|
@@ -259,6 +259,16 @@ async function handleLaunchLocked(argv, stream) {
|
|
|
259
259
|
exitCode: EXIT.infra
|
|
260
260
|
});
|
|
261
261
|
}
|
|
262
|
+
if (adapter === "extension" && extensionEvmRpcUnreachable(attempt.output)) {
|
|
263
|
+
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
264
|
+
code: "EVM_RPC_UNREACHABLE",
|
|
265
|
+
message: "EVM RPC readiness failed; Infura configured does not mean RPC reachable.",
|
|
266
|
+
recoverable: false,
|
|
267
|
+
userAction: `Check the effective INFURA_PROJECT_ID from the environment, .metamaskprodrc, or ${shellQuote(path.join(target, ".metamaskrc"))}, then rerun: mm-harness launch --verify --target ${shellQuote(target)}`,
|
|
268
|
+
exitCode: EXIT.runtime,
|
|
269
|
+
originalError: attempt.output.trim() || void 0
|
|
270
|
+
});
|
|
271
|
+
}
|
|
262
272
|
if (heal === "off") {
|
|
263
273
|
return launchFail(jsonOutput, stream, adapter, mobileTarget, tier, state, target, {
|
|
264
274
|
code: "LAUNCH_FAILED",
|
|
@@ -374,6 +384,9 @@ function extensionBrowserMissing(output) {
|
|
|
374
384
|
function extensionProductConfigMissing(output) {
|
|
375
385
|
return /EXTENSION_PRODUCT_CONFIG_REQUIRED:/u.test(output);
|
|
376
386
|
}
|
|
387
|
+
function extensionEvmRpcUnreachable(output) {
|
|
388
|
+
return /EVM_RPC_UNREACHABLE|EVM RPC readiness probe failed|Infura configured ≠ RPC reachable/u.test(output);
|
|
389
|
+
}
|
|
377
390
|
function mobileProvisioningBlocked(output) {
|
|
378
391
|
return /open-device: configured iOS simulator '.+' does not exist|open-device: no MetaMask bundle found|fast mode requires an installed (?:iOS dev client|Android dev client)/u.test(output);
|
|
379
392
|
}
|
package/dist/doctor.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { color } from "./cli-color.js";
|
|
4
5
|
import {
|
|
@@ -148,7 +149,8 @@ function createDoctorReport(adapter, target, manifestValidation, actionManifestP
|
|
|
148
149
|
required: false,
|
|
149
150
|
message: mode === "unsupported/no bridge" ? `No ${adapter} bridge is available for this checkout.` : `${adapter} compatibility mode: ${mode}.`
|
|
150
151
|
},
|
|
151
|
-
...adapter === "mobile" ? mobileToolDoctorChecks(mobilePlatform) : []
|
|
152
|
+
...adapter === "mobile" ? mobileToolDoctorChecks(mobilePlatform) : [],
|
|
153
|
+
...adapter === "extension" ? [extensionBrowserDoctorCheck(target)] : []
|
|
152
154
|
];
|
|
153
155
|
const requiredChecks = requiredDoctorCheckSummary(checks);
|
|
154
156
|
return {
|
|
@@ -169,6 +171,32 @@ function createDoctorReport(adapter, target, manifestValidation, actionManifestP
|
|
|
169
171
|
manifestValidation: manifestValidation.summary
|
|
170
172
|
};
|
|
171
173
|
}
|
|
174
|
+
function extensionBrowserDoctorCheck(target) {
|
|
175
|
+
const requireFromTarget = createRequire(path.join(path.resolve(target), "package.json"));
|
|
176
|
+
for (const packageName of ["@playwright/test", "playwright"]) {
|
|
177
|
+
try {
|
|
178
|
+
const playwright = requireFromTarget(packageName);
|
|
179
|
+
const executable = playwright.chromium?.executablePath?.();
|
|
180
|
+
if (executable && fs.existsSync(executable)) {
|
|
181
|
+
return {
|
|
182
|
+
id: "playwright-chromium",
|
|
183
|
+
status: "pass",
|
|
184
|
+
required: true,
|
|
185
|
+
message: "Checkout-pinned Playwright Chromium is installed.",
|
|
186
|
+
detail: executable
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
} catch {
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
id: "playwright-chromium",
|
|
194
|
+
status: "fail",
|
|
195
|
+
required: true,
|
|
196
|
+
message: "Checkout-pinned Playwright Chromium is not installed.",
|
|
197
|
+
userAction: "yarn playwright install chromium"
|
|
198
|
+
};
|
|
199
|
+
}
|
|
172
200
|
function mobileToolDoctorChecks(platform) {
|
|
173
201
|
const specs = platform === "android" ? [{ tool: "adb", platform: "Android" }] : platform === "ios" ? [{ tool: "idb", platform: "iOS" }] : [
|
|
174
202
|
{ tool: "adb", platform: "Android" },
|
|
@@ -250,6 +278,7 @@ function readJsonObject(file) {
|
|
|
250
278
|
export {
|
|
251
279
|
compatibilityMode,
|
|
252
280
|
createDoctorReport,
|
|
281
|
+
extensionBrowserDoctorCheck,
|
|
253
282
|
fixtureFileSummary,
|
|
254
283
|
fixtureSummary,
|
|
255
284
|
mobileToolDoctorChecks,
|
package/dist/mm-harness-cli.js
CHANGED
|
@@ -452,7 +452,7 @@ Example:
|
|
|
452
452
|
Extension quick launch REUSES a healthy live runtime (reload in place, no rebuild);
|
|
453
453
|
--build is the escape hatch that always clean-builds + relaunches a fresh runtime.
|
|
454
454
|
|
|
455
|
-
--build Full
|
|
455
|
+
--build Full production-like build + relaunch (escape hatch; skips extension reuse)
|
|
456
456
|
--clear-metro Mobile only: clear Metro's transform cache before launch
|
|
457
457
|
--verify Launch then poll CDP/bridge until ready (absorbs the old \`live\`)
|
|
458
458
|
--runway Post-launch runway check (mobile only; teaching error elsewhere)
|
|
@@ -539,9 +539,9 @@ Example:
|
|
|
539
539
|
},
|
|
540
540
|
{
|
|
541
541
|
name: "fixtures",
|
|
542
|
-
summary: "Manage the canonical wallet fixture (wallet DATA only) \u2014 init / sync / set / generate / finalize.",
|
|
542
|
+
summary: "Manage the canonical wallet fixture (wallet DATA only) \u2014 init / sync / set / reset / generate / finalize.",
|
|
543
543
|
example: "mm-harness fixtures set",
|
|
544
|
-
helpText: `mm-harness fixtures [<init|sync|set|generate|finalize>] [flags]
|
|
544
|
+
helpText: `mm-harness fixtures [<init|sync|set|reset|generate|finalize>] [flags]
|
|
545
545
|
|
|
546
546
|
Manage the ONE canonical wallet fixture per checkout \u2014 wallet DATA only.
|
|
547
547
|
With no action, show the current fixture status and safe next command.
|
|
@@ -550,6 +550,8 @@ Example:
|
|
|
550
550
|
sync Refresh the wallet fixture files on the target.
|
|
551
551
|
set Apply the canonical fixture (SRP/password/accounts); the password is
|
|
552
552
|
read FROM the fixture, never typed.
|
|
553
|
+
reset Delete the current Mobile/Extension wallet, then apply and validate
|
|
554
|
+
the canonical fixture from a clean state.
|
|
553
555
|
generate Render the extension fixture-state.json from a wallet fixture, for
|
|
554
556
|
pre-launch profile prefill (extension-only). Requires --fixture and --out.
|
|
555
557
|
finalize Seed account labels/selection into the LIVE extension over CDP and
|
|
@@ -578,6 +580,7 @@ Example:
|
|
|
578
580
|
mm-harness fixtures init --dev
|
|
579
581
|
mm-harness fixtures sync
|
|
580
582
|
mm-harness fixtures set
|
|
583
|
+
mm-harness fixtures reset
|
|
581
584
|
mm-harness fixtures generate --fixture wallet-fixture.json --out fixture-state.json
|
|
582
585
|
mm-harness fixtures finalize --fixture wallet-fixture.json --state fixture-state.json --cdp-port 6661 --extension-dir dist/chrome`
|
|
583
586
|
}
|
package/dist/recipe-security.js
CHANGED
|
@@ -4,6 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
const READ_ONLY_CUSTOM_ACTIONS = /* @__PURE__ */ new Set([
|
|
5
5
|
"ui.locators",
|
|
6
6
|
"metamask.wallet.fixture_status",
|
|
7
|
+
"metamask.wallet.validate_import",
|
|
7
8
|
"metamask.wallet.list_accounts",
|
|
8
9
|
"metamask.wallet.read_state",
|
|
9
10
|
"metamask.perps.read_positions",
|
|
@@ -19,6 +20,8 @@ const READ_ONLY_CUSTOM_ACTIONS = /* @__PURE__ */ new Set([
|
|
|
19
20
|
]);
|
|
20
21
|
const APP_MUTATION_CUSTOM_ACTIONS = /* @__PURE__ */ new Set([
|
|
21
22
|
"metamask.wallet.setup",
|
|
23
|
+
"metamask.wallet.import",
|
|
24
|
+
"metamask.wallet.reset",
|
|
22
25
|
"metamask.wallet.ensure_unlocked",
|
|
23
26
|
"metamask.wallet.select_account",
|
|
24
27
|
"metamask.deeplink.open",
|
package/docs/RECIPES.md
CHANGED
|
@@ -58,6 +58,35 @@ mm-harness run recipe.json account="Account 2" --plan
|
|
|
58
58
|
mm-harness run recipe.json account="Account 2"
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
+
Use the shared wallet setup recipe on either Mobile or Extension. `auto`
|
|
62
|
+
reuses a ready fixture-backed profile and otherwise completes visible
|
|
63
|
+
onboarding; `ui` requires a fresh onboarding state.
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
mm-harness run wallet.import method=auto
|
|
67
|
+
mm-harness run wallet.import method=ui credential_source=environment
|
|
68
|
+
mm-harness run wallet.import method=ui metametrics=true 'interests=["trade_perpetuals"]'
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The UI path reads the primary mnemonic from `MM_HARNESS_WALLET_SRP` and the
|
|
72
|
+
password from `MM_HARNESS_WALLET_PASSWORD`, or from the canonical wallet
|
|
73
|
+
fixture. Secret values are redacted from commands and evidence. Visible imports
|
|
74
|
+
default to MetaMetrics off and skip Mobile's optional interests; password terms
|
|
75
|
+
are required and always accepted by the action.
|
|
76
|
+
|
|
77
|
+
To prove the full real-user recovery journey, delete the current wallet through
|
|
78
|
+
visible client UI and import it again:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
mm-harness run wallet.reset-import credential_source=fixture
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
For fixture-backed development clients, `mm-harness fixtures reset` performs a
|
|
85
|
+
clean reset and reapplies the canonical fixture. Extension resets its owned
|
|
86
|
+
Chrome profile; Mobile clears only the selected app's data, preserves the
|
|
87
|
+
installed build, relaunches it, and then applies the fixture. Use
|
|
88
|
+
`wallet.reset-import` when the reset itself must be exercised through visible UI.
|
|
89
|
+
|
|
61
90
|
### Mobile device targeting
|
|
62
91
|
|
|
63
92
|
Always pass `--device <name|udid|serial>` when more than one simulator/device may
|