@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,1181 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ usage() {
5
+ cat >&2 <<'EOF'
6
+ Usage:
7
+ mme-recipe home status screen (default)
8
+ mme-recipe status [--json] readiness summary + suggested next commands
9
+ mme-recipe ports [--json] slot ports, runtime paths, tmux hint
10
+ mme-recipe up [--json] [--dry-run] decide + run minimum work to reach ready
11
+ mme-recipe sync [--json] refresh harness scripts + canonicalize wallet fixture
12
+ mme-recipe completion [install] install shell tab completion (bash + zsh)
13
+ mme-recipe prepare install harness and optionally validate CDP health
14
+ --target <repo> --cdp-port <port> --runtime-dir <dir> [--validate]
15
+ mme-recipe runtime-status [--json] structured decision/health status
16
+ mme-recipe decision runtime-decision (what next?)
17
+ mme-recipe update perform the recommended runtime action
18
+ mme-recipe ready ensure one healthy extension home tab
19
+ mme-recipe watch start/reuse one webpack watcher for fast refresh
20
+ mme-recipe stop stop this checkout's watcher
21
+ mme-recipe logs [--full] tail last build events (default) or full webpack log
22
+ mme-recipe rebuild clean webpack + browser launch + verify (2-8 min)
23
+ mme-recipe reset alias for rebuild
24
+ mme-recipe reload alias for rebuild (browser refresh semantics)
25
+ mme-recipe reopen|launch aliases for rebuild
26
+ mme-recipe refresh fast refresh using an active watcher only
27
+ mme-recipe build rebuild when no watcher; refresh when watcher is active
28
+ mme-recipe refresh:once one-shot refresh-build helper, then ready
29
+ mme-recipe sidepanel [cycle|open] use harness sidepanel helper
30
+ mme-recipe debug [--json] [worker] open Chrome DevTools for extension UI or worker
31
+ mme-recipe interactive interactive menu
32
+ mme-recipe actions [--json]
33
+ mme-recipe doctor [--json]
34
+ mme-recipe run <recipe.json> [args] run recipe with detected CDP/env
35
+ mme-recipe <recipe.json> [args] shortcut for run
36
+
37
+ Detects Extension root, recipe runtime context, CDP_PORT, WATCHER_PORT.
38
+ EOF
39
+ recipe_speed_ladder
40
+ }
41
+
42
+ script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
43
+ runner="$script_dir/metamask-recipe"
44
+ # This script lives at <root>/orchestration/porcelain/; the runner root is two up.
45
+ runner_root="$(cd "$script_dir/../.." && pwd -P)"
46
+ # shellcheck disable=SC1091
47
+ . "$runner_root/orchestration/lib/harness-path.sh"
48
+ . "$runner_root/orchestration/lib/cli-ux.sh"
49
+ # shellcheck disable=SC1091
50
+ . "$runner_root/orchestration/lib/resolve-farmslot-ports.sh"
51
+ RECIPE_RUNNER_ROOT="$runner_root"
52
+ install_script="$runner_root/orchestration/extension/inject.mjs"
53
+ open_debug_script="$runner_root/orchestration/lib/open-debug.mjs"
54
+
55
+ find_extension_root() {
56
+ local dir="$(pwd -P)"
57
+ while [ "$dir" != "/" ]; do
58
+ if [ -f "$dir/package.json" ] && { grep -q 'metamask-extension' "$dir/package.json" 2>/dev/null || [[ "$(basename "$dir")" == metamask-extension* ]]; }; then
59
+ printf '%s\n' "$dir"
60
+ return 0
61
+ fi
62
+ dir="$(dirname "$dir")"
63
+ done
64
+ return 1
65
+ }
66
+
67
+ json_field() {
68
+ local file="$1" field="$2"
69
+ [ -f "$file" ] || return 1
70
+ node - "$file" "$field" <<'NODE'
71
+ const fs = require('fs');
72
+ const [file, field] = process.argv.slice(-2);
73
+ try {
74
+ const data = JSON.parse(fs.readFileSync(file, 'utf8'));
75
+ const value = field.split('.').reduce((node, key) => node?.[key], data);
76
+ if (value !== undefined && value !== null && value !== '') process.stdout.write(String(value));
77
+ } catch { process.exit(1); }
78
+ NODE
79
+ }
80
+
81
+ load_env_file() {
82
+ local file="$1"
83
+ [ -f "$file" ] || return 0
84
+ while IFS= read -r line || [ -n "$line" ]; do
85
+ line="${line#export }"
86
+ case "$line" in
87
+ CDP_PORT=*|RECIPE_CDP_PORT=*|WATCHER_PORT=*|RECIPE_WATCHER_PORT=*)
88
+ key="${line%%=*}"
89
+ val="${line#*=}"
90
+ val="${val%%#*}"
91
+ val="$(printf '%s' "$val" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' -e 's/^"//' -e 's/"$//' -e "s/^'//" -e "s/'$//")"
92
+ [ -n "${!key:-}" ] || export "$key=$val"
93
+ ;;
94
+ esac
95
+ done < "$file"
96
+ }
97
+
98
+ infer_slot_number() {
99
+ local root="$1" base
100
+ base="$(basename "$root")"
101
+ [[ "$base" =~ -([0-9]+)$ ]] && printf '%s\n' "${BASH_REMATCH[1]}"
102
+ }
103
+
104
+ ensure_extension_node() {
105
+ local arn
106
+ for arn in \
107
+ "$runner_root/orchestration/lib/activate-repo-node.sh" \
108
+ "$root/$harness_root/extension/scripts/lib/activate-repo-node.sh"; do
109
+ [ -f "$arn" ] || continue
110
+ # shellcheck disable=SC1090
111
+ . "$arn"
112
+ break
113
+ done
114
+ if command -v require_repo_node >/dev/null 2>&1; then
115
+ require_repo_node "$root"
116
+ return $?
117
+ fi
118
+ echo "mme-recipe: activate-repo-node.sh missing; run: recipe sync" >&2
119
+ return 1
120
+ }
121
+
122
+ write_extension_runtime_context() {
123
+ local farmslot="${FARMSLOT_ROOT:-$HOME/dev/farmslot}"
124
+ local slot_id="${RECIPE_SLOT_ID:-}"
125
+ [ -n "$slot_id" ] || slot_id="$(json_field "$runtime_json" slotId 2>/dev/null || true)"
126
+ [ -n "$slot_id" ] || { [ -n "${slot:-}" ] && slot_id="local-extension-${slot}"; }
127
+ apply_resolved_extension_ports "$root"
128
+ [ -n "${CDP_PORT:-}" ] || return 0
129
+ [ -n "${WATCHER_PORT:-}" ] || return 0
130
+ if [ -x "$farmslot/scripts/write-runtime-context.sh" ]; then
131
+ bash "$farmslot/scripts/write-runtime-context.sh" \
132
+ --repo "$root" \
133
+ --slot-id "${slot_id:-local-extension-${slot:-1}}" \
134
+ --runtime-dir "$runtime_dir" \
135
+ --platform chrome-extension \
136
+ --cdp-port "$CDP_PORT" \
137
+ --watcher-port "$WATCHER_PORT" \
138
+ --strict false >/dev/null
139
+ return 0
140
+ fi
141
+ mkdir -p "$root/$runtime_dir"
142
+ cat > "$root/$runtime_dir/agentic-runtime.json" <<JSON
143
+ {
144
+ "slotId": "${slot_id:-local-extension-${slot:-1}}",
145
+ "cdpPort": ${CDP_PORT},
146
+ "watcherPort": ${WATCHER_PORT}
147
+ }
148
+ JSON
149
+ cat > "$root/$runtime_dir/agentic-runtime.env" <<ENV
150
+ # Generated by mme-recipe sync
151
+ export CDP_PORT='${CDP_PORT}'
152
+ export RECIPE_CDP_PORT='${CDP_PORT}'
153
+ export WATCHER_PORT='${WATCHER_PORT}'
154
+ export RECIPE_WATCHER_PORT='${WATCHER_PORT}'
155
+ ENV
156
+ }
157
+
158
+ init_context() {
159
+ root="$(find_extension_root)" || { echo "mme-recipe: run from a metamask-extension checkout" >&2; exit 2; }
160
+ runtime_dir="$(recipe_runtime_dir)"
161
+ harness_root="$(harness_root)"
162
+ export RECIPE_RUNTIME_DIR="$runtime_dir" RECIPE_HARNESS_ROOT="$harness_root"
163
+ runtime_json="$root/$runtime_dir/recipe-runtime.json"
164
+ [ -f "$runtime_json" ] || runtime_json="$root/$runtime_dir/agentic-runtime.json"
165
+ load_env_file "$root/$runtime_dir/recipe-runtime.env"
166
+ load_env_file "$root/$runtime_dir/agentic-runtime.env"
167
+ load_env_file "$root/.js.env"
168
+ load_env_file "$root/.env"
169
+ load_env_file "$root/.env.local"
170
+ [ -n "${CDP_PORT:-}" ] || CDP_PORT="$(json_field "$runtime_json" cdpPort || true)"
171
+ [ -n "${WATCHER_PORT:-}" ] || WATCHER_PORT="$(json_field "$runtime_json" watcherPort || json_field "$runtime_json" devServerPort || true)"
172
+ slot="$(infer_slot_number "$root" || true)"
173
+ apply_resolved_extension_ports "$root"
174
+ if [ -n "$slot" ]; then
175
+ [ -n "${CDP_PORT:-}" ] || CDP_PORT="$((6660 + slot))"
176
+ [ -n "${WATCHER_PORT:-}" ] || WATCHER_PORT="$((9010 + slot))"
177
+ fi
178
+ export CDP_PORT RECIPE_CDP_PORT="${CDP_PORT:-}" WATCHER_PORT RECIPE_WATCHER_PORT="${WATCHER_PORT:-}"
179
+ mkdir -p "$root/temp/recipe-artifacts"
180
+ }
181
+
182
+
183
+ pretty_json() {
184
+ node -e '
185
+ let d="";
186
+ process.stdin.on("data", c => d += c).on("end", () => {
187
+ const color = process.env.RECIPE_COLOR === "1";
188
+ const c = (code, s) => color ? `\x1b[${code}m${s}\x1b[0m` : s;
189
+ try {
190
+ const v = JSON.parse(d);
191
+ const printDecision = (value) => {
192
+ const decision = value && typeof value === "object" ? value.decision : value;
193
+ if (!decision) return false;
194
+ const reasonCode = value && typeof value === "object" ? value.reasonCode : undefined;
195
+ const reasons = value && typeof value === "object" ? value.reasons : undefined;
196
+ console.log(`${c("1;33", "Decision")}: ${c("1;32", decision)}${reasonCode ? ` ${c("2", "-")} ${c("0;37", reasonCode)}` : ""}`);
197
+ if (Array.isArray(reasons)) for (const r of reasons) console.log(` ${c("2", "-")} ${r}`);
198
+ return true;
199
+ };
200
+ if (v.schemaVersion === 1 && v.status && v.cdpPort !== undefined) {
201
+ const status = String(v.status).toUpperCase();
202
+ const code = status === "PASS" ? "1;32" : "1;31";
203
+ console.log(`${c(code, status)} ${c("1;36", "runtime-launch")} ${c("2", `cdp=${v.cdpPort}`)}`);
204
+ if (v.reason) console.log(` ${c("2", "reason")}: ${c("0;37", v.reason)}`);
205
+ if (v.fix) console.log(` ${c("1;33", "fix")}: ${v.fix}`);
206
+ if (v.artifactsDir) console.log(` ${c("2", "artifacts")}: ${v.artifactsDir}`);
207
+ return;
208
+ }
209
+ if (v.schemaVersion === 1 && v.adapter && typeof v.ready === "boolean") {
210
+ const label = v.ready ? "READY" : "NOT READY";
211
+ const code = v.ready ? "1;32" : "1;33";
212
+ console.log(`${c(code, label)} ${c("1;36", v.adapter)}${v.reason ? ` ${c("2", "-")} ${c("0;37", v.reason)}` : ""}`);
213
+ if (v.cdp && v.cdp.port) console.log(`${c("1;36", "CDP")}: ${v.cdp.port}${v.cdp.health && v.cdp.health.status ? ` ${c("2", `(${v.cdp.health.status})`)}` : ""}`);
214
+ if (v.watcher && v.watcher.port) console.log(`${c("1;36", "Watcher")}: ${v.watcher.port}`);
215
+ if (v.fixture) console.log(`${c("1;36", "Fixture")}: ${v.fixture.present ? "present" : "missing"}${v.fixture.path ? ` ${c("2", v.fixture.path)}` : ""}`);
216
+ if (v.decision) printDecision(v.decision);
217
+ if (Array.isArray(v.errors) && v.errors.length) for (const e of v.errors) console.log(`${c("1;31", "Error")}: ${e}`);
218
+ return;
219
+ }
220
+ if (v.decision) {
221
+ printDecision(v);
222
+ return;
223
+ }
224
+ if (v.adapter && Array.isArray(v.actions) && v.actions.every(a => a && a.name)) {
225
+ console.log(`${c("1;36", "Actions")} ${c("2", `(${v.adapter})`)}:`);
226
+ for (const a of v.actions) {
227
+ console.log(` ${c("1;32", a.name)}${a.description ? ` ${c("2", "-")} ${c("0;37", a.description)}` : ""}`);
228
+ }
229
+ return;
230
+ }
231
+ if (v.status && v.adapter) {
232
+ const status = String(v.status).toUpperCase();
233
+ const code = status === "PASS" || status === "OK" ? "1;32" : status === "FAIL" || status === "ERROR" ? "1;31" : "1;33";
234
+ console.log(`${c(code, status)} ${c("1;36", v.adapter)}${v.compatibilityMode ? ` ${c("2", "-")} ${v.compatibilityMode}` : ""}`);
235
+ return;
236
+ }
237
+ if (v.account || v.route || v.deviceName) {
238
+ console.log(`${c("1;36", "Device")}: ${v.deviceName || "?"}${v.platform ? ` ${c("2", `(${v.platform})`)}` : ""}`);
239
+ console.log(`${c("1;36", "Route")}: ${v.route && v.route.name ? v.route.name : JSON.stringify(v.route || null)}`);
240
+ if (v.account) console.log(`${c("1;36", "Account")}: ${v.account.name || "?"} ${c("2", v.account.address || "")}`);
241
+ return;
242
+ }
243
+ console.log(JSON.stringify(v, null, 2));
244
+ } catch {
245
+ process.stdout.write(d);
246
+ }
247
+ })'
248
+ }
249
+
250
+ json_requested() {
251
+ for arg in "$@"; do [ "$arg" = "--json" ] && return 0; done
252
+ return 1
253
+ }
254
+
255
+ full_logs_requested() {
256
+ for arg in "$@"; do
257
+ case "$arg" in
258
+ --full|-f) return 0 ;;
259
+ esac
260
+ done
261
+ return 1
262
+ }
263
+
264
+ dry_run_requested() {
265
+ for arg in "$@"; do
266
+ case "$arg" in
267
+ --dry-run|--explain|-n) return 0 ;;
268
+ esac
269
+ done
270
+ return 1
271
+ }
272
+
273
+ color_requested() {
274
+ [ "${RECIPE_NO_COLOR:-}" != "1" ] && { [ "${RECIPE_COLOR:-}" = "1" ] || [ "${FORCE_COLOR:-}" = "1" ] || [ -t 1 ]; }
275
+ }
276
+
277
+ pipe_pretty() {
278
+ if color_requested; then (export RECIPE_COLOR=1; pretty_json); else pretty_json; fi
279
+ }
280
+
281
+ run_pretty() {
282
+ if json_requested "$@"; then "$@"; else "$@" | pipe_pretty; fi
283
+ }
284
+
285
+ run_json_pretty() {
286
+ if json_requested "$@"; then "$@"; else "$@" --json | pipe_pretty; fi
287
+ }
288
+ log() { printf 'mme-recipe: %s\n' "$*" >&2; }
289
+ need_cdp() { [ -n "${CDP_PORT:-}" ] || { echo "mme-recipe: CDP_PORT not found" >&2; exit 2; }; }
290
+
291
+ report_open_debug() {
292
+ local payload="$1" want_json="$2"
293
+ if [ "$want_json" = true ]; then
294
+ printf '%s\n' "$payload"
295
+ return 0
296
+ fi
297
+ node - "$payload" <<'NODE'
298
+ const [text] = process.argv.slice(2);
299
+ try {
300
+ const v = JSON.parse(text);
301
+ if (v.ok) {
302
+ const detail = v.method ? ` via ${v.method}` : '';
303
+ const where = v.cdpPort ? ` (cdp ${v.cdpPort})` : '';
304
+ const kind = v.targetKind === 'worker' ? 'service worker' : 'extension page';
305
+ console.log(`mme-recipe: opened ${kind} DevTools${detail}${where}`);
306
+ if (v.target?.url) console.log(`mme-recipe: target ${v.target.url}`);
307
+ if (v.configureHint) console.log(`mme-recipe: chrome://inspect target ${v.configureHint}`);
308
+ if (v.hint) console.log(`mme-recipe: ${v.hint}`);
309
+ process.exit(0);
310
+ }
311
+ console.error(`mme-recipe: ${v.error || 'failed to open debugger'}`);
312
+ if (v.hint) console.error(`mme-recipe: ${v.hint}`);
313
+ process.exit(1);
314
+ } catch (error) {
315
+ console.error(`mme-recipe: ${error.message}`);
316
+ process.exit(1);
317
+ }
318
+ NODE
319
+ }
320
+
321
+ open_debug_ui() {
322
+ init_context
323
+ need_cdp
324
+ local target_kind="page" want_json=false
325
+ while [ "$#" -gt 0 ]; do
326
+ case "$1" in
327
+ --json) want_json=true; shift ;;
328
+ --target)
329
+ target_kind="$2"
330
+ shift 2
331
+ ;;
332
+ worker|service-worker|background)
333
+ target_kind="worker"
334
+ shift
335
+ ;;
336
+ page|ui)
337
+ target_kind="page"
338
+ shift
339
+ ;;
340
+ -h|--help)
341
+ echo "Usage: mme-recipe debug [--json] [worker|page]"
342
+ return 0
343
+ ;;
344
+ *) echo "mme-recipe debug: unknown arg: $1" >&2; return 2 ;;
345
+ esac
346
+ done
347
+ local payload exit_code=0
348
+ payload="$(node "$open_debug_script" --adapter extension --cdp-port "$CDP_PORT" --target "$target_kind" --json 2>/dev/null)" || exit_code=$?
349
+ if [ "$exit_code" -eq 0 ]; then
350
+ report_open_debug "$payload" "$want_json"
351
+ return 0
352
+ fi
353
+ report_open_debug "$payload" "$want_json" || return 1
354
+ }
355
+
356
+ do_completion() {
357
+ local install_script="$runner_root/scripts/install-completions.sh"
358
+ bash "$install_script" "${1:-install}" "${@:2}"
359
+ }
360
+
361
+ do_sync() {
362
+ init_context
363
+ local start=$SECONDS farmslot_slot="" seed_script="" fixture_ready=false summary_file=""
364
+ RECIPE_CLI_PREFIX=mme-recipe
365
+ recipe_progress sync start "refreshing harness + fixture context"
366
+ farmslot_slot="$(json_field "$runtime_json" slotId 2>/dev/null || true)"
367
+ [ -n "$farmslot_slot" ] || { [ -n "${slot:-}" ] && farmslot_slot="local-extension-${slot}"; }
368
+ seed_script="$root/$harness_root/extension/scripts/seed-fixture.sh"
369
+ log "sync: refreshing harness scripts"
370
+ if ! RECIPE_RUNTIME_DIR="$runtime_dir" "$install_script" --target "$root"; then
371
+ recipe_done sync fail "harness inject failed" $((SECONDS - start))
372
+ return 1
373
+ fi
374
+ log "sync: canonicalizing wallet fixture"
375
+ if recipe_sync_wallet_fixture "$root" "$runtime_dir" "${CDP_PORT:-}" "$seed_script" "$farmslot_slot"; then
376
+ fixture_ready=true
377
+ log "sync: fixture ready at ${runtime_dir}/wallet-fixture.json"
378
+ else
379
+ log "sync: fixture missing - create ${runtime_dir}/wallet-fixture.json or set RECIPE_WALLET_FIXTURE"
380
+ fi
381
+ if recipe_check_harness_stale extension "$root" "$harness_root" "$runner_root"; then
382
+ log "sync: warning - harness still reports stale after inject"
383
+ else
384
+ log "sync: harness up to date"
385
+ fi
386
+ log "sync: writing runtime context (cdp=${CDP_PORT:-?} watcher=${WATCHER_PORT:-?})"
387
+ write_extension_runtime_context
388
+ summary_file="$root/$runtime_dir/sync-summary.json"
389
+ mkdir -p "$(dirname "$summary_file")"
390
+ node - "$summary_file" "$fixture_ready" <<'NODE'
391
+ const fs = require('fs');
392
+ const [file, fixtureReady] = process.argv.slice(2);
393
+ fs.writeFileSync(file, `${JSON.stringify({
394
+ schemaVersion: 1,
395
+ adapter: 'extension',
396
+ status: 'pass',
397
+ harnessRefreshed: true,
398
+ fixtureReady: fixtureReady === 'true',
399
+ generatedAt: new Date().toISOString(),
400
+ }, null, 2)}\n`);
401
+ NODE
402
+ if json_requested "$@"; then
403
+ cat "$summary_file"
404
+ fi
405
+ if $fixture_ready; then
406
+ recipe_done sync pass "harness refreshed; fixture ready" $((SECONDS - start))
407
+ else
408
+ recipe_done sync pass "harness refreshed; fixture missing" $((SECONDS - start))
409
+ fi
410
+ return 0
411
+ }
412
+
413
+ prepare_extension() {
414
+ local target="" cdp_port="${CDP_PORT:-${RECIPE_CDP_PORT:-}}" runtime_dir="$(recipe_runtime_dir)" validate=false
415
+ while [ "$#" -gt 0 ]; do
416
+ case "$1" in
417
+ --target) target="$2"; shift 2 ;;
418
+ --cdp-port) cdp_port="$2"; validate=true; shift 2 ;;
419
+ --runtime-dir) runtime_dir="$2"; shift 2 ;;
420
+ --validate) validate=true; shift ;;
421
+ -h|--help)
422
+ echo "Usage: mme-recipe prepare [--target <repo>] [--cdp-port <port>] [--runtime-dir <dir>] [--validate]"
423
+ return 0
424
+ ;;
425
+ *) echo "mme-recipe prepare: unknown arg: $1" >&2; return 2 ;;
426
+ esac
427
+ done
428
+ if [ -n "$target" ]; then
429
+ target="$(cd "$target" && pwd -P)"
430
+ (cd "$target" && RECIPE_RUNTIME_DIR="$runtime_dir" "$install_script" --target "$target")
431
+ else
432
+ init_context
433
+ target="$root"
434
+ RECIPE_RUNTIME_DIR="$runtime_dir" "$install_script" --target "$target"
435
+ fi
436
+ if $validate; then
437
+ [ -n "$cdp_port" ] || { echo "mme-recipe prepare: --validate requires --cdp-port or CDP_PORT" >&2; return 2; }
438
+ run_json_pretty "$runner" runtime-health --adapter extension --target "$target" --cdp-port "$cdp_port"
439
+ fi
440
+ }
441
+
442
+ harness_helper() {
443
+ local field="$1" fallback="$2" rel script manifest
444
+ manifest="$root/$harness_root/extension/manifest.json"
445
+ rel="$(json_field "$manifest" "runtimeHelpers.$field" || true)"
446
+ if [ -n "$rel" ]; then
447
+ case "$rel" in
448
+ /*) script="$rel" ;;
449
+ *) script="$root/$rel" ;;
450
+ esac
451
+ else
452
+ script="$root/$fallback"
453
+ fi
454
+ [ -f "$script" ] || { echo "mme-recipe: missing $field helper. Run: recipe sync" >&2; exit 1; }
455
+ printf '%s\n' "$script"
456
+ }
457
+
458
+ status_home() {
459
+ init_context
460
+ if ! json_requested "$@"; then
461
+ recipe_instant_hint "checking extension runtime..."
462
+ recipe_progress status start "probing CDP, watcher, and readiness"
463
+ fi
464
+ local rs_file="$root/$runtime_dir/runtime-status.json"
465
+ local dec_file="$root/$runtime_dir/.recipe-decision.json"
466
+ local slot_id harness_stale=false
467
+ mkdir -p "$root/$runtime_dir"
468
+ recipe_warn_harness_stale extension "$root" "$harness_root" "$runner_root" && harness_stale=false || harness_stale=true
469
+ runtime_status --json > "$rs_file"
470
+ "$runner" runtime-decision --adapter extension --target "$root" \
471
+ ${CDP_PORT:+--cdp-port "$CDP_PORT"} \
472
+ ${WATCHER_PORT:+--watch-log "$root/$runtime_dir/webpack.log"} \
473
+ --json > "$dec_file" 2>/dev/null || : > "$dec_file"
474
+ slot_id="$(json_field "$runtime_json" slotId || true)"
475
+ local home_args=(
476
+ --adapter extension --target "$root" --runtime-dir "$runtime_dir"
477
+ --cdp-port "${CDP_PORT:-}" --watcher-port "${WATCHER_PORT:-}"
478
+ --runtime-status-file "$rs_file" --decision-file "$dec_file"
479
+ )
480
+ [ "$harness_stale" = true ] && home_args+=(--harness-stale)
481
+ if json_requested "$@"; then
482
+ node "$(recipe_home_script)" "${home_args[@]}" --json
483
+ else
484
+ node "$(recipe_home_script)" "${home_args[@]}"
485
+ fi
486
+ }
487
+
488
+ show_ports() {
489
+ init_context
490
+ local slot_id
491
+ slot_id="$(json_field "$runtime_json" slotId || true)"
492
+ local args=(
493
+ --mode ports --adapter extension --target "$root" --runtime-dir "$runtime_dir"
494
+ --cdp-port "${CDP_PORT:-}" --watcher-port "${WATCHER_PORT:-}" --slot-id "${slot_id:-}"
495
+ )
496
+ if json_requested "$@"; then
497
+ node "$(recipe_home_script)" "${args[@]}" --json
498
+ else
499
+ node "$(recipe_home_script)" "${args[@]}"
500
+ fi
501
+ }
502
+
503
+ status() { status_home "$@"; }
504
+ decision() { init_context; run_json_pretty "$runner" runtime-decision --adapter extension --target "$root" ${CDP_PORT:+--cdp-port "$CDP_PORT"} ${WATCHER_PORT:+--watch-log "$root/$runtime_dir/webpack.log"} "$@"; }
505
+ ready() { init_context; need_cdp; exec "$runner" ensure-ready --adapter extension --target "$root" --cdp-port "$CDP_PORT" "$@"; }
506
+
507
+ decision_field() {
508
+ node - "$1" "$2" <<'NODE'
509
+ const [text, field] = process.argv.slice(2);
510
+ try {
511
+ const value = JSON.parse(text);
512
+ const result = field.split('.').reduce((node, key) => node?.[key], value);
513
+ if (result !== undefined && result !== null) process.stdout.write(String(result));
514
+ } catch {
515
+ process.exit(1);
516
+ }
517
+ NODE
518
+ }
519
+
520
+ runtime_status() {
521
+ local target="" cdp_port="" runtime_dir_arg=""
522
+ while [ "$#" -gt 0 ]; do
523
+ case "$1" in
524
+ --target) target="$2"; shift 2 ;;
525
+ --cdp-port) cdp_port="$2"; shift 2 ;;
526
+ --runtime-dir) runtime_dir_arg="$2"; shift 2 ;;
527
+ --json) shift ;;
528
+ -h|--help)
529
+ echo "Usage: mme-recipe runtime-status [--target <repo>] [--cdp-port <port>] [--runtime-dir <dir>] [--json]"
530
+ return 0
531
+ ;;
532
+ *) echo "mme-recipe runtime-status: unknown arg: $1" >&2; return 2 ;;
533
+ esac
534
+ done
535
+ [ -n "$target" ] && cd "$target"
536
+ [ -n "$runtime_dir_arg" ] && export RECIPE_RUNTIME_DIR="$runtime_dir_arg"
537
+ [ -n "$cdp_port" ] && export CDP_PORT="$cdp_port" RECIPE_CDP_PORT="$cdp_port"
538
+ init_context
539
+ local runtime_abs="$root/$runtime_dir"
540
+ local status_file="$runtime_abs/runtime-status.json"
541
+ mkdir -p "$runtime_abs"
542
+ local decision_json health_json health_exit=0
543
+ decision_json="$("$runner" runtime-decision --adapter extension --target "$root" ${CDP_PORT:+--cdp-port "$CDP_PORT"} ${WATCHER_PORT:+--watch-log "$root/$runtime_dir/webpack.log"} --json)"
544
+ if [ -n "${CDP_PORT:-}" ]; then
545
+ health_json="$("$runner" runtime-health --adapter extension --target "$root" --cdp-port "$CDP_PORT" --json 2>/dev/null)" || health_exit=$?
546
+ else
547
+ health_json=""
548
+ fi
549
+ node - "$root" "$runtime_dir" "${CDP_PORT:-}" "${WATCHER_PORT:-}" "$status_file" "$decision_json" "$health_json" "$health_exit" <<'NODE'
550
+ const fs = require('fs');
551
+ const path = require('path');
552
+ const [target, runtimeDir, cdpPort, watcherPort, statusFile, decisionText, healthText, healthExit] = process.argv.slice(2);
553
+ const parse = (label, text) => {
554
+ if (!text) return { value: null, error: null };
555
+ try {
556
+ return { value: JSON.parse(text), error: null };
557
+ } catch (error) {
558
+ return { value: null, error: `${label} JSON parse failed: ${error.message}` };
559
+ }
560
+ };
561
+ const decisionResult = parse('runtime-decision', decisionText);
562
+ const healthResult = parse('runtime-health', healthText);
563
+ const decision = decisionResult.value;
564
+ const health = healthResult.value;
565
+ const ready = decision?.decision === 'ready' && (!health || health.status === 'PASS');
566
+ function fixtureStatus() {
567
+ const candidates = [
568
+ process.env.RECIPE_WALLET_FIXTURE,
569
+ path.join(target, runtimeDir, 'wallet-fixture.json'),
570
+ path.join(target, 'temp/runtime/wallet-fixture.json'),
571
+ cdpPort ? path.join(target, `temp/.recipe-validation-${cdpPort}/wallet-fixture.json`) : null,
572
+ path.join(target, 'temp/.agent-validation/wallet-fixture.json'),
573
+ ].filter(Boolean);
574
+ const fixturePath = candidates.find((candidate) => fs.existsSync(candidate)) || null;
575
+ return {
576
+ present: Boolean(fixturePath),
577
+ path: fixturePath,
578
+ candidates,
579
+ };
580
+ }
581
+ const status = {
582
+ schemaVersion: 1,
583
+ adapter: 'extension',
584
+ target,
585
+ runtimeDir,
586
+ updatedAt: new Date().toISOString(),
587
+ ready,
588
+ reason: ready ? 'ready' : decision?.reasonCode || health?.findings?.[0] || 'not-ready',
589
+ cdp: { port: cdpPort ? Number(cdpPort) : null, healthExit: Number(healthExit || 0), health },
590
+ watcher: { port: watcherPort ? Number(watcherPort) : null },
591
+ fixture: fixtureStatus(),
592
+ decision,
593
+ errors: [decisionResult.error, healthResult.error].filter(Boolean),
594
+ };
595
+ fs.writeFileSync(statusFile, `${JSON.stringify(status, null, 2)}\n`);
596
+ console.log(JSON.stringify(status, null, 2));
597
+ NODE
598
+ }
599
+
600
+ do_reopen() {
601
+ init_context; need_cdp
602
+ local pids launch_args start=$SECONDS exit_code=0 rebuild_log watch_pid=""
603
+ recipe_progress rebuild start "preparing clean runtime rebuild"
604
+ pids="$(watcher_pids || true)"
605
+ if [ -n "$pids" ]; then
606
+ log "stopping existing watcher before rebuild ($(pids_inline "$pids"))"
607
+ printf '%s\n' "$pids" | xargs kill 2>/dev/null || true
608
+ sleep 2
609
+ pids="$(watcher_pids || true)"
610
+ if [ -n "$pids" ]; then
611
+ log "force stopping existing watcher ($(pids_inline "$pids"))"
612
+ printf '%s\n' "$pids" | xargs kill -9 2>/dev/null || true
613
+ fi
614
+ fi
615
+ rm -f "$root/$runtime_dir/webpack.pid" "$root/$runtime_dir/recipe-harness-webpack.pid"
616
+ rebuild_log="$root/$runtime_dir/rebuild.log"
617
+ mkdir -p "$(dirname "$rebuild_log")"
618
+ : > "$rebuild_log"
619
+ log "rebuild cdp=$CDP_PORT watcher=${WATCHER_PORT:-?}"
620
+ log "phases: (1) clean webpack build (2) dist snapshot (3) browser+fixture (4) live verify"
621
+ log "typical duration 2-8 min; compact events below (full log: ${runtime_dir}/rebuild.log, recipe logs --full)"
622
+ recipe_progress rebuild webpack-compile "clean webpack build starting"
623
+ if [ "$(recipe_log_ui_mode)" != "quiet" ] && ! full_logs_requested "$@"; then
624
+ recipe_watch_rebuild_log "$rebuild_log" &
625
+ watch_pid=$!
626
+ fi
627
+ launch_args=(runtime-launch --adapter extension --target "$root" --cdp-port "$CDP_PORT" --start-watch)
628
+ if json_requested "$@"; then
629
+ env RECIPE_LOG_TUI_PARENT=1 "$runner" "${launch_args[@]}" --json "$@" 2>&1 | tee "$rebuild_log" | pipe_pretty || exit_code=$?
630
+ else
631
+ env RECIPE_LOG_TUI_PARENT=1 "$runner" "${launch_args[@]}" "$@" 2>&1 | tee "$rebuild_log" || exit_code=$?
632
+ fi
633
+ [ -n "$watch_pid" ] && kill "$watch_pid" 2>/dev/null && wait "$watch_pid" 2>/dev/null || true
634
+ if [ "$exit_code" -eq 0 ]; then
635
+ # The console viewer is created by launch.sh inside runtime-launch; no
636
+ # second create here (avoids killing/truncating the just-opened tab).
637
+ recipe_done rebuild pass "runtime rebuild complete" $((SECONDS - start))
638
+ else
639
+ recipe_done rebuild fail "runtime rebuild failed" $((SECONDS - start))
640
+ node "$(recipe_log_tui_script)" events --log "$rebuild_log" --events "$(recipe_log_events_limit)" >&2 || true
641
+ recipe_failure_playbook rebuild "runtime-launch did not pass" \
642
+ --prefix mme-recipe \
643
+ --log "${runtime_dir}/rebuild.log" \
644
+ --artifact "temp/recipe/harness/extension/runtime-launch/"
645
+ fi
646
+ return "$exit_code"
647
+ }
648
+
649
+ # Live extension-console viewer tab: tail the running extension's service worker
650
+ # + UI-page console in a tmux window, mirroring the webpack tab. Guarded so a
651
+ # missing tmux/session/CDP is a silent no-op (never affects the bringup result).
652
+ start_console_viewer_window() {
653
+ local cdp_port="${1:-}" session window console_tail log_file hr
654
+ # Numeric-validate before interpolating into the tmux shell command.
655
+ case "$cdp_port" in ''|*[!0-9]*) return 0 ;; esac
656
+ command -v tmux >/dev/null 2>&1 || return 0
657
+ hr="$(harness_root 2>/dev/null || true)"
658
+ [ -n "$hr" ] || return 0
659
+ console_tail="$root/$hr/extension/scripts/console-tail.mjs"
660
+ [ -f "$console_tail" ] || return 0
661
+ session="${RECIPE_TMUX_SESSION:-}"
662
+ if [ -z "$session" ] && [ -n "${slot:-}" ]; then session="mme-$slot"; fi
663
+ [ -n "$session" ] || return 0
664
+ tmux has-session -t "$session" 2>/dev/null || return 0
665
+ window="console-${cdp_port}"
666
+ log_file="$root/$runtime_dir/extension-console.log"
667
+ mkdir -p "$(dirname "$log_file")"
668
+ : > "$log_file"
669
+ tmux kill-window -t "${session}:${window}" >/dev/null 2>&1 || true
670
+ tmux new-window -d -t "$session" -n "$window" "exec node $(printf '%q' "$console_tail") --cdp-port ${cdp_port} --log $(printf '%q' "$log_file")"
671
+ log "extension console viewer in tmux window ${session}:${window}"
672
+ }
673
+
674
+ # Close the webpack `tail -F` viewer window paired with the watcher do_stop kills.
675
+ # Guarded no-op when there is no tmux/session/port. The console viewer is NOT
676
+ # closed here: it tails the running browser, which `recipe stop` leaves up.
677
+ close_webpack_viewer_window() {
678
+ command -v tmux >/dev/null 2>&1 || return 0
679
+ local session="${RECIPE_TMUX_SESSION:-}"
680
+ if [ -z "$session" ] && [ -n "${slot:-}" ]; then session="mme-$slot"; fi
681
+ [ -n "$session" ] || return 0
682
+ # Match the open-side name (do_watch/start-watch use the same :-default fallback).
683
+ tmux kill-window -t "${session}:webpack-${WATCHER_PORT:-default}" >/dev/null 2>&1 || true
684
+ }
685
+
686
+ do_up() {
687
+ init_context
688
+ local start=$SECONDS report action exit_code=0
689
+ recipe_progress up start "evaluating extension runtime"
690
+ report="$("$runner" runtime-decision --adapter extension --target "$root" \
691
+ ${CDP_PORT:+--cdp-port "$CDP_PORT"} \
692
+ ${WATCHER_PORT:+--watch-log "$root/$runtime_dir/webpack.log"} --json)"
693
+ action="$(decision_field "$report" decision || true)"
694
+ if dry_run_requested "$@"; then
695
+ if json_requested "$@"; then
696
+ printf '%s\n' "$report"
697
+ else
698
+ printf '%s\n' "$report" | pipe_pretty
699
+ fi
700
+ case "$action" in
701
+ ready) log "dry-run: would run ensure-ready (seconds)" ;;
702
+ relaunch|build) log "dry-run: would run recipe rebuild (2-8 min)" ;;
703
+ install) log "dry-run: would run yarn install --immutable, then re-check" ;;
704
+ blocked|unknown|'') log "dry-run: blocked — inspect recipe status" ;;
705
+ *) log "dry-run: unsupported decision ($action)" ;;
706
+ esac
707
+ return 0
708
+ fi
709
+ if json_requested "$@"; then
710
+ printf '%s\n' "$report"
711
+ else
712
+ printf '%s\n' "$report" | pipe_pretty
713
+ fi
714
+ case "$action" in
715
+ ready)
716
+ if [ -n "${CDP_PORT:-}" ]; then
717
+ recipe_progress up ready "ensuring healthy extension tab"
718
+ if json_requested "$@"; then
719
+ "$runner" ensure-ready --adapter extension --target "$root" --cdp-port "$CDP_PORT" --json || exit_code=$?
720
+ else
721
+ "$runner" ensure-ready --adapter extension --target "$root" --cdp-port "$CDP_PORT" || exit_code=$?
722
+ fi
723
+ fi
724
+ if [ "$exit_code" -eq 0 ]; then
725
+ start_console_viewer_window "${CDP_PORT:-}"
726
+ recipe_done up pass "runtime ready" $((SECONDS - start))
727
+ else
728
+ recipe_done up fail "ensure-ready failed" $((SECONDS - start))
729
+ recipe_failure_playbook up "ensure-ready failed" --prefix mme-recipe --log "${runtime_dir}/webpack.log"
730
+ fi
731
+ return "$exit_code"
732
+ ;;
733
+ relaunch|build)
734
+ recipe_progress up "$action" "running rebuild path"
735
+ do_reopen "$@" || exit_code=$?
736
+ if [ "$exit_code" -eq 0 ]; then
737
+ recipe_done up pass "rebuild finished" $((SECONDS - start))
738
+ fi
739
+ return "$exit_code"
740
+ ;;
741
+ install)
742
+ if [ "${RECIPE_UP_INSTALL_ATTEMPTED:-0}" = "1" ]; then
743
+ local reason_code=""
744
+ report="$("$runner" runtime-decision --adapter extension --target "$root" \
745
+ ${CDP_PORT:+--cdp-port "$CDP_PORT"} \
746
+ ${WATCHER_PORT:+--watch-log "$root/$runtime_dir/webpack.log"} --json)"
747
+ reason_code="$(decision_field "$report" reasonCode || true)"
748
+ recipe_done up fail "install did not resolve runtime (${reason_code:-install})" $((SECONDS - start))
749
+ if json_requested "$@"; then
750
+ printf '%s\n' "$report"
751
+ else
752
+ printf '%s\n' "$report" | pipe_pretty
753
+ fi
754
+ recipe_failure_playbook up "yarn install completed but runtime-decision is still install" \
755
+ --prefix mme-recipe
756
+ return 2
757
+ fi
758
+ recipe_progress up install "installing dependencies"
759
+ export RECIPE_UP_INSTALL_ATTEMPTED=1
760
+ if ! (cd "$root" && yarn install --immutable); then
761
+ recipe_done up fail "yarn install failed" $((SECONDS - start))
762
+ return 1
763
+ fi
764
+ "$runner" runtime-decision --adapter extension --target "$root" \
765
+ ${CDP_PORT:+--cdp-port "$CDP_PORT"} \
766
+ ${WATCHER_PORT:+--watch-log "$root/$runtime_dir/webpack.log"} \
767
+ --record-baseline --json >/dev/null
768
+ RECIPE_UP_INSTALL_ATTEMPTED=1 do_up "$@"
769
+ ;;
770
+ blocked|unknown|'')
771
+ recipe_done up fail "blocked (${action:-unknown})" $((SECONDS - start))
772
+ recipe_failure_playbook up "blocked (${action:-unknown})" --prefix mme-recipe
773
+ return 2
774
+ ;;
775
+ *)
776
+ recipe_done up fail "unsupported decision: $action" $((SECONDS - start))
777
+ recipe_failure_playbook up "unsupported decision: $action" --prefix mme-recipe
778
+ return 2
779
+ ;;
780
+ esac
781
+ }
782
+
783
+ do_update() {
784
+ init_context
785
+ local report action
786
+ report="$("$runner" runtime-decision --adapter extension --target "$root" ${CDP_PORT:+--cdp-port "$CDP_PORT"} ${WATCHER_PORT:+--watch-log "$root/$runtime_dir/webpack.log"} --json)"
787
+ printf '%s\n' "$report" | pipe_pretty
788
+ action="$(decision_field "$report" decision || true)"
789
+ case "$action" in
790
+ ready)
791
+ if [ -n "${CDP_PORT:-}" ]; then
792
+ "$runner" ensure-ready --adapter extension --target "$root" --cdp-port "$CDP_PORT" --json | pipe_pretty
793
+ else
794
+ log "runtime decision is ready; CDP_PORT is not set, so no browser action was needed"
795
+ fi
796
+ ;;
797
+ relaunch)
798
+ do_reopen "$@"
799
+ ;;
800
+ build)
801
+ do_reopen "$@"
802
+ ;;
803
+ install)
804
+ log "installing dependencies before runtime update"
805
+ (cd "$root" && yarn install --immutable)
806
+ do_update "$@"
807
+ ;;
808
+ blocked|unknown|'')
809
+ echo "mme-recipe: runtime decision is ${action:-unknown}; inspect with: recipe decision --json" >&2
810
+ return 2
811
+ ;;
812
+ *)
813
+ echo "mme-recipe: unsupported runtime decision: $action" >&2
814
+ return 2
815
+ ;;
816
+ esac
817
+ }
818
+
819
+ watcher_pids() {
820
+ local pids pid_file pid current_pid command
821
+ pids="$(
822
+ ps -axo pid=,command= | while read -r current_pid command; do
823
+ [ -n "${current_pid:-}" ] || continue
824
+ case "$command" in
825
+ *"yarn start"*|*"webpack --watch"*|*"development/webpack/launch.ts --watch"*) ;;
826
+ *) continue ;;
827
+ esac
828
+ if printf '%s' "$command" | grep -Fq "$root"; then
829
+ printf '%s\n' "$current_pid"
830
+ continue
831
+ fi
832
+ if command -v lsof >/dev/null 2>&1; then
833
+ if lsof -a -p "$current_pid" -d cwd -Fn 2>/dev/null | sed -n 's/^n//p' | grep -Fxq "$root"; then
834
+ printf '%s\n' "$current_pid"
835
+ fi
836
+ fi
837
+ done
838
+ true
839
+ )"
840
+ pid_file="$root/$runtime_dir/webpack.pid"
841
+ pid="$(cat "$pid_file" 2>/dev/null || true)"
842
+ if [ -n "$pid" ] && kill -0 "$pid" >/dev/null 2>&1; then
843
+ if ! printf '%s\n' "$pids" | grep -qx "$pid"; then
844
+ pids="$(printf '%s\n%s\n' "$pids" "$pid" | sed '/^$/d')"
845
+ fi
846
+ fi
847
+ printf '%s\n' "$pids" | sed '/^$/d'
848
+ }
849
+
850
+ do_watch() {
851
+ init_context
852
+ local pids log_file pid_file session window log_q compiled elapsed
853
+ pids="$(watcher_pids || true)"
854
+ if [ -n "$pids" ]; then
855
+ log "watcher already running ($(pids_inline "$pids"))"
856
+ return 0
857
+ fi
858
+ log_file="$root/$runtime_dir/webpack.log"
859
+ pid_file="$root/$runtime_dir/webpack.pid"
860
+ mkdir -p "$(dirname "$log_file")"
861
+ rm -f "$pid_file"
862
+ : > "$log_file"
863
+ rm -rf "$root/node_modules/.cache/webpack"
864
+ log "starting watcher port=${WATCHER_PORT:-default} log=${log_file#$root/}"
865
+ ensure_extension_node
866
+ local path_prefix="${REPO_NODE_PATH_PREFIX:-}"
867
+ # Launch the watcher detached writing to the log; the tmux window is a read-only tail viewer.
868
+ (cd "$root" && nohup bash -lc "exec env -u BUNDLED_DEBUGPY_PATH ${path_prefix:+PATH=\"${path_prefix}\$PATH\"} ${WATCHER_PORT:+PORT=\"$WATCHER_PORT\"} yarn start" > "$log_file" 2>&1 & echo $! > "$pid_file")
869
+ if command -v tmux >/dev/null 2>&1; then
870
+ session="${RECIPE_TMUX_SESSION:-}"
871
+ if [ -z "$session" ] && [ -n "${slot:-}" ]; then session="mme-$slot"; fi
872
+ if [ -n "$session" ] && tmux has-session -t "$session" 2>/dev/null; then
873
+ window="webpack-${WATCHER_PORT:-default}"
874
+ log_q="$(printf '%q' "$log_file")"
875
+ tmux kill-window -t "${session}:${window}" >/dev/null 2>&1 || true
876
+ tmux new-window -d -t "$session" -n "$window" "exec tail -n +1 -F $log_q"
877
+ fi
878
+ fi
879
+ compiled=false
880
+ local tail_pid=""
881
+ if full_logs_requested "$@"; then
882
+ tail -f "$log_file" &
883
+ tail_pid=$!
884
+ elif [ "$(recipe_log_ui_mode)" != "quiet" ]; then
885
+ node "$(recipe_log_tui_script)" watch \
886
+ --log "$log_file" \
887
+ --events "$(recipe_log_events_limit)" \
888
+ --mode "$(recipe_log_ui_mode)" \
889
+ --label "webpack watcher" \
890
+ --success-pattern 'compiled successfully|compiled with [0-9]+ warning|MetaMask .* compiled|Bundle end:' &
891
+ tail_pid=$!
892
+ fi
893
+ for elapsed in $(seq 1 240); do
894
+ pids="$(watcher_pids || true)"
895
+ if [ -z "$pids" ]; then
896
+ if [ "$elapsed" -le 10 ]; then
897
+ sleep 2
898
+ continue
899
+ fi
900
+ [ -n "$tail_pid" ] && kill "$tail_pid" >/dev/null 2>&1 || true
901
+ wait "$tail_pid" 2>/dev/null || true
902
+ recipe_failure_playbook watch "watcher exited before compile completed" \
903
+ --prefix mme-recipe --log "${log_file#$root/}"
904
+ node "$(recipe_log_tui_script)" events --log "$log_file" --events 15 >&2 || true
905
+ rm -f "$pid_file"
906
+ return 1
907
+ fi
908
+ printf '%s\n' "$pids" | head -1 > "$pid_file"
909
+ if grep -Eq 'Module build failed|^ERROR in |compiled with [1-9][0-9]* error' "$log_file" 2>/dev/null; then
910
+ [ -n "$tail_pid" ] && kill "$tail_pid" >/dev/null 2>&1 || true
911
+ wait "$tail_pid" 2>/dev/null || true
912
+ recipe_failure_playbook watch "webpack build failed" \
913
+ --prefix mme-recipe --log "${log_file#$root/}"
914
+ node "$(recipe_log_tui_script)" events --log "$log_file" --events 15 >&2 || true
915
+ return 1
916
+ fi
917
+ if grep -Eq 'compiled successfully|compiled with [0-9]+ warning|MetaMask .* compiled|Bundle end: service worker|Bundle end:.*app-init' "$log_file" 2>/dev/null; then
918
+ compiled=true
919
+ [ -n "$tail_pid" ] && kill "$tail_pid" >/dev/null 2>&1 || true
920
+ wait "$tail_pid" 2>/dev/null || true
921
+ break
922
+ fi
923
+ sleep 2
924
+ done
925
+ if [ "$compiled" != true ]; then
926
+ [ -n "$tail_pid" ] && kill "$tail_pid" >/dev/null 2>&1 || true
927
+ wait "$tail_pid" 2>/dev/null || true
928
+ recipe_failure_playbook watch "compile timeout (8 min)" \
929
+ --prefix mme-recipe --log "${log_file#$root/}"
930
+ node "$(recipe_log_tui_script)" events --log "$log_file" --events 15 >&2 || true
931
+ return 1
932
+ fi
933
+ pids="$(watcher_pids || true)"
934
+ printf '%s\n' "$pids" | head -1 > "$pid_file"
935
+ log "watcher compiled ($(pids_inline "$pids")); use: recipe refresh"
936
+ }
937
+
938
+ pids_inline() {
939
+ printf '%s' "$1" | tr '\n' ' '
940
+ }
941
+
942
+ latest_source_stamp() {
943
+ node - "$root" <<'NODE'
944
+ const { execFileSync } = require('child_process');
945
+ const fs = require('fs');
946
+ const path = require('path');
947
+ const root = process.argv[2];
948
+ function lines(args) {
949
+ try { return execFileSync('git', ['-C', root, ...args], { encoding: 'utf8' }).split('\n').filter(Boolean); }
950
+ catch { return []; }
951
+ }
952
+ const files = new Set([
953
+ ...lines(['diff', '--name-only', 'HEAD']),
954
+ ...lines(['ls-files', '--others', '--exclude-standard']),
955
+ ]);
956
+ let best = { mtime: 0, file: '' };
957
+ for (const rel of files) {
958
+ if (!/\.(tsx?|jsx?|mjs|cjs|json|scss|css)$/u.test(rel)) continue;
959
+ const abs = path.join(root, rel);
960
+ if (!fs.existsSync(abs) || !fs.statSync(abs).isFile()) continue;
961
+ const mtime = fs.statSync(abs).mtimeMs;
962
+ if (mtime > best.mtime) best = { mtime, file: rel };
963
+ }
964
+ if (best.mtime) console.log(`${Math.floor(best.mtime)}\t${best.file}`);
965
+ NODE
966
+ }
967
+
968
+ latest_dist_mtime_ms() {
969
+ node - "$root" <<'NODE'
970
+ const fs = require('fs');
971
+ const path = require('path');
972
+ const root = process.argv[2];
973
+ const dir = path.join(root, 'dist/chrome');
974
+ let best = 0;
975
+ function walk(p) {
976
+ if (!fs.existsSync(p)) return;
977
+ for (const name of fs.readdirSync(p)) {
978
+ const abs = path.join(p, name);
979
+ const st = fs.statSync(abs);
980
+ if (st.isDirectory()) walk(abs);
981
+ else if (st.isFile()) best = Math.max(best, st.mtimeMs);
982
+ }
983
+ }
984
+ walk(dir);
985
+ console.log(Math.floor(best));
986
+ NODE
987
+ }
988
+
989
+ do_stop() {
990
+ init_context
991
+ local pids
992
+ pids="$(watcher_pids || true)"
993
+ if [ -z "$pids" ]; then
994
+ log "no watcher running for this checkout"
995
+ rm -f "$root/$runtime_dir/webpack.pid"
996
+ close_webpack_viewer_window
997
+ return 0
998
+ fi
999
+ log "stopping watcher ($(pids_inline "$pids"))"
1000
+ printf '%s
1001
+ ' "$pids" | xargs kill 2>/dev/null || true
1002
+ sleep 2
1003
+ pids="$(watcher_pids || true)"
1004
+ if [ -n "$pids" ]; then
1005
+ log "force stopping watcher ($(pids_inline "$pids"))"
1006
+ printf '%s
1007
+ ' "$pids" | xargs kill -9 2>/dev/null || true
1008
+ fi
1009
+ rm -f "$root/$runtime_dir/webpack.pid"
1010
+ close_webpack_viewer_window
1011
+ log "next: recipe watch | recipe rebuild"
1012
+ }
1013
+
1014
+ tail_logs() {
1015
+ init_context
1016
+ local log_file="$root/$runtime_dir/webpack.log"
1017
+ [ -f "$log_file" ] || { echo "mme-recipe: webpack log missing: $runtime_dir/webpack.log. Start with: recipe watch" >&2; return 1; }
1018
+ if full_logs_requested "$@"; then
1019
+ log "full log: $runtime_dir/webpack.log (Ctrl-C to stop)"
1020
+ tail -f "$log_file"
1021
+ return 0
1022
+ fi
1023
+ log "compact tail — full stream: recipe logs --full"
1024
+ exec node "$(recipe_log_tui_script)" tail \
1025
+ --log "$log_file" \
1026
+ --events "$(recipe_log_events_limit)" \
1027
+ --follow \
1028
+ --mode compact
1029
+ }
1030
+
1031
+ do_refresh_once() {
1032
+ init_context
1033
+ local s
1034
+ s="$(harness_helper refreshBuild "$runtime_dir/refresh-build.sh")"
1035
+ log "one-shot refresh watcher=${WATCHER_PORT:-?}"
1036
+ bash "$s" --repo "$root" ${WATCHER_PORT:+--watcher-port "$WATCHER_PORT"} "$@"
1037
+ if [ -n "${CDP_PORT:-}" ]; then
1038
+ "$runner" ensure-ready --adapter extension --target "$root" --cdp-port "$CDP_PORT" --json || true
1039
+ fi
1040
+ }
1041
+
1042
+ do_build() {
1043
+ init_context
1044
+ local pids
1045
+ pids="$(watcher_pids || true)"
1046
+ if [ -n "$pids" ]; then
1047
+ do_refresh "$@"
1048
+ return $?
1049
+ fi
1050
+ log "build: no active watcher — running full rebuild path"
1051
+ do_reopen "$@"
1052
+ }
1053
+
1054
+ do_refresh() {
1055
+ init_context
1056
+ local pids stamp src_mtime src_file dist_mtime timeout elapsed
1057
+ pids="$(watcher_pids || true)"
1058
+ if [ -z "$pids" ]; then
1059
+ recipe_failure_playbook refresh "no active watcher" --prefix mme-recipe
1060
+ return 2
1061
+ fi
1062
+
1063
+ stamp="$(latest_source_stamp || true)"
1064
+ if [ -z "$stamp" ]; then
1065
+ log "watcher active ($(pids_inline "$pids")); no changed source files"
1066
+ [ -n "${CDP_PORT:-}" ] && "$runner" ensure-ready --adapter extension --target "$root" --cdp-port "$CDP_PORT" --json || true
1067
+ return
1068
+ fi
1069
+ src_mtime="${stamp%%$'\t'*}"
1070
+ src_file="${stamp#*$'\t'}"
1071
+ timeout="${RECIPE_REFRESH_TIMEOUT:-90}"
1072
+ log "watcher active ($(pids_inline "$pids")); waiting for incremental build of $src_file"
1073
+
1074
+ for elapsed in $(seq 0 "$timeout"); do
1075
+ dist_mtime="$(latest_dist_mtime_ms)"
1076
+ if [ "$dist_mtime" -ge "$src_mtime" ]; then
1077
+ log "dist/chrome timestamp is current (${elapsed}s); checking runtime"
1078
+ if [ -n "${CDP_PORT:-}" ]; then
1079
+ local ready_json
1080
+ ready_json="$($runner ensure-ready --adapter extension --target "$root" --cdp-port "$CDP_PORT" --json 2>/dev/null || true)"
1081
+ printf '%s
1082
+ ' "$ready_json"
1083
+ if printf '%s' "$ready_json" | grep -q '"ready"[[:space:]]*:[[:space:]]*true'; then
1084
+ log "runtime ready; run: recipe reopen only if the visible page is stale"
1085
+ return
1086
+ fi
1087
+ recipe_failure_playbook refresh "dist is current but runtime is unhealthy" --prefix mme-recipe
1088
+ return 2
1089
+ fi
1090
+ log "dist/chrome timestamp is current; run: recipe reopen"
1091
+ return
1092
+ fi
1093
+ if [ $elapsed -gt 0 ] && [ $((elapsed % 10)) -eq 0 ]; then
1094
+ log "still waiting for watcher... (${elapsed}s)"
1095
+ fi
1096
+ sleep 1
1097
+ done
1098
+
1099
+ recipe_failure_playbook refresh "watcher did not update dist/chrome within ${timeout}s" \
1100
+ --prefix mme-recipe --log "${runtime_dir}/webpack.log"
1101
+ return 2
1102
+ }
1103
+
1104
+ sidepanel() {
1105
+ init_context; need_cdp
1106
+ local s
1107
+ s="$(harness_helper sidepanelToggle "$runtime_dir/sidepanel-toggle.sh")"
1108
+ bash "$s" "${1:-cycle}" --cdp-port "$CDP_PORT" --agent-dir "$root/$runtime_dir"
1109
+ }
1110
+
1111
+ run_recipe() {
1112
+ init_context; need_cdp
1113
+ local recipe="$1"; shift
1114
+ [ -f "$recipe" ] || { echo "mme-recipe: recipe not found: $recipe" >&2; exit 2; }
1115
+ local out="$root/temp/recipe-artifacts/$(basename "$recipe" .json)-$(date -u +%Y%m%dT%H%M%SZ)"
1116
+ log "root=$root cdp=$CDP_PORT watcher=${WATCHER_PORT:-?} out=$out"
1117
+ exec "$runner" run "$recipe" --adapter extension --project-root "$root" --cdp-port "$CDP_PORT" --artifacts-dir "$out" "$@"
1118
+ }
1119
+
1120
+ interactive() {
1121
+ init_context
1122
+ echo "mme-recipe (${root}, cdp=${CDP_PORT:-?}, watcher=${WATCHER_PORT:-?})"
1123
+ PS3='choose> '
1124
+ select choice in status ports up runtime-status decision update ready rebuild refresh refresh-once watch stop logs debug sidepanel actions doctor quit; do
1125
+ case "$choice" in
1126
+ status) status_home ;;
1127
+ ports) show_ports ;;
1128
+ up) do_up ;;
1129
+ runtime-status) runtime_status | pipe_pretty ;;
1130
+ decision) run_json_pretty "$runner" runtime-decision --adapter extension --target "$root" ${CDP_PORT:+--cdp-port "$CDP_PORT"} ;;
1131
+ update) do_update ;;
1132
+ ready) "$runner" ensure-ready --adapter extension --target "$root" --cdp-port "$CDP_PORT" ;;
1133
+ watch) do_watch ;;
1134
+ stop) do_stop ;;
1135
+ rebuild) do_reopen ;;
1136
+ refresh) do_refresh ;;
1137
+ refresh-once) do_refresh_once ;;
1138
+ logs) tail_logs ;;
1139
+ debug) open_debug_ui ;;
1140
+ sidepanel) sidepanel cycle ;;
1141
+ actions) run_json_pretty "$runner" actions --adapter extension ;;
1142
+ doctor) run_json_pretty "$runner" doctor --adapter extension --target "$root" ;;
1143
+ quit|'') break ;;
1144
+ esac
1145
+ echo
1146
+ done
1147
+ }
1148
+
1149
+ cmd="${1:-status}"
1150
+ [ "$#" -gt 0 ] && shift
1151
+ case "$cmd" in
1152
+ -h|--help) usage; exit 0 ;;
1153
+ interactive|menu) interactive ;;
1154
+ sync) do_sync "$@" ;;
1155
+ completion|complete) do_completion "$@" ;;
1156
+ prepare) prepare_extension "$@" ;;
1157
+ status|health|home) status_home "$@" ;;
1158
+ ports) show_ports "$@" ;;
1159
+ up) do_up "$@" ;;
1160
+ runtime-status) if json_requested "$@"; then runtime_status "$@"; else runtime_status "$@" | pipe_pretty; fi ;;
1161
+ decision|decide) decision "$@" ;;
1162
+ update|sync-runtime) do_update "$@" ;;
1163
+ ready|ensure-ready) ready "$@" ;;
1164
+ watch|start-watch|watcher) do_watch "$@" ;;
1165
+ stop|stop-watch|stop-watcher) do_stop "$@" ;;
1166
+ logs|tail|tail-webpack) tail_logs "$@" ;;
1167
+ rebuild|reset|reopen|reload|browser|launch|full-launch|runtime-launch)
1168
+ [ "$cmd" = "reload" ] && log "reload runs a full rebuild (not a tab refresh); for fast updates use: recipe refresh"
1169
+ do_reopen "$@"
1170
+ ;;
1171
+ refresh) do_refresh "$@" ;;
1172
+ build) do_build "$@" ;;
1173
+ refresh:once|refresh-once|build:once|build-once) do_refresh_once "$@" ;;
1174
+ debug|devtools|inspect) open_debug_ui "$@" ;;
1175
+ sidepanel) sidepanel "$@" ;;
1176
+ actions) init_context; run_json_pretty "$runner" actions --adapter extension "$@" ;;
1177
+ doctor) init_context; run_json_pretty "$runner" doctor --adapter extension --target "$root" "$@" ;;
1178
+ run) recipe="${1:-}"; [ -n "$recipe" ] || { usage; exit 2; }; shift; run_recipe "$recipe" "$@" ;;
1179
+ *.json) run_recipe "$cmd" "$@" ;;
1180
+ *) echo "mme-recipe: unknown command or missing recipe: $cmd" >&2; usage; exit 2 ;;
1181
+ esac