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