@deeeed/metamask-harness 0.5.0 → 0.6.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 (102) hide show
  1. package/CHANGELOG.md +51 -20
  2. package/adapters/core/inject.sh +1 -0
  3. package/adapters/extension/cleanup.mjs +1 -1
  4. package/adapters/extension/inject.mjs +4 -1
  5. package/adapters/extension/launch-browser.cjs +18 -4
  6. package/adapters/extension/live.sh +14 -2
  7. package/adapters/extension/refresh-build.sh +2 -2
  8. package/adapters/extension/sidepanel-toggle.sh +33 -12
  9. package/adapters/extension/start-watch.sh +2 -2
  10. package/adapters/extension/wallet-fixture-state.cjs +38 -24
  11. package/adapters/manifest.json +32 -3
  12. package/adapters/mobile/inject.sh +1 -0
  13. package/adapters/mobile/start-metro.sh +1 -1
  14. package/adapters/mobile/stop-metro.sh +20 -0
  15. package/adapters/mobile/verify.sh +1 -1
  16. package/adapters/shared/activate-repo-node.sh +1 -1
  17. package/adapters/shared/cli-ux.sh +44 -25
  18. package/adapters/shared/ensure-runner-deps.sh +30 -0
  19. package/adapters/shared/log-tui.mjs +4 -4
  20. package/adapters/shared/open-debug.mjs +1 -1
  21. package/adapters/shared/reap-checkout-metros.sh +53 -0
  22. package/adapters/shared/recipe-harness-root.mjs +23 -0
  23. package/adapters/shared/resolve-farmslot-ports-core.mjs +205 -0
  24. package/adapters/shared/resolve-farmslot-ports.mjs +20 -0
  25. package/adapters/shared/resolve-farmslot-ports.sh +19 -126
  26. package/bin/mm-harness +45 -4
  27. package/dist/adapters/extension/runtime.js +3 -1
  28. package/dist/adapters/mobile/provision.js +34 -1
  29. package/dist/adapters/mobile/runtime-decision.js +3 -1
  30. package/dist/adapters/resolve-farmslot-ports.js +22 -0
  31. package/dist/adapters/slot-ports.js +18 -29
  32. package/dist/cli.js +32 -1463
  33. package/dist/commands/call.js +183 -0
  34. package/dist/commands/completion-candidates.js +58 -0
  35. package/dist/commands/doctor.js +101 -0
  36. package/dist/commands/ensure-ready.js +24 -0
  37. package/dist/commands/flows.js +62 -0
  38. package/dist/commands/launch/extension.js +40 -0
  39. package/dist/commands/{launch.js → launch/index.js} +15 -47
  40. package/dist/commands/launch/mobile.js +10 -0
  41. package/dist/commands/manifest.js +72 -0
  42. package/dist/commands/parse-args.js +189 -0
  43. package/dist/commands/provision.js +136 -0
  44. package/dist/commands/resolve-extension.js +23 -0
  45. package/dist/commands/run-engine.js +341 -0
  46. package/dist/commands/run.js +217 -0
  47. package/dist/commands/runtime-decision.js +58 -0
  48. package/dist/commands/runtime-health.js +25 -0
  49. package/dist/commands/runtime-launch.js +139 -0
  50. package/dist/commands/self-test.js +52 -0
  51. package/dist/commands/stop.js +52 -0
  52. package/dist/harness.js +8 -48
  53. package/dist/mm-harness-cli.js +13 -8
  54. package/docs/CLI-SPEC.md +1 -1
  55. package/docs/CODE-MAP.md +62 -0
  56. package/library/README.md +14 -0
  57. package/library/actions/core/perps/_controller.mjs +1 -1
  58. package/library/actions/extension/platform/cdp.mjs +2 -2
  59. package/library/actions/extension/wallet/ensure_unlocked.mjs +7 -1
  60. package/library/actions/harness-exports.mjs +27 -0
  61. package/library/actions/mobile/wallet/ensure_unlocked.mjs +14 -2
  62. package/library/actions/mobile/wallet/setup.mjs +1 -1
  63. package/package.json +6 -7
  64. package/src/adapters/core/surface.ts +0 -71
  65. package/src/adapters/extension/ensure-ready.ts +0 -185
  66. package/src/adapters/extension/extension-id.ts +0 -107
  67. package/src/adapters/extension/runtime-decision.ts +0 -445
  68. package/src/adapters/extension/runtime.ts +0 -407
  69. package/src/adapters/extension/surface.ts +0 -88
  70. package/src/adapters/mobile/deps-markers.ts +0 -21
  71. package/src/adapters/mobile/prepare.ts +0 -246
  72. package/src/adapters/mobile/provision.ts +0 -594
  73. package/src/adapters/mobile/runtime-decision.ts +0 -459
  74. package/src/adapters/mobile/surface.ts +0 -71
  75. package/src/adapters/slot-ports.ts +0 -165
  76. package/src/adapters/surface.ts +0 -117
  77. package/src/adapters.ts +0 -601
  78. package/src/cli-color.ts +0 -92
  79. package/src/cli-commands.ts +0 -250
  80. package/src/cli-version.ts +0 -141
  81. package/src/cli.ts +0 -2091
  82. package/src/commands/debug.ts +0 -65
  83. package/src/commands/fixtures.ts +0 -198
  84. package/src/commands/launch.ts +0 -470
  85. package/src/commands/logs.ts +0 -99
  86. package/src/commands/shared.ts +0 -235
  87. package/src/commands/update.ts +0 -316
  88. package/src/completions-cache.ts +0 -86
  89. package/src/doctor.ts +0 -215
  90. package/src/harness.ts +0 -797
  91. package/src/heal-bounds.ts +0 -198
  92. package/src/index.ts +0 -15
  93. package/src/leaf-invoke.ts +0 -28
  94. package/src/live-adapter-contract.ts +0 -274
  95. package/src/manifest.ts +0 -47
  96. package/src/mm-harness-cli.ts +0 -655
  97. package/src/paths.ts +0 -198
  98. package/src/progress.ts +0 -117
  99. package/src/recording-target.ts +0 -147
  100. package/src/run-recording.ts +0 -329
  101. package/src/runner.ts +0 -108
  102. package/src/types.ts +0 -57
@@ -1,655 +0,0 @@
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 fs from 'node:fs';
11
- import path from 'node:path';
12
- import { fileURLToPath } from 'node:url';
13
-
14
- import { Command } from 'commander';
15
-
16
- import { color } from './cli-color.ts';
17
- import { handleUpdate, maybeNudge } from './commands/update.ts';
18
-
19
- // …/src → its parent is the package root that holds scripts/.
20
- const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
21
-
22
- // Signal cli.ts NOT to auto-run its own dispatch when we import it below: we
23
- // reuse its `main` in-process. Must be set BEFORE the dynamic import runs the
24
- // module body. (A dynamic import guarantees ordering; a static import would hoist
25
- // and execute cli.ts first.)
26
- (globalThis as Record<string, unknown>).__MM_HARNESS_WRAPPER__ = true;
27
- const { main: recipeMain } = await import('./cli.ts');
28
-
29
- const rawArgv = process.argv.slice(2);
30
-
31
- // --- Surface definition (single source for both dispatch and grouped help) ----
32
-
33
- interface CliCommand {
34
- name: string;
35
- summary: string;
36
- example: string;
37
- /** Per-command flags help printed when the user passes --help / -h. */
38
- helpText: string;
39
- }
40
-
41
-
42
- // Commands that route straight to the dispatch in cli.ts. `actions --raw` is the
43
- // one translation (raw manifest dump).
44
- const REAL: CliCommand[] = [
45
- {
46
- name: 'actions',
47
- summary: 'List the action vocabulary + field schemas (--raw dumps the raw action registry JSON).',
48
- example: 'mm-harness actions --adapter mobile',
49
- helpText: `mm-harness actions [flags]
50
-
51
- List the action vocabulary + field schemas for the checkout adapter.
52
-
53
- --adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
54
- --target <path> Checkout path (default: cwd)
55
- --raw Dump raw action registry JSON
56
- --json Machine-readable output
57
-
58
- Example:
59
- mm-harness actions --adapter mobile
60
- mm-harness actions --adapter extension --raw`,
61
- },
62
- {
63
- name: 'stop',
64
- summary: 'Stop the dev server this checkout owns (mobile Metro / extension webpack watcher) and close its log window.',
65
- example: 'mm-harness stop',
66
- helpText: `mm-harness stop [flags]
67
-
68
- Stop the dev server this checkout owns and close its tmux log-tail window,
69
- scoped to this checkout so concurrent slots are untouched. Idempotent —
70
- nothing running is success, not an error. Behavior is per platform:
71
- mobile stop the port-scoped Metro dev server
72
- extension stop the checkout's webpack watcher (pid file + orphan scan)
73
- core headless — no dev server to stop (teaching error)
74
-
75
- --port <port> Dev-server port (default: the checkout's slot context)
76
- --target <path> Checkout path (default: cwd)
77
- --json Machine-readable output
78
-
79
- Example:
80
- mm-harness stop
81
- mm-harness stop --port 8061`,
82
- },
83
- {
84
- name: 'call',
85
- 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).',
86
- example: 'mm-harness call ensure_unlocked',
87
- helpText: `mm-harness call <action> [--arg k=v ...] [flags]
88
-
89
- Run one action in isolation as a one-node recipe through the real engine path.
90
- Fuzzy short-name: 'ensure_unlocked' resolves to 'metamask.wallet.ensure_unlocked'
91
- if unique; ambiguous = exit 2. Actions differ per adapter — list this checkout's
92
- with: mm-harness actions.
93
-
94
- --arg k=v Action field value (repeatable)
95
- --adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
96
- --target <path> Checkout path (default: cwd)
97
- --artifacts-dir <dir> Where to write evidence (default: temp dir)
98
- --action-manifest <path> Override the action manifest
99
- --heal <off|infra-only|auto> Healing policy (default: infra-only); auto-ensures the overlay
100
- --json Machine-readable output
101
-
102
- Example (real actions; run mm-harness actions for this checkout's full set):
103
- mm-harness call ensure_unlocked --adapter extension # a wallet action (extension/mobile)
104
- mm-harness call command --arg cmd="echo hi" --adapter core # the universal action (all adapters)`,
105
- },
106
- {
107
- name: 'flows',
108
- 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.',
109
- example: 'mm-harness flows',
110
- helpText: `mm-harness flows [flags]
111
-
112
- Browse the reusable flow library. Flows resolve across libraries by precedence
113
- (personal > team > canonical); the highest-tier copy wins and shadows lower ones.
114
- Flow resolution is adapter-global, so there is no --adapter flag here.
115
-
116
- --library <name=path> Add/override a library source (repeatable)
117
- --target <path> Checkout path (default: cwd)
118
- --json Machine-readable output
119
-
120
- Example:
121
- mm-harness flows`,
122
- },
123
- {
124
- name: 'run',
125
- summary: 'Validate + run a recipe and write evidence (summary/trace/artifacts). --plan validates + prints the plan, touching nothing.',
126
- example: 'mm-harness run recipe.json',
127
- helpText: `mm-harness run <recipe.json> [flags]
128
-
129
- Validate + run a recipe and write evidence (summary / trace / artifacts).
130
-
131
- --plan Validate + print execution plan, touching nothing. Exit 5 if invalid.
132
- --adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
133
- --target <path> Checkout path (default: cwd)
134
- --artifacts-dir <dir> Where to write evidence (required unless --plan)
135
- --heal <off|infra-only|auto> Healing policy (default: infra-only); auto-ensures the overlay
136
- --json Machine-readable output
137
- --record-video=full-run Record a video of the run
138
-
139
- Example:
140
- mm-harness run recipe.json --plan --adapter mobile
141
- mm-harness run recipe.json --adapter extension --artifacts-dir ./out`,
142
- },
143
- {
144
- name: 'doctor',
145
- summary: 'Readiness check for a checkout — no app launch. --fix repairs the overlay/runtime without launching.',
146
- example: 'mm-harness doctor',
147
- helpText: `mm-harness doctor [flags]
148
-
149
- Readiness check for a checkout — no app launch. Reports the fixture and runtime-context
150
- sections so there is no hunting for files.
151
-
152
- --fix Repair the overlay/runtime-context WITHOUT launching (no fixture reseed); --json adds fixed[]/failed[]
153
- --adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
154
- --target <path> Checkout path (default: cwd)
155
- --json Machine-readable output
156
-
157
- Example:
158
- mm-harness doctor
159
- mm-harness doctor --fix --json
160
- mm-harness doctor --adapter mobile --target /path/to/checkout`,
161
- },
162
- {
163
- name: 'provision',
164
- summary: 'Install the cached Runway iOS dev client on a prepared mobile slot (no deps, no Metro).',
165
- example: 'mm-harness provision runway ios --adapter mobile',
166
- helpText: `mm-harness provision [runway ios] [flags]
167
-
168
- Install the cached Runway iOS dev client on the slot simulator. This is a thin
169
- provisioning path only: artifact cache + simulator create + simctl install.
170
- JavaScript dependencies and Metro remain dispatch-time launch concerns.
171
-
172
- --adapter <mobile|extension|core> Target adapter (mobile supported; extension/core teach)
173
- --target <path> Slot checkout path (default: cwd)
174
- --platform <ios> Platform (default ios)
175
- --simulator <name|udid> Override agentic-runtime.json simulator (alias: --device)
176
- --device <name|udid> Alias for --simulator
177
- --slot <id> Farm slot id recorded in the provision baseline
178
- --watcher-port <port> Farm Metro/watcher port carried through context and Next:
179
- --runtime-dir <dir> Runtime dir containing agentic-runtime.json (relative to target)
180
- --runtime <id> iOS runtime id used if simulator must be created
181
- --device-type <id> Device type id used if simulator must be created
182
- --branch <ref> Probe this ref before default branch
183
- --default-branch <ref> Fallback ref (default main)
184
- --run <id> Exact GitHub Actions run id
185
- --cache-root <dir> Override shared runway cache root
186
- --force Reinstall even when the app is already present
187
- --resolve-only Resolve artifact metadata only; no simulator/cache/install
188
- --json Machine-readable envelope; progress stays stderr
189
-
190
- Example:
191
- mm-harness provision runway ios --adapter mobile --target /path/to/slot
192
- mm-harness provision runway ios --adapter mobile --slot scratch-1 --runtime-dir temp/recipe/runtime-8081
193
- mm-harness provision runway ios --run 28676856835 --resolve-only --json`,
194
- },
195
- {
196
- name: 'install',
197
- summary: 'Install the per-checkout runtime overlay, or --runway to install a cached mobile dev client.',
198
- example: 'mm-harness install',
199
- helpText: `mm-harness install [flags]
200
-
201
- Install the per-checkout runtime overlay (for CI / agents).
202
- Add --runway on a mobile slot to install the cached Runway iOS dev client only:
203
- artifact cache + simulator create + simctl install. JavaScript dependencies
204
- and Metro remain dispatch-time launch concerns.
205
-
206
- --adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
207
- --target <path> Checkout path (default: cwd)
208
- --runway Thin mobile Runway artifact install instead of overlay install
209
- --platform <ios> Runway platform (default ios)
210
- --simulator <name|udid> Override agentic-runtime.json simulator
211
- --runtime <id> iOS runtime id used if simulator must be created
212
- --device-type <id> Device type id used if simulator must be created
213
- --branch <ref> Probe this ref before default branch
214
- --default-branch <ref> Fallback ref (default main)
215
- --run <id> Exact GitHub Actions run id
216
- --cache-root <dir> Override shared runway cache root
217
- --force Reinstall even when the app is already present
218
- --resolve-only Runway metadata only; no simulator/cache/install
219
-
220
- Example:
221
- mm-harness install
222
- mm-harness install --adapter extension --target /path/to/checkout
223
- mm-harness install --runway --adapter mobile --target /path/to/slot`,
224
- },
225
- {
226
- name: 'verify',
227
- summary: 'Check the overlay/runtime is present and healthy (no launch).',
228
- example: 'mm-harness verify',
229
- helpText: `mm-harness verify [flags]
230
-
231
- Check the runtime overlay is present and healthy (no app launch).
232
-
233
- --adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
234
- --target <path> Checkout path (default: cwd)
235
- --json Machine-readable output
236
-
237
- Example:
238
- mm-harness verify
239
- mm-harness verify --adapter core --target /path/to/checkout`,
240
- },
241
- {
242
- name: 'cleanup',
243
- summary: 'Remove the installed overlay and restore the checkout.',
244
- example: 'mm-harness cleanup',
245
- helpText: `mm-harness cleanup [flags]
246
-
247
- Remove the installed overlay and restore the checkout.
248
-
249
- --adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
250
- --target <path> Checkout path (default: cwd)
251
-
252
- Example:
253
- mm-harness cleanup
254
- mm-harness cleanup --adapter mobile --target /path/to/checkout`,
255
- },
256
- {
257
- name: 'launch',
258
- summary: 'Launch the app (Metro/build + boot), auto-ensuring the runtime overlay first. Mobile: ios|android required.',
259
- example: 'mm-harness launch ios',
260
- helpText: `mm-harness launch [ios|android] [flags]
261
-
262
- Launch the app — quick relaunch by default; --build for a full native/webpack build.
263
- Mobile: target is MANDATORY (ios | android). Extension: no target (--fullscreen default;
264
- --sidepanel to override). core is headless (teaching error — use verify/run).
265
-
266
- --build Full native/webpack build tier (default = quick relaunch)
267
- --verify Launch then poll CDP/bridge until ready (absorbs the old \`live\`)
268
- --runway Post-launch runway check (mobile only; teaching error elsewhere)
269
- --watch Persistent webpack watcher then relaunch (extension only)
270
- --sidepanel | --fullscreen Extension display mode (default --fullscreen)
271
- --heal <off|infra-only|auto> Healing policy (default auto); bounds always enforced
272
- --device <udid|name> Target simulator/device (env: IOS_SIMULATOR / ADB_SERIAL)
273
- --cdp-port <port> Extension CDP port (env: CDP_PORT / RECIPE_CDP_PORT)
274
- --watcher-port <port> Metro/webpack port (env: WATCHER_PORT / METRO_PORT)
275
- --adapter <mobile|extension> Target adapter (auto-detected inside a checkout)
276
- --target <path> Checkout path (default: cwd)
277
- --json Machine-readable summary (recovered[] / mutations[] / phase)
278
-
279
- Example:
280
- mm-harness launch ios
281
- mm-harness launch ios --build
282
- mm-harness launch --verify --json`,
283
- },
284
- {
285
- name: 'logs',
286
- summary: 'Tail Metro/webpack + app logs for the active checkout.',
287
- example: 'mm-harness logs',
288
- helpText: `mm-harness logs [flags]
289
-
290
- Tail Metro/webpack + app logs for the active checkout.
291
- Teaching error if nothing is running (points at launch).
292
-
293
- --full Raw log tail (default = compact) (env: RECIPE_LOG_UI)
294
- --events <n> Compact event count (default 10) (env: RECIPE_LOG_EVENTS)
295
- --source <label> Log source per adapter — mobile: metro|app (default metro);
296
- extension: webpack|watcher|rebuild|app (default webpack).
297
- Core is headless (teaching error).
298
- --adapter <mobile|extension|core> Target adapter (auto-detected inside a checkout)
299
- --target <path> Checkout path (default: cwd)
300
- --json Machine-readable output
301
-
302
- Example:
303
- mm-harness logs
304
- mm-harness logs --full`,
305
- },
306
- {
307
- name: 'debug',
308
- summary: 'Open the debug console — extension: Chrome DevTools via CDP; mobile: RN DevTools.',
309
- example: 'mm-harness debug',
310
- helpText: `mm-harness debug [flags]
311
-
312
- Open the debug console — extension: Chrome DevTools via CDP; mobile: RN DevTools.
313
-
314
- --worker Extension service-worker DevTools
315
- --dev-menu Mobile RN developer menu
316
- --adapter <mobile|extension> Target adapter (auto-detected inside a checkout)
317
- --target <path> Checkout path (default: cwd)
318
- --json Machine-readable output
319
-
320
- Example:
321
- mm-harness debug
322
- mm-harness debug --worker`,
323
- },
324
- {
325
- name: 'update',
326
- summary: 'Update the installed mm-harness to the published latest (--check reports only; --json = {current, latest, updateAvailable}).',
327
- example: 'mm-harness update',
328
- helpText: `mm-harness update [flags]
329
-
330
- Update the globally-installed mm-harness to the npm registry's latest.
331
- Bare form upgrades (npm i -g @deeeed/metamask-harness@latest) and prints old → new;
332
- already-current exits 0.
333
-
334
- --check Report only — exit 0 up-to-date / exit 1 update available (no install)
335
- --json Machine-readable { current, latest, updateAvailable }
336
-
337
- A passive once-a-day nudge also prints one stderr line when a newer version exists.
338
- Silence it with MM_HARNESS_NO_UPDATE_CHECK=1 (auto-off in CI).
339
-
340
- Example:
341
- mm-harness update
342
- mm-harness update --check --json`,
343
- },
344
- {
345
- name: 'fixtures',
346
- summary: 'Manage the canonical wallet fixture (wallet DATA only) — sync files / set the wallet.',
347
- example: 'mm-harness fixtures set',
348
- helpText: `mm-harness fixtures <sync|set> [flags]
349
-
350
- Manage the ONE canonical wallet fixture per checkout — wallet DATA only.
351
- sync Refresh the wallet fixture files on the target.
352
- set Apply the canonical fixture (SRP/password/accounts); the password is read
353
- FROM the fixture, never typed.
354
- Want different accounts? Edit the fixture file directly:
355
- <checkout>/temp/recipe/runtime/wallet-fixture.json
356
-
357
- --fixture <path> Override the fixture path (agent form; env: RECIPE_WALLET_FIXTURE)
358
- --adapter <mobile|extension> Target adapter (auto-detected inside a checkout)
359
- --target <path> Checkout path (default: cwd)
360
- --json Machine-readable output
361
-
362
- Example:
363
- mm-harness fixtures sync
364
- mm-harness fixtures set`,
365
- },
366
- ];
367
-
368
- // Retired names: NOT on the surface, but they must TEACH rather than fall to
369
- // commander's generic "unknown command" path. Each names its replacement and
370
- // exits 2 (bad-args/unsupported, distinct from a genuinely-unknown command's
371
- // exit 1). Genuinely-unknown commands stay on commander's default (which points
372
- // at --help via showHelpAfterError).
373
- interface RetiredCommand {
374
- name: string;
375
- message: string;
376
- }
377
-
378
- const RETIRED_INTERNAL = [
379
- 'runtime-health',
380
- 'runtime-decision',
381
- 'runtime-launch',
382
- 'resolve-extension',
383
- 'ensure-ready',
384
- 'self-test',
385
- ];
386
-
387
- const RETIRED: RetiredCommand[] = [
388
- {
389
- name: 'live',
390
- message:
391
- `mm-harness live is retired (exit 2).\n\n` +
392
- `Replacement: mm-harness launch --verify (install overlay → launch → CDP/bridge poll → smoke verify).`,
393
- },
394
- {
395
- name: 'manifest',
396
- message:
397
- `mm-harness manifest is retired (exit 2).\n\n` +
398
- `Replacement: mm-harness actions --raw (works now — dumps the raw action registry JSON,\n` +
399
- `identical to the old \`manifest --json\`). Manifest validation moved into doctor / run --plan.`,
400
- },
401
- ...RETIRED_INTERNAL.map((name) => ({
402
- name,
403
- message:
404
- `mm-harness ${name} is retired (exit 2).\n\n` +
405
- `It is internal now — its logic lives inside doctor / launch / verify self-healing.\n` +
406
- `Use: mm-harness doctor`,
407
- })),
408
- ];
409
-
410
- // --- Grouped help (custom formatHelp; commander owns parse/dispatch) ---------
411
-
412
- interface HelpGroup {
413
- title: string;
414
- blurb: string;
415
- commands: string[];
416
- }
417
-
418
- const HELP_GROUPS: HelpGroup[] = [
419
- {
420
- title: 'DAILY LOOP',
421
- blurb: 'what a teammate runs many times a day (auto-ensures the overlay; --heal owns recovery)',
422
- commands: ['launch', 'stop', 'logs', 'debug', 'fixtures'],
423
- },
424
- {
425
- title: 'DISCOVER',
426
- blurb: 'compose recipes from the action vocabulary + flow library (--json is the agent-primary form)',
427
- commands: ['actions', 'call', 'flows'],
428
- },
429
- {
430
- title: 'PROVE',
431
- blurb: 'run recipes and inspect readiness',
432
- commands: ['run', 'doctor'],
433
- },
434
- {
435
- title: 'RUNTIME OVERLAY',
436
- blurb: 'install/verify/clean the overlay, plus thin mobile slot provisioning',
437
- commands: ['provision', 'install', 'verify', 'cleanup'],
438
- },
439
- {
440
- title: 'MAINTAIN',
441
- blurb: 'keep the installed harness current with the npm registry',
442
- commands: ['update'],
443
- },
444
- ];
445
-
446
- function commandMeta(name: string): { summary: string; example: string; planned: boolean } {
447
- const real = REAL.find((command) => command.name === name);
448
- if (real) return { summary: real.summary, example: real.example, planned: false };
449
- return { summary: '', example: '', planned: false };
450
- }
451
-
452
- // Slot context the orchestrator's prepare wrote into the checkout, when the
453
- // help is run from inside one. Presence-gated: no context, no line.
454
- function detectedSlotLine(out: (style: string, text: string) => string): string | null {
455
- const ctxPath = path.join(
456
- process.cwd(),
457
- process.env.RECIPE_RUNTIME_DIR || 'temp/recipe/runtime',
458
- 'agentic-runtime.json',
459
- );
460
- try {
461
- const ctx = JSON.parse(fs.readFileSync(ctxPath, 'utf8')) as Record<string, unknown>;
462
- const parts: string[] = [];
463
- if (ctx.slotId) parts.push(`slot ${out('ok', String(ctx.slotId))}`);
464
- if (ctx.simulator) parts.push(`device ${out('ok', String(ctx.simulator))}`);
465
- if (ctx.metroPort) parts.push(`metro :${out('ok', String(ctx.metroPort))}`);
466
- if (ctx.gitBranch) parts.push(`branch ${out('info', String(ctx.gitBranch))}`);
467
- if (parts.length === 0) return null;
468
- return `${out('label', 'SLOT')} — this checkout is a prepared slot: ${parts.join(' · ')}`;
469
- } catch {
470
- return null;
471
- }
472
- }
473
-
474
- function groupedHelp(): string {
475
- const out = (style: string, text: string) => color(style, text, { stream: process.stdout });
476
- const lines: string[] = [];
477
- lines.push(`${out('bold', 'mm-harness')} — one front door for the MetaMask recipe loop: launch the app, prove behavior, manage the runtime overlay.`);
478
- lines.push('Run it from inside a MetaMask checkout; the platform (mobile | extension | core) is auto-detected.');
479
- lines.push(`Grammar: ${out('cmd', 'mm-harness <command> [target] [flags]')} (target is a positional: ios | android; flags add agent depth; --json is the agent contract)`);
480
- if (process.env.MM_HARNESS_BIN) {
481
- lines.push('');
482
- lines.push(`${out('warn', 'DEV OVERRIDE ACTIVE')} — this run is served by MM_HARNESS_BIN=${out('path', process.env.MM_HARNESS_BIN)} (unset it to return to the installed/global bin).`);
483
- }
484
- const slotLine = detectedSlotLine(out);
485
- if (slotLine) {
486
- lines.push('');
487
- lines.push(slotLine);
488
- }
489
- for (const group of HELP_GROUPS) {
490
- lines.push('');
491
- lines.push(`${out('label', group.title)} — ${group.blurb}:`);
492
- for (const name of group.commands) {
493
- const meta = commandMeta(name);
494
- const planned = meta.planned ? ' (planned)' : '';
495
- lines.push(` ${out('cmd', name.padEnd(10))} ${meta.summary}${planned}`);
496
- lines.push(` ${out('comment', meta.example)}`);
497
- }
498
- }
499
- lines.push('');
500
- lines.push('ONE bin: mm-harness is the only command. Platform is auto-detected; the positional target forces it');
501
- lines.push('(mm-harness launch ios); platform-specific needs are FLAGS on the same command. Human happy path = the bare');
502
- lines.push('command; agents add depth via flags (--json, --target, ports).');
503
- lines.push('');
504
- lines.push('SETUP — tab-completion (zsh + bash), sourced from the bundled scripts:');
505
- lines.push(' mm-harness completions install # print the lines to add to ~/.zshrc / ~/.bashrc');
506
- lines.push('');
507
- lines.push('DEV/PROD — the global npm install is prod; set MM_HARNESS_BIN to a dev checkout to override:');
508
- lines.push(' MM_HARNESS_BIN=/path/to/checkout/bin/mm-harness # unset = the installed/global bin');
509
- lines.push('');
510
- lines.push('See docs/MENTAL-MODEL.md (overview) and docs/CLI-SPEC.md (full contract).');
511
- return `${lines.join('\n')}\n`;
512
- }
513
-
514
- // --- Delegation -------------------------------------------------------------
515
-
516
- async function delegate(argv: string[]): Promise<number> {
517
- try {
518
- return await recipeMain(argv);
519
- } catch (error) {
520
- console.error(error instanceof Error ? error.message : String(error));
521
- // CliError (from cli.ts) carries an exitCode so usage errors (2) and
522
- // validation errors (5) propagate correctly rather than always returning 1.
523
- return error !== null &&
524
- typeof error === 'object' &&
525
- 'exitCode' in error &&
526
- typeof (error as Record<string, unknown>).exitCode === 'number'
527
- ? (error as { exitCode: number }).exitCode
528
- : 1;
529
- }
530
- }
531
-
532
- // `actions --raw` == the retired `manifest --adapter <p> --json` raw dump.
533
- function translateActionsRaw(argv: string[]): string[] {
534
- const rest = argv.slice(1).filter((arg) => arg !== '--raw');
535
- const withJson = rest.includes('--json') ? rest : [...rest, '--json'];
536
- return ['manifest', ...withJson];
537
- }
538
-
539
- // --- Program ----------------------------------------------------------------
540
-
541
- const program = new Command();
542
- program
543
- .name('mm-harness')
544
- .description('the MetaMask recipe harness: launch the app, prove behavior, manage the runtime overlay')
545
- .helpOption('-h, --help', 'Show grouped help')
546
- .showHelpAfterError('(run `mm-harness --help` for the full surface)')
547
- .configureHelp({ formatHelp: () => groupedHelp() });
548
-
549
- for (const command of REAL) {
550
- program
551
- .command(command.name)
552
- .description(command.summary)
553
- .allowUnknownOption()
554
- .helpOption(false)
555
- .argument('[args...]')
556
- .action(async () => {
557
- // Intercept --help / -h before delegation: the hand-rolled parser in cli.ts
558
- // treats unknown flags as value-consuming and throws "Missing value for --help".
559
- // Each command exposes its own flags via helpText; exit 0 so shell completion
560
- // and tooling work correctly.
561
- // Only check flags BEFORE the `--` separator so `install -- --help` correctly
562
- // forwards --help to the underlying install script rather than intercepting it.
563
- const ddIdx = rawArgv.indexOf('--');
564
- const flagsBeforeSep = ddIdx === -1 ? rawArgv : rawArgv.slice(0, ddIdx);
565
- if (flagsBeforeSep.includes('--help') || flagsBeforeSep.includes('-h')) {
566
- process.stdout.write(`${command.helpText}\n`);
567
- process.exit(0);
568
- }
569
- // `update` is self-contained registry tooling (no engine, no overlay), so it
570
- // runs directly rather than through the dispatch in cli.ts.
571
- if (command.name === 'update') {
572
- process.exit(await handleUpdate(rawArgv.slice(1)));
573
- }
574
- const argv = command.name === 'actions' && rawArgv.includes('--raw') ? translateActionsRaw(rawArgv) : rawArgv;
575
- process.exit(await delegate(argv));
576
- });
577
- }
578
-
579
- // Hidden commands: real routing, but intentionally absent from the grouped help.
580
- // `completion-candidates <actions|flows>` is what the shell completion scripts
581
- // source; humans never type it.
582
- const HIDDEN: readonly string[] = ['completion-candidates'];
583
- for (const name of HIDDEN) {
584
- program
585
- .command(name, { hidden: true })
586
- .allowUnknownOption()
587
- .helpOption(false)
588
- .argument('[args...]')
589
- .action(async () => {
590
- process.exit(await delegate(rawArgv));
591
- });
592
- }
593
-
594
- // `completions` installs/prints the bundled zsh + bash tab-completion. It is
595
- // shell tooling (no engine), so it runs the bundled installer script directly
596
- // rather than routing through the dispatch in cli.ts.
597
- program
598
- .command('completions')
599
- .description('Install/print bundled shell tab-completion (zsh + bash)')
600
- .allowUnknownOption()
601
- .helpOption(false)
602
- .argument('[args...]')
603
- .action(() => {
604
- const script = path.join(packageRoot, 'scripts', 'install-completions.sh');
605
- const result = spawnSync('bash', [script, ...rawArgv.slice(1)], { stdio: 'inherit' });
606
- process.exit(result.status ?? 1);
607
- });
608
-
609
- // Retired names teach their replacement and exit 2 (not commander's generic
610
- // "unknown command" exit 1). They are NOT listed in the grouped help.
611
- for (const retired of RETIRED) {
612
- program
613
- .command(retired.name)
614
- .description(`${retired.name} is retired (see teaching error)`)
615
- .allowUnknownOption()
616
- .helpOption(false)
617
- .argument('[args...]')
618
- .action(async () => {
619
- // `live --adapter mobile` (or android) is handled in-process via handleMobileLive.
620
- // All other `live` invocations still teach the replacement.
621
- if (retired.name === 'live') {
622
- const adapterIdx = rawArgv.indexOf('--adapter');
623
- const aliasIdx = rawArgv.indexOf('-a');
624
- const adapterVal =
625
- adapterIdx !== -1
626
- ? rawArgv[adapterIdx + 1]
627
- : aliasIdx !== -1
628
- ? rawArgv[aliasIdx + 1]
629
- : undefined;
630
- if (adapterVal === 'mobile' || adapterVal === 'android') {
631
- process.exit(await delegate(rawArgv));
632
- return;
633
- }
634
- }
635
- console.error(retired.message);
636
- process.exit(2);
637
- });
638
- }
639
-
640
- // Passive update nudge: one throttled stderr line if a newer version is published.
641
- // Skipped for update itself (redundant) and for shell/completion tooling (whose
642
- // stdout is consumed by the shell). stderr-only keeps every --json stdout clean.
643
- const NUDGE_SKIP: readonly string[] = ['update', 'completions', 'completion-candidates'];
644
- if (rawArgv.length > 0 && !NUDGE_SKIP.includes(rawArgv[0])) {
645
- // Fully deferred: no nudge code runs before parseAsync starts. The exit hook
646
- // registered inside maybeNudge prints the line after command output.
647
- setImmediate(() => void maybeNudge());
648
- }
649
-
650
- if (rawArgv.length === 0) {
651
- process.stdout.write(groupedHelp());
652
- process.exit(0);
653
- }
654
-
655
- await program.parseAsync(process.argv);