@deeeed/metamask-harness 0.17.0 → 0.17.2

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 (106) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +90 -140
  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/adapters/shared/update-check-worker.mjs +37 -0
  61. package/dist/adapters/extension/console-capture.js +84 -0
  62. package/dist/adapters/extension/runtime-decision.js +14 -2
  63. package/dist/adapters/extension/surface.js +3 -2
  64. package/dist/adapters/mobile/prepare.js +22 -7
  65. package/dist/adapters/mobile/surface.js +1 -1
  66. package/dist/adapters/resolve-slot-ports.js +0 -2
  67. package/dist/adapters/slot-ports.js +8 -0
  68. package/dist/cli.js +3 -11
  69. package/dist/commands/call.js +9 -4
  70. package/dist/commands/debug.js +38 -31
  71. package/dist/commands/fixtures.js +18 -10
  72. package/dist/commands/launch/index.js +10 -0
  73. package/dist/commands/logs.js +8 -6
  74. package/dist/commands/manifest.js +5 -17
  75. package/dist/commands/run-engine.js +1 -32
  76. package/dist/commands/run-report.js +1 -1
  77. package/dist/commands/run.js +50 -3
  78. package/dist/commands/status-probe.js +1 -1
  79. package/dist/commands/update.js +15 -28
  80. package/dist/doctor.js +1 -4
  81. package/dist/harness.js +5 -123
  82. package/dist/live-adapter-contract.js +6 -20
  83. package/dist/mm-harness-cli.js +6 -43
  84. package/dist/paths.js +2 -5
  85. package/dist/run-diagnostics.js +35 -40
  86. package/dist/runtime-context.js +26 -8
  87. package/docs/CHEATSHEET.md +39 -52
  88. package/docs/CLI-ERGONOMICS-HUMAN-QA.md +48 -104
  89. package/docs/CODE-MAP.md +1 -1
  90. package/docs/VIDEO-DEMO-VALIDATION.md +62 -0
  91. package/docs/architecture.md +65 -375
  92. package/docs/live-adapter-contract.md +3 -1
  93. package/library/actions/core/perps/_controller.mjs +10 -55
  94. package/library/manifests/mobile.action-manifest.json +1 -1
  95. package/package.json +2 -2
  96. package/scripts/completions.sh +0 -0
  97. package/scripts/install-completions.sh +0 -0
  98. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +0 -185
  99. package/adapters/mobile/overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +0 -1662
  100. package/adapters/mobile/overlay/compat/README.md +0 -28
  101. package/adapters/mobile/overlay/compat/rn81-message-event-source.patch +0 -42
  102. package/adapters/shared/resolve-farmslot-ports-core.mjs +0 -3
  103. package/adapters/shared/resolve-farmslot-ports.mjs +0 -5
  104. package/adapters/shared/resolve-farmslot-ports.sh +0 -7
  105. package/docs/CLI-SPEC.md +0 -1142
  106. package/docs/MENTAL-MODEL.md +0 -295
package/CHANGELOG.md CHANGED
@@ -2,6 +2,40 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.17.2 - 2026-07-15
6
+
7
+ ### Fixed
8
+
9
+ - Passive update discovery now runs in a detached, once-daily registry probe, so fast commands populate the cache without adding latency or contaminating machine output.
10
+
11
+ ## 0.17.1 - 2026-07-15
12
+
13
+ ### Fixed
14
+
15
+ - 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.
16
+ - Extension webpack watchers now launch in their own process group, so they remain alive for incremental rebuilds after the initial `launch --build` command exits.
17
+ - 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.
18
+ - 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.
19
+ - 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.
20
+
21
+ ### Changed
22
+
23
+ - 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.
24
+ - Mobile fixture setup now requires the current multichain account-tree path, and Core signing requires the canonical checkout wallet fixture.
25
+
26
+ ### Removed
27
+
28
+ - 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.
29
+ - Removed the historical CLI migration/spec documents; current behavior is documented in the README, cheatsheet, architecture, and human QA checklist.
30
+
31
+ ### Fixed
32
+
33
+ - 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.
34
+ - 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.
35
+ - 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.
36
+ - 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.
37
+ - 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.
38
+
5
39
  ## 0.17.0 - 2026-07-14
6
40
 
7
41
  ### Added
package/README.md CHANGED
@@ -1,181 +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.
32
- Release candidates have a short [human QA checklist](docs/CLI-ERGONOMICS-HUMAN-QA.md).
15
+ ## Install
33
16
 
34
- One package, two responsibilities:
35
-
36
- - **Recipe layer:** manifests, recipes, Mobile/Extension adapters, proof output.
37
- - **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
+ ```
38
22
 
39
- Farmslot and skills are wrappers around this CLI; they should not copy runner
40
- logic.
23
+ Use `mm-harness update` to install the latest published version.
41
24
 
42
- ## Quick start
25
+ ## Daily workflow
43
26
 
44
- Run from inside a MetaMask checkout (platform auto-detected):
27
+ From a MetaMask checkout:
45
28
 
46
29
  ```bash
47
- # 1) Launch the app (installs the runtime overlay on first use, boots Metro/build)
48
- mm-harness launch ios # mobile: ios | android; extension: just `launch`
49
-
50
- # 2) Run a recipe and write evidence
51
- 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
52
43
  ```
53
44
 
54
- Mobile and Extension runs also write `diagnostics.json` from application events
55
- emitted during that run. Mobile combines its bounded in-app issue buffer with
56
- run-scoped log bytes; Extension owns a CDP console subscription for the run.
57
- These redacted warnings/errors are non-blocking side findings: they help spot
58
- adjacent bugs without claiming the recipe or current change caused them. Core
59
- 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.
60
49
 
61
- Outputs: `summary.json`, `trace.json`, screenshots, logs, and an artifact
62
- manifest.
50
+ `debug` opens DevTools for the active Extension target in that checkout's Chrome
51
+ profile. Mobile opens the React Native developer surface.
63
52
 
64
- ## Mental model
53
+ Filter logs when needed:
65
54
 
66
- ```text
67
- mm-harness launch # runtime/orchestration: the app is ready
68
- 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
69
61
  ```
70
62
 
71
- Do not mix those layers. library/ = what recipes ARE (recipes, actions,
72
- manifests — the content). src/ = what EXECUTES them (TypeScript CLI/engine glue,
73
- typed adapters). adapters/ = what CONTROLS the app and its instances (start,
74
- windows, wallet state, health, ports, parallel).
75
-
76
- ## Useful commands
63
+ ## Build and run proof
77
64
 
78
65
  ```bash
79
- # Capabilities (compose recipes from the vocabulary + flow library)
80
- mm-harness actions positions --adapter mobile --json # focused discovery
81
- mm-harness actions --adapter mobile --category ui --json # bounded vocabulary
82
- mm-harness actions --adapter mobile --raw # raw action-manifest dump
83
- mm-harness call unlock --adapter extension # run one action via the real engine path
84
- mm-harness last --json # resume from the last significant command
85
- mm-harness flows --json # reusable library flows, with provenance
86
-
87
- # Readiness + proof
88
- mm-harness doctor # read-only readiness check (no launch)
89
- mm-harness doctor --fix # heal the overlay without launching
90
- mm-harness run recipe.json --plan --adapter mobile # validate + plan, touch nothing
91
- mm-harness run recipe.json --json-stream # line-flushed JSONL progress for agents
92
-
93
- # Daily loop (run from inside a checkout)
94
- mm-harness launch ios # launch (mobile: ios | android)
95
- mm-harness logs # tail Metro/webpack + app logs
96
- mm-harness debug # open the debug console
97
- mm-harness fixtures sync # sync fixtures; `fixtures set` seeds the wallet
98
-
99
- # Keep the install current
100
- mm-harness update # upgrade to the published latest (prints old → new)
101
- mm-harness update --check --json # report only — { current, latest, updateAvailable }
102
- ```
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
103
70
 
104
- A passive once-a-day nudge prints one `stderr` line when a newer version is
105
- published (`mm-harness <cur> <latest> available · run: mm-harness update`). It
106
- never blocks and never auto-updates. Silence it with `MM_HARNESS_NO_UPDATE_CHECK=1`
107
- (auto-off in CI).
108
-
109
- Set `RECIPE_LOG_UI=compact|full|quiet` (default `compact`) and `RECIPE_LOG_EVENTS=10`
110
- to tune the compact log view during launch and Metro/webpack startup.
111
-
112
- ## Layout
113
-
114
- ```text
115
- bin/ the mm-harness entrypoint
116
- src/ what EXECUTES recipes: TypeScript CLI/engine glue +
117
- typed per-adapter logic (src/adapters/)
118
- adapters/ what CONTROLS the app and its instances: launch/live/
119
- watch/windows/wallet state/health/inject/cleanup per
120
- platform, shared glue, porcelain, overlay payload +
121
- manifest.json
122
- library/ what recipes ARE: recipes/ (recipe JSONs), actions/
123
- (per-platform implementations), manifests/ (capability)
124
- scripts/ dev tooling (yarn check, local farmslot link, e2e
125
- validation, adapter-surface-doctor)
126
- 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
127
73
  ```
128
74
 
129
- Defaults for installed harness/runtime paths live in
130
- `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.
131
83
 
132
- ## Runtime-readiness ownership
84
+ Use `mm-harness last --json` to resume after interruption without repeating a
85
+ side-effecting command.
133
86
 
134
- Generic readiness mechanics live in `@farmslot/recipe-harness` (0.3+):
87
+ ## Team recipe libraries
135
88
 
136
- - `@farmslot/recipe-harness/runtime/deps-readiness` install fingerprint + baseline
137
- - `@farmslot/recipe-harness/runtime/log-analysis` — bundle-log boundaries, unresolved-module scoping
138
- - `@farmslot/recipe-harness/runtime/metro-probe` — Metro `/status` reachability
89
+ Add a named shared directory with `RECIPE_LIBRARY_PATH`:
139
90
 
140
- This repo adds **MetaMask adapters only**: `adapters/mobile/deps-markers.ts`,
141
- `adapters/mobile/runtime-decision.ts`, `adapters/extension/runtime-decision.ts`,
142
- and shell launch in `adapters/porcelain/mm-recipe` / `adapters/porcelain/mme-recipe`. Do not re-copy harness
143
- primitives into `adapters/shared/`. CI installs harness from npm — publish
144
- `@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
+ ```
145
95
 
146
- ## 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).
147
98
 
148
- The global npm install owns the `mm-harness` on `PATH` — that is the **prod**
149
- harness. To run a **dev** checkout with active pre-release changes alongside it,
150
- set `MM_HARNESS_BIN` to that checkout's `bin/mm-harness`:
99
+ ## Health and recovery
151
100
 
152
101
  ```bash
153
- export MM_HARNESS_BIN=/path/to/checkout/bin/mm-harness # dev: this checkout owns the run
154
- 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
155
106
  ```
156
107
 
157
- `MM_HARNESS_BIN` is the single override point: when set, the entrypoint hands the
158
- whole invocation to that path before doing any dependency work; when unset it
159
- resolves the installed bin. A dev checkout is never installed globally, so there is
160
- no `PATH` collision — dev is reached only through `MM_HARNESS_BIN` (or a shell
161
- alias). A per-farm environment can export it so the dev farm dispatches through the
162
- 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.
163
111
 
164
- ## 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
+ ```
165
120
 
166
- - `tmux` is recommended for long-lived Metro/webpack processes; standalone use
167
- falls back to detached `nohup` where possible.
168
- - Mobile may inject a local development bridge/HUD into older checkouts. Do not
169
- commit those product patches.
170
- - Extension does not patch product source; it drives `dist/chrome` through Chrome
171
- CDP.
121
+ Unset `MM_HARNESS_BIN` to return to the published install.
172
122
 
173
- ## Validate changes
123
+ Validate changes with:
174
124
 
175
125
  ```bash
176
126
  yarn check
177
- bash -n bin/mm-harness adapters/porcelain/{metamask-recipe,mm-recipe,mme-recipe} adapters/{mobile,extension,core}/*.sh
178
- 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
179
127
  ```
180
128
 
181
- 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
+ });