@deeeed/metamask-harness 0.35.0 → 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.
Files changed (38) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +9 -1
  3. package/adapters/extension/check-infura-readiness.cjs +102 -0
  4. package/adapters/extension/inject.mjs +1 -0
  5. package/adapters/extension/live.sh +25 -10
  6. package/adapters/extension/start-watch.sh +15 -0
  7. package/adapters/extension/wallet-fixture-state.cjs +3 -1
  8. package/adapters/manifest.json +16 -0
  9. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +73 -42
  10. package/adapters/mobile/reset-app-data.sh +154 -0
  11. package/dist/adapters/extension/product-config.js +29 -1
  12. package/dist/adapters.js +18 -0
  13. package/dist/cli-commands.js +1 -1
  14. package/dist/cli.js +2 -2
  15. package/dist/command-contract.js +1 -1
  16. package/dist/commands/device-target.js +5 -0
  17. package/dist/commands/fixtures.js +106 -31
  18. package/dist/commands/launch/extension.js +39 -3
  19. package/dist/commands/launch/index.js +13 -0
  20. package/dist/mm-harness-cli.js +6 -3
  21. package/dist/recipe-security.js +3 -0
  22. package/docs/RECIPES.md +29 -0
  23. package/library/actions/extension/wallet/import.mjs +234 -0
  24. package/library/actions/extension/wallet/reset.mjs +98 -0
  25. package/library/actions/extension/wallet/state.mjs +1 -0
  26. package/library/actions/mobile/analytics/consent-settings.mjs +112 -0
  27. package/library/actions/mobile/analytics/set_consent.mjs +4 -112
  28. package/library/actions/mobile/platform/bridge.mjs +8 -0
  29. package/library/actions/mobile/wallet/import.mjs +259 -0
  30. package/library/actions/mobile/wallet/reset-helper.mjs +99 -0
  31. package/library/actions/mobile/wallet/reset.mjs +7 -0
  32. package/library/actions/shared/wallet/import-source.mjs +101 -0
  33. package/library/manifests/extension.action-manifest.json +112 -0
  34. package/library/manifests/mobile.action-manifest.json +108 -0
  35. package/library/recipes/wallet/import.recipe.json +83 -0
  36. package/library/recipes/wallet/reset-import.recipe.json +88 -0
  37. package/package.json +1 -1
  38. package/scripts/completions.sh +2 -2
@@ -1,6 +1,31 @@
1
+ import { createHash } from "node:crypto";
2
+ import fs from "node:fs";
1
3
  import path from "node:path";
2
4
  import productConfig from "../../../adapters/extension/lib/product-config.cjs";
3
5
  import { shellQuote } from "../../commands/parse-args.js";
6
+ const EXTENSION_PRODUCT_CONFIG_FINGERPRINT_FILENAME = "extension-product-config.sha256";
7
+ function extensionProductConfigFingerprint(target, environment = process.env) {
8
+ const resolution = productConfig.resolveExtensionInfuraProjectId(target, environment);
9
+ if (resolution.kind !== "configured") return null;
10
+ return createHash("sha256").update(resolution.value).digest("hex");
11
+ }
12
+ function extensionCompiledScriptsMatchProductConfig(target, dist, environment = process.env) {
13
+ const resolution = productConfig.resolveExtensionInfuraProjectId(target, environment);
14
+ if (resolution.kind !== "configured") return true;
15
+ const pending = [dist];
16
+ while (pending.length > 0) {
17
+ const current = pending.pop();
18
+ if (!current || !fs.existsSync(current)) continue;
19
+ for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
20
+ const file = path.join(current, entry.name);
21
+ if (entry.isDirectory()) pending.push(file);
22
+ else if (entry.isFile() && entry.name.endsWith(".js") && fs.readFileSync(file, "utf8").includes(resolution.value)) {
23
+ return true;
24
+ }
25
+ }
26
+ }
27
+ return false;
28
+ }
4
29
  function extensionProductConfigBlock(target) {
5
30
  const resolution = productConfig.resolveExtensionInfuraProjectId(
6
31
  target,
@@ -55,5 +80,8 @@ function invalidConfigBlock(target, name, reason) {
55
80
  };
56
81
  }
57
82
  export {
58
- extensionProductConfigBlock
83
+ EXTENSION_PRODUCT_CONFIG_FINGERPRINT_FILENAME,
84
+ extensionCompiledScriptsMatchProductConfig,
85
+ extensionProductConfigBlock,
86
+ extensionProductConfigFingerprint
59
87
  };
package/dist/adapters.js CHANGED
@@ -9,6 +9,7 @@ import { withExtensionPage } from "../library/actions/extension/platform/cdp.mjs
9
9
  import { bridgeCommand, evalAsync, evalSync, MOBILE_BRIDGE_ERROR_CODES, selectBridgeStatusEntry, simulatorScreenshot } from "../library/actions/mobile/platform/bridge.mjs";
10
10
  import { observeNativeUi } from "../library/actions/mobile/platform/observe-ui.mjs";
11
11
  import { resolveMobileToolPath } from "../library/actions/mobile/platform/tool-paths.mjs";
12
+ import { resolveWalletImportCredentials, validateWalletImportOptions } from "../library/actions/shared/wallet/import-source.mjs";
12
13
  const execFileAsync = promisify(execFile);
13
14
  const NATIVE_PROVIDER_UI_ACTIONS = /* @__PURE__ */ new Set([
14
15
  "ui.swipe",
@@ -51,6 +52,8 @@ const CORE_ONLY_PERPS_ACTIONS = /* @__PURE__ */ new Set([
51
52
  ]);
52
53
  const LIVE_ONLY_WALLET_ACTIONS = /* @__PURE__ */ new Set([
53
54
  "metamask.wallet.setup",
55
+ "metamask.wallet.import",
56
+ "metamask.wallet.reset",
54
57
  "metamask.wallet.ensure_unlocked",
55
58
  "metamask.wallet.select_account",
56
59
  "metamask.wallet.list_accounts",
@@ -110,6 +113,18 @@ async function semanticResult(platform, action, node, context, forceLive = false
110
113
  }
111
114
  const output = { platform, action, redacted: true };
112
115
  if (action === "metamask.wallet.fixture_status") return { output: fixtureSummary(context.projectRoot) };
116
+ if (action === "metamask.wallet.validate_import") {
117
+ validateWalletImportOptions(platform, node);
118
+ const credentials = await resolveWalletImportCredentials({ node, context });
119
+ return {
120
+ output: {
121
+ ...output,
122
+ credentialSource: credentials.source,
123
+ credentialSourceName: credentials.sourceName,
124
+ expectedAddress: credentials.expectedAddress
125
+ }
126
+ };
127
+ }
113
128
  return {
114
129
  output: {
115
130
  ...output,
@@ -183,7 +198,10 @@ function probeHttpJson(url, timeoutMs = 1e3) {
183
198
  function createMetaMaskSemanticAdapters(platform, declaredCustomActions = [], preparedLiveAdapters) {
184
199
  const walletActions = [
185
200
  "metamask.wallet.fixture_status",
201
+ "metamask.wallet.validate_import",
186
202
  "metamask.wallet.setup",
203
+ "metamask.wallet.import",
204
+ "metamask.wallet.reset",
187
205
  "metamask.wallet.ensure_unlocked",
188
206
  "metamask.wallet.select_account",
189
207
  "metamask.wallet.list_accounts",
@@ -15,7 +15,7 @@ const SPEC = {
15
15
  { name: "sync", desc: "Refresh harness + canonicalize wallet fixture", flags: ["--json"] },
16
16
  { name: "logs", aliases: ["tail"], desc: "Compact build events or full log", flags: ["--full", "-f", "--window", "--events", "--source", "--json"] },
17
17
  { name: "debug", aliases: ["devtools", "inspect"], desc: "Open DevTools UI", flags: ["--json", "--no-open"] },
18
- { name: "fixtures", desc: "Manage the canonical wallet fixture (sync/set/generate)", args: ["sync", "set", "generate"], flags: ["--fixture", "--out", "--adapter", "--target", "--device", "--json"] },
18
+ { name: "fixtures", desc: "Manage the canonical wallet fixture (sync/set/reset/generate)", args: ["sync", "set", "reset", "generate"], flags: ["--fixture", "--out", "--adapter", "--target", "--device", "--json"] },
19
19
  { name: "actions", desc: "List runnable recipe actions", flags: ["--json", "--matrix", "--categories", "--category", "--action", "--library"] },
20
20
  { name: "doctor", desc: "Check harness/orchestration health", flags: ["--json", "--target", "--adapter", "--runtime-dir", "--expect-live", "--print-ready", "--cdp-port", "--device"] },
21
21
  { name: "run", desc: "Execute a proof recipe (path or library name, e.g. run perps.smoke)", args: ["recipe.json|name"], flags: ["--list", "--device"] },
package/dist/cli.js CHANGED
@@ -47,8 +47,8 @@ DAILY LOOP \u2014 what a teammate runs many times a day:
47
47
  mm-harness logs
48
48
  debug Open the debug console (extension DevTools / mobile RN).
49
49
  mm-harness debug
50
- fixtures Sync files + set the wallet + generate fixture-state + finalize labels over CDP.
51
- mm-harness fixtures sync # or: set | generate --fixture <f> --out <o> | finalize \u2026
50
+ fixtures Sync files, set/reset the wallet, generate fixture-state, or finalize labels over CDP.
51
+ mm-harness fixtures sync # or: set | reset | generate --fixture <f> --out <o> | finalize \u2026
52
52
 
53
53
  PROVE \u2014 run recipes and inspect capabilities:
54
54
  run Run a recipe and write evidence (summary/trace/artifacts).
@@ -270,7 +270,7 @@ const PUBLIC_COMMAND_CONTRACTS = {
270
270
  "--extension-id-file": value(),
271
271
  "--action-manifest": value()
272
272
  }),
273
- positionals: [{ label: "action", choices: ["init", "sync", "set", "generate", "finalize"] }],
273
+ positionals: [{ label: "action", choices: ["init", "sync", "set", "reset", "generate", "finalize"] }],
274
274
  minimumPositionals: 0
275
275
  }
276
276
  };
@@ -256,6 +256,11 @@ ${formatConnectedDevices(connected)}
256
256
  userAction: deviceRecovery(options, opts.rerun)
257
257
  };
258
258
  }
259
+ if (targetable.length === 1) {
260
+ const selected = targetable[0];
261
+ if (selected.platform === "android") setAndroidDeviceEnv(selected.id, selected.name);
262
+ else setIosDeviceEnv(selected.id, selected.name);
263
+ }
259
264
  return { ok: true };
260
265
  }
261
266
  function scopedDevices(devices, allDevices = false) {
@@ -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: false, rerun: "" });
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 set`;
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
- process.stderr.write(`\u2192 fixtures set ${adapter} \u2014 connecting bridge + applying wallet fixture (can take ~30s)\u2026
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
- mobileSetupResult = await spawnScriptStreaming(
180
- setupWalletSh,
181
- ["--fixture", fixturePath],
182
- target,
183
- {
184
- env: {
185
- APP_ROOT: target,
186
- WATCHER_PORT: process.env.WATCHER_PORT ?? "8081",
187
- METRO_PORT: process.env.METRO_PORT ?? process.env.WATCHER_PORT ?? "8081"
188
- },
189
- timeoutMs
190
- }
191
- );
192
- status = mobileSetupResult.status === 0 ? "pass" : "fail";
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: "set",
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: "set",
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 set: ${message2}
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("\u2192 fixtures set extension \u2014 resetting and seeding the slot-owned profile from the existing build\u2026")}
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: "set",
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 set: ${message}
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 { extensionProductConfigBlock } from "../../adapters/extension/product-config.js";
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 extensionRebuild(target);
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
  }
@@ -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 clean build + relaunch (escape hatch; skips extension reuse)
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
  }
@@ -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