@deeeed/metamask-harness 0.2.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 (158) hide show
  1. package/CHANGELOG.md +161 -0
  2. package/README.md +140 -0
  3. package/bin/mm-harness +99 -0
  4. package/docs/CHEATSHEET.md +61 -0
  5. package/docs/CLI-SPEC.md +915 -0
  6. package/docs/MENTAL-MODEL.md +295 -0
  7. package/docs/architecture.md +367 -0
  8. package/docs/extension-runtime-commands.md +60 -0
  9. package/docs/harness-cli.md +43 -0
  10. package/docs/live-adapter-contract.md +188 -0
  11. package/docs/package-boundaries.md +47 -0
  12. package/docs/perps-flow-catalog.md +235 -0
  13. package/docs/recipe-libraries.md +95 -0
  14. package/docs/runtime-file-conventions.md +36 -0
  15. package/library/actions/core/perps/_controller.mjs +727 -0
  16. package/library/actions/core/perps/assert_orders.mjs +53 -0
  17. package/library/actions/core/perps/assert_positions.mjs +52 -0
  18. package/library/actions/core/perps/close_orders.mjs +97 -0
  19. package/library/actions/core/perps/close_positions.mjs +118 -0
  20. package/library/actions/core/perps/ensure_orders.mjs +40 -0
  21. package/library/actions/core/perps/ensure_positions.mjs +37 -0
  22. package/library/actions/core/perps/place_order.mjs +201 -0
  23. package/library/actions/core/perps/read_account.mjs +30 -0
  24. package/library/actions/core/perps/read_orders.mjs +27 -0
  25. package/library/actions/core/perps/read_positions.mjs +27 -0
  26. package/library/actions/core/perps/start_state.mjs +92 -0
  27. package/library/actions/core/perps/teardown_state.mjs +86 -0
  28. package/library/actions/extension/perps/assert_orders.mjs +11 -0
  29. package/library/actions/extension/perps/assert_positions.mjs +11 -0
  30. package/library/actions/extension/perps/close_orders.mjs +8 -0
  31. package/library/actions/extension/perps/close_positions.mjs +8 -0
  32. package/library/actions/extension/perps/ensure_orders.mjs +4 -0
  33. package/library/actions/extension/perps/ensure_positions.mjs +4 -0
  34. package/library/actions/extension/perps/perps.mjs +730 -0
  35. package/library/actions/extension/perps/place_order.mjs +7 -0
  36. package/library/actions/extension/perps/read_orders.mjs +4 -0
  37. package/library/actions/extension/perps/read_positions.mjs +3 -0
  38. package/library/actions/extension/platform/cdp.mjs +541 -0
  39. package/library/actions/extension/ui/navigate.mjs +44 -0
  40. package/library/actions/extension/wallet/ensure_unlocked.mjs +36 -0
  41. package/library/actions/extension/wallet/read_state.mjs +27 -0
  42. package/library/actions/extension/wallet/select_account.mjs +48 -0
  43. package/library/actions/extension/wallet/setup.mjs +35 -0
  44. package/library/actions/mobile/app-overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +185 -0
  45. package/library/actions/mobile/app-overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +1662 -0
  46. package/library/actions/mobile/bridge-runtime/cdp-bridge.cjs +686 -0
  47. package/library/actions/mobile/bridge-runtime/lib/cdp-eval.cjs +110 -0
  48. package/library/actions/mobile/bridge-runtime/lib/config.cjs +39 -0
  49. package/library/actions/mobile/bridge-runtime/lib/issue-capture.cjs +446 -0
  50. package/library/actions/mobile/bridge-runtime/lib/target-discovery.cjs +204 -0
  51. package/library/actions/mobile/bridge-runtime/lib/ws-client.cjs +108 -0
  52. package/library/actions/mobile/bridge-runtime/setup-wallet.sh +442 -0
  53. package/library/actions/mobile/perps/assert_orders.mjs +11 -0
  54. package/library/actions/mobile/perps/assert_positions.mjs +11 -0
  55. package/library/actions/mobile/perps/close_orders.mjs +8 -0
  56. package/library/actions/mobile/perps/close_positions.mjs +8 -0
  57. package/library/actions/mobile/perps/ensure_orders.mjs +4 -0
  58. package/library/actions/mobile/perps/ensure_positions.mjs +4 -0
  59. package/library/actions/mobile/perps/perps.mjs +709 -0
  60. package/library/actions/mobile/perps/place_order.mjs +7 -0
  61. package/library/actions/mobile/perps/read_orders.mjs +4 -0
  62. package/library/actions/mobile/perps/read_positions.mjs +3 -0
  63. package/library/actions/mobile/platform/bridge.mjs +283 -0
  64. package/library/actions/mobile/ui/navigate.mjs +38 -0
  65. package/library/actions/mobile/wallet/ensure_unlocked.mjs +107 -0
  66. package/library/actions/mobile/wallet/home.mjs +35 -0
  67. package/library/actions/mobile/wallet/read_state.mjs +40 -0
  68. package/library/actions/mobile/wallet/select_account.mjs +48 -0
  69. package/library/actions/mobile/wallet/setup.mjs +220 -0
  70. package/library/flows/perps.flows.json +64 -0
  71. package/library/library.json +7 -0
  72. package/library/manifests/core.action-manifest.json +1282 -0
  73. package/library/manifests/extension.action-manifest.json +1749 -0
  74. package/library/manifests/mobile.action-manifest.json +1753 -0
  75. package/library/recipes/action-validation.extension.recipe.json +417 -0
  76. package/library/recipes/action-validation.mobile.recipe.json +422 -0
  77. package/library/recipes/order-lifecycle.core.recipe.json +78 -0
  78. package/library/recipes/perps-lifecycle.recipe.json +194 -0
  79. package/library/recipes/read-markets.core.recipe.json +38 -0
  80. package/library/recipes/smoke.extension.recipe.json +31 -0
  81. package/library/recipes/smoke.mobile.recipe.json +31 -0
  82. package/library/recipes/trading-lifecycle.core.recipe.json +76 -0
  83. package/orchestration/compat-overlays/README.md +19 -0
  84. package/orchestration/compat-overlays/mobile/README.md +13 -0
  85. package/orchestration/compat-overlays/mobile/rn81-message-event-source.patch +42 -0
  86. package/orchestration/core/cleanup.sh +37 -0
  87. package/orchestration/core/inject.sh +154 -0
  88. package/orchestration/doctor.mjs +72 -0
  89. package/orchestration/extension/cleanup.mjs +60 -0
  90. package/orchestration/extension/console-tail.mjs +228 -0
  91. package/orchestration/extension/ensure-browser.sh +416 -0
  92. package/orchestration/extension/ensure-ready.ts +185 -0
  93. package/orchestration/extension/extension-id.ts +107 -0
  94. package/orchestration/extension/inject.mjs +266 -0
  95. package/orchestration/extension/launch-browser.cjs +216 -0
  96. package/orchestration/extension/launch.sh +175 -0
  97. package/orchestration/extension/live.sh +320 -0
  98. package/orchestration/extension/pin-remote-flags.cjs +45 -0
  99. package/orchestration/extension/readiness.mjs +414 -0
  100. package/orchestration/extension/refresh-build.sh +190 -0
  101. package/orchestration/extension/runtime-decision.ts +445 -0
  102. package/orchestration/extension/runtime.ts +407 -0
  103. package/orchestration/extension/seed-fixture.sh +177 -0
  104. package/orchestration/extension/sidepanel-toggle.sh +291 -0
  105. package/orchestration/extension/snapshot-dist.sh +84 -0
  106. package/orchestration/extension/start-watch.sh +339 -0
  107. package/orchestration/extension/wallet-fixture-state.cjs +1086 -0
  108. package/orchestration/lib/activate-repo-node.sh +144 -0
  109. package/orchestration/lib/cli-color.mjs +84 -0
  110. package/orchestration/lib/cli-commands.mjs +243 -0
  111. package/orchestration/lib/cli-home.mjs +354 -0
  112. package/orchestration/lib/cli-ux.sh +252 -0
  113. package/orchestration/lib/cli-version.mjs +123 -0
  114. package/orchestration/lib/ensure-runner-deps.sh +56 -0
  115. package/orchestration/lib/harness-path.sh +55 -0
  116. package/orchestration/lib/hash-helpers.sh +44 -0
  117. package/orchestration/lib/json-field.sh +23 -0
  118. package/orchestration/lib/log-tui.mjs +304 -0
  119. package/orchestration/lib/open-debug.mjs +317 -0
  120. package/orchestration/lib/path-defaults.json +4 -0
  121. package/orchestration/lib/progress.mjs +107 -0
  122. package/orchestration/lib/recipe-paths.mjs +26 -0
  123. package/orchestration/lib/resolve-farmslot-ports.sh +144 -0
  124. package/orchestration/manifest.json +358 -0
  125. package/orchestration/mobile/cleanup.sh +192 -0
  126. package/orchestration/mobile/deps-markers.ts +21 -0
  127. package/orchestration/mobile/inject.sh +681 -0
  128. package/orchestration/mobile/launch.sh +137 -0
  129. package/orchestration/mobile/live.sh +125 -0
  130. package/orchestration/mobile/runtime-decision.ts +292 -0
  131. package/orchestration/porcelain/metamask-recipe +99 -0
  132. package/orchestration/porcelain/mm-recipe +1591 -0
  133. package/orchestration/porcelain/mme-recipe +1181 -0
  134. package/package.json +59 -0
  135. package/runner/extension/verify.sh +511 -0
  136. package/runner/mobile/verify.sh +501 -0
  137. package/runner/src/adapters.ts +601 -0
  138. package/runner/src/cli.ts +1820 -0
  139. package/runner/src/commands/debug.ts +44 -0
  140. package/runner/src/commands/fixtures.ts +99 -0
  141. package/runner/src/commands/launch.ts +397 -0
  142. package/runner/src/commands/logs.ts +60 -0
  143. package/runner/src/commands/shared.ts +138 -0
  144. package/runner/src/completions-cache.ts +86 -0
  145. package/runner/src/doctor.ts +203 -0
  146. package/runner/src/harness.ts +516 -0
  147. package/runner/src/heal-bounds.ts +179 -0
  148. package/runner/src/index.ts +6 -0
  149. package/runner/src/live-adapter-contract.ts +274 -0
  150. package/runner/src/manifest.ts +47 -0
  151. package/runner/src/mm-harness-cli.ts +488 -0
  152. package/runner/src/paths.ts +198 -0
  153. package/runner/src/recording-target.ts +147 -0
  154. package/runner/src/run-recording.ts +329 -0
  155. package/runner/src/runner.ts +108 -0
  156. package/runner/src/types.ts +57 -0
  157. package/scripts/completions.sh +125 -0
  158. package/scripts/install-completions.sh +62 -0
@@ -0,0 +1,488 @@
1
+ // mm-harness — the single front door for the MetaMask recipe loop.
2
+ //
3
+ // This is the commander-based CLI entry (same stack as @farmslot/recipe-harness,
4
+ // which depends on commander ^12). It presents ONLY the end-state surface
5
+ // (docs/MENTAL-MODEL.md + docs/CLI-SPEC.md) and delegates to the dispatch in
6
+ // `cli.ts` (`main`) so no orchestration logic is forked. Retired names still
7
+ // teach; genuinely-unknown commands fall to commander's default.
8
+
9
+ import { spawnSync } from 'node:child_process';
10
+ import path from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
12
+
13
+ import { Command } from 'commander';
14
+
15
+ // runner root (…/runner) → its parent is the package root that holds scripts/.
16
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
17
+
18
+ // Signal cli.ts NOT to auto-run its own dispatch when we import it below: we
19
+ // reuse its `main` in-process. Must be set BEFORE the dynamic import runs the
20
+ // module body. (A dynamic import guarantees ordering; a static import would hoist
21
+ // and execute cli.ts first.)
22
+ (globalThis as Record<string, unknown>).__MM_HARNESS_WRAPPER__ = true;
23
+ const { main: recipeMain } = await import('./cli.ts');
24
+
25
+ const rawArgv = process.argv.slice(2);
26
+
27
+ // --- Surface definition (single source for both dispatch and grouped help) ----
28
+
29
+ interface CliCommand {
30
+ name: string;
31
+ summary: string;
32
+ example: string;
33
+ /** Per-command flags help printed when the user passes --help / -h. */
34
+ helpText: string;
35
+ }
36
+
37
+
38
+ // Commands that route straight to the dispatch in cli.ts. `actions --raw` is the
39
+ // one translation (raw manifest dump).
40
+ const REAL: CliCommand[] = [
41
+ {
42
+ name: 'actions',
43
+ summary: 'List the action vocabulary + field schemas (--raw dumps the raw action registry JSON).',
44
+ example: 'mm-harness actions --adapter mobile',
45
+ helpText: `mm-harness actions [flags]
46
+
47
+ List the action vocabulary + field schemas for the checkout adapter.
48
+
49
+ --adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
50
+ --target <path> Checkout path (default: cwd)
51
+ --raw Dump raw action registry JSON
52
+ --json Machine-readable output
53
+
54
+ Example:
55
+ mm-harness actions --adapter mobile
56
+ mm-harness actions --adapter extension --raw`,
57
+ },
58
+ {
59
+ name: 'call',
60
+ summary: 'Run one action in isolation as a one-node recipe through the real engine path (fuzzy short names; --arg k=v; same trace/evidence as run).',
61
+ example: 'mm-harness call unlock',
62
+ helpText: `mm-harness call <action> [--arg k=v ...] [flags]
63
+
64
+ Run one action in isolation as a one-node recipe through the real engine path.
65
+ Fuzzy short-name: 'unlock' resolves to 'metamask.wallet.unlock' if unique; ambiguous = exit 2.
66
+
67
+ --arg k=v Action field value (repeatable)
68
+ --adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
69
+ --target <path> Checkout path (default: cwd)
70
+ --artifacts-dir <dir> Where to write evidence (default: temp dir)
71
+ --action-manifest <path> Override the action manifest
72
+ --heal <off|infra-only|auto> Healing policy (default: infra-only); auto-ensures the overlay
73
+ --json Machine-readable output
74
+
75
+ Example:
76
+ mm-harness call unlock --adapter extension
77
+ mm-harness call command --arg cmd="echo hi" --adapter core --json`,
78
+ },
79
+ {
80
+ name: 'flows',
81
+ summary: 'Browse the reusable flow library (bare = list; `flows promote` publishes a flow up a tier). Resolves across libraries — personal > team > canonical, highest tier wins.',
82
+ example: 'mm-harness flows',
83
+ helpText: `mm-harness flows [flags]
84
+
85
+ Browse the reusable flow library. Flows resolve across libraries by precedence
86
+ (personal > team > canonical); the highest-tier copy wins and shadows lower ones.
87
+ Flow resolution is adapter-global, so there is no --adapter flag here.
88
+
89
+ --library <name=path> Add/override a library source (repeatable)
90
+ --target <path> Checkout path (default: cwd)
91
+ --json Machine-readable output
92
+
93
+ Example:
94
+ mm-harness flows`,
95
+ },
96
+ {
97
+ name: 'run',
98
+ summary: 'Validate + run a recipe and write evidence (summary/trace/artifacts). --plan validates + prints the plan, touching nothing.',
99
+ example: 'mm-harness run recipe.json',
100
+ helpText: `mm-harness run <recipe.json> [flags]
101
+
102
+ Validate + run a recipe and write evidence (summary / trace / artifacts).
103
+
104
+ --plan Validate + print execution plan, touching nothing. Exit 5 if invalid.
105
+ --adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
106
+ --target <path> Checkout path (default: cwd)
107
+ --artifacts-dir <dir> Where to write evidence (required unless --plan)
108
+ --heal <off|infra-only|auto> Healing policy (default: infra-only); auto-ensures the overlay
109
+ --json Machine-readable output
110
+ --record-video=full-run Record a video of the run
111
+
112
+ Example:
113
+ mm-harness run recipe.json --plan --adapter mobile
114
+ mm-harness run recipe.json --adapter extension --artifacts-dir ./out`,
115
+ },
116
+ {
117
+ name: 'doctor',
118
+ summary: 'Readiness check for a checkout — no app launch. --fix repairs the overlay/runtime without launching.',
119
+ example: 'mm-harness doctor',
120
+ helpText: `mm-harness doctor [flags]
121
+
122
+ Readiness check for a checkout — no app launch. Reports the fixture and runtime-context
123
+ sections so there is no hunting for files.
124
+
125
+ --fix Repair the overlay/runtime-context WITHOUT launching (no fixture reseed); --json adds fixed[]/failed[]
126
+ --adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
127
+ --target <path> Checkout path (default: cwd)
128
+ --json Machine-readable output
129
+
130
+ Example:
131
+ mm-harness doctor
132
+ mm-harness doctor --fix --json
133
+ mm-harness doctor --adapter mobile --target /path/to/checkout`,
134
+ },
135
+ {
136
+ name: 'install',
137
+ summary: 'Install the per-checkout runtime overlay (CI/agents; the everyday commands auto-ensure it).',
138
+ example: 'mm-harness install',
139
+ helpText: `mm-harness install [flags]
140
+
141
+ Install the per-checkout runtime overlay (for CI / agents).
142
+ The everyday commands auto-ensure it on first use.
143
+
144
+ --adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
145
+ --target <path> Checkout path (default: cwd)
146
+
147
+ Example:
148
+ mm-harness install
149
+ mm-harness install --adapter extension --target /path/to/checkout`,
150
+ },
151
+ {
152
+ name: 'verify',
153
+ summary: 'Check the overlay/runtime is present and healthy (no launch).',
154
+ example: 'mm-harness verify',
155
+ helpText: `mm-harness verify [flags]
156
+
157
+ Check the runtime overlay is present and healthy (no app launch).
158
+
159
+ --adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
160
+ --target <path> Checkout path (default: cwd)
161
+ --json Machine-readable output
162
+
163
+ Example:
164
+ mm-harness verify
165
+ mm-harness verify --adapter core --target /path/to/checkout`,
166
+ },
167
+ {
168
+ name: 'cleanup',
169
+ summary: 'Remove the installed overlay and restore the checkout.',
170
+ example: 'mm-harness cleanup',
171
+ helpText: `mm-harness cleanup [flags]
172
+
173
+ Remove the installed overlay and restore the checkout.
174
+
175
+ --adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
176
+ --target <path> Checkout path (default: cwd)
177
+
178
+ Example:
179
+ mm-harness cleanup
180
+ mm-harness cleanup --adapter mobile --target /path/to/checkout`,
181
+ },
182
+ {
183
+ name: 'launch',
184
+ summary: 'Launch the app (Metro/build + boot), auto-ensuring the runtime overlay first. Mobile: ios|android required.',
185
+ example: 'mm-harness launch ios',
186
+ helpText: `mm-harness launch [ios|android] [flags]
187
+
188
+ Launch the app — quick relaunch by default; --build for a full native/webpack build.
189
+ Mobile: target is MANDATORY (ios | android). Extension: no target (--fullscreen default;
190
+ --sidepanel to override). core is headless (teaching error — use verify/run).
191
+
192
+ --build Full native/webpack build tier (default = quick relaunch)
193
+ --verify Launch then poll CDP/bridge until ready (absorbs the old \`live\`)
194
+ --runway Post-launch runway check (mobile only; teaching error elsewhere)
195
+ --watch Persistent webpack watcher then relaunch (extension only)
196
+ --sidepanel | --fullscreen Extension display mode (default --fullscreen)
197
+ --heal <off|infra-only|auto> Healing policy (default auto); bounds always enforced
198
+ --device <udid|name> Target simulator/device (env: IOS_SIMULATOR / ADB_SERIAL)
199
+ --cdp-port <port> Extension CDP port (env: CDP_PORT / RECIPE_CDP_PORT)
200
+ --watcher-port <port> Metro/webpack port (env: WATCHER_PORT / METRO_PORT)
201
+ --adapter <mobile|extension> Target adapter (auto-detected inside a checkout)
202
+ --target <path> Checkout path (default: cwd)
203
+ --json Machine-readable summary (recovered[] / mutations[] / phase)
204
+
205
+ Example:
206
+ mm-harness launch ios
207
+ mm-harness launch ios --build
208
+ mm-harness launch --verify --json`,
209
+ },
210
+ {
211
+ name: 'logs',
212
+ summary: 'Tail Metro/webpack + app logs for the active checkout.',
213
+ example: 'mm-harness logs',
214
+ helpText: `mm-harness logs [flags]
215
+
216
+ Tail Metro/webpack + app logs for the active checkout.
217
+ Teaching error if nothing is running (points at launch).
218
+
219
+ --full Raw log tail (default = compact) (env: RECIPE_LOG_UI)
220
+ --events <n> Compact event count (default 10) (env: RECIPE_LOG_EVENTS)
221
+ --source <metro|app> Log source (default metro)
222
+ --adapter <mobile|extension> Target adapter (auto-detected inside a checkout)
223
+ --target <path> Checkout path (default: cwd)
224
+ --json Machine-readable output
225
+
226
+ Example:
227
+ mm-harness logs
228
+ mm-harness logs --full`,
229
+ },
230
+ {
231
+ name: 'debug',
232
+ summary: 'Open the debug console — extension: Chrome DevTools via CDP; mobile: RN DevTools.',
233
+ example: 'mm-harness debug',
234
+ helpText: `mm-harness debug [flags]
235
+
236
+ Open the debug console — extension: Chrome DevTools via CDP; mobile: RN DevTools.
237
+
238
+ --worker Extension service-worker DevTools
239
+ --dev-menu Mobile RN developer menu
240
+ --adapter <mobile|extension> Target adapter (auto-detected inside a checkout)
241
+ --target <path> Checkout path (default: cwd)
242
+ --json Machine-readable output
243
+
244
+ Example:
245
+ mm-harness debug
246
+ mm-harness debug --worker`,
247
+ },
248
+ {
249
+ name: 'fixtures',
250
+ summary: 'Manage the canonical wallet fixture (wallet DATA only) — sync files / set the wallet.',
251
+ example: 'mm-harness fixtures set',
252
+ helpText: `mm-harness fixtures <sync|set> [flags]
253
+
254
+ Manage the ONE canonical wallet fixture per checkout — wallet DATA only.
255
+ sync Refresh the wallet fixture files on the target.
256
+ set Apply the canonical fixture (SRP/password/accounts); the password is read
257
+ FROM the fixture, never typed.
258
+ Want different accounts? Edit the fixture file directly:
259
+ <checkout>/temp/recipe/runtime/wallet-fixture.json
260
+
261
+ --fixture <path> Override the fixture path (agent form; env: RECIPE_WALLET_FIXTURE)
262
+ --adapter <mobile|extension> Target adapter (auto-detected inside a checkout)
263
+ --target <path> Checkout path (default: cwd)
264
+ --json Machine-readable output
265
+
266
+ Example:
267
+ mm-harness fixtures sync
268
+ mm-harness fixtures set`,
269
+ },
270
+ ];
271
+
272
+ // Retired names: NOT on the surface, but they must TEACH rather than fall to
273
+ // commander's generic "unknown command" path. Each names its replacement and
274
+ // exits 2 (bad-args/unsupported, distinct from a genuinely-unknown command's
275
+ // exit 1). Genuinely-unknown commands stay on commander's default (which points
276
+ // at --help via showHelpAfterError).
277
+ interface RetiredCommand {
278
+ name: string;
279
+ message: string;
280
+ }
281
+
282
+ const RETIRED_INTERNAL = [
283
+ 'runtime-health',
284
+ 'runtime-decision',
285
+ 'runtime-launch',
286
+ 'resolve-extension',
287
+ 'ensure-ready',
288
+ 'self-test',
289
+ ];
290
+
291
+ const RETIRED: RetiredCommand[] = [
292
+ {
293
+ name: 'live',
294
+ message:
295
+ `mm-harness live is retired (exit 2).\n\n` +
296
+ `Replacement: mm-harness launch --verify (install overlay → launch → CDP/bridge poll → smoke verify).`,
297
+ },
298
+ {
299
+ name: 'manifest',
300
+ message:
301
+ `mm-harness manifest is retired (exit 2).\n\n` +
302
+ `Replacement: mm-harness actions --raw (works now — dumps the raw action registry JSON,\n` +
303
+ `identical to the old \`manifest --json\`). Manifest validation moved into doctor / run --plan.`,
304
+ },
305
+ ...RETIRED_INTERNAL.map((name) => ({
306
+ name,
307
+ message:
308
+ `mm-harness ${name} is retired (exit 2).\n\n` +
309
+ `It is internal now — its logic lives inside doctor / launch / verify self-healing.\n` +
310
+ `Use: mm-harness doctor`,
311
+ })),
312
+ ];
313
+
314
+ // --- Grouped help (custom formatHelp; commander owns parse/dispatch) ---------
315
+
316
+ interface HelpGroup {
317
+ title: string;
318
+ blurb: string;
319
+ commands: string[];
320
+ }
321
+
322
+ const HELP_GROUPS: HelpGroup[] = [
323
+ {
324
+ title: 'DAILY LOOP',
325
+ blurb: 'what a teammate runs many times a day (auto-ensures the overlay; --heal owns recovery)',
326
+ commands: ['launch', 'logs', 'debug', 'fixtures'],
327
+ },
328
+ {
329
+ title: 'DISCOVER',
330
+ blurb: 'compose recipes from the action vocabulary + flow library (--json is the agent-primary form)',
331
+ commands: ['actions', 'call', 'flows'],
332
+ },
333
+ {
334
+ title: 'PROVE',
335
+ blurb: 'run recipes and inspect readiness',
336
+ commands: ['run', 'doctor'],
337
+ },
338
+ {
339
+ title: 'RUNTIME OVERLAY',
340
+ blurb: 'install/verify/clean the per-checkout overlay (the everyday commands auto-ensure it)',
341
+ commands: ['install', 'verify', 'cleanup'],
342
+ },
343
+ ];
344
+
345
+ function commandMeta(name: string): { summary: string; example: string; planned: boolean } {
346
+ const real = REAL.find((command) => command.name === name);
347
+ if (real) return { summary: real.summary, example: real.example, planned: false };
348
+ return { summary: '', example: '', planned: false };
349
+ }
350
+
351
+ function groupedHelp(): string {
352
+ const lines: string[] = [];
353
+ lines.push('mm-harness — one front door for the MetaMask recipe loop: launch the app, prove behavior, manage the runtime overlay.');
354
+ lines.push('Run it from inside a MetaMask checkout; the platform (mobile | extension | core) is auto-detected.');
355
+ lines.push('Grammar: mm-harness <command> [target] [flags] (target is a positional: ios | android; flags add agent depth; --json is the agent contract)');
356
+ for (const group of HELP_GROUPS) {
357
+ lines.push('');
358
+ lines.push(`${group.title} — ${group.blurb}:`);
359
+ for (const name of group.commands) {
360
+ const meta = commandMeta(name);
361
+ const planned = meta.planned ? ' (planned)' : '';
362
+ lines.push(` ${name.padEnd(10)} ${meta.summary}${planned}`);
363
+ lines.push(` ${meta.example}`);
364
+ }
365
+ }
366
+ lines.push('');
367
+ lines.push('ONE bin: mm-harness is the only command. Platform is auto-detected; the positional target forces it');
368
+ lines.push('(mm-harness launch ios); platform-specific needs are FLAGS on the same command. Human happy path = the bare');
369
+ lines.push('command; agents add depth via flags (--json, --target, ports).');
370
+ lines.push('');
371
+ lines.push('SETUP — tab-completion (zsh + bash), sourced from the bundled scripts:');
372
+ lines.push(' mm-harness completions install # print the lines to add to ~/.zshrc / ~/.bashrc');
373
+ lines.push('');
374
+ lines.push('See docs/MENTAL-MODEL.md (overview) and docs/CLI-SPEC.md (full contract).');
375
+ return `${lines.join('\n')}\n`;
376
+ }
377
+
378
+ // --- Delegation -------------------------------------------------------------
379
+
380
+ async function delegate(argv: string[]): Promise<number> {
381
+ try {
382
+ return await recipeMain(argv);
383
+ } catch (error) {
384
+ console.error(error instanceof Error ? error.message : String(error));
385
+ // CliError (from cli.ts) carries an exitCode so usage errors (2) and
386
+ // validation errors (5) propagate correctly rather than always returning 1.
387
+ return error !== null &&
388
+ typeof error === 'object' &&
389
+ 'exitCode' in error &&
390
+ typeof (error as Record<string, unknown>).exitCode === 'number'
391
+ ? (error as { exitCode: number }).exitCode
392
+ : 1;
393
+ }
394
+ }
395
+
396
+ // `actions --raw` == the retired `manifest --adapter <p> --json` raw dump.
397
+ function translateActionsRaw(argv: string[]): string[] {
398
+ const rest = argv.slice(1).filter((arg) => arg !== '--raw');
399
+ const withJson = rest.includes('--json') ? rest : [...rest, '--json'];
400
+ return ['manifest', ...withJson];
401
+ }
402
+
403
+ // --- Program ----------------------------------------------------------------
404
+
405
+ const program = new Command();
406
+ program
407
+ .name('mm-harness')
408
+ .description('the MetaMask recipe harness: launch the app, prove behavior, manage the runtime overlay')
409
+ .helpOption('-h, --help', 'Show grouped help')
410
+ .showHelpAfterError('(run `mm-harness --help` for the full surface)')
411
+ .configureHelp({ formatHelp: () => groupedHelp() });
412
+
413
+ for (const command of REAL) {
414
+ program
415
+ .command(command.name)
416
+ .description(command.summary)
417
+ .allowUnknownOption()
418
+ .helpOption(false)
419
+ .argument('[args...]')
420
+ .action(async () => {
421
+ // Intercept --help / -h before delegation: the hand-rolled parser in cli.ts
422
+ // treats unknown flags as value-consuming and throws "Missing value for --help".
423
+ // Each command exposes its own flags via helpText; exit 0 so shell completion
424
+ // and tooling work correctly.
425
+ // Only check flags BEFORE the `--` separator so `install -- --help` correctly
426
+ // forwards --help to the underlying install script rather than intercepting it.
427
+ const ddIdx = rawArgv.indexOf('--');
428
+ const flagsBeforeSep = ddIdx === -1 ? rawArgv : rawArgv.slice(0, ddIdx);
429
+ if (flagsBeforeSep.includes('--help') || flagsBeforeSep.includes('-h')) {
430
+ process.stdout.write(`${command.helpText}\n`);
431
+ process.exit(0);
432
+ }
433
+ const argv = command.name === 'actions' && rawArgv.includes('--raw') ? translateActionsRaw(rawArgv) : rawArgv;
434
+ process.exit(await delegate(argv));
435
+ });
436
+ }
437
+
438
+ // Hidden commands: real routing, but intentionally absent from the grouped help.
439
+ // `completion-candidates <actions|flows>` is what the shell completion scripts
440
+ // source; humans never type it.
441
+ const HIDDEN: readonly string[] = ['completion-candidates'];
442
+ for (const name of HIDDEN) {
443
+ program
444
+ .command(name, { hidden: true })
445
+ .allowUnknownOption()
446
+ .helpOption(false)
447
+ .argument('[args...]')
448
+ .action(async () => {
449
+ process.exit(await delegate(rawArgv));
450
+ });
451
+ }
452
+
453
+ // `completions` installs/prints the bundled zsh + bash tab-completion. It is
454
+ // shell tooling (no engine), so it runs the bundled installer script directly
455
+ // rather than routing through the dispatch in cli.ts.
456
+ program
457
+ .command('completions')
458
+ .description('Install/print bundled shell tab-completion (zsh + bash)')
459
+ .allowUnknownOption()
460
+ .helpOption(false)
461
+ .argument('[args...]')
462
+ .action(() => {
463
+ const script = path.join(packageRoot, 'scripts', 'install-completions.sh');
464
+ const result = spawnSync('bash', [script, ...rawArgv.slice(1)], { stdio: 'inherit' });
465
+ process.exit(result.status ?? 1);
466
+ });
467
+
468
+ // Retired names teach their replacement and exit 2 (not commander's generic
469
+ // "unknown command" exit 1). They are NOT listed in the grouped help.
470
+ for (const retired of RETIRED) {
471
+ program
472
+ .command(retired.name)
473
+ .description(`${retired.name} is retired (see teaching error)`)
474
+ .allowUnknownOption()
475
+ .helpOption(false)
476
+ .argument('[args...]')
477
+ .action(() => {
478
+ console.error(retired.message);
479
+ process.exit(2);
480
+ });
481
+ }
482
+
483
+ if (rawArgv.length === 0) {
484
+ process.stdout.write(groupedHelp());
485
+ process.exit(0);
486
+ }
487
+
488
+ await program.parseAsync(process.argv);
@@ -0,0 +1,198 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath, pathToFileURL } from 'node:url';
4
+
5
+ import type {
6
+ RecipeHarnessBrowserExtensionModule,
7
+ RecipeHarnessCdpModule,
8
+ RecipeHarnessModule,
9
+ RecipeHarnessReactNativeBridgeModule,
10
+ RecipeProtocolModule,
11
+ MetaMaskRecipeAdapter,
12
+ } from './types.ts';
13
+
14
+ export const runnerDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..');
15
+ const pathDefaults = readPathDefaults();
16
+ export const DEFAULT_RECIPE_RUNTIME_DIR = pathDefaults.recipeRuntimeDir;
17
+ export const DEFAULT_RECIPE_HARNESS_ROOT = pathDefaults.recipeHarnessRoot;
18
+
19
+ export function recipeRuntimeDir() {
20
+ return validateRelativeRecipePath('RECIPE_RUNTIME_DIR', process.env.RECIPE_RUNTIME_DIR || DEFAULT_RECIPE_RUNTIME_DIR);
21
+ }
22
+
23
+ export function recipeHarnessRoot() {
24
+ return validateRelativeRecipePath('RECIPE_HARNESS_ROOT', process.env.RECIPE_HARNESS_ROOT || DEFAULT_RECIPE_HARNESS_ROOT);
25
+ }
26
+
27
+
28
+ function readPathDefaults(): { recipeRuntimeDir: string; recipeHarnessRoot: string } {
29
+ const defaultsPath = path.join(runnerDir, 'orchestration/lib/path-defaults.json');
30
+ const parsed = JSON.parse(fs.readFileSync(defaultsPath, 'utf8')) as Partial<{ recipeRuntimeDir: string; recipeHarnessRoot: string }>;
31
+ return {
32
+ recipeRuntimeDir: validateRelativeRecipePath('recipeRuntimeDir', parsed.recipeRuntimeDir || ''),
33
+ recipeHarnessRoot: validateRelativeRecipePath('recipeHarnessRoot', parsed.recipeHarnessRoot || ''),
34
+ };
35
+ }
36
+
37
+ function validateRelativeRecipePath(name: string, value: string): string {
38
+ if (!value || path.isAbsolute(value)) throw new Error(`${name} must be a non-empty relative path: ${value}`);
39
+ if (!/^[A-Za-z0-9._/-]+$/u.test(value)) throw new Error(`${name} contains unsupported characters: ${value}`);
40
+ for (const part of value.split('/')) {
41
+ if (!part || part === '.' || part === '..') throw new Error(`${name} contains unsafe path component: ${value}`);
42
+ }
43
+ return value;
44
+ }
45
+
46
+ export function recipeRuntimePath(projectRoot: string, ...segments: string[]) {
47
+ return path.join(projectRoot, recipeRuntimeDir(), ...segments);
48
+ }
49
+
50
+ export function recipeHarnessPath(projectRoot: string, ...segments: string[]) {
51
+ return path.join(projectRoot, recipeHarnessRoot(), ...segments);
52
+ }
53
+
54
+ export function walletFixturePath(projectRoot: string) {
55
+ return recipeRuntimePath(projectRoot, 'wallet-fixture.json');
56
+ }
57
+
58
+ export function extensionIdPath(projectRoot: string) {
59
+ return recipeRuntimePath(projectRoot, 'extension.id');
60
+ }
61
+
62
+ export function recipeWatchLogCandidates() {
63
+ return [
64
+ path.join(recipeRuntimeDir(), 'webpack.log'),
65
+ path.join(recipeRuntimeDir(), 'recipe-harness-webpack.log'),
66
+ ];
67
+ }
68
+
69
+ export function resolveLocalProtocolRoot() {
70
+ const candidates = [
71
+ process.env.FARMSLOT_ROOT,
72
+ readConfiguredProtocolRoot(),
73
+ findProtocolRoot(runnerDir),
74
+ findProtocolRoot(process.cwd()),
75
+ ].filter(Boolean);
76
+ const root = candidates[0];
77
+ return root ? path.resolve(root) : undefined;
78
+ }
79
+
80
+ export function resolveRequiredLocalProtocolRoot(reason: string) {
81
+ const root = resolveLocalProtocolRoot();
82
+ if (!root) {
83
+ throw new Error(
84
+ `${reason} requires a local protocol/runtime checkout. Set FARMSLOT_ROOT or create .farmslot-root for this dev-only path.`,
85
+ );
86
+ }
87
+ return root;
88
+ }
89
+
90
+ function readConfiguredProtocolRoot() {
91
+ const configPath = path.join(runnerDir, '.farmslot-root');
92
+ if (!fs.existsSync(configPath)) return undefined;
93
+ const value = fs.readFileSync(configPath, 'utf8').trim();
94
+ return value || undefined;
95
+ }
96
+
97
+ function findProtocolRoot(start) {
98
+ let dir = path.resolve(start);
99
+ while (dir !== path.dirname(dir)) {
100
+ if (isProtocolRoot(dir)) return dir;
101
+ const sibling = path.join(dir, 'farmslot');
102
+ if (isProtocolRoot(sibling)) return sibling;
103
+ dir = path.dirname(dir);
104
+ }
105
+ return undefined;
106
+ }
107
+
108
+ function isProtocolRoot(candidate) {
109
+ return (
110
+ fs.existsSync(path.join(candidate, 'packages/recipe-harness/package.json')) &&
111
+ fs.existsSync(path.join(candidate, 'packages/protocol/package.json'))
112
+ );
113
+ }
114
+
115
+ export function assertAdapter(adapter: unknown): asserts adapter is MetaMaskRecipeAdapter {
116
+ if (adapter !== 'mobile' && adapter !== 'extension' && adapter !== 'core') {
117
+ throw new Error('Adapter must be mobile, extension, or core.');
118
+ }
119
+ }
120
+
121
+ export function manifestPath(adapter: MetaMaskRecipeAdapter) {
122
+ assertAdapter(adapter);
123
+ return path.join(runnerDir, 'library/manifests', `${adapter}.action-manifest.json`);
124
+ }
125
+
126
+ export function recipePath(name: string) {
127
+ return path.join(runnerDir, 'library/recipes', name);
128
+ }
129
+
130
+ export function readJson(file: string): unknown {
131
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
132
+ }
133
+
134
+ export async function importRecipeHarness(): Promise<RecipeHarnessModule> {
135
+ return importProtocolPackage(
136
+ '@farmslot/recipe-harness',
137
+ 'packages/recipe-harness/src/index.ts',
138
+ ) as Promise<RecipeHarnessModule>;
139
+ }
140
+
141
+ export async function importRecipeHarnessRuntimeCdp(): Promise<RecipeHarnessCdpModule> {
142
+ return importProtocolPackage(
143
+ '@farmslot/recipe-harness/runtime/cdp',
144
+ 'packages/recipe-harness/src/runtime/cdp.ts',
145
+ ) as Promise<RecipeHarnessCdpModule>;
146
+ }
147
+
148
+ export async function importRecipeHarnessRuntimeBrowserExtension(): Promise<RecipeHarnessBrowserExtensionModule> {
149
+ return importProtocolPackage(
150
+ '@farmslot/recipe-harness/runtime/browser-extension',
151
+ 'packages/recipe-harness/src/runtime/browser-extension.ts',
152
+ ) as Promise<RecipeHarnessBrowserExtensionModule>;
153
+ }
154
+
155
+ export async function importRecipeHarnessRuntimeReactNativeBridge(): Promise<RecipeHarnessReactNativeBridgeModule> {
156
+ return importProtocolPackage(
157
+ '@farmslot/recipe-harness/runtime/react-native-bridge',
158
+ 'packages/recipe-harness/src/runtime/react-native-bridge.ts',
159
+ ) as Promise<RecipeHarnessReactNativeBridgeModule>;
160
+ }
161
+
162
+ export async function importRecipeHarnessCli(): Promise<{
163
+ runRecipeHarnessCli: (argv: string[]) => Promise<void>;
164
+ }> {
165
+ return importProtocolPackage(
166
+ '@farmslot/recipe-harness/cli',
167
+ 'packages/recipe-harness/src/cli/index.ts',
168
+ ) as Promise<{ runRecipeHarnessCli: (argv: string[]) => Promise<void> }>;
169
+ }
170
+
171
+ export async function importRecipeProtocol(): Promise<RecipeProtocolModule> {
172
+ return importProtocolPackage(
173
+ '@farmslot/protocol',
174
+ 'packages/protocol/src/index.ts',
175
+ ) as Promise<RecipeProtocolModule>;
176
+ }
177
+
178
+ async function importProtocolPackage(packageName: string, localSourceEntry: string) {
179
+ try {
180
+ return await import(packageName);
181
+ } catch (error) {
182
+ if (!isMissingPackageError(error, packageName)) throw error;
183
+ }
184
+
185
+ const root = resolveLocalProtocolRoot();
186
+ if (!root) {
187
+ throw new Error(
188
+ `${packageName} is not installed. Install @farmslot/* packages normally, or set FARMSLOT_ROOT/use npm run dev:link-farmslot while co-developing protocol packages locally.`,
189
+ );
190
+ }
191
+ return import(pathToFileURL(path.join(root, localSourceEntry)).href);
192
+ }
193
+
194
+ function isMissingPackageError(error: unknown, packageName: string) {
195
+ if (!(error instanceof Error)) return false;
196
+ const code = (error as NodeJS.ErrnoException).code;
197
+ return code === 'ERR_MODULE_NOT_FOUND' && error.message.includes(packageName);
198
+ }