@deeeed/metamask-harness 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/CHANGELOG.md +161 -0
  2. package/README.md +140 -0
  3. package/bin/mm-harness +99 -0
  4. package/docs/CHEATSHEET.md +61 -0
  5. package/docs/CLI-SPEC.md +915 -0
  6. package/docs/MENTAL-MODEL.md +295 -0
  7. package/docs/architecture.md +367 -0
  8. package/docs/extension-runtime-commands.md +60 -0
  9. package/docs/harness-cli.md +43 -0
  10. package/docs/live-adapter-contract.md +188 -0
  11. package/docs/package-boundaries.md +47 -0
  12. package/docs/perps-flow-catalog.md +235 -0
  13. package/docs/recipe-libraries.md +95 -0
  14. package/docs/runtime-file-conventions.md +36 -0
  15. package/library/actions/core/perps/_controller.mjs +727 -0
  16. package/library/actions/core/perps/assert_orders.mjs +53 -0
  17. package/library/actions/core/perps/assert_positions.mjs +52 -0
  18. package/library/actions/core/perps/close_orders.mjs +97 -0
  19. package/library/actions/core/perps/close_positions.mjs +118 -0
  20. package/library/actions/core/perps/ensure_orders.mjs +40 -0
  21. package/library/actions/core/perps/ensure_positions.mjs +37 -0
  22. package/library/actions/core/perps/place_order.mjs +201 -0
  23. package/library/actions/core/perps/read_account.mjs +30 -0
  24. package/library/actions/core/perps/read_orders.mjs +27 -0
  25. package/library/actions/core/perps/read_positions.mjs +27 -0
  26. package/library/actions/core/perps/start_state.mjs +92 -0
  27. package/library/actions/core/perps/teardown_state.mjs +86 -0
  28. package/library/actions/extension/perps/assert_orders.mjs +11 -0
  29. package/library/actions/extension/perps/assert_positions.mjs +11 -0
  30. package/library/actions/extension/perps/close_orders.mjs +8 -0
  31. package/library/actions/extension/perps/close_positions.mjs +8 -0
  32. package/library/actions/extension/perps/ensure_orders.mjs +4 -0
  33. package/library/actions/extension/perps/ensure_positions.mjs +4 -0
  34. package/library/actions/extension/perps/perps.mjs +730 -0
  35. package/library/actions/extension/perps/place_order.mjs +7 -0
  36. package/library/actions/extension/perps/read_orders.mjs +4 -0
  37. package/library/actions/extension/perps/read_positions.mjs +3 -0
  38. package/library/actions/extension/platform/cdp.mjs +541 -0
  39. package/library/actions/extension/ui/navigate.mjs +44 -0
  40. package/library/actions/extension/wallet/ensure_unlocked.mjs +36 -0
  41. package/library/actions/extension/wallet/read_state.mjs +27 -0
  42. package/library/actions/extension/wallet/select_account.mjs +48 -0
  43. package/library/actions/extension/wallet/setup.mjs +35 -0
  44. package/library/actions/mobile/app-overlay/app/dev-tools/AgenticService/AgentStepHud.tsx.patch +185 -0
  45. package/library/actions/mobile/app-overlay/app/dev-tools/AgenticService/AgenticService.ts.patch +1662 -0
  46. package/library/actions/mobile/bridge-runtime/cdp-bridge.cjs +686 -0
  47. package/library/actions/mobile/bridge-runtime/lib/cdp-eval.cjs +110 -0
  48. package/library/actions/mobile/bridge-runtime/lib/config.cjs +39 -0
  49. package/library/actions/mobile/bridge-runtime/lib/issue-capture.cjs +446 -0
  50. package/library/actions/mobile/bridge-runtime/lib/target-discovery.cjs +204 -0
  51. package/library/actions/mobile/bridge-runtime/lib/ws-client.cjs +108 -0
  52. package/library/actions/mobile/bridge-runtime/setup-wallet.sh +442 -0
  53. package/library/actions/mobile/perps/assert_orders.mjs +11 -0
  54. package/library/actions/mobile/perps/assert_positions.mjs +11 -0
  55. package/library/actions/mobile/perps/close_orders.mjs +8 -0
  56. package/library/actions/mobile/perps/close_positions.mjs +8 -0
  57. package/library/actions/mobile/perps/ensure_orders.mjs +4 -0
  58. package/library/actions/mobile/perps/ensure_positions.mjs +4 -0
  59. package/library/actions/mobile/perps/perps.mjs +709 -0
  60. package/library/actions/mobile/perps/place_order.mjs +7 -0
  61. package/library/actions/mobile/perps/read_orders.mjs +4 -0
  62. package/library/actions/mobile/perps/read_positions.mjs +3 -0
  63. package/library/actions/mobile/platform/bridge.mjs +283 -0
  64. package/library/actions/mobile/ui/navigate.mjs +38 -0
  65. package/library/actions/mobile/wallet/ensure_unlocked.mjs +107 -0
  66. package/library/actions/mobile/wallet/home.mjs +35 -0
  67. package/library/actions/mobile/wallet/read_state.mjs +40 -0
  68. package/library/actions/mobile/wallet/select_account.mjs +48 -0
  69. package/library/actions/mobile/wallet/setup.mjs +220 -0
  70. package/library/flows/perps.flows.json +64 -0
  71. package/library/library.json +7 -0
  72. package/library/manifests/core.action-manifest.json +1282 -0
  73. package/library/manifests/extension.action-manifest.json +1749 -0
  74. package/library/manifests/mobile.action-manifest.json +1753 -0
  75. package/library/recipes/action-validation.extension.recipe.json +417 -0
  76. package/library/recipes/action-validation.mobile.recipe.json +422 -0
  77. package/library/recipes/order-lifecycle.core.recipe.json +78 -0
  78. package/library/recipes/perps-lifecycle.recipe.json +194 -0
  79. package/library/recipes/read-markets.core.recipe.json +38 -0
  80. package/library/recipes/smoke.extension.recipe.json +31 -0
  81. package/library/recipes/smoke.mobile.recipe.json +31 -0
  82. package/library/recipes/trading-lifecycle.core.recipe.json +76 -0
  83. package/orchestration/compat-overlays/README.md +19 -0
  84. package/orchestration/compat-overlays/mobile/README.md +13 -0
  85. package/orchestration/compat-overlays/mobile/rn81-message-event-source.patch +42 -0
  86. package/orchestration/core/cleanup.sh +37 -0
  87. package/orchestration/core/inject.sh +154 -0
  88. package/orchestration/doctor.mjs +72 -0
  89. package/orchestration/extension/cleanup.mjs +60 -0
  90. package/orchestration/extension/console-tail.mjs +228 -0
  91. package/orchestration/extension/ensure-browser.sh +416 -0
  92. package/orchestration/extension/ensure-ready.ts +185 -0
  93. package/orchestration/extension/extension-id.ts +107 -0
  94. package/orchestration/extension/inject.mjs +266 -0
  95. package/orchestration/extension/launch-browser.cjs +216 -0
  96. package/orchestration/extension/launch.sh +175 -0
  97. package/orchestration/extension/live.sh +320 -0
  98. package/orchestration/extension/pin-remote-flags.cjs +45 -0
  99. package/orchestration/extension/readiness.mjs +414 -0
  100. package/orchestration/extension/refresh-build.sh +190 -0
  101. package/orchestration/extension/runtime-decision.ts +445 -0
  102. package/orchestration/extension/runtime.ts +407 -0
  103. package/orchestration/extension/seed-fixture.sh +177 -0
  104. package/orchestration/extension/sidepanel-toggle.sh +291 -0
  105. package/orchestration/extension/snapshot-dist.sh +84 -0
  106. package/orchestration/extension/start-watch.sh +339 -0
  107. package/orchestration/extension/wallet-fixture-state.cjs +1086 -0
  108. package/orchestration/lib/activate-repo-node.sh +144 -0
  109. package/orchestration/lib/cli-color.mjs +84 -0
  110. package/orchestration/lib/cli-commands.mjs +243 -0
  111. package/orchestration/lib/cli-home.mjs +354 -0
  112. package/orchestration/lib/cli-ux.sh +252 -0
  113. package/orchestration/lib/cli-version.mjs +123 -0
  114. package/orchestration/lib/ensure-runner-deps.sh +56 -0
  115. package/orchestration/lib/harness-path.sh +55 -0
  116. package/orchestration/lib/hash-helpers.sh +44 -0
  117. package/orchestration/lib/json-field.sh +23 -0
  118. package/orchestration/lib/log-tui.mjs +304 -0
  119. package/orchestration/lib/open-debug.mjs +317 -0
  120. package/orchestration/lib/path-defaults.json +4 -0
  121. package/orchestration/lib/progress.mjs +107 -0
  122. package/orchestration/lib/recipe-paths.mjs +26 -0
  123. package/orchestration/lib/resolve-farmslot-ports.sh +144 -0
  124. package/orchestration/manifest.json +358 -0
  125. package/orchestration/mobile/cleanup.sh +192 -0
  126. package/orchestration/mobile/deps-markers.ts +21 -0
  127. package/orchestration/mobile/inject.sh +681 -0
  128. package/orchestration/mobile/launch.sh +137 -0
  129. package/orchestration/mobile/live.sh +125 -0
  130. package/orchestration/mobile/runtime-decision.ts +292 -0
  131. package/orchestration/porcelain/metamask-recipe +99 -0
  132. package/orchestration/porcelain/mm-recipe +1591 -0
  133. package/orchestration/porcelain/mme-recipe +1181 -0
  134. package/package.json +59 -0
  135. package/runner/extension/verify.sh +511 -0
  136. package/runner/mobile/verify.sh +501 -0
  137. package/runner/src/adapters.ts +601 -0
  138. package/runner/src/cli.ts +1820 -0
  139. package/runner/src/commands/debug.ts +44 -0
  140. package/runner/src/commands/fixtures.ts +99 -0
  141. package/runner/src/commands/launch.ts +397 -0
  142. package/runner/src/commands/logs.ts +60 -0
  143. package/runner/src/commands/shared.ts +138 -0
  144. package/runner/src/completions-cache.ts +86 -0
  145. package/runner/src/doctor.ts +203 -0
  146. package/runner/src/harness.ts +516 -0
  147. package/runner/src/heal-bounds.ts +179 -0
  148. package/runner/src/index.ts +6 -0
  149. package/runner/src/live-adapter-contract.ts +274 -0
  150. package/runner/src/manifest.ts +47 -0
  151. package/runner/src/mm-harness-cli.ts +488 -0
  152. package/runner/src/paths.ts +198 -0
  153. package/runner/src/recording-target.ts +147 -0
  154. package/runner/src/run-recording.ts +329 -0
  155. package/runner/src/runner.ts +108 -0
  156. package/runner/src/types.ts +57 -0
  157. package/scripts/completions.sh +125 -0
  158. package/scripts/install-completions.sh +62 -0
@@ -0,0 +1,175 @@
1
+ #!/usr/bin/env bash
2
+ # launch.sh — extension instance launch: prepare command + CDP readiness
3
+ #
4
+ # Purpose:
5
+ # Runs the caller-supplied prepare command (cwd = target) for one slot,
6
+ # then confirms the app-control runtime via extension-readiness.
7
+ #
8
+ # Inputs (flags / env):
9
+ # --target <metamask-extension> (default $PWD)
10
+ # --cdp-port <port> (numeric-validated)
11
+ # --prepare-cmd <cmd> (env RECIPE_HARNESS_EXTENSION_LAUNCH_CMD)
12
+ # --artifacts-dir <dir> (default <harness>/launch/<UTC>)
13
+ #
14
+ # Outputs:
15
+ # <artifacts>/summary.json (adapter/action/status/prepare/appControl),
16
+ # <artifacts>/logs/{launch.log,extension-readiness.json}.
17
+ # Exit 0 — launch pass; 1 — harness not installed / prepare or readiness
18
+ # failed; 2 — bad args.
19
+ #
20
+ # Never touches: product source files; anything outside the target's
21
+ # harness dir and the artifacts dir.
22
+ set -euo pipefail
23
+
24
+ TARGET="$PWD"
25
+ CDP_PORT=""
26
+ ARTIFACTS=""
27
+ PREPARE_CMD="${RECIPE_HARNESS_EXTENSION_LAUNCH_CMD:-}"
28
+ while [ "$#" -gt 0 ]; do
29
+ case "$1" in
30
+ --target) TARGET="$2"; shift 2 ;;
31
+ --cdp-port) CDP_PORT="$2"; shift 2 ;;
32
+ --artifacts-dir) ARTIFACTS="$2"; shift 2 ;;
33
+ --prepare-cmd) PREPARE_CMD="$2"; shift 2 ;;
34
+ -h|--help) echo "Usage: launch.sh [--target <metamask-extension>] [--cdp-port <port>] [--prepare-cmd <cmd>] [--artifacts-dir <dir>]"; exit 0 ;;
35
+ *) echo "Unknown arg: $1" >&2; exit 2 ;;
36
+ esac
37
+ done
38
+
39
+ # Reject a non-numeric --cdp-port before it reaches the prepare/launch command.
40
+ if [ -n "$CDP_PORT" ]; then
41
+ case "$CDP_PORT" in
42
+ *[!0-9]*) echo "Invalid --cdp-port (must be numeric): $CDP_PORT" >&2; exit 2 ;;
43
+ esac
44
+ fi
45
+
46
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
47
+ # shellcheck disable=SC1091
48
+ for _hp in "$SCRIPT_DIR/lib/harness-path.sh" "$SCRIPT_DIR/../lib/harness-path.sh"; do
49
+ [ -f "$_hp" ] && { . "$_hp"; break; }
50
+ done
51
+ unset _hp
52
+ if ! command -v harness_root >/dev/null 2>&1; then
53
+ echo "mm-harness: shared lib orchestration/lib/harness-path.sh not found; reinstall the runner." >&2
54
+ exit 1
55
+ fi
56
+ READINESS_MJS="$SCRIPT_DIR/readiness.mjs"
57
+ if [ ! -f "$READINESS_MJS" ]; then
58
+ echo "mm-harness: readiness.mjs not found next to $SCRIPT_DIR; reinstall the runner." >&2
59
+ exit 1
60
+ fi
61
+ TARGET="$(cd "$TARGET" && pwd)"
62
+ HARNESS_DIR="$(harness_dir "$TARGET" extension)"
63
+ ARTIFACTS="${ARTIFACTS:-$HARNESS_DIR/launch/$(date -u +%Y%m%dT%H%M%SZ)}"
64
+ mkdir -p "$ARTIFACTS/logs"
65
+
66
+ if [ ! -f "$HARNESS_DIR/manifest.json" ]; then
67
+ echo "Extension recipe harness is not installed in $TARGET. Run metamask-recipe extension prepare --target $TARGET first." >&2
68
+ exit 1
69
+ fi
70
+
71
+ LIB_DIR=""
72
+ for _lib in "$SCRIPT_DIR/lib" "$SCRIPT_DIR/../lib"; do
73
+ [ -f "$_lib/log-tui.mjs" ] && LIB_DIR="$_lib" && break
74
+ done
75
+ unset _lib
76
+
77
+ if [ -n "$PREPARE_CMD" ]; then
78
+ launch_log="$ARTIFACTS/logs/launch.log"
79
+ : > "$launch_log"
80
+ echo "[recipe-harness] running prepare pipeline (watch/build, dist snapshot, browser, fixture seed)..." | tee "$launch_log"
81
+ watch_pid=""
82
+ if [ -n "$LIB_DIR" ] && [ "${RECIPE_LOG_UI:-compact}" != "full" ] && [ "${RECIPE_LOG_TUI_PARENT:-}" != "1" ]; then
83
+ node "$LIB_DIR/log-tui.mjs" watch \
84
+ --log "$launch_log" \
85
+ --events "${RECIPE_LOG_EVENTS:-10}" \
86
+ --mode "${RECIPE_LOG_UI:-compact}" \
87
+ --label "extension prepare" \
88
+ --success-pattern 'yarn start compiled|runtime-dist snapshot complete|Extension harness launch pass|phase 2/2' \
89
+ --poll-ms 1000 &
90
+ watch_pid=$!
91
+ fi
92
+ set +e
93
+ (
94
+ cd "$TARGET"
95
+ bash -lc "$PREPARE_CMD"
96
+ ) 2>&1 | tee -a "$launch_log"
97
+ prepare_status=${PIPESTATUS[0]}
98
+ set -e
99
+ [ -n "$watch_pid" ] && kill "$watch_pid" >/dev/null 2>&1 && wait "$watch_pid" 2>/dev/null || true
100
+ else
101
+ echo "No Extension prepare command supplied; reusing existing CDP runtime if reachable." | tee "$ARTIFACTS/logs/launch.log"
102
+ prepare_status=0
103
+ fi
104
+
105
+ status="pass"
106
+ if [ "$prepare_status" -ne 0 ]; then
107
+ status="fail"
108
+ elif [ -z "$CDP_PORT" ]; then
109
+ echo "Missing --cdp-port; cannot confirm Extension app-control runtime." | tee -a "$ARTIFACTS/logs/launch.log"
110
+ status="fail"
111
+ elif node "$READINESS_MJS" --target "$TARGET" --cdp-port "$CDP_PORT" --json > "$ARTIFACTS/logs/extension-readiness.json" 2>&1; then
112
+ :
113
+ else
114
+ status="fail"
115
+ fi
116
+
117
+ # Live extension-console viewer tab (default-on): tail the running extension's
118
+ # service worker + UI-page console in a tmux window, mirroring the Metro/webpack
119
+ # tabs. Skipped silently when there is no tmux session or CDP (headless/CI), so
120
+ # it never affects the launch result.
121
+ if [ "$status" = "pass" ] && [ -n "$CDP_PORT" ] && command -v tmux >/dev/null 2>&1; then
122
+ console_session="${RECIPE_TMUX_SESSION:-}"
123
+ if [ -z "$console_session" ] && [[ "$(basename "$TARGET")" =~ -([0-9]+)$ ]]; then
124
+ console_session="mme-${BASH_REMATCH[1]}"
125
+ fi
126
+ console_tail="$SCRIPT_DIR/console-tail.mjs"
127
+ if [ -n "$console_session" ] && [ -f "$console_tail" ] && tmux has-session -t "$console_session" 2>/dev/null; then
128
+ console_runtime="$TARGET/$( (cd "$TARGET" && recipe_runtime_dir) 2>/dev/null || echo temp/recipe/runtime)"
129
+ mkdir -p "$console_runtime"
130
+ console_log="$console_runtime/extension-console.log"
131
+ : > "$console_log"
132
+ console_window="console-${CDP_PORT}"
133
+ tmux kill-window -t "${console_session}:${console_window}" >/dev/null 2>&1 || true
134
+ tmux new-window -d -t "$console_session" -n "$console_window" \
135
+ "exec node $(printf '%q' "$console_tail") --cdp-port ${CDP_PORT} --log $(printf '%q' "$console_log")"
136
+ echo "[recipe-harness] extension console viewer in tmux window ${console_session}:${console_window}"
137
+ fi
138
+ fi
139
+
140
+ TARGET_FOR_SUMMARY="$TARGET" ARTIFACTS_FOR_SUMMARY="$ARTIFACTS" STATUS_FOR_SUMMARY="$status" CDP_PORT_FOR_SUMMARY="$CDP_PORT" PREPARE_SUPPLIED="$([ -n "$PREPARE_CMD" ] && echo true || echo false)" PREPARE_STATUS="$prepare_status" node <<'NODE'
141
+ const fs = require('fs');
142
+ const path = require('path');
143
+ const target = process.env.TARGET_FOR_SUMMARY;
144
+ const artifacts = process.env.ARTIFACTS_FOR_SUMMARY;
145
+ let readiness = null;
146
+ try { readiness = JSON.parse(fs.readFileSync(path.join(artifacts, 'logs/extension-readiness.json'), 'utf8')); } catch {}
147
+ const appControlStatus =
148
+ process.env.STATUS_FOR_SUMMARY === 'pass' && readiness && readiness.status !== 'fail' ? 'pass' : 'fail';
149
+ fs.writeFileSync(path.join(artifacts, 'summary.json'), `${JSON.stringify({
150
+ adapter: 'extension',
151
+ action: 'launch',
152
+ status: process.env.STATUS_FOR_SUMMARY,
153
+ target,
154
+ cdpPort: process.env.CDP_PORT_FOR_SUMMARY || null,
155
+ prepare: {
156
+ commandSupplied: process.env.PREPARE_SUPPLIED === 'true',
157
+ status: Number(process.env.PREPARE_STATUS) === 0 ? 'pass' : 'fail',
158
+ exitCode: Number(process.env.PREPARE_STATUS),
159
+ logPath: path.join(artifacts, 'logs/launch.log'),
160
+ },
161
+ runtimePolicy: {
162
+ runtimeReusePolicy: 'reuse a running harness-compatible CDP target when possible; caller-supplied startup commands must use cached/watch-only paths unless the human explicitly permits a rebuild',
163
+ },
164
+ appControl: {
165
+ status: appControlStatus,
166
+ readiness,
167
+ },
168
+ cleanupCommand: 'Use the installed harness manifest cleanupCommand.',
169
+ note: 'Launch starts/reuses the harness runtime only; it does not run a recipe or claim evidence validation. Extension startup commands are caller-supplied so the runner does not encode local machine aliases.',
170
+ generatedAt: new Date().toISOString(),
171
+ }, null, 2)}\n`);
172
+ NODE
173
+
174
+ echo "Extension harness launch $status: $ARTIFACTS/summary.json"
175
+ [ "$status" = "pass" ]
@@ -0,0 +1,320 @@
1
+ #!/usr/bin/env bash
2
+ # live.sh — extension launch-then-verify sequencer for one live slot run
3
+ # mega-string now decomposed into start-watch.sh / snapshot-dist.sh /
4
+ # seed-fixture.sh)
5
+ #
6
+ # Purpose:
7
+ # Builds the per-run prepare command from the named feature scripts
8
+ # (watch, dist snapshot, fixture seed, detached chrome, CDP poll), then
9
+ # runs launch.sh and verify.sh under a timestamped artifact dir.
10
+ #
11
+ # Inputs (flags / env):
12
+ # --target <metamask-extension> (default $PWD)
13
+ # --cdp-port <port> (required, numeric-validated)
14
+ # --launch-existing-dist | --start-watch | --prepare-cmd <cmd>
15
+ # (env RECIPE_HARNESS_EXTENSION_LAUNCH_CMD)
16
+ # --dist-dir <rel> (default dist/chrome), --chrome-user-data-dir <dir>,
17
+ # --remote-flag <KEY=VARIANT[,KEY=VARIANT...]> (pins manifest _flags into the
18
+ # ephemeral runtime-dist snapshot before launch; omit for default behavior)
19
+ # --out <recipes-dir>, --artifacts-dir <dir>
20
+ # env RECIPE_HARNESS_CHROME_BIN (validated executable; else Playwright
21
+ # chromium with approval-gated install messaging), RECIPE_WALLET_FIXTURE,
22
+ # RECIPE_HARNESS_LIVE_KEEP (artifact pruning, default 5)
23
+ #
24
+ # Outputs:
25
+ # <artifacts>/summary.json (adapter/action/status, launch/verify exit
26
+ # codes + child summary paths, easyCommand), <artifacts>/{launch,verify},
27
+ # <artifacts>/logs/fixture-source.json provenance.
28
+ # Exit 0 — launch and verify both passed; 1 — either failed; 2 — bad args.
29
+ #
30
+ # Never touches: product source files; live artifact dirs newer than the
31
+ # pruning window; anything outside the target's harness dir.
32
+ set -euo pipefail
33
+
34
+ TARGET="$PWD"
35
+ CDP_PORT=""
36
+ ARTIFACTS=""
37
+ OUT=""
38
+ PREPARE_CMD="${RECIPE_HARNESS_EXTENSION_LAUNCH_CMD:-}"
39
+ LAUNCH_EXISTING_DIST=false
40
+ START_WATCH=false
41
+ DIST_DIR="dist/chrome"
42
+ CHROME_USER_DATA_DIR=""
43
+ REMOTE_FLAGS=""
44
+ REMOTE_FLAGS_APPLIED=false
45
+ while [ "$#" -gt 0 ]; do
46
+ case "$1" in
47
+ --target) [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; TARGET="$2"; shift 2 ;;
48
+ --out) [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; OUT="$2"; shift 2 ;;
49
+ --cdp-port) [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; CDP_PORT="$2"; shift 2 ;;
50
+ --artifacts-dir) [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; ARTIFACTS="$2"; shift 2 ;;
51
+ --prepare-cmd) [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; PREPARE_CMD="$2"; shift 2 ;;
52
+ --launch-existing-dist) LAUNCH_EXISTING_DIST=true; shift ;;
53
+ --start-watch|--start-test-watch) START_WATCH=true; LAUNCH_EXISTING_DIST=true; shift ;;
54
+ --dist-dir) [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; DIST_DIR="$2"; shift 2 ;;
55
+ --chrome-user-data-dir) [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; CHROME_USER_DATA_DIR="$2"; shift 2 ;;
56
+ --remote-flag) [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; REMOTE_FLAGS="$2"; shift 2 ;;
57
+ -h|--help) echo "Usage: live.sh [--target <metamask-extension>] [--out <recipes-dir>] --cdp-port <port> [--launch-existing-dist|--start-watch|--prepare-cmd <cmd>] [--dist-dir dist/chrome] [--remote-flag KEY=VARIANT] [--artifacts-dir <dir>]"; exit 0 ;;
58
+ *) echo "Unknown arg: $1" >&2; exit 2 ;;
59
+ esac
60
+ done
61
+
62
+ [ -n "$CDP_PORT" ] || { echo "Missing --cdp-port for Extension live validation" >&2; exit 2; }
63
+ # Reject a non-numeric port before it is interpolated into the Chrome launch
64
+ # command and the CDP HTTP probes.
65
+ case "$CDP_PORT" in
66
+ *[!0-9]*) echo "Invalid --cdp-port (must be numeric): $CDP_PORT" >&2; exit 2 ;;
67
+ esac
68
+
69
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
70
+ # shellcheck disable=SC1091
71
+ for _hp in "$SCRIPT_DIR/lib/harness-path.sh" "$SCRIPT_DIR/../lib/harness-path.sh"; do
72
+ [ -f "$_hp" ] && { . "$_hp"; break; }
73
+ done
74
+ unset _hp
75
+ if ! command -v harness_root >/dev/null 2>&1; then
76
+ echo "mm-harness: shared lib orchestration/lib/harness-path.sh not found; reinstall the runner." >&2
77
+ exit 1
78
+ fi
79
+ # verify.sh is a recipe-tree feature: co-located in installed copies, under
80
+ # runner/extension in a repo checkout.
81
+ VERIFY_SH=""
82
+ for _v in "$SCRIPT_DIR/verify.sh" "$SCRIPT_DIR/../../runner/extension/verify.sh"; do
83
+ [ -f "$_v" ] && { VERIFY_SH="$_v"; break; }
84
+ done
85
+ unset _v
86
+ if [ -z "$VERIFY_SH" ]; then
87
+ echo "mm-harness: extension verify.sh not found next to $SCRIPT_DIR; reinstall the runner." >&2
88
+ exit 1
89
+ fi
90
+ TARGET="$(cd "$TARGET" && pwd)"
91
+ RUNTIME_DIR="$(recipe_runtime_dir)"
92
+ # Runner bin (installed wrapper → source runner). Used by the watch prepare path
93
+ # to defer the cache-clear DECISION to `runtime-decision` (single source) and to
94
+ # record the deps/cache baseline after a confirmed-good build.
95
+ RUNNER_BIN="$(harness_dir "$TARGET" extension)/runner/bin/metamask-recipe"
96
+ OUT="${OUT:-$(harness_root)/extension/runner/recipes}"
97
+ ARTIFACTS="${ARTIFACTS:-$(harness_dir "$TARGET" extension)/live/$(date -u +%Y%m%dT%H%M%SZ)}"
98
+ mkdir -p "$ARTIFACTS/logs"
99
+
100
+ # Prune stale live artifact dirs (configurable harness root, not a hardcoded path)
101
+ # so old runtime-dist snapshots can't be loaded and don't accumulate to GBs.
102
+ # Keep the most recent few; override count with RECIPE_HARNESS_LIVE_KEEP.
103
+ LIVE_ROOT="$(harness_dir "$TARGET" extension)/live"
104
+ LIVE_KEEP="${RECIPE_HARNESS_LIVE_KEEP:-5}"
105
+ # Must be a positive integer: a non-numeric value would break the arithmetic and a
106
+ # 0 would `tail -n +1` and delete every dir including this run's fresh $ARTIFACTS.
107
+ case "$LIVE_KEEP" in ''|*[!0-9]*) LIVE_KEEP=5 ;; esac
108
+ [ "$LIVE_KEEP" -ge 1 ] 2>/dev/null || LIVE_KEEP=5
109
+ if [ -d "$LIVE_ROOT" ]; then
110
+ # `|| true`: an empty live/ (no child dirs) makes ls exit non-zero, which would
111
+ # abort the script under `set -euo pipefail`. Pruning is best-effort.
112
+ ls -1dt "$LIVE_ROOT"/*/ 2>/dev/null | tail -n "+$((LIVE_KEEP + 1))" | while IFS= read -r _old; do rm -rf "$_old"; done || true
113
+ fi
114
+
115
+ if $LAUNCH_EXISTING_DIST && [ -z "$PREPARE_CMD" ]; then
116
+ DIST_ABS="$TARGET/$DIST_DIR"
117
+ RUNTIME_DIST_ABS="$ARTIFACTS/runtime-dist"
118
+ PROFILE_ABS="${CHROME_USER_DATA_DIR:-$ARTIFACTS/chrome-profile}"
119
+ FIXTURE_STATE_ABS="$ARTIFACTS/fixture-state.json"
120
+ FIXTURE_VALIDATION_ABS="$ARTIFACTS/logs/fixture-account-parity.json"
121
+ # Wallet fixture resolution chain + provenance live in seed-fixture.sh.
122
+ WALLET_FIXTURE_ABS="$(bash "$SCRIPT_DIR/seed-fixture.sh" resolve --target "$TARGET" --cdp-port "$CDP_PORT" --source-out "$ARTIFACTS/logs/fixture-source.json")"
123
+ if [ ! -f "$ARTIFACTS/logs/fixture-source.json" ]; then
124
+ # resolve failed before writing provenance (invalid RECIPE_WALLET_FIXTURE);
125
+ # keep the documented missing-provenance artifact.
126
+ node - "$ARTIFACTS/logs/fixture-source.json" "" <<'NODE'
127
+ const fs = require('fs');
128
+ const [out, fixture] = process.argv.slice(2);
129
+ fs.mkdirSync(require('path').dirname(out), { recursive: true });
130
+ fs.writeFileSync(out, `${JSON.stringify({
131
+ status: fixture ? 'present' : 'missing',
132
+ fixturePath: fixture || null,
133
+ generatedAt: new Date().toISOString(),
134
+ }, null, 2)}\n`);
135
+ NODE
136
+ fi
137
+ mkdir -p "$PROFILE_ABS"
138
+ quoted_dist="$(printf '%q' "$DIST_ABS")"
139
+ quoted_runtime_dist="$(printf '%q' "$RUNTIME_DIST_ABS")"
140
+ quoted_profile="$(printf '%q' "$PROFILE_ABS")"
141
+ quoted_seed_fixture="$(printf '%q' "$SCRIPT_DIR/seed-fixture.sh")"
142
+ quoted_start_watch="$(printf '%q' "$SCRIPT_DIR/start-watch.sh")"
143
+ quoted_snapshot_dist="$(printf '%q' "$SCRIPT_DIR/snapshot-dist.sh")"
144
+ quoted_chrome_launcher="$(printf '%q' "$SCRIPT_DIR/launch-browser.cjs")"
145
+ quoted_fixture_state="$(printf '%q' "$FIXTURE_STATE_ABS")"
146
+ quoted_fixture_validation="$(printf '%q' "$FIXTURE_VALIDATION_ABS")"
147
+ quoted_extension_id_file="$(printf '%q' "$TARGET/$RUNTIME_DIR/extension.id")"
148
+ quoted_target="$(printf '%q' "$TARGET")"
149
+ quoted_runner="$(printf '%q' "$RUNNER_BIN")"
150
+ quoted_runtime_dir="$(printf '%q' "$RUNTIME_DIR")"
151
+ if [ -n "${RECIPE_HARNESS_CHROME_BIN:-}" ]; then
152
+ CHROME_BIN="$RECIPE_HARNESS_CHROME_BIN"
153
+ if [ ! -f "$CHROME_BIN" ] || [ ! -x "$CHROME_BIN" ]; then
154
+ echo "[recipe-harness] RECIPE_HARNESS_CHROME_BIN is not an executable file: $CHROME_BIN" >&2
155
+ exit 1
156
+ fi
157
+ fi
158
+
159
+ if [ -z "${CHROME_BIN:-}" ]; then
160
+ # bash 3.2 (macOS /bin/bash) mis-parses heredocs inside $(...): its scanner
161
+ # trips on quotes/backticks in the JS. Write the probe to a temp file and
162
+ # command-substitute a plain `node` invocation instead.
163
+ _chrome_probe="$(mktemp -t chrome-probe-XXXXXX)"
164
+ cat > "$_chrome_probe" <<'NODE'
165
+ const fs = require('fs');
166
+ const path = require('path');
167
+ const { createRequire } = require('module');
168
+ const requireFromTarget = createRequire(path.join(process.cwd(), 'package.json'));
169
+
170
+ let chromium = null;
171
+ function shellQuote(value) {
172
+ return `'${String(value).replace(/'/g, `'\\''`)}'`;
173
+ }
174
+ for (const pkg of ['@playwright/test', 'playwright']) {
175
+ try {
176
+ chromium = requireFromTarget(pkg).chromium;
177
+ if (chromium) break;
178
+ } catch (_error) {
179
+ // Optional Playwright package unavailable; try the next package name.
180
+ }
181
+ }
182
+ if (!chromium) {
183
+ console.error('[recipe-harness] Playwright is not available from this checkout; install dependencies first, or set RECIPE_HARNESS_CHROME_BIN to an explicitly approved browser.');
184
+ process.exit(1);
185
+ }
186
+
187
+ let executable = '';
188
+ try {
189
+ executable = chromium.executablePath();
190
+ } catch (error) {
191
+ const message = error && error.message ? error.message : String(error);
192
+ console.error(`[recipe-harness] Could not resolve Playwright Chromium executable: ${message}. Manual approval required before installing the Playwright Chromium browser cache (no package.json changes); ask the user before running yarn exec playwright install chromium.`);
193
+ process.exit(1);
194
+ }
195
+ if (!fs.existsSync(executable)) {
196
+ console.error(`[recipe-harness] Playwright Chromium is not installed at ${executable}. Manual approval required before installing the Playwright Chromium browser cache (no package.json changes). Ask the user for approval; if they agree, run: cd ${shellQuote(process.cwd())} && yarn exec playwright install chromium`);
197
+ console.error('[recipe-harness] To use a browser that is already installed, set RECIPE_HARNESS_CHROME_BIN=/path/to/chrome explicitly.');
198
+ process.exit(1);
199
+ }
200
+
201
+ process.stdout.write(executable);
202
+ NODE
203
+ CHROME_PROBE_LOG="$ARTIFACTS/logs/chrome-probe.log"
204
+ CHROME_BIN="$(cd "$TARGET" && node "$_chrome_probe" 2>"$CHROME_PROBE_LOG" || true)"
205
+ rm -f "$_chrome_probe"
206
+ fi
207
+
208
+ if [ -z "${CHROME_BIN:-}" ]; then
209
+ for candidate in \
210
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
211
+ "/Applications/Chromium.app/Contents/MacOS/Chromium" \
212
+ "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary" \
213
+ "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"; do
214
+ if [ -x "$candidate" ]; then
215
+ CHROME_BIN="$candidate"
216
+ break
217
+ fi
218
+ done
219
+ if [ -n "${CHROME_BIN:-}" ]; then
220
+ echo "[recipe-harness] WARN: Playwright Chromium unavailable; falling back to system browser: $CHROME_BIN" >&2
221
+ fi
222
+ fi
223
+
224
+ if [ -z "${CHROME_BIN:-}" ]; then
225
+ [ -n "${CHROME_PROBE_LOG:-}" ] && cat "$CHROME_PROBE_LOG" >&2 2>/dev/null || true
226
+ echo "[recipe-harness] No approved Chromium binary selected; stopping before live Extension launch." >&2
227
+ exit 1
228
+ fi
229
+ quoted_chrome="$(printf '%q' "$CHROME_BIN")"
230
+ quoted_chrome_log="$(printf '%q' "$ARTIFACTS/logs/chrome.log")"
231
+ quoted_chrome_pid="$(printf '%q' "$ARTIFACTS/logs/chrome.pid")"
232
+ prepare_parts=()
233
+ if $START_WATCH; then
234
+ prepare_parts+=("bash ${quoted_start_watch} --runtime-dir ${quoted_runtime_dir} --runner-bin ${quoted_runner}")
235
+ fi
236
+ prepare_parts+=("bash ${quoted_snapshot_dist} --dist ${quoted_dist} --runtime-dist ${quoted_runtime_dist}")
237
+ # Optional A/B feature-flag pinning: patch the ephemeral snapshot manifest so
238
+ # manifest._flags wins over the fetched ClientConfigApi value. No-op unless
239
+ # --remote-flag was passed; the snapshot is re-created each run, so no revert.
240
+ if [ -n "$REMOTE_FLAGS" ]; then
241
+ quoted_pin_flags="$(printf '%q' "$SCRIPT_DIR/pin-remote-flags.cjs")"
242
+ quoted_remote_flags="$(printf '%q' "$REMOTE_FLAGS")"
243
+ quoted_runtime_manifest="$(printf '%q' "$RUNTIME_DIST_ABS/manifest.json")"
244
+ prepare_parts+=("node ${quoted_pin_flags} ${quoted_runtime_manifest} ${quoted_remote_flags}")
245
+ REMOTE_FLAGS_APPLIED=true
246
+ fi
247
+ if [ -n "$WALLET_FIXTURE_ABS" ]; then
248
+ quoted_wallet_fixture="$(printf '%q' "$WALLET_FIXTURE_ABS")"
249
+ prepare_parts+=("bash ${quoted_seed_fixture} prefill --target ${quoted_target} --fixture ${quoted_wallet_fixture} --state ${quoted_fixture_state} --profile ${quoted_profile} --extension-dir ${quoted_runtime_dist} --extension-id-file ${quoted_extension_id_file}")
250
+ fi
251
+ chrome_launch_cmd="node ${quoted_chrome_launcher} --chrome-bin ${quoted_chrome} --profile ${quoted_profile} --cdp-port ${CDP_PORT} --extension-dir ${quoted_runtime_dist} --chrome-log ${quoted_chrome_log} --chrome-pid ${quoted_chrome_pid}"
252
+ prepare_parts+=("$chrome_launch_cmd")
253
+ prepare_parts+=("for i in {1..60}; do curl -fsS --max-time 1 http://127.0.0.1:${CDP_PORT}/json/version >/dev/null 2>&1 && break; sleep 1; done; curl -fsS --max-time 1 http://127.0.0.1:${CDP_PORT}/json/version >/dev/null")
254
+ if [ -n "$WALLET_FIXTURE_ABS" ]; then
255
+ prepare_parts+=("bash ${quoted_seed_fixture} seed-cdp --target ${quoted_target} --fixture ${quoted_wallet_fixture} --state ${quoted_fixture_state} --cdp-port ${CDP_PORT} --extension-dir ${quoted_runtime_dist} --extension-id-file ${quoted_extension_id_file} --out ${quoted_fixture_validation}")
256
+ fi
257
+ PREPARE_CMD="set -euo pipefail; $(IFS='; '; printf '%s' "${prepare_parts[*]}")"
258
+ fi
259
+
260
+ if [ -n "$REMOTE_FLAGS" ] && [ "$REMOTE_FLAGS_APPLIED" != "true" ]; then
261
+ echo "[recipe-harness] WARN: --remote-flag '$REMOTE_FLAGS' had no effect: flag pinning only applies to the snapshot launch path (--launch-existing-dist/--start-watch without a custom --prepare-cmd)." >&2
262
+ fi
263
+
264
+ echo "Extension live validation command:"
265
+ display_args=(mm-harness launch --verify --target "$TARGET")
266
+ $LAUNCH_EXISTING_DIST && display_args+=(--launch-existing-dist)
267
+ $START_WATCH && display_args+=(--start-watch)
268
+ [ -n "$REMOTE_FLAGS" ] && display_args+=(--remote-flag "$REMOTE_FLAGS")
269
+ printf ' '
270
+ printf '%q ' "${display_args[@]}"
271
+ printf '\n'
272
+ echo "Launch artifacts: $ARTIFACTS/launch"
273
+ echo "Verify artifacts: $ARTIFACTS/verify"
274
+
275
+ launch_args=(--target "$TARGET" --cdp-port "$CDP_PORT" --artifacts-dir "$ARTIFACTS/launch")
276
+ [ -n "$PREPARE_CMD" ] && launch_args+=(--prepare-cmd "$PREPARE_CMD")
277
+
278
+ echo "[recipe-harness] phase 1/2: launch (prepare + CDP readiness)" >&2
279
+ set +e
280
+ "$SCRIPT_DIR/launch.sh" "${launch_args[@]}"
281
+ launch_status=$?
282
+ set -e
283
+
284
+ verify_status=1
285
+ if [ "$launch_status" -eq 0 ]; then
286
+ echo "[recipe-harness] phase 2/2: live verify (readiness + smoke recipe)" >&2
287
+ set +e
288
+ "$VERIFY_SH" --target "$TARGET" --out "$OUT" --cdp-port "$CDP_PORT" --artifacts-dir "$ARTIFACTS/verify"
289
+ verify_status=$?
290
+ set -e
291
+ else
292
+ echo "Skipping Extension live verify because launch failed; see $ARTIFACTS/launch/summary.json" >&2
293
+ fi
294
+
295
+ TARGET_FOR_SUMMARY="$TARGET" ARTIFACTS_FOR_SUMMARY="$ARTIFACTS" CDP_PORT_FOR_SUMMARY="$CDP_PORT" LAUNCH_STATUS="$launch_status" VERIFY_STATUS="$verify_status" LAUNCH_EXISTING_DIST="$LAUNCH_EXISTING_DIST" START_WATCH="$START_WATCH" node <<'NODE'
296
+ const fs = require('fs');
297
+ const path = require('path');
298
+ const artifacts = process.env.ARTIFACTS_FOR_SUMMARY;
299
+ const launchSummary = path.join(artifacts, 'launch', 'summary.json');
300
+ const verifySummary = path.join(artifacts, 'verify', 'summary.json');
301
+ const launchStatus = Number(process.env.LAUNCH_STATUS);
302
+ const verifyStatus = Number(process.env.VERIFY_STATUS);
303
+ fs.writeFileSync(path.join(artifacts, 'summary.json'), `${JSON.stringify({
304
+ adapter: 'extension',
305
+ action: 'live',
306
+ status: launchStatus === 0 && verifyStatus === 0 ? 'pass' : 'fail',
307
+ target: process.env.TARGET_FOR_SUMMARY,
308
+ cdpPort: process.env.CDP_PORT_FOR_SUMMARY,
309
+ launchExistingDist: process.env.LAUNCH_EXISTING_DIST === 'true',
310
+ startWatch: process.env.START_WATCH === 'true',
311
+ launch: { exitCode: launchStatus, summaryPath: fs.existsSync(launchSummary) ? launchSummary : null },
312
+ verify: { exitCode: verifyStatus, summaryPath: fs.existsSync(verifySummary) ? verifySummary : null },
313
+ easyCommand: `mm-harness launch --verify --target <repo>`,
314
+ note: 'Runs launch then live verify so a developer can validate browser startup, CDP readiness, recipe bridge, screenshots/fallback classification, and sample recipes from one runner-owned command.',
315
+ generatedAt: new Date().toISOString(),
316
+ }, null, 2)}\n`);
317
+ NODE
318
+
319
+ echo "Extension live validation summary: $ARTIFACTS/summary.json"
320
+ [ "$launch_status" -eq 0 ] && [ "$verify_status" -eq 0 ]
@@ -0,0 +1,45 @@
1
+ #!/usr/bin/env node
2
+ // pin-remote-flags.cjs <manifest.json> <KEY=VARIANT[,KEY=VARIANT...]>
3
+ //
4
+ // Pins A/B remote feature-flag variants into an extension manifest's
5
+ // `_flags.remoteFeatureFlags`. MetaMask resolves the active flags as
6
+ // getRemoteFeatureFlags = merge({}, fetchedState, manifest._flags.remoteFeatureFlags)
7
+ // with the manifest winning, so this deterministically forces a variant in the
8
+ // live browser regardless of the real ClientConfigApi response — which otherwise
9
+ // overwrites any seeded controller state on refetch.
10
+ //
11
+ // Intended for the per-run runtime-dist SNAPSHOT (ephemeral, re-created from
12
+ // dist/chrome on every launch), so it never mutates the real dist/chrome build
13
+ // and needs no revert bookkeeping. Values are written as { name: VARIANT }
14
+ // (named-variant A/B flags); boolean/other flag shapes are out of scope.
15
+ 'use strict';
16
+
17
+ const fs = require('node:fs');
18
+
19
+ const [, , manifestPath, spec] = process.argv;
20
+ if (!manifestPath || !spec) {
21
+ throw new Error('usage: pin-remote-flags.cjs <manifest.json> <KEY=VARIANT[,KEY=VARIANT...]>');
22
+ }
23
+
24
+ const pairs = spec
25
+ .split(',')
26
+ .map((entry) => entry.trim())
27
+ .filter(Boolean);
28
+ if (pairs.length === 0) throw new Error(`--remote-flag had no KEY=VARIANT pairs: ${spec}`);
29
+
30
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
31
+ manifest._flags = manifest._flags || {};
32
+ const flags = manifest._flags.remoteFeatureFlags || (manifest._flags.remoteFeatureFlags = {});
33
+
34
+ const applied = [];
35
+ for (const pair of pairs) {
36
+ const eq = pair.indexOf('=');
37
+ const key = eq > 0 ? pair.slice(0, eq) : '';
38
+ const variant = eq > 0 ? pair.slice(eq + 1) : '';
39
+ if (!key || !variant) throw new Error(`--remote-flag expects KEY=VARIANT, got: ${pair}`);
40
+ flags[key] = { name: variant };
41
+ applied.push(`${key}=${variant}`);
42
+ }
43
+
44
+ fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
45
+ console.log(`[flags] Pinned ${applied.join(', ')} into ${manifestPath}`);