@deeeed/metamask-harness 0.34.4 → 0.35.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 CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.35.0 - 2026-08-12
6
+
7
+ ### Fixed
8
+
9
+ - Make Extension and Mobile launch verification fail when their in-app EVM network client cannot complete a live RPC, with exact Infura configuration and retry guidance.
10
+ - Make Extension doctor require checkout-pinned Playwright Chromium and report the exact install command when it is missing.
11
+ - Make doctor report the exact dependency-install command when `--fix` finds stale or missing dependencies instead of implying they were repaired.
12
+ - Make the setup prompt reuse configured skill sources and require consent before cloning or editing shell startup files.
13
+ - Keep installed overlays delegated to the source runner that installed them before falling back to a potentially stale global runner.
14
+
5
15
  ## 0.34.4 - 2026-08-12
6
16
 
7
17
  ### Changed
@@ -88,11 +88,11 @@ install_v1_runner_assets() {
88
88
  if [ -n "$METAMASK_RUNNER_PROTOCOL_ROOT" ]; then
89
89
  printf 'export FARMSLOT_ROOT=${FARMSLOT_ROOT:-%s}\n' "$runner_protocol_root_q"
90
90
  fi
91
- # Resolve the runner at run time (override > global install > install-time
92
- # record); the recorded path is last resort so a moved checkout is not pinned.
91
+ # The recorded runner owns the installed overlay and must remain ahead of an
92
+ # unrelated global version. MM_HARNESS_BIN is the explicit override.
93
93
  printf '%s\n' 'if [ -n "${MM_HARNESS_BIN:-}" ] && [ -x "${MM_HARNESS_BIN}" ]; then exec "${MM_HARNESS_BIN}" "$@"; fi'
94
- printf '%s\n' 'if command -v mm-harness >/dev/null 2>&1; then exec mm-harness "$@"; fi'
95
94
  printf 'if [ -x %s ]; then exec %s "$@"; fi\n' "$runner_exec_q" "$runner_exec_q"
95
+ printf '%s\n' 'if command -v mm-harness >/dev/null 2>&1; then exec mm-harness "$@"; fi'
96
96
  printf '%s\n' 'echo "mm-harness not found. Next: npm i -g @deeeed/metamask-harness" >&2'
97
97
  printf '%s\n' 'exit 127'
98
98
  } > "$HARNESS_DIR/runner/bin/mm-harness"
@@ -69,17 +69,16 @@ fs.mkdirSync(path.join(harnessDir, 'runner/recipes'), { recursive: true });
69
69
  fs.rmSync(path.join(harnessDir, 'scripts'), { recursive: true, force: true });
70
70
  fs.mkdirSync(path.join(harnessDir, 'scripts/lib'), { recursive: true });
71
71
 
72
- // Resolve the runner at run time so a moved or shared checkout is never bound to
73
- // an install-time absolute path: explicit override, then a global install, then
74
- // the runner recorded at install time as a last resort; teach install on miss.
72
+ // Prefer the runner that installed the overlay so its runtime checks match the
73
+ // installed scripts. Explicit overrides still support controlled delegation.
75
74
  const pinnedBin = shellQuote(path.join(runnerDir, 'bin/mm-harness'));
76
75
  const delegate = [
77
76
  '#!/usr/bin/env bash',
78
77
  'set -euo pipefail',
79
78
  protocolRoot ? `export FARMSLOT_ROOT=\${FARMSLOT_ROOT:-${shellQuote(protocolRoot)}}` : null,
80
79
  'if [ -n "${MM_HARNESS_BIN:-}" ] && [ -x "${MM_HARNESS_BIN}" ]; then exec "${MM_HARNESS_BIN}" "$@"; fi',
81
- 'if command -v mm-harness >/dev/null 2>&1; then exec mm-harness "$@"; fi',
82
80
  `if [ -x ${pinnedBin} ]; then exec ${pinnedBin} "$@"; fi`,
81
+ 'if command -v mm-harness >/dev/null 2>&1; then exec mm-harness "$@"; fi',
83
82
  'echo "mm-harness not found. Next: npm i -g @deeeed/metamask-harness" >&2',
84
83
  'exit 127',
85
84
  ].filter(Boolean).join('\n') + '\n';
@@ -44,7 +44,7 @@ for rel in "$HARNESS_REL" "$HARNESS_REL/runner" "$HARNESS_REL/scripts"; do
44
44
  done
45
45
  done
46
46
 
47
- rm -rf "$HARNESS_DIR/runner" "$HARNESS_DIR/scripts"
47
+ rm -rf "$HARNESS_DIR/runner" "$HARNESS_DIR/scripts" "$HARNESS_DIR/bridge-runtime"
48
48
  mkdir -p "$HARNESS_DIR/runner/bin" "$HARNESS_DIR/runner/manifests" "$HARNESS_DIR/runner/recipes" "$HARNESS_DIR/scripts/lib"
49
49
 
50
50
  RUNNER_EXEC_Q="$(printf '%q' "$RUNNER_DIR/bin/mm-harness")"
@@ -62,8 +62,8 @@ PROTOCOL_ROOT_Q="$(printf '%q' "$PROTOCOL_ROOT")"
62
62
  printf 'export METAMASK_RUNNER_PROTOCOL_ROOT=${METAMASK_RUNNER_PROTOCOL_ROOT:-%s}\n' "$PROTOCOL_ROOT_Q"
63
63
  fi
64
64
  printf '%s\n' 'if [ -n "${MM_HARNESS_BIN:-}" ] && [ -x "$MM_HARNESS_BIN" ]; then exec "$MM_HARNESS_BIN" "$@"; fi'
65
- printf '%s\n' 'if command -v mm-harness >/dev/null 2>&1; then exec mm-harness "$@"; fi'
66
65
  printf 'if [ -x %s ]; then exec %s "$@"; fi\n' "$RUNNER_EXEC_Q" "$RUNNER_EXEC_Q"
66
+ printf '%s\n' 'if command -v mm-harness >/dev/null 2>&1; then exec mm-harness "$@"; fi'
67
67
  printf '%s\n' 'echo "mm-harness not found. Next: npm i -g @deeeed/metamask-harness@latest" >&2' 'exit 127'
68
68
  } > "$HARNESS_DIR/runner/bin/mm-harness"
69
69
  chmod +x "$HARNESS_DIR/runner/bin/mm-harness"
@@ -77,6 +77,7 @@ rsync -a --delete "$RUNNER_DIR/library/recipes/" "$HARNESS_DIR/runner/recipes/"
77
77
  rm -rf "$HARNESS_DIR/runner/flows"
78
78
  rm -f "$HARNESS_DIR/runner/library.json"
79
79
  cp "$RUNNER_DIR/adapters/mobile/verify.sh" "$HARNESS_DIR/scripts/verify.sh"
80
+ cp -R "$RUNNER_DIR/adapters/mobile/bridge-runtime" "$HARNESS_DIR/bridge-runtime"
80
81
  cp "$RUNNER_DIR/adapters/shared/harness-path.sh" "$HARNESS_DIR/scripts/lib/harness-path.sh"
81
82
  cp "$RUNNER_DIR/adapters/shared/path-defaults.json" "$HARNESS_DIR/scripts/lib/path-defaults.json"
82
83
  chmod -R u+rwX,go+rX "$HARNESS_DIR"
@@ -114,7 +115,7 @@ fs.writeFileSync(manifestPath, JSON.stringify({
114
115
  actionManifestPath: `${harnessRel}/action-manifest.json`,
115
116
  runnerEntrypoint: `${harnessRel}/runner/bin/mm-harness`,
116
117
  runtimeHelpers: { verify: `${harnessRel}/scripts/verify.sh` },
117
- harnessInstalledPaths: [`${harnessRel}/runner`, `${harnessRel}/scripts`, `${harnessRel}/action-manifest.json`],
118
+ harnessInstalledPaths: [`${harnessRel}/runner`, `${harnessRel}/scripts`, `${harnessRel}/bridge-runtime`, `${harnessRel}/action-manifest.json`],
118
119
  productOwnedPaths: ['app/dev-tools/AgenticService', 'app/core/NavigationService/NavigationService.ts'],
119
120
  cleanupCommand,
120
121
  productDiffExcludes: [`:(exclude)${harnessRoot}`],
@@ -325,6 +325,35 @@ JSON
325
325
  ) > "$log_path" 2>&1
326
326
  }
327
327
 
328
+ evm_rpc_ready() {
329
+ local log_path="$1"
330
+ local bridge="$HARNESS_DIR/bridge-runtime/cdp-bridge.cjs"
331
+ local expression
332
+ expression="(async function(){try{var engine=globalThis.Engine;var controller=engine&&engine.context&&engine.context.NetworkController;if(!controller||typeof controller.getSelectedNetworkClient!=='function')throw new Error('NetworkController selected client is unavailable');var client=controller.getSelectedNetworkClient();if(!client||!client.provider||typeof client.provider.request!=='function')throw new Error('selected EVM provider is unavailable');var result=await client.provider.request({method:'eth_blockNumber'});if(typeof result!=='string'||!/^0x[0-9a-f]+$/i.test(result))throw new Error('eth_blockNumber returned an invalid result');return {ok:true,result:result};}catch(error){return {ok:false,error:error&&(error.message||String(error))};}})()"
333
+ [ -f "$bridge" ] || {
334
+ printf 'Mobile bridge is missing at %s\n' "$bridge" > "$log_path"
335
+ return 1
336
+ }
337
+ if ! run_with_timeout "$log_path" 25 env \
338
+ APP_ROOT="$TARGET" \
339
+ WATCHER_PORT="$port" \
340
+ IOS_SIMULATOR="${IOS_SIMULATOR:-$ios_simulator_resolved}" \
341
+ ADB_SERIAL="${ADB_SERIAL:-$adb_serial_resolved}" \
342
+ ANDROID_SERIAL="${ANDROID_SERIAL:-$adb_serial_resolved}" \
343
+ node "$bridge" eval-async "$expression"; then
344
+ return 1
345
+ fi
346
+ node - "$log_path" <<'NODE'
347
+ const fs = require('fs');
348
+ try {
349
+ const result = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
350
+ process.exit(result.ok === true ? 0 : 1);
351
+ } catch {
352
+ process.exit(1);
353
+ }
354
+ NODE
355
+ }
356
+
328
357
  ensure_live_runtime() {
329
358
  local device_target="$1"
330
359
  local attempt
@@ -419,6 +448,14 @@ JSON
419
448
  add_note "Runner v1 live bridge smoke failed; inspect logs/runner-live-smoke.log and runner-live-smoke/trace.json."
420
449
  status="fail"
421
450
  fi
451
+
452
+ if evm_rpc_ready "$ARTIFACTS/logs/evm-rpc-readiness.log"; then
453
+ checks+=("{\"name\":\"live EVM RPC readiness\",\"status\":\"pass\"}")
454
+ else
455
+ checks+=("{\"name\":\"live EVM RPC readiness\",\"status\":\"fail\",\"detail\":\"see logs/evm-rpc-readiness.log\"}")
456
+ add_note "EVM RPC readiness probe failed. Infura configured ≠ RPC reachable. Next: confirm $TARGET/.js.env has a real MM_INFURA_PROJECT_ID (not 00000000000), then rerun mm-harness launch $PLATFORM --verify --target '$TARGET'."
457
+ status="fail"
458
+ fi
422
459
  fi
423
460
 
424
461
  RECIPE_HARNESS_PREFLIGHT_MODE="$PREFLIGHT_MODE" RECIPE_HARNESS_ROOT_EXCLUDE="$HARNESS_ROOT" node - "$ARTIFACTS" "$TARGET" "$status" "${checks[@]}" <<'NODE'
@@ -153,12 +153,26 @@ async function checkExtensionRuntimeHealth(projectRoot, cdpPort, options = {}) {
153
153
  if (runtime.backgroundUnresponsive === true) {
154
154
  findings.push("Extension UI reports background connection unresponsive.");
155
155
  }
156
+ if (runtime.ethereumConnectionUnavailable === true) {
157
+ findings.push('Extension UI reports "Unable to connect to Ethereum".');
158
+ }
159
+ if (runtime.evmRpcProbeOk !== true) {
160
+ const probeError = String(runtime.evmRpcProbeError ?? "unknown error").replace(/[.\s]+$/u, "");
161
+ findings.push(
162
+ `EVM RPC readiness probe failed: ${probeError}. Infura configured \u2260 RPC reachable.`
163
+ );
164
+ }
156
165
  if (runtime.hasSubmitRequest === true && runtime.backgroundProbeOk !== true) {
157
166
  warnings.push(`Perps background read probe failed: ${runtime.backgroundProbeError ?? "unknown error"}.`);
158
167
  }
159
168
  const extensionId = safeExtensionId(target);
169
+ const evmRpcUnreachable = runtime.ethereumConnectionUnavailable === true || runtime.evmRpcProbeOk !== true;
160
170
  return {
161
171
  status: findings.length === 0 ? "PASS" : "FAIL",
172
+ ...evmRpcUnreachable ? {
173
+ errorCode: "EVM_RPC_UNREACHABLE",
174
+ userAction: evmRpcRecoveryAction(projectRoot)
175
+ } : {},
162
176
  warnings,
163
177
  cdpPort,
164
178
  targetUrl: target.url,
@@ -219,21 +233,42 @@ async function evaluateHealth(session, timeoutMs) {
219
233
  const storeState = hooks.store?.getState?.() || {};
220
234
  const manager = hooks.getPerpsStreamManager?.();
221
235
  const accountCache = manager?.account?.cache;
236
+ const networkConnectionBanner = storeState.metamask?.networkConnectionBanner;
237
+ const networkClientId = storeState.metamask?.selectedNetworkClientId || networkConnectionBanner?.networkClientId;
222
238
  return Promise.race([
223
239
  (async () => {
224
- let backgroundProbeOk = false;
225
- let backgroundProbeError = null;
226
- if (typeof hooks.submitRequestToBackground === 'function') {
227
- try {
228
- const accountState = await hooks.submitRequestToBackground('perpsGetAccountState', []);
229
- backgroundProbeOk = Boolean(accountState && typeof accountState === 'object');
230
- if (!backgroundProbeOk) backgroundProbeError = 'perpsGetAccountState returned an empty result';
231
- } catch (error) {
232
- backgroundProbeError = String(error?.message || error);
233
- }
234
- } else {
235
- backgroundProbeError = 'submitRequestToBackground is not a function';
236
- }
240
+ const [backgroundProbe, evmRpcProbe] = await Promise.all([
241
+ (async () => {
242
+ if (typeof hooks.submitRequestToBackground !== 'function') {
243
+ return { ok: false, error: 'submitRequestToBackground is not a function' };
244
+ }
245
+ try {
246
+ const accountState = await hooks.submitRequestToBackground('perpsGetAccountState', []);
247
+ const ok = Boolean(accountState && typeof accountState === 'object');
248
+ return { ok, error: ok ? null : 'perpsGetAccountState returned an empty result' };
249
+ } catch (error) {
250
+ return { ok: false, error: String(error?.message || error) };
251
+ }
252
+ })(),
253
+ (async () => {
254
+ if (typeof hooks.submitRequestToBackground !== 'function') {
255
+ return { ok: false, error: 'submitRequestToBackground is not a function' };
256
+ }
257
+ if (typeof networkClientId !== 'string' || networkClientId.length === 0) {
258
+ return { ok: false, error: 'selected EVM network client is unavailable' };
259
+ }
260
+ try {
261
+ const code = await hooks.submitRequestToBackground('getCode', [
262
+ '0x0000000000000000000000000000000000000000',
263
+ networkClientId,
264
+ ]);
265
+ const ok = typeof code === 'string' && code.startsWith('0x');
266
+ return { ok, error: ok ? null : 'background EVM RPC returned an invalid result' };
267
+ } catch (error) {
268
+ return { ok: false, error: String(error?.message || error) };
269
+ }
270
+ })(),
271
+ ]);
237
272
  return {
238
273
  href: location.href,
239
274
  title: document.title,
@@ -242,14 +277,17 @@ async function evaluateHealth(session, timeoutMs) {
242
277
  hasStore: Boolean(hooks.store),
243
278
  hasPerpsStreamManager: typeof hooks.getPerpsStreamManager === 'function',
244
279
  backgroundUnresponsive: bodyText.includes('Background connection unresponsive') || bodyText.includes('MetaMask had trouble starting'),
280
+ ethereumConnectionUnavailable: bodyText.includes('Unable to connect to Ethereum'),
245
281
  activeProvider: storeState.metamask?.activeProvider || null,
246
282
  isTestnet: Boolean(storeState.metamask?.isTestnet),
247
283
  perpsManagerInitialized: Boolean(manager?.isInitialized?.()),
248
284
  positionsCacheIsArray: Array.isArray(manager?.positions?.cache),
249
285
  ordersCacheIsArray: Array.isArray(manager?.orders?.cache),
250
286
  accountCachePresent: Boolean(accountCache && typeof accountCache === 'object'),
251
- backgroundProbeOk,
252
- backgroundProbeError,
287
+ backgroundProbeOk: backgroundProbe.ok,
288
+ backgroundProbeError: backgroundProbe.error,
289
+ evmRpcProbeOk: evmRpcProbe.ok,
290
+ evmRpcProbeError: evmRpcProbe.error,
253
291
  };
254
292
  })(),
255
293
  new Promise((resolve) => setTimeout(() => resolve({
@@ -260,8 +298,11 @@ async function evaluateHealth(session, timeoutMs) {
260
298
  hasStore: Boolean(hooks.store),
261
299
  hasPerpsStreamManager: typeof hooks.getPerpsStreamManager === 'function',
262
300
  backgroundUnresponsive: bodyText.includes('Background connection unresponsive') || bodyText.includes('MetaMask had trouble starting'),
301
+ ethereumConnectionUnavailable: bodyText.includes('Unable to connect to Ethereum'),
263
302
  backgroundProbeOk: false,
264
303
  backgroundProbeError: 'perpsGetAccountState timed out after ${backgroundProbeTimeoutMs}ms',
304
+ evmRpcProbeOk: false,
305
+ evmRpcProbeError: 'background EVM RPC timed out after ${backgroundProbeTimeoutMs}ms',
265
306
  }), ${backgroundProbeTimeoutMs})),
266
307
  ]);
267
308
  })()`,
@@ -293,6 +334,9 @@ function compositorRecoveryAction(projectRoot) {
293
334
  const recovery = `mm-harness launch --verify --target ${JSON.stringify(projectRoot)}`;
294
335
  return process.platform === "darwin" ? `Unlock the macOS session, then rerun: ${recovery}` : `Restore an active display compositor, then rerun: ${recovery}`;
295
336
  }
337
+ function evmRpcRecoveryAction(projectRoot) {
338
+ return `Confirm ${shellQuote(path.join(projectRoot, ".metamaskrc"))} has a real INFURA_PROJECT_ID (not 00000000000), then rerun: mm-harness launch --verify --target ${shellQuote(projectRoot)}`;
339
+ }
296
340
  function resolveCdpPort(rawPort, slot) {
297
341
  const raw = rawPort ?? process.env.CDP_PORT ?? process.env.RECIPE_CDP_PORT ?? slot?.cdpPort;
298
342
  const port = Number(raw);
@@ -759,6 +803,7 @@ export {
759
803
  assertHealthyExtensionRuntime,
760
804
  checkExtensionRuntimeHealth,
761
805
  compositorRecoveryAction,
806
+ evmRpcRecoveryAction,
762
807
  extensionBackgroundProbeTimeoutMs,
763
808
  formatHealthFailure,
764
809
  prepareExtensionRuntime
@@ -14,6 +14,10 @@ function watcherStatus(buildLog) {
14
14
  if (buildLog === "no-watch") return "down";
15
15
  return buildLog;
16
16
  }
17
+ function extensionRuntimeNextAction(reasonCode, target) {
18
+ const resolved = shellQuote(path.resolve(target));
19
+ return reasonCode === "deps-missing" || reasonCode === "deps-stale" ? `cd ${resolved} && yarn install --immutable` : `mm-harness launch --adapter extension --target ${resolved}`;
20
+ }
17
21
  const extensionSurface = {
18
22
  adapter: "extension",
19
23
  headless: false,
@@ -42,7 +46,7 @@ const extensionSurface = {
42
46
  decision: report.decision,
43
47
  reasonCode: report.reasonCode,
44
48
  reasons: report.reasons,
45
- nextAction: report.decision === "ready" ? void 0 : `mm-harness launch --adapter extension --target ${shellQuote(path.resolve(target))}`,
49
+ nextAction: report.decision === "ready" ? void 0 : extensionRuntimeNextAction(report.reasonCode, target),
46
50
  deps: report.checks.deps.status,
47
51
  devServer: {
48
52
  label: "webpack",
@@ -96,5 +100,6 @@ const extensionSurface = {
96
100
  }
97
101
  };
98
102
  export {
103
+ extensionRuntimeNextAction,
99
104
  extensionSurface
100
105
  };
@@ -5,6 +5,10 @@ import { shellQuote } from "../../commands/parse-args.js";
5
5
  import { resolveMobileSlotPorts } from "../slot-ports.js";
6
6
  import { mobileRuntimeStatus } from "./prepare.js";
7
7
  import { hasRunwayProvisionBaseline, provisionRunwayMobile } from "./provision.js";
8
+ function mobileRuntimeNextAction(reasonCode, target, platform) {
9
+ const resolved = shellQuote(path.resolve(target));
10
+ return reasonCode === "deps-missing" || reasonCode === "deps-partial" || reasonCode === "deps-stale" ? `cd ${resolved} && yarn setup` : `mm-harness launch ${platform} --adapter mobile --target ${resolved}`;
11
+ }
8
12
  const mobileSurface = {
9
13
  adapter: "mobile",
10
14
  headless: false,
@@ -21,7 +25,7 @@ const mobileSurface = {
21
25
  decision: depsPending ? "launch" : report.decision,
22
26
  reasonCode: depsPending ? "app-installed-deps-pending" : report.reasonCode,
23
27
  reasons: depsPending ? ["Runway app is installed; JavaScript dependencies are pending until dispatch-time launch."] : report.reasons,
24
- nextAction: report.decision === "ready" && !depsPending ? void 0 : `mm-harness launch ${platform} --adapter mobile --target ${shellQuote(path.resolve(target))}`,
28
+ nextAction: report.decision === "ready" && !depsPending ? void 0 : mobileRuntimeNextAction(report.reasonCode, target, platform),
25
29
  deps: runwayProvisioned && report.checks?.deps?.status !== "current" ? "pending" : report.checks?.deps?.status,
26
30
  devServer: { label: "metro", status: report.checks?.metro?.status ?? "unprobed" }
27
31
  };
@@ -58,5 +62,6 @@ const mobileSurface = {
58
62
  }
59
63
  };
60
64
  export {
65
+ mobileRuntimeNextAction,
61
66
  mobileSurface
62
67
  };
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deeeed/metamask-harness",
3
- "version": "0.34.4",
3
+ "version": "0.35.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mm-harness": "bin/mm-harness"
@@ -358,6 +358,13 @@ async function checkBehavior(browserEndpoint, port, pages) {
358
358
  tab.click();
359
359
  const panel = document.getElementById(tab.getAttribute('aria-controls'));
360
360
  if (!panel || panel.hidden) throw new Error('tab ' + tab.id + ' did not reveal its panel');
361
+ // Panels ship visible so a dead module degrades to readable prompts
362
+ // rather than dead controls — which also means "revealed" alone
363
+ // proves nothing. Selecting one has to hide the others.
364
+ const shown = [...document.querySelectorAll('[role="tabpanel"]')].filter((p) => !p.hidden);
365
+ if (shown.length !== 1) {
366
+ throw new Error('tab ' + tab.id + ' left ' + shown.length + ' panels visible');
367
+ }
361
368
  out.work.push(await grab(panel.querySelector('.cmd .copy')));
362
369
  }
363
370
  return out;
@@ -5,7 +5,8 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <title>Architecture — how the pieces fit</title>
7
7
  <meta name="description" content="Skills, mm-harness, team recipe libraries, product repos: what lives where, the three workflows, and the mistakes newcomers make.">
8
- <link rel="stylesheet" href="assets/style.css">
8
+ <link rel="icon" href="assets/metamask-fox.svg" type="image/svg+xml">
9
+ <link rel="stylesheet" href="assets/style.css?v=4">
9
10
  </head>
10
11
  <body data-progress-page="architecture">
11
12
  <a class="skip" href="#stack">Skip to the stack map</a>
@@ -13,18 +14,16 @@
13
14
  <header class="topbar">
14
15
  <div class="wrap topbar-inner">
15
16
  <a class="brand" href="index.html">
16
- <span class="brand-mark" aria-hidden="true"></span>
17
+ <img class="brand-mark" src="assets/metamask-fox.svg" alt="" width="22" height="22">
17
18
  <span class="brand-name">recipes</span>
18
19
  </a>
19
20
  <nav class="nav" aria-label="Main">
20
21
  <a class="nav-cta" href="index.html#prompt">Quick start</a>
21
22
  <a href="how-it-works.html">How it works</a>
22
- <a href="recipes.html">Recipe anatomy</a>
23
- <a href="perps.html">Perps</a>
23
+ <a href="perps.html">Team / Perps</a>
24
24
  <a href="cheatsheet.html">Cheatsheet</a>
25
25
  <a href="architecture.html" aria-current="page">Architecture</a>
26
26
  <a href="tutorials/index.html">Tutorials</a>
27
- <a href="reviewers.html">For Reviewers</a>
28
27
  </nav>
29
28
  </div>
30
29
  </header>
@@ -471,8 +470,16 @@
471
470
  </ol>
472
471
 
473
472
  <hr class="sep">
473
+ <h2>Which repo is which</h2>
474
+ <p>
475
+ The layers above are the design. The <a href="ecosystem.html">Ecosystem map</a> is the concrete
476
+ version: the six repositories and packages behind a run, each with its URL, and which way the
477
+ dependencies point.
478
+ </p>
479
+
474
480
  <div class="btn-row">
475
- <a class="btn btn-primary" href="how-it-works.html#steps">Do the walkthrough →</a>
481
+ <a class="btn btn-primary" href="ecosystem.html">See the repo map →</a>
482
+ <a class="btn btn-ghost" href="how-it-works.html#steps">Do the walkthrough</a>
476
483
  <a class="btn btn-ghost" href="reviewers.html">Read an evidence bundle</a>
477
484
  </div>
478
485
  </section>
@@ -485,6 +492,6 @@
485
492
  </div>
486
493
  </footer>
487
494
 
488
- <script type="module" src="assets/progress.mjs"></script>
495
+ <script type="module" src="assets/progress.mjs?v=4"></script>
489
496
  </body>
490
497
  </html>
@@ -0,0 +1,24 @@
1
+ <svg width="35" height="34" viewBox="0 0 35 34" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M32.7077 32.7522L25.1688 30.5174L19.4833 33.9008L15.5167 33.8991L9.82793 30.5174L2.29225 32.7522L0 25.0489L2.29225 16.4993L0 9.27094L2.29225 0.312256L14.0674 7.31554H20.9326L32.7077 0.312256L35 9.27094L32.7077 16.4993L35 25.0489L32.7077 32.7522Z" fill="#FF5C16"/>
3
+ <path d="M2.29395 0.312256L14.0691 7.32047L13.6008 12.1301L2.29395 0.312256Z" fill="#FF5C16"/>
4
+ <path d="M9.82959 25.0522L15.0106 28.9811L9.82959 30.5175V25.0522Z" fill="#FF5C16"/>
5
+ <path d="M14.5966 18.5565L13.6009 12.1333L7.22692 16.5009L7.22363 16.4993V16.5025L7.24335 20.9983L9.82809 18.5565H9.82974H14.5966Z" fill="#FF5C16"/>
6
+ <path d="M32.7077 0.312256L20.9326 7.32047L21.3993 12.1301L32.7077 0.312256Z" fill="#FF5C16"/>
7
+ <path d="M25.1722 25.0522L19.9912 28.9811L25.1722 30.5175V25.0522Z" fill="#FF5C16"/>
8
+ <path d="M27.7766 16.5025H27.7783H27.7766V16.4993L27.775 16.5009L21.401 12.1333L20.4053 18.5565H25.1722L27.7586 20.9983L27.7766 16.5025Z" fill="#FF5C16"/>
9
+ <path d="M9.82793 30.5175L2.29225 32.7522L0 25.0522H9.82793V30.5175Z" fill="#E34807"/>
10
+ <path d="M14.5947 18.5549L16.0341 27.8406L14.0393 22.6777L7.23975 20.9984L9.82613 18.5549H14.593H14.5947Z" fill="#E34807"/>
11
+ <path d="M25.1721 30.5175L32.7078 32.7522L35.0001 25.0522H25.1721V30.5175Z" fill="#E34807"/>
12
+ <path d="M20.4053 18.5549L18.9658 27.8406L20.9607 22.6777L27.7602 20.9984L25.1722 18.5549H20.4053Z" fill="#E34807"/>
13
+ <path d="M0 25.0488L2.29225 16.4993H7.22183L7.23991 20.9967L14.0394 22.676L16.0343 27.8389L15.0089 28.976L9.82793 25.0472H0V25.0488Z" fill="#FF8D5D"/>
14
+ <path d="M35.0001 25.0488L32.7078 16.4993H27.7783L27.7602 20.9967L20.9607 22.676L18.9658 27.8389L19.9912 28.976L25.1722 25.0472H35.0001V25.0488Z" fill="#FF8D5D"/>
15
+ <path d="M20.9325 7.31543H17.4999H14.0673L13.6006 12.1251L16.0342 27.834H18.9656L21.4008 12.1251L20.9325 7.31543Z" fill="#FF8D5D"/>
16
+ <path d="M2.29225 0.312256L0 9.27094L2.29225 16.4993H7.22183L13.5991 12.1301L2.29225 0.312256Z" fill="#661800"/>
17
+ <path d="M13.17 20.4199H10.9369L9.72095 21.6062L14.0409 22.6727L13.17 20.4182V20.4199Z" fill="#661800"/>
18
+ <path d="M32.7077 0.312256L34.9999 9.27094L32.7077 16.4993H27.7781L21.4009 12.1301L32.7077 0.312256Z" fill="#661800"/>
19
+ <path d="M21.833 20.4199H24.0694L25.2853 21.6079L20.9604 22.676L21.833 20.4182V20.4199Z" fill="#661800"/>
20
+ <path d="M19.4817 30.8362L19.9911 28.9794L18.9658 27.8423H16.0327L15.0073 28.9794L15.5167 30.8362" fill="#661800"/>
21
+ <path d="M19.4816 30.8359V33.9021H15.5166V30.8359H19.4816Z" fill="#C0C4CD"/>
22
+ <path d="M9.82959 30.5142L15.52 33.9008V30.8346L15.0106 28.9778L9.82959 30.5142Z" fill="#E7EBF6"/>
23
+ <path d="M25.1721 30.5142L19.4817 33.9008V30.8346L19.9911 28.9778L25.1721 30.5142Z" fill="#E7EBF6"/>
24
+ </svg>
@@ -241,11 +241,17 @@
241
241
  * One tablist per group, roving tabindex: only the selected tab is in the
242
242
  * tab order, arrows move between them. Panels stay in the DOM so their
243
243
  * commands remain copyable the moment a tab is shown.
244
+ *
245
+ * The markup ships every panel visible and the tablist hidden; hiding is
246
+ * this function's first act. Without it — stale cache, blocked module,
247
+ * file:// — the reader gets all three prompts stacked instead of controls
248
+ * that do nothing.
244
249
  */
245
250
  function initTabs() {
246
251
  [].slice.call(document.querySelectorAll('[role="tablist"]')).forEach(function (list) {
247
252
  var tabs = [].slice.call(list.querySelectorAll('[role="tab"]'));
248
253
  if (!tabs.length) return;
254
+ list.hidden = false;
249
255
 
250
256
  function select(tab, focus) {
251
257
  tabs.forEach(function (other) {