@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.
Files changed (65) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +9 -1
  3. package/adapters/core/inject.sh +3 -3
  4. package/adapters/extension/check-infura-readiness.cjs +102 -0
  5. package/adapters/extension/inject.mjs +4 -4
  6. package/adapters/extension/live.sh +25 -10
  7. package/adapters/extension/start-watch.sh +15 -0
  8. package/adapters/extension/wallet-fixture-state.cjs +3 -1
  9. package/adapters/manifest.json +16 -0
  10. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +73 -42
  11. package/adapters/mobile/inject.sh +4 -3
  12. package/adapters/mobile/reset-app-data.sh +154 -0
  13. package/adapters/mobile/verify.sh +37 -0
  14. package/dist/adapters/extension/product-config.js +29 -1
  15. package/dist/adapters/extension/runtime.js +60 -15
  16. package/dist/adapters/extension/surface.js +6 -1
  17. package/dist/adapters/mobile/surface.js +6 -1
  18. package/dist/adapters.js +18 -0
  19. package/dist/cli-commands.js +1 -1
  20. package/dist/cli.js +2 -2
  21. package/dist/command-contract.js +1 -1
  22. package/dist/commands/device-target.js +5 -0
  23. package/dist/commands/fixtures.js +106 -31
  24. package/dist/commands/launch/extension.js +39 -3
  25. package/dist/commands/launch/index.js +13 -0
  26. package/dist/doctor.js +30 -1
  27. package/dist/mm-harness-cli.js +6 -3
  28. package/dist/recipe-security.js +3 -0
  29. package/docs/RECIPES.md +29 -0
  30. package/library/actions/extension/wallet/import.mjs +234 -0
  31. package/library/actions/extension/wallet/reset.mjs +98 -0
  32. package/library/actions/extension/wallet/state.mjs +1 -0
  33. package/library/actions/mobile/analytics/consent-settings.mjs +112 -0
  34. package/library/actions/mobile/analytics/set_consent.mjs +4 -112
  35. package/library/actions/mobile/platform/bridge.mjs +8 -0
  36. package/library/actions/mobile/wallet/import.mjs +259 -0
  37. package/library/actions/mobile/wallet/reset-helper.mjs +99 -0
  38. package/library/actions/mobile/wallet/reset.mjs +7 -0
  39. package/library/actions/shared/wallet/import-source.mjs +101 -0
  40. package/library/manifests/extension.action-manifest.json +112 -0
  41. package/library/manifests/mobile.action-manifest.json +108 -0
  42. package/library/recipes/wallet/import.recipe.json +83 -0
  43. package/library/recipes/wallet/reset-import.recipe.json +88 -0
  44. package/package.json +1 -1
  45. package/scripts/completions.sh +2 -2
  46. package/scripts/site-contrast.mjs +7 -0
  47. package/site/architecture.html +14 -7
  48. package/site/assets/metamask-fox.svg +24 -0
  49. package/site/assets/progress.mjs +6 -0
  50. package/site/assets/style.css +88 -29
  51. package/site/cheatsheet.html +5 -6
  52. package/site/ecosystem.html +162 -0
  53. package/site/how-it-works.html +8 -9
  54. package/site/index.html +17 -12
  55. package/site/perps.html +103 -45
  56. package/site/recipes.html +5 -6
  57. package/site/reviewers.html +5 -6
  58. package/site/tutorials/index.html +5 -6
  59. package/site/tutorials/v1.html +5 -6
  60. package/site/tutorials/v2.html +6 -8
  61. package/site/tutorials/v3.html +5 -6
  62. package/site/tutorials/v4.html +5 -6
  63. package/site/tutorials/v5.html +5 -6
  64. package/site/tutorials/v6.html +5 -6
  65. package/site/tutorials/v7.html +5 -6
@@ -1237,6 +1237,45 @@
1237
1237
  ],
1238
1238
  "execution_capabilities": []
1239
1239
  },
1240
+ "metamask.wallet.validate_import": {
1241
+ "description": "Validate wallet recovery credentials and derive the expected primary address without launching or mutating the app.",
1242
+ "schema": {
1243
+ "type": "object",
1244
+ "properties": {
1245
+ "credential_source": {
1246
+ "type": "string",
1247
+ "enum": ["auto", "environment", "fixture"]
1248
+ },
1249
+ "srp_env": {
1250
+ "type": "string"
1251
+ },
1252
+ "password_env": {
1253
+ "type": "string"
1254
+ },
1255
+ "metametrics": {
1256
+ "type": "boolean"
1257
+ },
1258
+ "interests": {
1259
+ "type": "array",
1260
+ "items": {
1261
+ "type": "string"
1262
+ }
1263
+ }
1264
+ },
1265
+ "additionalProperties": false
1266
+ },
1267
+ "examples": [
1268
+ {
1269
+ "action": "metamask.wallet.validate_import",
1270
+ "credential_source": "auto",
1271
+ "intent": "Validate recovery credentials before resetting a wallet",
1272
+ "next": "done"
1273
+ }
1274
+ ],
1275
+ "execution_capabilities": [
1276
+ "host-read-export"
1277
+ ]
1278
+ },
1240
1279
  "metamask.wallet.setup": {
1241
1280
  "description": "Import the wallet fixture accounts and password so the app starts from a known signed-in state.",
1242
1281
  "schema": {
@@ -1253,6 +1292,75 @@
1253
1292
  ],
1254
1293
  "execution_capabilities": ["app-mutation"]
1255
1294
  },
1295
+ "metamask.wallet.import": {
1296
+ "description": "Make the wallet ready by reusing a fixture-backed profile or importing the primary mnemonic through visible onboarding UI.",
1297
+ "schema": {
1298
+ "type": "object",
1299
+ "properties": {
1300
+ "method": {
1301
+ "type": "string",
1302
+ "enum": ["auto", "profile", "ui"]
1303
+ },
1304
+ "credential_source": {
1305
+ "type": "string",
1306
+ "enum": ["auto", "environment", "fixture"]
1307
+ },
1308
+ "srp_env": {
1309
+ "type": "string"
1310
+ },
1311
+ "password_env": {
1312
+ "type": "string"
1313
+ },
1314
+ "metametrics": {
1315
+ "type": "boolean",
1316
+ "default": false,
1317
+ "description": "Enable MetaMetrics during visible onboarding."
1318
+ },
1319
+ "interests": {
1320
+ "type": "array",
1321
+ "items": {
1322
+ "type": "string",
1323
+ "enum": ["swap_tokens", "trade_perpetuals", "prediction_markets", "send_receive_crypto", "earn_and_spend", "use_other_crypto_apps"]
1324
+ },
1325
+ "default": [],
1326
+ "description": "Select optional onboarding interests; an empty list skips the questionnaire."
1327
+ },
1328
+ "timeout_ms": {
1329
+ "type": "number"
1330
+ }
1331
+ },
1332
+ "additionalProperties": false
1333
+ },
1334
+ "examples": [
1335
+ {
1336
+ "action": "metamask.wallet.import",
1337
+ "method": "auto",
1338
+ "intent": "Use an existing fixture profile or import through onboarding",
1339
+ "next": "done"
1340
+ }
1341
+ ],
1342
+ "execution_capabilities": ["app-mutation"]
1343
+ },
1344
+ "metamask.wallet.reset": {
1345
+ "description": "Delete the current wallet through the visible client reset flow and reach fresh onboarding.",
1346
+ "schema": {
1347
+ "type": "object",
1348
+ "properties": {
1349
+ "timeout_ms": {
1350
+ "type": "number"
1351
+ }
1352
+ },
1353
+ "additionalProperties": false
1354
+ },
1355
+ "examples": [
1356
+ {
1357
+ "action": "metamask.wallet.reset",
1358
+ "intent": "Reset the wallet through visible UI before importing it again",
1359
+ "next": "done"
1360
+ }
1361
+ ],
1362
+ "execution_capabilities": ["app-mutation"]
1363
+ },
1256
1364
  "metamask.wallet.ensure_unlocked": {
1257
1365
  "description": "Unlock the wallet with the fixture password when it is currently locked.",
1258
1366
  "schema": {
@@ -0,0 +1,83 @@
1
+ {
2
+ "$schema": "https://farmslot.io/schemas/recipe-v1.schema.json",
3
+ "title": "Import or reuse a MetaMask wallet",
4
+ "description": "Make a Mobile or Extension wallet ready by reusing its fixture-backed profile or importing the primary mnemonic through visible onboarding UI.",
5
+ "paramsSchema": {
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "properties": {
9
+ "method": {
10
+ "type": "string",
11
+ "enum": ["auto", "profile", "ui"],
12
+ "default": "auto",
13
+ "description": "Choose automatically from visible wallet state, require an existing profile, or require real-user onboarding UI."
14
+ },
15
+ "credential_source": {
16
+ "type": "string",
17
+ "enum": ["auto", "environment", "fixture"],
18
+ "default": "auto",
19
+ "description": "Read secrets from the named environment variables first, or require one source explicitly."
20
+ },
21
+ "srp_env": {
22
+ "type": "string",
23
+ "default": "MM_HARNESS_WALLET_SRP",
24
+ "description": "Environment variable containing the Secret Recovery Phrase; the value is never stored in the recipe or evidence."
25
+ },
26
+ "password_env": {
27
+ "type": "string",
28
+ "default": "MM_HARNESS_WALLET_PASSWORD",
29
+ "description": "Environment variable containing the wallet password; the value is never stored in the recipe or evidence."
30
+ },
31
+ "metametrics": {
32
+ "type": "boolean",
33
+ "default": false,
34
+ "description": "Enable MetaMetrics during visible onboarding; defaults off."
35
+ },
36
+ "interests": {
37
+ "type": "array",
38
+ "items": {
39
+ "type": "string",
40
+ "enum": ["swap_tokens", "trade_perpetuals", "prediction_markets", "send_receive_crypto", "earn_and_spend", "use_other_crypto_apps"]
41
+ },
42
+ "default": [],
43
+ "description": "Mobile-only onboarding interests; an empty list skips the optional questionnaire."
44
+ },
45
+ "timeout_ms": {
46
+ "type": "number",
47
+ "default": 120000,
48
+ "description": "Maximum time for each onboarding transition and final wallet readiness."
49
+ }
50
+ }
51
+ },
52
+ "workflow": {
53
+ "entry": "fixture-status",
54
+ "nodes": {
55
+ "fixture-status": {
56
+ "action": "metamask.wallet.fixture_status",
57
+ "intent": "Record whether a canonical fixture is available without exposing its secrets",
58
+ "next": "import"
59
+ },
60
+ "import": {
61
+ "action": "metamask.wallet.import",
62
+ "method": "{{params.method}}",
63
+ "credential_source": "{{params.credential_source}}",
64
+ "srp_env": "{{params.srp_env}}",
65
+ "password_env": "{{params.password_env}}",
66
+ "metametrics": "{{params.metametrics}}",
67
+ "interests": "{{params.interests}}",
68
+ "timeout_ms": "{{params.timeout_ms}}",
69
+ "intent": "Reuse the fixture-backed wallet or import it through the visible onboarding flow",
70
+ "next": "state"
71
+ },
72
+ "state": {
73
+ "action": "metamask.wallet.read_state",
74
+ "intent": "Prove the wallet has a selected account after setup",
75
+ "next": "done"
76
+ },
77
+ "done": {
78
+ "action": "end",
79
+ "status": "pass"
80
+ }
81
+ }
82
+ }
83
+ }
@@ -0,0 +1,88 @@
1
+ {
2
+ "$schema": "https://farmslot.io/schemas/recipe-v1.schema.json",
3
+ "title": "Reset and import a MetaMask wallet through visible UI",
4
+ "description": "Delete the current Mobile or Extension wallet through its visible reset flow, import the primary mnemonic through onboarding, and prove a selected account is ready.",
5
+ "paramsSchema": {
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "properties": {
9
+ "credential_source": {
10
+ "type": "string",
11
+ "enum": ["auto", "environment", "fixture"],
12
+ "default": "auto",
13
+ "description": "Read secrets from the named environment variables first, or require one source explicitly."
14
+ },
15
+ "srp_env": {
16
+ "type": "string",
17
+ "default": "MM_HARNESS_WALLET_SRP",
18
+ "description": "Environment variable containing the Secret Recovery Phrase; the value is never stored in the recipe or evidence."
19
+ },
20
+ "password_env": {
21
+ "type": "string",
22
+ "default": "MM_HARNESS_WALLET_PASSWORD",
23
+ "description": "Environment variable containing the wallet password; the value is never stored in the recipe or evidence."
24
+ },
25
+ "metametrics": {
26
+ "type": "boolean",
27
+ "default": false,
28
+ "description": "Enable MetaMetrics during visible onboarding; defaults off."
29
+ },
30
+ "interests": {
31
+ "type": "array",
32
+ "items": {
33
+ "type": "string",
34
+ "enum": ["swap_tokens", "trade_perpetuals", "prediction_markets", "send_receive_crypto", "earn_and_spend", "use_other_crypto_apps"]
35
+ },
36
+ "default": [],
37
+ "description": "Mobile-only onboarding interests; an empty list skips the optional questionnaire."
38
+ },
39
+ "timeout_ms": {
40
+ "type": "number",
41
+ "default": 120000,
42
+ "description": "Maximum time for each reset/import transition and final wallet readiness."
43
+ }
44
+ }
45
+ },
46
+ "workflow": {
47
+ "entry": "validate_import",
48
+ "nodes": {
49
+ "validate_import": {
50
+ "action": "metamask.wallet.validate_import",
51
+ "credential_source": "{{params.credential_source}}",
52
+ "srp_env": "{{params.srp_env}}",
53
+ "password_env": "{{params.password_env}}",
54
+ "metametrics": "{{params.metametrics}}",
55
+ "interests": "{{params.interests}}",
56
+ "intent": "Validate recovery credentials before deleting the current wallet",
57
+ "next": "reset"
58
+ },
59
+ "reset": {
60
+ "action": "metamask.wallet.reset",
61
+ "timeout_ms": "{{params.timeout_ms}}",
62
+ "intent": "Delete the current wallet through the client-visible reset flow and reach fresh onboarding",
63
+ "next": "import"
64
+ },
65
+ "import": {
66
+ "action": "metamask.wallet.import",
67
+ "method": "ui",
68
+ "credential_source": "{{params.credential_source}}",
69
+ "srp_env": "{{params.srp_env}}",
70
+ "password_env": "{{params.password_env}}",
71
+ "metametrics": "{{params.metametrics}}",
72
+ "interests": "{{params.interests}}",
73
+ "timeout_ms": "{{params.timeout_ms}}",
74
+ "intent": "Import the primary mnemonic through visible onboarding UI",
75
+ "next": "state"
76
+ },
77
+ "state": {
78
+ "action": "metamask.wallet.read_state",
79
+ "intent": "Prove the imported wallet has a selected account",
80
+ "next": "done"
81
+ },
82
+ "done": {
83
+ "action": "end",
84
+ "status": "pass"
85
+ }
86
+ }
87
+ }
88
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deeeed/metamask-harness",
3
- "version": "0.34.4",
3
+ "version": "0.36.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "mm-harness": "bin/mm-harness"
@@ -77,7 +77,7 @@ if [ -n "${BASH_VERSION:-}" ]; then
77
77
 
78
78
  case "$cmd" in
79
79
  launch) [ "$cword" -eq 2 ] && COMPREPLY=($(compgen -W "ios android" -- "$cur")) ;;
80
- fixtures) [ "$cword" -eq 2 ] && COMPREPLY=($(compgen -W "init sync set generate finalize" -- "$cur")) ;;
80
+ fixtures) [ "$cword" -eq 2 ] && COMPREPLY=($(compgen -W "init sync set reset generate finalize" -- "$cur")) ;;
81
81
  checklist) [ "$cword" -eq 2 ] && COMPREPLY=($(compgen -W "mark closeout" -- "$cur")) ;;
82
82
  completions) [ "$cword" -eq 2 ] && COMPREPLY=($(compgen -W "install bash zsh status" -- "$cur")) ;;
83
83
  call) [ "$cword" -eq 2 ] && COMPREPLY=($(compgen -W "$(_mmh_actions)" -- "$cur")) ;;
@@ -111,7 +111,7 @@ if [ -n "${ZSH_VERSION:-}" ]; then
111
111
 
112
112
  case "$cmd" in
113
113
  launch) (( CURRENT == 3 )) && { candidates=(ios android); compadd -a candidates; } ;;
114
- fixtures) (( CURRENT == 3 )) && { candidates=(init sync set generate finalize); compadd -a candidates; } ;;
114
+ fixtures) (( CURRENT == 3 )) && { candidates=(init sync set reset generate finalize); compadd -a candidates; } ;;
115
115
  checklist) (( CURRENT == 3 )) && { candidates=(mark closeout); compadd -a candidates; } ;;
116
116
  completions) (( CURRENT == 3 )) && { candidates=(install bash zsh status); compadd -a candidates; } ;;
117
117
  call) (( CURRENT == 3 )) && { candidates=("${(@f)$(_mmh_actions)}"); compadd -a candidates; } ;;
@@ -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) {
@@ -110,8 +110,10 @@ main { padding-bottom: 6rem; }
110
110
  .topbar-inner {
111
111
  display: flex;
112
112
  align-items: center;
113
- gap: 1.5rem;
114
- height: 60px;
113
+ flex-wrap: wrap;
114
+ gap: 0.5rem 1.5rem;
115
+ min-height: 60px;
116
+ padding: 0.5rem 0;
115
117
  }
116
118
 
117
119
  .brand {
@@ -125,31 +127,24 @@ main { padding-bottom: 6rem; }
125
127
  }
126
128
  .brand:hover { text-decoration: none; }
127
129
 
128
- /* Brand-adjacent mark: a geometric diamond, not a product logo. */
129
- .brand-mark {
130
- width: 20px;
131
- height: 20px;
132
- flex: none;
133
- border-radius: 5px;
134
- background: linear-gradient(145deg, var(--accent) 0%, var(--accent-deep) 100%);
135
- transform: rotate(45deg);
136
- box-shadow: 0 0 0 1px rgba(246, 133, 27, 0.3), 0 3px 12px rgba(246, 133, 27, 0.22);
137
- }
130
+ .brand-mark { width: 22px; height: 22px; flex: none; display: block; }
138
131
 
139
132
  .brand-name { font-family: var(--mono); font-size: 0.95rem; }
140
133
 
134
+ /* Six items fit a laptop on one line; below that they wrap onto a second row
135
+ rather than scrolling a label out of sight. */
141
136
  .nav {
142
137
  display: flex;
143
138
  gap: 0.3rem;
144
139
  margin-left: auto;
145
140
  min-width: 0;
146
- overflow-x: auto;
147
- flex-wrap: nowrap;
148
- scrollbar-width: thin;
141
+ flex-wrap: wrap;
142
+ justify-content: flex-end;
149
143
  }
150
144
 
151
145
  .nav a {
152
146
  flex: none;
147
+ white-space: nowrap;
153
148
  color: var(--text-dim);
154
149
  font-size: 0.885rem;
155
150
  font-weight: 500;
@@ -272,16 +267,7 @@ main { padding-bottom: 6rem; }
272
267
  text-align: center;
273
268
  }
274
269
 
275
- .lobby-mark {
276
- display: block;
277
- width: 26px;
278
- height: 26px;
279
- margin: 0 auto 2rem;
280
- border-radius: 7px;
281
- background: linear-gradient(145deg, var(--accent) 0%, var(--accent-deep) 100%);
282
- transform: rotate(45deg);
283
- box-shadow: 0 0 0 1px rgba(246, 133, 27, 0.3), 0 4px 18px rgba(246, 133, 27, 0.25);
284
- }
270
+ .lobby-mark { display: block; width: 52px; height: 52px; margin: 0 auto 1.6rem; }
285
271
 
286
272
  .lobby h1 {
287
273
  font-size: clamp(2rem, 4.6vw, 2.85rem);
@@ -352,17 +338,19 @@ main { padding-bottom: 6rem; }
352
338
  .qs-title {
353
339
  display: flex;
354
340
  align-items: center;
355
- gap: 0.65rem;
356
341
  font-size: 1.16rem;
357
342
  font-weight: 640;
358
343
  margin: 0 0 0.35rem;
359
344
  }
360
345
 
346
+ /* The margin rather than a flex gap: the badge stays separated from the title
347
+ even when the flex layout does not apply. */
361
348
  .qs-num {
362
349
  flex: none;
363
- width: 1.6rem;
364
- height: 1.6rem;
365
- display: grid;
350
+ width: 1.7rem;
351
+ height: 1.7rem;
352
+ margin-right: 0.75rem;
353
+ display: inline-grid;
366
354
  place-items: center;
367
355
  border-radius: 50%;
368
356
  background: var(--accent-soft);
@@ -370,6 +358,7 @@ main { padding-bottom: 6rem; }
370
358
  color: var(--accent);
371
359
  font-family: var(--mono);
372
360
  font-size: 0.8rem;
361
+ line-height: 1;
373
362
  }
374
363
 
375
364
  .qs-why { color: var(--text-dim); font-size: 0.95rem; margin-bottom: 0.9rem; }
@@ -960,6 +949,76 @@ tr[hidden], .filter-hidden { display: none !important; }
960
949
  .verdict.pass { color: var(--pass); }
961
950
  .verdict.fail { color: var(--fail); }
962
951
 
952
+ /* ---------- dependency map ----------
953
+ A column of nodes with the edge label between them, so the direction reads
954
+ top to bottom without a diagram library. */
955
+
956
+ .map { margin: 1.5rem 0 2.5rem; }
957
+
958
+ .node {
959
+ background: linear-gradient(180deg, var(--surface) 0%, var(--bg-raised) 100%);
960
+ border: 1px solid var(--line);
961
+ border-radius: var(--radius);
962
+ padding: 1.25rem 1.4rem;
963
+ }
964
+ .node:hover { border-color: var(--accent-line); }
965
+
966
+ /* The one you install. */
967
+ .node-key { border-color: var(--accent-line); }
968
+
969
+ .node-tag {
970
+ display: block;
971
+ font-family: var(--mono);
972
+ font-size: 0.72rem;
973
+ letter-spacing: 0.08em;
974
+ text-transform: uppercase;
975
+ color: var(--text-faint);
976
+ margin-bottom: 0.35rem;
977
+ }
978
+
979
+ .node-name { font-size: 1.12rem; font-weight: 640; margin: 0 0 0.4rem; }
980
+ .node-what { font-size: 0.94rem; color: var(--text-dim); margin-bottom: 0.7rem; }
981
+
982
+ .node-list {
983
+ list-style: none;
984
+ margin: 0 0 0.8rem;
985
+ padding: 0;
986
+ display: flex;
987
+ flex-wrap: wrap;
988
+ gap: 0.35rem 0.5rem;
989
+ font-size: 0.86rem;
990
+ }
991
+ .node-list li { color: var(--text-dim); }
992
+
993
+ .node-url {
994
+ display: block;
995
+ font-family: var(--mono);
996
+ font-size: 0.8rem;
997
+ word-break: break-all;
998
+ margin-bottom: 0.15rem;
999
+ }
1000
+
1001
+ .node .badge { margin: 0.55rem 0.4rem 0 0; display: inline-block; }
1002
+ .node-more { display: block; font-size: 0.88rem; margin-top: 0.6rem; }
1003
+
1004
+ /* Edge labels carry the verb; the arrow alone would not say what flows. */
1005
+ .map-edge {
1006
+ margin: 0.7rem 0;
1007
+ text-align: center;
1008
+ font-family: var(--mono);
1009
+ font-size: 0.8rem;
1010
+ color: var(--text-faint);
1011
+ }
1012
+
1013
+ .map-split { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem; }
1014
+ .map-row { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem; align-items: start; }
1015
+
1016
+ @media (max-width: 780px) {
1017
+ .map-split { grid-template-columns: 1fr; }
1018
+ .map-split .map-edge:last-child { margin-top: 0; }
1019
+ .map-row { grid-template-columns: 1fr; }
1020
+ }
1021
+
963
1022
  /* ---------- misc ---------- */
964
1023
 
965
1024
  .btn-row { display: flex; flex-wrap: wrap; gap: 0.7rem; margin-top: 1.8rem; }
@@ -5,7 +5,8 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
6
  <title>Cheatsheet — mm-harness</title>
7
7
  <meta name="description" content="Every mm-harness command worth knowing, filterable by platform, with copy buttons.">
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="cheatsheet">
11
12
  <a class="skip" href="#daily">Skip to the commands</a>
@@ -13,18 +14,16 @@
13
14
  <header class="topbar">
14
15
  <div class="wrap-wide 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" aria-current="page">Cheatsheet</a>
25
25
  <a href="architecture.html">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>
@@ -303,6 +302,6 @@
303
302
  </div>
304
303
  </footer>
305
304
 
306
- <script type="module" src="assets/progress.mjs"></script>
305
+ <script type="module" src="assets/progress.mjs?v=4"></script>
307
306
  </body>
308
307
  </html>