@deeeed/metamask-harness 0.16.0 → 0.17.1

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 (119) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +90 -137
  3. package/adapters/core/cleanup.sh +0 -0
  4. package/adapters/core/inject.sh +0 -0
  5. package/adapters/extension/cleanup.mjs +0 -0
  6. package/adapters/extension/console-tail.mjs +51 -24
  7. package/adapters/extension/ensure-browser.sh +0 -0
  8. package/adapters/extension/inject.mjs +5 -9
  9. package/adapters/extension/launch-browser.cjs +0 -0
  10. package/adapters/extension/launch-webpack.cjs +83 -0
  11. package/adapters/extension/launch.sh +8 -11
  12. package/adapters/extension/live.sh +2 -0
  13. package/adapters/extension/readiness.mjs +0 -0
  14. package/adapters/extension/reattach.sh +1 -0
  15. package/adapters/extension/refresh-build.sh +0 -0
  16. package/adapters/extension/seed-fixture.sh +4 -12
  17. package/adapters/extension/sidepanel-toggle.sh +4 -1
  18. package/adapters/extension/snapshot-dist.sh +0 -0
  19. package/adapters/extension/stamp-runtime-title.cjs +49 -0
  20. package/adapters/extension/start-watch.sh +16 -4
  21. package/adapters/extension/stop-viewers.sh +1 -1
  22. package/adapters/extension/sync-webpack-dist.cjs +105 -0
  23. package/adapters/extension/verify.sh +1 -1
  24. package/adapters/extension/wallet-fixture-state.cjs +0 -0
  25. package/adapters/manifest.json +46 -38
  26. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +0 -0
  27. package/adapters/mobile/bridge-runtime/console-forwarder.cjs +1 -1
  28. package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +2 -3
  29. package/adapters/mobile/bridge-runtime/setup-wallet.sh +0 -0
  30. package/adapters/mobile/cleanup.sh +16 -176
  31. package/adapters/mobile/inject.sh +88 -671
  32. package/adapters/mobile/launch-console-forwarder.cjs +39 -0
  33. package/adapters/mobile/lib/metro-listener.sh +0 -0
  34. package/adapters/mobile/lib/tmux-viewer.sh +0 -0
  35. package/adapters/mobile/open-device.sh +0 -0
  36. package/adapters/mobile/prewarm-bundle.sh +0 -0
  37. package/adapters/mobile/start-console-forwarder.sh +70 -0
  38. package/adapters/mobile/start-metro.sh +0 -41
  39. package/adapters/mobile/stop-metro.sh +0 -0
  40. package/adapters/mobile/verify.sh +4 -61
  41. package/adapters/mobile/wait-for-bridge.sh +0 -0
  42. package/adapters/mobile/yarn-setup.sh +0 -0
  43. package/adapters/shared/activate-repo-node.sh +0 -0
  44. package/adapters/shared/activate-repo-ruby.sh +0 -0
  45. package/adapters/shared/cli-ux.sh +0 -0
  46. package/adapters/shared/ensure-runner-deps.sh +0 -0
  47. package/adapters/shared/harness-path.sh +0 -0
  48. package/adapters/shared/hash-helpers.sh +0 -0
  49. package/adapters/shared/install-repo-deps.sh +0 -0
  50. package/adapters/shared/json-field.sh +0 -0
  51. package/adapters/shared/open-debug.mjs +90 -36
  52. package/adapters/shared/open-log-window.sh +0 -0
  53. package/adapters/shared/reap-checkout-metros.sh +0 -0
  54. package/adapters/shared/resolve-slot-ports-core.mjs +0 -6
  55. package/adapters/shared/resolve-slot-ports.mjs +0 -0
  56. package/adapters/shared/resolve-slot-ports.sh +0 -5
  57. package/adapters/shared/sync-wallet-fixture.sh +0 -0
  58. package/adapters/shared/tmux-session.sh +0 -0
  59. package/adapters/shared/tmux-viewer.sh +0 -0
  60. package/dist/adapters/extension/console-capture.js +84 -0
  61. package/dist/adapters/extension/runtime-decision.js +14 -2
  62. package/dist/adapters/extension/surface.js +3 -2
  63. package/dist/adapters/mobile/prepare.js +22 -7
  64. package/dist/adapters/mobile/surface.js +1 -1
  65. package/dist/adapters/resolve-slot-ports.js +0 -2
  66. package/dist/adapters/slot-ports.js +8 -0
  67. package/dist/cli.js +5 -11
  68. package/dist/command-contract.js +441 -0
  69. package/dist/command-journal.js +225 -0
  70. package/dist/commands/call.js +43 -20
  71. package/dist/commands/check.js +9 -3
  72. package/dist/commands/debug.js +38 -31
  73. package/dist/commands/device-target.js +27 -12
  74. package/dist/commands/doctor.js +19 -6
  75. package/dist/commands/fixtures.js +19 -11
  76. package/dist/commands/last.js +52 -0
  77. package/dist/commands/launch/index.js +166 -59
  78. package/dist/commands/logs.js +8 -6
  79. package/dist/commands/manifest.js +114 -29
  80. package/dist/commands/parse-args.js +1 -0
  81. package/dist/commands/provision.js +10 -3
  82. package/dist/commands/run-engine.js +27 -42
  83. package/dist/commands/run-report.js +1 -1
  84. package/dist/commands/run.js +243 -41
  85. package/dist/commands/shared.js +11 -1
  86. package/dist/commands/status-probe.js +1 -1
  87. package/dist/commands/status.js +1 -1
  88. package/dist/commands/stop.js +7 -2
  89. package/dist/doctor.js +1 -4
  90. package/dist/harness.js +19 -125
  91. package/dist/json-stream.js +57 -0
  92. package/dist/live-adapter-contract.js +6 -20
  93. package/dist/mm-harness-cli.js +111 -41
  94. package/dist/paths.js +2 -5
  95. package/dist/run-diagnostics.js +35 -40
  96. package/dist/runner.js +32 -1
  97. package/dist/runtime-context.js +26 -8
  98. package/docs/CHEATSHEET.md +39 -52
  99. package/docs/CLI-ERGONOMICS-AUDIT.md +32 -0
  100. package/docs/CLI-ERGONOMICS-HUMAN-QA.md +48 -0
  101. package/docs/CODE-MAP.md +1 -1
  102. package/docs/UX-PRINCIPLES.md +2 -0
  103. package/docs/VIDEO-DEMO-VALIDATION.md +62 -0
  104. package/docs/architecture.md +65 -375
  105. package/docs/live-adapter-contract.md +3 -1
  106. package/library/actions/core/perps/_controller.mjs +10 -55
  107. package/library/manifests/mobile.action-manifest.json +1 -1
  108. package/package.json +3 -2
  109. package/scripts/completions.sh +0 -0
  110. package/scripts/install-completions.sh +0 -0
  111. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +0 -185
  112. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +0 -1662
  113. package/adapters/mobile/overlay/compat/README.md +0 -28
  114. package/adapters/mobile/overlay/compat/rn81-message-event-source.patch +0 -42
  115. package/adapters/shared/resolve-farmslot-ports-core.mjs +0 -3
  116. package/adapters/shared/resolve-farmslot-ports.mjs +0 -5
  117. package/adapters/shared/resolve-farmslot-ports.sh +0 -7
  118. package/docs/CLI-SPEC.md +0 -1102
  119. package/docs/MENTAL-MODEL.md +0 -295
package/CHANGELOG.md CHANGED
@@ -2,6 +2,50 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ### Fixed
6
+
7
+ - Extension `debug` now uses Chrome's native DevTools command for the active slot target and opens directly on Console; `debug --worker` targets the service worker without creating a frontend browser tab.
8
+ - Extension webpack watchers now launch in their own process group, so they remain alive for incremental rebuilds after the initial `launch --build` command exits.
9
+ - Successful Extension watch rebuilds now update the isolated loaded runtime in place, so a normal refresh of the current fullscreen or sidepanel UI shows source edits without another harness build or relaunch.
10
+ - Extension slot titles are stamped into the isolated runtime after snapshots and successful rebuilds, preserving the slot identifier across an operator's normal browser refresh without changing product source.
11
+ - Extension sidepanel opening no longer waits for the harness-owned user-gesture button to pass visual actionability checks; occluded wallet loading surfaces cannot block the trusted click.
12
+
13
+ ### Changed
14
+
15
+ - Current documentation now teaches the auto-detected workflow and verified human outcomes only: launch, incremental refresh, target-specific debugging/logs, fixture setup, capability discovery, and proof evidence.
16
+ - Mobile fixture setup now requires the current multichain account-tree path, and Core signing requires the canonical checkout wallet fixture.
17
+
18
+ ### Removed
19
+
20
+ - Removed compatibility-only runtime-start fields, hidden `live` execution, removed-command handlers, undomained recipe aliases, flat/non-ESM action lookup, old port-resolver filenames, duplicate Extension launcher naming, obsolete fixture search locations, and the alternate Mobile product-source bridge. Mobile install now requires the current product-owned bridge and writes metadata only.
21
+ - Removed the historical CLI migration/spec documents; current behavior is documented in the README, cheatsheet, architecture, and human QA checklist.
22
+
23
+ ## 0.17.1 - 2026-07-14
24
+
25
+ ### Fixed
26
+
27
+ - Human recipe results now print an absolute-path artifact inventory and expand run-scoped application diagnostics inline; single-action calls show the same `CLEAN`/`REVIEW` diagnostics instead of requiring another query.
28
+ - Extension console collection now starts after every successful public launch path, including quick reattach, and persists as one checkout-scoped process until `stop`. MetaMask UI/service-worker events and dapp-page events are routed to separate `extension-console.log` and `dapp-console.log` files; bare `logs` defaults to MetaMask while `--source dapp` and `--source webpack` remain explicit.
29
+ - Mobile CDP-forwarded app events now write to `app-console.log`, so `logs --source app` and run diagnostics are no longer mixed with Metro bundler output; warm and cold launches start a process-group-detached collector only after bridge readiness so capture remains live after the launch command exits.
30
+ - Extension `debug` now opens Chrome's local DevTools frontend inside the exact slot-owned profile, targets the MetaMask extension page rather than the dapp, and verifies the debugging connection before reporting success.
31
+ - Mobile fixture application now fails immediately when the checkout fixture is absent, rejects the misplaced `fixtures set --dev` flag with the correct initialization command, and never prints a success message after failure.
32
+
33
+ ## 0.17.0 - 2026-07-14
34
+
35
+ ### Added
36
+
37
+ - Command-specific JSON failures now preserve a structured `userAction` across device targeting, action calls, recipe validation/execution, checks, and dev-server stop. Successful install/verify/cleanup, provision, and stop transitions add `next` only when one truthful follow-up exists; discovery and terminal evidence results remain quiet.
38
+ - `run` and `launch` now implement the documented `--json-stream` contract: line-flushed JSONL phase/progress events, per-node recipe status, compact evidence paths, and one terminal `complete` event. Stream mode keeps stdout machine-clean and takes precedence when combined with `--json`; existing `--json` output is unchanged.
39
+ - Interactive Mobile launch, Runway provisioning, and recipe execution now print contextual intent before their first potentially slow readiness or validation probe. The new lines are suppressed for `--json` and non-TTY stdout.
40
+ - `last --json` reads an atomic, mode-`0600`, per-checkout resumability journal for significant runtime/proof commands, including redacted args, verdict, evidence paths, and timestamps. Interrupted commands remain `running`; discovery commands do not overwrite the record.
41
+ - `actions <query>` now searches action names, categories, fields, and descriptions with typo tolerance, while single-action detail includes compact related-action names.
42
+
43
+ ### Changed
44
+
45
+ - Action categories now group protocol primitives into useful `assertion`, `control`, `evidence`, and `runtime` domains instead of exposing one category per unnamespaced action.
46
+ - Public commands now reject unknown commands/options, missing values and positionals, invalid enums, and excess positionals before dispatch. Human errors include valid command-local options, a closest match when available, and a runnable example; `--json` returns stable `CLI_*` codes with `error.message` and `error.userAction`.
47
+ - `docs/CLI-ERGONOMICS-AUDIT.md` is generated from the public command contract so every command remains covered by the shared bad-flag and JSON usage-error policy as the surface evolves.
48
+
5
49
  ## 0.16.0 - 2026-07-14
6
50
 
7
51
  ### Added
package/README.md CHANGED
@@ -1,178 +1,131 @@
1
- # MetaMask Recipe Runner — `mm-harness`
1
+ # mm-harness
2
2
 
3
- Architecture: [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md)
3
+ `mm-harness` gives people and agents one CLI for operating MetaMask Mobile,
4
+ Extension, and Core. Run it inside a checkout; the product is auto-detected.
4
5
 
5
- **`mm-harness` is built for the agent but easy for the human to use and
6
- understand.** It is the MetaMask recipe harness: one front door to launch the app,
7
- prove behavior with recipes, and manage the per-checkout runtime overlay. It is a
8
- thin MetaMask adapter over the generic `@farmslot/recipe-harness` engine — the
9
- runner never re-implements graph execution, it binds to it. The agent gets full
10
- depth (explicit flags, `--json`, ports, build tiers); the human gets short
11
- commands, positional targets, good defaults, completions, and teaching
12
- help/errors. Run it from inside a MetaMask Mobile/Extension/Core checkout and the
13
- platform is auto-detected.
6
+ It has two jobs:
14
7
 
15
- ```
16
- DAILY LOOP launch · logs · debug · fixtures what a teammate runs many times a day
17
- DISCOVER actions · call · flows compose recipes from the vocabulary + flow library
18
- PROVE run · doctor run recipes, check readiness
19
- RUNTIME OVERLAY install · verify · cleanup manage the per-checkout runtime overlay
20
- ```
8
+ - operate the app with the same controls a developer needs: launch, logs, debug,
9
+ fixtures, actions, and reusable flows;
10
+ - execute deterministic recipes and save reviewable proof.
21
11
 
22
- **One bin, no per-platform binaries.** `mm-harness` is the only command. Grammar:
23
- `mm-harness <command> [target] [flags]` — the human types the bare command
24
- (platform auto-detected) or a positional target (`mm-harness launch ios`); depth
25
- lives in FLAGS with good defaults, never in extra command names. It ships shell
26
- completions (zsh + bash) for commands, targets, and flags.
12
+ The generic graph engine lives in `@farmslot/recipe-harness`. This package owns
13
+ only MetaMask-specific actions and runtime control.
27
14
 
28
- Naming: **"harness" is the tool** (`mm-harness`) and the generic engine package
29
- (`@farmslot/recipe-harness`); the files it installs into a checkout are the
30
- **runtime overlay**. Read [MENTAL-MODEL.md](docs/MENTAL-MODEL.md) first (the
31
- before→after map), then [CLI-SPEC.md](docs/CLI-SPEC.md) for the full contract.
15
+ ## Install
32
16
 
33
- One package, two responsibilities:
34
-
35
- - **Recipe layer:** manifests, recipes, Mobile/Extension adapters, proof output.
36
- - **Runtime layer:** start/reuse Metro or Chrome, seed fixtures, wait for readiness.
17
+ ```bash
18
+ npm install -g @deeeed/metamask-harness@latest
19
+ mm-harness --version
20
+ mm-harness doctor
21
+ ```
37
22
 
38
- Farmslot and skills are wrappers around this CLI; they should not copy runner
39
- logic.
23
+ Use `mm-harness update` to install the latest published version.
40
24
 
41
- ## Quick start
25
+ ## Daily workflow
42
26
 
43
- Run from inside a MetaMask checkout (platform auto-detected):
27
+ From a MetaMask checkout:
44
28
 
45
29
  ```bash
46
- # 1) Launch the app (installs the runtime overlay on first use, boots Metro/build)
47
- mm-harness launch ios # mobile: ios | android; extension: just `launch`
48
-
49
- # 2) Run a recipe and write evidence
50
- mm-harness run recipe.json --adapter mobile --artifacts-dir /tmp/recipe-artifacts --json
30
+ # Extension
31
+ mm-harness launch # starts/reuses Chrome and the incremental watcher
32
+ mm-harness launch --sidepanel # open the sidepanel surface
33
+
34
+ # Mobile
35
+ mm-harness launch ios
36
+ mm-harness launch android
37
+
38
+ # Any product
39
+ mm-harness status
40
+ mm-harness logs
41
+ mm-harness debug
42
+ mm-harness fixtures set
51
43
  ```
52
44
 
53
- Mobile and Extension runs also write `diagnostics.json` from application events
54
- emitted during that run. Mobile combines its bounded in-app issue buffer with
55
- run-scoped log bytes; Extension owns a CDP console subscription for the run.
56
- These redacted warnings/errors are non-blocking side findings: they help spot
57
- adjacent bugs without claiming the recipe or current change caused them. Core
58
- is headless, so this is N/A there.
45
+ `launch` installs the checkout-local runtime overlay when needed. On Extension,
46
+ the normal launch keeps `yarn start` running after the command exits. Source
47
+ edits compile incrementally into the isolated runtime; refresh the active page or
48
+ sidepanel to see them. Use `launch --build` only when a clean rebuild is needed.
59
49
 
60
- Outputs: `summary.json`, `trace.json`, screenshots, logs, and an artifact
61
- manifest.
50
+ `debug` opens DevTools for the active Extension target in that checkout's Chrome
51
+ profile. Mobile opens the React Native developer surface.
62
52
 
63
- ## Mental model
53
+ Filter logs when needed:
64
54
 
65
- ```text
66
- mm-harness launch # runtime/orchestration: the app is ready
67
- mm-harness run # runner/proof: actions execute and evidence is saved
55
+ ```bash
56
+ mm-harness logs --source extension # Extension console
57
+ mm-harness logs --source dapp # active dapp console
58
+ mm-harness logs --source webpack # Extension compiler
59
+ mm-harness logs --source app # Mobile app events
60
+ mm-harness logs --source metro # Mobile bundler
68
61
  ```
69
62
 
70
- Do not mix those layers. library/ = what recipes ARE (recipes, actions,
71
- manifests — the content). src/ = what EXECUTES them (TypeScript CLI/engine glue,
72
- typed adapters). adapters/ = what CONTROLS the app and its instances (start,
73
- windows, wallet state, health, ports, parallel).
74
-
75
- ## Useful commands
63
+ ## Build and run proof
76
64
 
77
65
  ```bash
78
- # Capabilities (compose recipes from the vocabulary + flow library)
79
- mm-harness actions --adapter mobile --categories --json # compact discovery
80
- mm-harness actions --adapter mobile --category ui --json # bounded vocabulary
81
- mm-harness actions --adapter mobile --raw # raw action-manifest dump
82
- mm-harness call unlock --adapter extension # run one action via the real engine path
83
- mm-harness flows --json # reusable library flows, with provenance
84
-
85
- # Readiness + proof
86
- mm-harness doctor # read-only readiness check (no launch)
87
- mm-harness doctor --fix # heal the overlay without launching
88
- mm-harness run recipe.json --plan --adapter mobile # validate + plan, touch nothing
89
-
90
- # Daily loop (run from inside a checkout)
91
- mm-harness launch ios # launch (mobile: ios | android)
92
- mm-harness logs # tail Metro/webpack + app logs
93
- mm-harness debug # open the debug console
94
- mm-harness fixtures sync # sync fixtures; `fixtures set` seeds the wallet
95
-
96
- # Keep the install current
97
- mm-harness update # upgrade to the published latest (prints old → new)
98
- mm-harness update --check --json # report only — { current, latest, updateAvailable }
99
- ```
66
+ mm-harness actions positions
67
+ mm-harness actions --action metamask.wallet.ensure_unlocked
68
+ mm-harness call metamask.wallet.ensure_unlocked
69
+ mm-harness flows
100
70
 
101
- A passive once-a-day nudge prints one `stderr` line when a newer version is
102
- published (`mm-harness <cur> <latest> available · run: mm-harness update`). It
103
- never blocks and never auto-updates. Silence it with `MM_HARNESS_NO_UPDATE_CHECK=1`
104
- (auto-off in CI).
105
-
106
- Set `RECIPE_LOG_UI=compact|full|quiet` (default `compact`) and `RECIPE_LOG_EVENTS=10`
107
- to tune the compact log view during launch and Metro/webpack startup.
108
-
109
- ## Layout
110
-
111
- ```text
112
- bin/ the mm-harness entrypoint
113
- src/ what EXECUTES recipes: TypeScript CLI/engine glue +
114
- typed per-adapter logic (src/adapters/)
115
- adapters/ what CONTROLS the app and its instances: launch/live/
116
- watch/windows/wallet state/health/inject/cleanup per
117
- platform, shared glue, porcelain, overlay payload +
118
- manifest.json
119
- library/ what recipes ARE: recipes/ (recipe JSONs), actions/
120
- (per-platform implementations), manifests/ (capability)
121
- scripts/ dev tooling (yarn check, local farmslot link, e2e
122
- validation, adapter-surface-doctor)
123
- docs/ details when this README is not enough
71
+ mm-harness run path/to/recipe.json --plan
72
+ mm-harness run path/to/recipe.json --artifacts-dir temp/recipe-proof
124
73
  ```
125
74
 
126
- Defaults for installed harness/runtime paths live in
127
- `adapters/shared/path-defaults.json`.
75
+ `actions` is the executable vocabulary. `call` exercises one action. `flows`
76
+ lists reusable parameterized behavior from personal, team, and bundled recipe
77
+ libraries. A recipe connects those capabilities into a deterministic proof.
78
+
79
+ A run prints its findings, diagnostics, and absolute evidence paths. The artifact
80
+ directory contains the executed recipe, summary, trace, diagnostics when
81
+ applicable, screenshots/logs, and `artifact-manifest.json`. Core is headless, so
82
+ browser/device diagnostics do not apply.
128
83
 
129
- ## Runtime-readiness ownership
84
+ Use `mm-harness last --json` to resume after interruption without repeating a
85
+ side-effecting command.
130
86
 
131
- Generic readiness mechanics live in `@farmslot/recipe-harness` (0.3+):
87
+ ## Team recipe libraries
132
88
 
133
- - `@farmslot/recipe-harness/runtime/deps-readiness` install fingerprint + baseline
134
- - `@farmslot/recipe-harness/runtime/log-analysis` — bundle-log boundaries, unresolved-module scoping
135
- - `@farmslot/recipe-harness/runtime/metro-probe` — Metro `/status` reachability
89
+ Add a named shared directory with `RECIPE_LIBRARY_PATH`:
136
90
 
137
- This repo adds **MetaMask adapters only**: `adapters/mobile/deps-markers.ts`,
138
- `adapters/mobile/runtime-decision.ts`, `adapters/extension/runtime-decision.ts`,
139
- and shell launch in `adapters/porcelain/mm-recipe` / `adapters/porcelain/mme-recipe`. Do not re-copy harness
140
- primitives into `adapters/shared/`. CI installs harness from npm — publish
141
- `@farmslot/recipe-harness` before bumping the dependency here.
91
+ ```bash
92
+ export RECIPE_LIBRARY_PATH="team=$HOME/shared-library/team-recipes"
93
+ mm-harness flows
94
+ ```
142
95
 
143
- ## Dev/prod harness (`MM_HARNESS_BIN`)
96
+ Team flows can wrap repeated product behavior while each checklist or task keeps
97
+ its own acceptance criteria. See [recipe libraries](docs/recipe-libraries.md).
144
98
 
145
- The global npm install owns the `mm-harness` on `PATH` — that is the **prod**
146
- harness. To run a **dev** checkout with active pre-release changes alongside it,
147
- set `MM_HARNESS_BIN` to that checkout's `bin/mm-harness`:
99
+ ## Health and recovery
148
100
 
149
101
  ```bash
150
- export MM_HARNESS_BIN=/path/to/checkout/bin/mm-harness # dev: this checkout owns the run
151
- unset MM_HARNESS_BIN # prod: the installed/global bin
102
+ mm-harness doctor # inspect; does not launch
103
+ mm-harness doctor --fix # repair local harness setup; does not launch
104
+ mm-harness verify # verify the installed runtime overlay
105
+ mm-harness cleanup # remove the runtime overlay
152
106
  ```
153
107
 
154
- `MM_HARNESS_BIN` is the single override point: when set, the entrypoint hands the
155
- whole invocation to that path before doing any dependency work; when unset it
156
- resolves the installed bin. A dev checkout is never installed globally, so there is
157
- no `PATH` collision — dev is reached only through `MM_HARNESS_BIN` (or a shell
158
- alias). A per-farm environment can export it so the dev farm dispatches through the
159
- dev harness while the prod farm leaves it unset.
108
+ Human output gives a next command. `--json` gives stable machine-readable errors
109
+ and keeps stdout free of decoration. `--json-stream` emits line-delimited progress
110
+ for long recipe runs.
160
111
 
161
- ## Runtime notes
112
+ ## Development
113
+
114
+ Use a source checkout without replacing the global install:
115
+
116
+ ```bash
117
+ export MM_HARNESS_BIN=/path/to/metamask-harness/bin/mm-harness
118
+ mm-harness --version
119
+ ```
162
120
 
163
- - `tmux` is recommended for long-lived Metro/webpack processes; standalone use
164
- falls back to detached `nohup` where possible.
165
- - Mobile may inject a local development bridge/HUD into older checkouts. Do not
166
- commit those product patches.
167
- - Extension does not patch product source; it drives `dist/chrome` through Chrome
168
- CDP.
121
+ Unset `MM_HARNESS_BIN` to return to the published install.
169
122
 
170
- ## Validate changes
123
+ Validate changes with:
171
124
 
172
125
  ```bash
173
126
  yarn check
174
- bash -n bin/mm-harness adapters/porcelain/{metamask-recipe,mm-recipe,mme-recipe} adapters/{mobile,extension,core}/*.sh
175
- node --check adapters/extension/inject.mjs adapters/extension/cleanup.mjs adapters/extension/readiness.mjs adapters/extension/launch-browser.cjs adapters/shared/open-debug.mjs adapters/shared/cli-home.mjs adapters/shared/log-tui.mjs adapters/shared/progress.mjs adapters/shared/recipe-paths.mjs scripts/check.mjs
176
127
  ```
177
128
 
178
- More detail: [Architecture](docs/architecture.md), [Package boundaries](docs/package-boundaries.md), [Runtime file conventions](docs/runtime-file-conventions.md).
129
+ Current references: [cheatsheet](docs/CHEATSHEET.md), [architecture](docs/architecture.md),
130
+ [recipe libraries](docs/recipe-libraries.md), [human QA](docs/CLI-ERGONOMICS-HUMAN-QA.md),
131
+ and the checked [video demo validation](docs/VIDEO-DEMO-VALIDATION.md).
File without changes
File without changes
File without changes
@@ -8,7 +8,7 @@
8
8
  // the tab shows live extension logs (the equivalent of the Metro/webpack tabs).
9
9
  //
10
10
  // Usage:
11
- // console-tail.mjs --cdp-port <port> [--log <file>]
11
+ // console-tail.mjs --cdp-port <port> [--extension-log <file>] [--dapp-log <file>]
12
12
  //
13
13
  // Resilience: MV3 service workers cycle on idle and UI pages open/close, so we
14
14
  // re-poll /json/list every ~2s and (re)attach to any extension target that is
@@ -19,9 +19,10 @@
19
19
  'use strict';
20
20
 
21
21
  import { appendFileSync } from 'node:fs';
22
+ import path from 'node:path';
22
23
 
23
24
  function parseArgs(argv) {
24
- const args = { cdpPort: null, log: null };
25
+ const args = { cdpPort: null, extensionLog: null, dappLog: null };
25
26
  for (let i = 0; i < argv.length; i += 1) {
26
27
  const arg = argv[i];
27
28
  switch (arg) {
@@ -29,12 +30,16 @@ function parseArgs(argv) {
29
30
  args.cdpPort = argv[++i];
30
31
  break;
31
32
  case '--log':
32
- args.log = argv[++i];
33
+ case '--extension-log':
34
+ args.extensionLog = argv[++i];
35
+ break;
36
+ case '--dapp-log':
37
+ args.dappLog = argv[++i];
33
38
  break;
34
39
  case '-h':
35
40
  case '--help':
36
41
  process.stdout.write(
37
- 'Usage: console-tail.mjs --cdp-port <port> [--log <file>]\n',
42
+ 'Usage: console-tail.mjs --cdp-port <port> [--extension-log <file>] [--dapp-log <file>]\n',
38
43
  );
39
44
  process.exit(0);
40
45
  break;
@@ -51,6 +56,9 @@ if (!Number.isInteger(cdpPort) || cdpPort <= 0) {
51
56
  throw new Error('console-tail requires a numeric --cdp-port.');
52
57
  }
53
58
  const HOST = `127.0.0.1:${cdpPort}`;
59
+ if (args.extensionLog && !args.dappLog) {
60
+ args.dappLog = path.join(path.dirname(path.resolve(args.extensionLog)), 'dapp-console.log');
61
+ }
54
62
 
55
63
  if (typeof WebSocket === 'undefined') {
56
64
  throw new Error('console-tail requires Node 22+ with a global WebSocket.');
@@ -65,9 +73,9 @@ const cooldown = new Map();
65
73
  // Writes formatted console-event strings (never raw HTTP response bodies) to
66
74
  // stdout and optionally to a caller-supplied log file. The source is the local
67
75
  // Chrome CDP WebSocket (127.0.0.1:<cdpPort>) — a trusted-local endpoint.
68
- function out(line) {
76
+ function out(line, destination = null) {
69
77
  process.stdout.write(`${line}\n`);
70
- if (args.log) appendFileSync(args.log, `${line}\n`);
78
+ if (destination) appendFileSync(destination, `${line}\n`);
71
79
  }
72
80
 
73
81
  function stamp() {
@@ -95,6 +103,22 @@ function isExtensionTarget(target) {
95
103
  return String(target.url || '').startsWith('chrome-extension://');
96
104
  }
97
105
 
106
+ function isDappTarget(target) {
107
+ if (!target || !target.webSocketDebuggerUrl || target.type !== 'page') return false;
108
+ try {
109
+ const url = new URL(String(target?.url || ''));
110
+ return url.protocol === 'http:' || url.protocol === 'https:';
111
+ } catch {
112
+ return false;
113
+ }
114
+ }
115
+
116
+ function targetStream(target) {
117
+ if (isExtensionTarget(target)) return { kind: 'extension', log: args.extensionLog };
118
+ if (isDappTarget(target)) return { kind: 'dapp', log: args.dappLog };
119
+ return null;
120
+ }
121
+
98
122
  function previewArg(arg) {
99
123
  if (!arg || typeof arg !== 'object') return String(arg);
100
124
  if ('value' in arg && arg.value !== undefined) {
@@ -108,29 +132,29 @@ function previewArg(arg) {
108
132
  return arg.subtype || arg.type || '?';
109
133
  }
110
134
 
111
- function emitConsole(label, params) {
135
+ function emitConsole(label, params, destination) {
112
136
  const level = String(params.type || 'log').toUpperCase();
113
137
  const text = (params.args || []).map(previewArg).join(' ');
114
- out(`${stamp()} [${label}] ${level} ${text}`);
138
+ out(`${stamp()} [${label}] ${level} ${text}`, destination);
115
139
  }
116
140
 
117
- function emitException(label, params) {
141
+ function emitException(label, params, destination) {
118
142
  const d = params.exceptionDetails || {};
119
143
  const text =
120
144
  d.exception?.description ||
121
145
  d.exception?.value ||
122
146
  d.text ||
123
147
  'uncaught exception';
124
- out(`${stamp()} [${label}] EXCEPTION ${text}`);
148
+ out(`${stamp()} [${label}] EXCEPTION ${text}`, destination);
125
149
  }
126
150
 
127
- function emitLogEntry(label, params) {
151
+ function emitLogEntry(label, params, destination) {
128
152
  const e = params.entry || {};
129
153
  const level = String(e.level || 'log').toUpperCase();
130
- out(`${stamp()} [${label}] ${level} ${e.text || ''}`);
154
+ out(`${stamp()} [${label}] ${level} ${e.text || ''}`, destination);
131
155
  }
132
156
 
133
- function handleMessage(label, data) {
157
+ function handleMessage(label, destination, data) {
134
158
  let message;
135
159
  try {
136
160
  message = JSON.parse(data);
@@ -141,13 +165,13 @@ function handleMessage(label, data) {
141
165
  if (message.id != null) return; // command ack, not an event
142
166
  switch (message.method) {
143
167
  case 'Runtime.consoleAPICalled':
144
- emitConsole(label, message.params || {});
168
+ emitConsole(label, message.params || {}, destination);
145
169
  break;
146
170
  case 'Runtime.exceptionThrown':
147
- emitException(label, message.params || {});
171
+ emitException(label, message.params || {}, destination);
148
172
  break;
149
173
  case 'Log.entryAdded':
150
- emitLogEntry(label, message.params || {});
174
+ emitLogEntry(label, message.params || {}, destination);
151
175
  break;
152
176
  default:
153
177
  break;
@@ -155,9 +179,11 @@ function handleMessage(label, data) {
155
179
  }
156
180
 
157
181
  function attach(target) {
158
- const label = shortLabel(target);
182
+ const stream = targetStream(target);
183
+ if (!stream) return;
184
+ const label = `${stream.kind}:${shortLabel(target)}`;
159
185
  const ws = new WebSocket(target.webSocketDebuggerUrl);
160
- const entry = { ws, label, opened: false };
186
+ const entry = { ws, label, destination: stream.log, opened: false };
161
187
  attached.set(target.id, entry);
162
188
 
163
189
  ws.addEventListener('open', () => {
@@ -166,16 +192,16 @@ function attach(target) {
166
192
  for (const method of ['Runtime.enable', 'Log.enable']) {
167
193
  ws.send(JSON.stringify({ id: ++id, method, params: {} }));
168
194
  }
169
- out(`${stamp()} [${label}] [attached]`);
195
+ out(`${stamp()} [${label}] [attached]`, stream.log);
170
196
  });
171
197
  ws.addEventListener('message', (event) => {
172
- handleMessage(label, typeof event.data === 'string' ? event.data : '');
198
+ handleMessage(label, stream.log, typeof event.data === 'string' ? event.data : '');
173
199
  });
174
200
  const drop = () => {
175
201
  if (attached.get(target.id) !== entry) return;
176
202
  attached.delete(target.id);
177
203
  if (entry.opened) {
178
- out(`${stamp()} [${label}] [reconnect] target detached; will re-attach`);
204
+ out(`${stamp()} [${label}] [reconnect] target detached; will re-attach`, stream.log);
179
205
  } else {
180
206
  // Never connected (target gone / not debuggable yet): back off so a dead
181
207
  // target is not retried — and logged — on every 2s poll.
@@ -196,8 +222,9 @@ async function poll() {
196
222
  // CDP not reachable this tick (browser starting/restarting); retry next poll.
197
223
  return;
198
224
  }
199
- for (const target of Array.isArray(targets) ? targets : []) {
200
- if (!isExtensionTarget(target)) continue;
225
+ const targetList = Array.isArray(targets) ? targets : [];
226
+ for (const target of targetList) {
227
+ if (!targetStream(target)) continue;
201
228
  if (attached.has(target.id)) continue;
202
229
  const until = cooldown.get(target.id);
203
230
  if (until !== undefined) {
@@ -226,6 +253,6 @@ function shutdown() {
226
253
  process.on('SIGINT', shutdown);
227
254
  process.on('SIGTERM', shutdown);
228
255
 
229
- out(`${stamp()} [console-tail] streaming extension console via cdp ${cdpPort}`);
256
+ out(`${stamp()} [console-tail] streaming extension and dapp consoles via cdp ${cdpPort}`);
230
257
  await poll();
231
258
  setInterval(poll, 2000);
File without changes
@@ -94,16 +94,16 @@ copyDir(path.join(runnerDir, 'library/recipes'), path.join(harnessDir, 'runner/r
94
94
  copyFile(path.join(runnerDir, 'library/library.json'), path.join(harnessDir, 'runner/library.json'));
95
95
  fs.mkdirSync(path.join(harnessDir, 'runner/flows'), { recursive: true });
96
96
  copyDir(path.join(runnerDir, 'library/flows'), path.join(harnessDir, 'runner/flows'));
97
- // Installed-layout back-compat (INTENTIONAL): targets keep a flat
98
- // <harness>/scripts/ subtree assembled from the runner trees, so installed
99
- // consumers are unaffected by runner-repo layout changes.
97
+ // The installed overlay has a flat scripts/ runtime API independent of the
98
+ // package's source layout.
100
99
  copyFile(path.join(runnerDir, 'adapters/extension/launch-browser.cjs'), path.join(harnessDir, 'scripts/launch-browser.cjs'));
101
- // installed back-compat alias: callers of the pre-rename installed name
102
- copyFile(path.join(runnerDir, 'adapters/extension/launch-browser.cjs'), path.join(harnessDir, 'scripts/launch-chrome-detached.cjs'));
103
100
  copyFile(path.join(runnerDir, 'adapters/extension/console-tail.mjs'), path.join(harnessDir, 'scripts/console-tail.mjs'));
104
101
  copyFile(path.join(runnerDir, 'adapters/extension/launch.sh'), path.join(harnessDir, 'scripts/launch.sh'));
105
102
  copyFile(path.join(runnerDir, 'adapters/extension/live.sh'), path.join(harnessDir, 'scripts/live.sh'));
106
103
  copyFile(path.join(runnerDir, 'adapters/extension/start-watch.sh'), path.join(harnessDir, 'scripts/start-watch.sh'));
104
+ copyFile(path.join(runnerDir, 'adapters/extension/launch-webpack.cjs'), path.join(harnessDir, 'scripts/launch-webpack.cjs'));
105
+ copyFile(path.join(runnerDir, 'adapters/extension/sync-webpack-dist.cjs'), path.join(harnessDir, 'scripts/sync-webpack-dist.cjs'));
106
+ copyFile(path.join(runnerDir, 'adapters/extension/stamp-runtime-title.cjs'), path.join(harnessDir, 'scripts/stamp-runtime-title.cjs'));
107
107
  copyFile(path.join(runnerDir, 'adapters/extension/stop-viewers.sh'), path.join(harnessDir, 'scripts/stop-viewers.sh'));
108
108
  copyFile(path.join(runnerDir, 'adapters/extension/snapshot-dist.sh'), path.join(harnessDir, 'scripts/snapshot-dist.sh'));
109
109
  copyFile(path.join(runnerDir, 'adapters/extension/pin-remote-flags.cjs'), path.join(harnessDir, 'scripts/pin-remote-flags.cjs'));
@@ -120,10 +120,6 @@ copyFile(path.join(runnerDir, 'adapters/shared/activate-repo-node.sh'), path.joi
120
120
  copyFile(path.join(runnerDir, 'adapters/shared/resolve-slot-ports.sh'), path.join(harnessDir, 'scripts/lib/resolve-slot-ports.sh'));
121
121
  copyFile(path.join(runnerDir, 'adapters/shared/resolve-slot-ports.mjs'), path.join(harnessDir, 'scripts/lib/resolve-slot-ports.mjs'));
122
122
  copyFile(path.join(runnerDir, 'adapters/shared/resolve-slot-ports-core.mjs'), path.join(harnessDir, 'scripts/lib/resolve-slot-ports-core.mjs'));
123
- // Back-compat shims (one release) so an overlay consumer using the old leaf names still resolves.
124
- copyFile(path.join(runnerDir, 'adapters/shared/resolve-farmslot-ports.sh'), path.join(harnessDir, 'scripts/lib/resolve-farmslot-ports.sh'));
125
- copyFile(path.join(runnerDir, 'adapters/shared/resolve-farmslot-ports.mjs'), path.join(harnessDir, 'scripts/lib/resolve-farmslot-ports.mjs'));
126
- copyFile(path.join(runnerDir, 'adapters/shared/resolve-farmslot-ports-core.mjs'), path.join(harnessDir, 'scripts/lib/resolve-farmslot-ports-core.mjs'));
127
123
  copyFile(path.join(runnerDir, 'adapters/shared/path-defaults.json'), path.join(harnessDir, 'scripts/lib/path-defaults.json'));
128
124
  copyFile(path.join(runnerDir, 'adapters/shared/json-field.sh'), path.join(harnessDir, 'scripts/lib/json-field.sh'));
129
125
  copyFile(path.join(runnerDir, 'adapters/shared/tmux-session.sh'), path.join(harnessDir, 'scripts/lib/tmux-session.sh'));
File without changes
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { spawn } = require('node:child_process');
5
+ const fs = require('node:fs');
6
+ const path = require('node:path');
7
+
8
+ const args = {};
9
+ for (let index = 2; index < process.argv.length; index += 1) {
10
+ const key = process.argv[index];
11
+ if (key === '--help' || key === '-h') {
12
+ process.stdout.write('Usage: launch-webpack.cjs --target <path> --log <path> --pid-file <path> --sync-script <path> --sync-pid-file <path> --sync-log <path> --dist <path> --runtime-dist <path> [--port <port>] [--path-prefix <path>]\n');
13
+ process.exit(0);
14
+ }
15
+ if (!key.startsWith('--') || index + 1 >= process.argv.length) throw new Error(`invalid argument: ${key}`);
16
+ args[key.slice(2)] = process.argv[++index];
17
+ }
18
+
19
+ async function main() {
20
+ for (const required of ['target', 'log', 'pid-file', 'sync-script', 'sync-pid-file', 'sync-log', 'dist', 'runtime-dist']) {
21
+ if (!args[required]) throw new Error(`--${required} is required`);
22
+ }
23
+
24
+ const target = path.resolve(args.target);
25
+ const log = path.resolve(args.log);
26
+ const pidFile = path.resolve(args['pid-file']);
27
+ const syncScript = path.resolve(args['sync-script']);
28
+ const syncPidFile = path.resolve(args['sync-pid-file']);
29
+ const syncLog = path.resolve(args['sync-log']);
30
+ fs.mkdirSync(path.dirname(log), { recursive: true });
31
+ fs.mkdirSync(path.dirname(pidFile), { recursive: true });
32
+ fs.mkdirSync(path.dirname(syncLog), { recursive: true });
33
+ if (!fs.existsSync(syncScript)) throw new Error(`sync script not found: ${syncScript}`);
34
+ const logFd = fs.openSync(log, 'a');
35
+ const syncLogFd = fs.openSync(syncLog, 'a');
36
+ const env = { ...process.env };
37
+ if (args.port) env.PORT = String(args.port);
38
+ if (args['path-prefix']) env.PATH = `${args['path-prefix']}${env.PATH || ''}`;
39
+
40
+ try {
41
+ const child = spawn('yarn', ['start'], {
42
+ cwd: target,
43
+ detached: true,
44
+ env,
45
+ stdio: ['ignore', logFd, logFd],
46
+ });
47
+ await new Promise((resolve, reject) => {
48
+ child.once('spawn', resolve);
49
+ child.once('error', reject);
50
+ });
51
+ fs.writeFileSync(pidFile, `${child.pid}\n`);
52
+ const syncChild = spawn(process.execPath, [
53
+ syncScript,
54
+ '--watcher-pid', String(child.pid),
55
+ '--watch-log', log,
56
+ '--target', target,
57
+ '--dist', path.resolve(args.dist),
58
+ '--runtime-dist', path.resolve(args['runtime-dist']),
59
+ '--pid-file', syncPidFile,
60
+ ], {
61
+ cwd: target,
62
+ detached: true,
63
+ env,
64
+ stdio: ['ignore', syncLogFd, syncLogFd],
65
+ });
66
+ await new Promise((resolve, reject) => {
67
+ syncChild.once('spawn', resolve);
68
+ syncChild.once('error', reject);
69
+ });
70
+ fs.writeFileSync(syncPidFile, `${syncChild.pid}\n`);
71
+ process.stdout.write(`${child.pid}\n`);
72
+ syncChild.unref();
73
+ child.unref();
74
+ } finally {
75
+ fs.closeSync(logFd);
76
+ fs.closeSync(syncLogFd);
77
+ }
78
+ }
79
+
80
+ main().catch((error) => {
81
+ process.stderr.write(`launch-webpack: ${error.message}\n`);
82
+ process.exit(1);
83
+ });