@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,291 @@
1
+ #!/usr/bin/env bash
2
+ # sidepanel-toggle.sh — window-mode control for the extension instance.
3
+ #
4
+ # Purpose:
5
+ # App control: drives chrome.sidePanel for one extension instance's CDP
6
+ # browser (status/open/close/toggle/cycle) without global shortcuts.
7
+ #
8
+ # Inputs: subcommand (status|open|close|toggle|cycle, default status);
9
+ # --cdp-port <port> (required), --ext-id <id>, --agent-dir <dir>,
10
+ # --settle-ms <ms>; env REPO, CDP_PORT, EXT_ID, SETTLE_MS, AGENT_DIR.
11
+ # Outputs: status/progress lines on stdout.
12
+ # Exit 0 — action done; 1 — repo/args missing or action failed;
13
+ # 2 — non-numeric port / CDP unreachable.
14
+ # Never touches: other instances' browsers (single CDP port scope); the
15
+ # dist build (read-only).
16
+ #
17
+ # Usage:
18
+ # bash sidepanel-toggle.sh status --cdp-port 6663
19
+ # bash sidepanel-toggle.sh open --cdp-port 6663 [--ext-id <extension id>]
20
+ # bash sidepanel-toggle.sh close --cdp-port 6663
21
+ # bash sidepanel-toggle.sh toggle --cdp-port 6663
22
+ # bash sidepanel-toggle.sh cycle --cdp-port 6663 [--settle-ms 4000]
23
+ #
24
+ # CDP can inspect, activate, and close sidepanel targets. Opening the side panel
25
+ # is done from a clicked extension-page button so Chrome treats
26
+ # `chrome.sidePanel.open()` as a user gesture while the action remains scoped to
27
+ # this slot's CDP browser. This avoids global Alt+Shift+M shortcut ambiguity
28
+ # when several Chromium profiles are running.
29
+ set -euo pipefail
30
+
31
+ ACTION="${1:-status}"
32
+ if [ "$ACTION" = "-h" ] || [ "$ACTION" = "--help" ]; then
33
+ echo "Usage: sidepanel-toggle.sh <status|open|close|toggle|cycle> --cdp-port <port> [--ext-id <id>] [--agent-dir <dir>] [--settle-ms <ms>]"
34
+ exit 0
35
+ fi
36
+ shift || true
37
+
38
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
39
+ REPO="${REPO:-}"
40
+ if [ -z "$REPO" ]; then
41
+ d="$SCRIPT_DIR"
42
+ while [ "$d" != "/" ]; do
43
+ if [ -f "$d/package.json" ] && [ -d "$d/dist/chrome" ]; then
44
+ REPO="$d"
45
+ break
46
+ fi
47
+ d="$(dirname "$d")"
48
+ done
49
+ fi
50
+ if [ -z "$REPO" ]; then
51
+ echo "FAIL: could not resolve repo root from ${SCRIPT_DIR}; pass REPO=/path/to/metamask-extension" >&2
52
+ exit 1
53
+ fi
54
+ cd "$REPO"
55
+
56
+ CDP_PORT="${CDP_PORT:-}"
57
+ EXT_ID="${EXT_ID:-}"
58
+ SETTLE_MS="${SETTLE_MS:-4000}"
59
+ AGENT_DIR="${AGENT_DIR:-$SCRIPT_DIR}"
60
+
61
+ while [[ $# -gt 0 ]]; do
62
+ case "$1" in
63
+ --cdp-port) CDP_PORT="$2"; shift 2 ;;
64
+ --ext-id) EXT_ID="$2"; shift 2 ;;
65
+ --agent-dir) AGENT_DIR="$2"; shift 2 ;;
66
+ --settle-ms) SETTLE_MS="$2"; shift 2 ;;
67
+ -h|--help)
68
+ echo "Usage: sidepanel-toggle.sh <status|open|close|toggle|cycle> --cdp-port <port> [--ext-id <id>] [--agent-dir <dir>] [--settle-ms <ms>]"
69
+ exit 0
70
+ ;;
71
+ *) echo "unknown arg: $1" >&2; exit 1 ;;
72
+ esac
73
+ done
74
+
75
+ if [ -z "$CDP_PORT" ]; then
76
+ echo "FAIL: --cdp-port is required" >&2
77
+ exit 1
78
+ fi
79
+ if ! [[ "$CDP_PORT" =~ ^[0-9]+$ ]]; then
80
+ echo "FAIL: CDP port \"$CDP_PORT\" must be numeric" >&2
81
+ exit 2
82
+ fi
83
+ if ! curl -s -m 3 "http://127.0.0.1:${CDP_PORT}/json/version" >/dev/null 2>&1; then
84
+ echo "FAIL: CDP not reachable on port ${CDP_PORT}" >&2
85
+ exit 2
86
+ fi
87
+
88
+ json_list() {
89
+ curl -s "http://127.0.0.1:${CDP_PORT}/json/list"
90
+ }
91
+
92
+ find_sidepanel_id() {
93
+ json_list | python3 -c "import json,sys; d=json.load(sys.stdin); ids=[t.get('id','') for t in d if t.get('type')=='page' and 'sidepanel.html' in t.get('url','')]; print(ids[0] if ids else '')"
94
+ }
95
+
96
+ resolve_ext_id() {
97
+ if [ -n "$EXT_ID" ]; then
98
+ printf '%s\n' "$EXT_ID"
99
+ return
100
+ fi
101
+ if [ -f "$AGENT_DIR/extension.id" ]; then
102
+ tr -d '[:space:]' < "$AGENT_DIR/extension.id"
103
+ return
104
+ fi
105
+ json_list | python3 -c "import json,re,sys; d=json.load(sys.stdin); ids=[]; [ids.extend(re.findall(r'^chrome-extension://([^/]+)/', t.get('url',''))) for t in d]; print(ids[0] if ids else '')"
106
+ }
107
+
108
+ activate_sandbox_page() {
109
+ local ext_id target_id
110
+ ext_id="$(resolve_ext_id)"
111
+ target_id="$(
112
+ EXT_ID="$ext_id" json_list | python3 -c "import json,os,sys; d=json.load(sys.stdin); ext=os.environ.get('EXT_ID',''); pages=[t for t in d if t.get('type')=='page']; target=next((t for t in pages if ext and t.get('url','').startswith('chrome-extension://'+ext+'/') and 'sidepanel.html' not in t.get('url','')), None) or next((t for t in pages if t.get('url','').startswith('http://') or t.get('url','').startswith('https://')), None) or (pages[0] if pages else None); print(target.get('id','') if target else '')"
113
+ )"
114
+ if [ -n "$target_id" ]; then
115
+ curl -s "http://127.0.0.1:${CDP_PORT}/json/activate/${target_id}" >/dev/null || true
116
+ fi
117
+ }
118
+
119
+ now_ms() {
120
+ python3 -c "import time; print(int(time.time() * 1000))"
121
+ }
122
+
123
+ wait_for_sidepanel() {
124
+ local start deadline now
125
+ start="$(now_ms)"
126
+ deadline=$((start + SETTLE_MS))
127
+ while :; do
128
+ if [ -n "$(find_sidepanel_id)" ]; then
129
+ return 0
130
+ fi
131
+ now="$(now_ms)"
132
+ if [ "$now" -ge "$deadline" ]; then
133
+ return 1
134
+ fi
135
+ sleep 0.2
136
+ done
137
+ }
138
+
139
+ print_targets() {
140
+ json_list | python3 -c "import json,sys; d=json.load(sys.stdin); [print(f' {t.get(\"type\",\"?\")[:18]:18s} {t.get(\"url\",\"\")[:100]}') for t in d]"
141
+ }
142
+
143
+ status_sidepanel() {
144
+ local sp
145
+ sp="$(find_sidepanel_id)"
146
+ if [ -n "$sp" ]; then
147
+ echo "[sidepanel] open target ${sp}"
148
+ else
149
+ echo "[sidepanel] closed"
150
+ fi
151
+ }
152
+
153
+ close_sidepanel() {
154
+ local sp
155
+ sp="$(find_sidepanel_id)"
156
+ if [ -z "$sp" ]; then
157
+ echo "[sidepanel] already closed"
158
+ return 0
159
+ fi
160
+ curl -s "http://127.0.0.1:${CDP_PORT}/json/close/${sp}" >/dev/null
161
+ echo "[sidepanel] closed target ${sp}"
162
+ }
163
+
164
+ open_sidepanel() {
165
+ if [ -n "$(find_sidepanel_id)" ]; then
166
+ echo "[sidepanel] already open"
167
+ return 0
168
+ fi
169
+
170
+ activate_sandbox_page
171
+
172
+ local ext_id
173
+ ext_id="$(resolve_ext_id)"
174
+ if [ -z "$ext_id" ]; then
175
+ echo "FAIL: could not resolve extension id for CDP ${CDP_PORT}" >&2
176
+ exit 4
177
+ fi
178
+
179
+ CDP_PORT="$CDP_PORT" EXT_ID="$ext_id" node <<'NODE'
180
+ const { chromium } = require('playwright');
181
+
182
+ (async () => {
183
+ const port = process.env.CDP_PORT;
184
+ const extId = process.env.EXT_ID;
185
+ const browser = await chromium.connectOverCDP(`http://127.0.0.1:${port}`);
186
+ const context = browser.contexts()[0];
187
+ let createdPage = false;
188
+ let page = context
189
+ .pages()
190
+ .find(
191
+ (candidate) =>
192
+ candidate.url().startsWith(`chrome-extension://${extId}/`) &&
193
+ !candidate.url().includes('/sidepanel.html'),
194
+ );
195
+
196
+ if (!page) {
197
+ page = await context.newPage();
198
+ createdPage = true;
199
+ await page.goto(`chrome-extension://${extId}/home.html`, {
200
+ waitUntil: 'domcontentloaded',
201
+ timeout: 15000,
202
+ });
203
+ }
204
+
205
+ await page.bringToFront();
206
+ const result = await page.evaluate(async () => {
207
+ const currentWindow = await chrome.windows.getCurrent();
208
+ const id = '__recipe_open_sidepanel__';
209
+ document.getElementById(id)?.remove();
210
+ const button = document.createElement('button');
211
+ button.id = id;
212
+ button.textContent = 'open sidepanel';
213
+ button.style.cssText =
214
+ 'position:fixed;left:8px;top:8px;z-index:2147483647';
215
+ button.onclick = async () => {
216
+ try {
217
+ await chrome.sidePanel.open({ windowId: currentWindow.id });
218
+ button.dataset.result = `ok:${currentWindow.id}`;
219
+ } catch (error) {
220
+ button.dataset.result = `error:${
221
+ error && error.message ? error.message : String(error)
222
+ }`;
223
+ }
224
+ };
225
+ document.documentElement.appendChild(button);
226
+ return { windowId: currentWindow.id };
227
+ });
228
+
229
+ await page.locator('#__recipe_open_sidepanel__').click({ timeout: 5000 });
230
+ const clickResult = await page
231
+ .locator('#__recipe_open_sidepanel__')
232
+ .evaluate((button) => button.dataset.result || '');
233
+ if (!clickResult.startsWith('ok:')) {
234
+ throw new Error(
235
+ `chrome.sidePanel.open failed for window ${result.windowId}: ${clickResult}`,
236
+ );
237
+ }
238
+ await page.evaluate(() => {
239
+ document.getElementById('__recipe_open_sidepanel__')?.remove();
240
+ });
241
+
242
+ if (createdPage) {
243
+ try {
244
+ await page.close();
245
+ } catch (error) {
246
+ console.warn(
247
+ `[sidepanel] helper page close failed after successful open: ${
248
+ error && error.message ? error.message : error
249
+ }`,
250
+ );
251
+ }
252
+ }
253
+ await browser.close();
254
+ })().catch((error) => {
255
+ console.error(`FAIL: ${error.message || error}`);
256
+ process.exit(4);
257
+ });
258
+ NODE
259
+
260
+ if wait_for_sidepanel; then
261
+ echo "[sidepanel] opened"
262
+ return 0
263
+ fi
264
+
265
+ echo "FAIL: sidepanel did not appear within ${SETTLE_MS}ms" >&2
266
+ echo " Current CDP targets:" >&2
267
+ print_targets >&2
268
+ exit 3
269
+ }
270
+
271
+ case "$ACTION" in
272
+ status) status_sidepanel ;;
273
+ open) open_sidepanel ;;
274
+ close) close_sidepanel ;;
275
+ toggle)
276
+ if [ -n "$(find_sidepanel_id)" ]; then
277
+ close_sidepanel
278
+ else
279
+ open_sidepanel
280
+ fi
281
+ ;;
282
+ cycle)
283
+ close_sidepanel
284
+ sleep 1
285
+ open_sidepanel
286
+ ;;
287
+ *)
288
+ echo "Usage: bash sidepanel-toggle.sh {status|open|close|toggle|cycle} --cdp-port PORT [--ext-id ID] [--agent-dir DIR] [--settle-ms N]" >&2
289
+ exit 1
290
+ ;;
291
+ esac
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env bash
2
+ # snapshot-dist.sh — runtime-dist snapshot with git-id freshness guard
3
+ # mega-string in scripts/extension/live.sh)
4
+ #
5
+ # Purpose:
6
+ # Snapshots dist/chrome into a per-run runtime-dist so Chrome's loaded
7
+ # extension can't be ripped out mid-rebuild, then verifies the snapshot's
8
+ # "from git id:" matches the source dist (catches mid-rebuild copies).
9
+ #
10
+ # Inputs (flags):
11
+ # --dist <dir> source dist (required, e.g. <repo>/dist/chrome)
12
+ # --runtime-dist <dir> snapshot destination (required, recreated)
13
+ # --wait-iterations <n> manifest wait loop length, 2s each (default 180)
14
+ # --summary <file> optional standard summary.json
15
+ #
16
+ # Outputs:
17
+ # <runtime-dist>/ snapshot (excludes _metadata); optional --summary file
18
+ # {feature,status,inputs,outputs,generatedAt}.
19
+ # Exit 0 — snapshot fresh; 1 — manifest never appeared, rsync failed, or
20
+ # git-id mismatch (mid-rebuild); 2 — bad args.
21
+ #
22
+ # Never touches: the source dist (read-only); anything outside
23
+ # --runtime-dist and --summary.
24
+ set -euo pipefail
25
+
26
+ DIST=""
27
+ RUNTIME_DIST=""
28
+ WAIT_ITERATIONS=180
29
+ SUMMARY=""
30
+ require_value() { [ "$#" -ge 2 ] || { echo "Missing value for $1" >&2; exit 2; }; }
31
+ while [ "$#" -gt 0 ]; do
32
+ case "$1" in
33
+ --dist) require_value "$@"; DIST="$2"; shift 2 ;;
34
+ --runtime-dist) require_value "$@"; RUNTIME_DIST="$2"; shift 2 ;;
35
+ --wait-iterations) require_value "$@"; WAIT_ITERATIONS="$2"; shift 2 ;;
36
+ --summary) require_value "$@"; SUMMARY="$2"; shift 2 ;;
37
+ -h|--help)
38
+ echo "Usage: snapshot-dist.sh --dist <dir> --runtime-dist <dir> [--wait-iterations <n>] [--summary <file>]"
39
+ exit 0
40
+ ;;
41
+ *) echo "Unknown arg: $1" >&2; exit 2 ;;
42
+ esac
43
+ done
44
+ [ -n "$DIST" ] || { echo "Missing --dist" >&2; exit 2; }
45
+ [ -n "$RUNTIME_DIST" ] || { echo "Missing --runtime-dist" >&2; exit 2; }
46
+ case "$WAIT_ITERATIONS" in ''|*[!0-9]*) echo "Invalid --wait-iterations (must be numeric): $WAIT_ITERATIONS" >&2; exit 2 ;; esac
47
+
48
+ status=fail
49
+ finish() {
50
+ if [ -n "$SUMMARY" ]; then
51
+ mkdir -p "$(dirname "$SUMMARY")"
52
+ STATUS_FOR_SUMMARY="$status" DIST_FOR_SUMMARY="$DIST" RUNTIME_DIST_FOR_SUMMARY="$RUNTIME_DIST" SUMMARY_PATH="$SUMMARY" node <<'NODE' || true
53
+ const fs = require('fs');
54
+ fs.writeFileSync(process.env.SUMMARY_PATH, `${JSON.stringify({
55
+ feature: 'extension/snapshot-dist',
56
+ status: process.env.STATUS_FOR_SUMMARY,
57
+ inputs: { dist: process.env.DIST_FOR_SUMMARY },
58
+ outputs: { runtimeDist: process.env.RUNTIME_DIST_FOR_SUMMARY },
59
+ generatedAt: new Date().toISOString(),
60
+ }, null, 2)}\n`);
61
+ NODE
62
+ fi
63
+ }
64
+ trap finish EXIT
65
+
66
+ for i in $(seq 1 "$WAIT_ITERATIONS"); do
67
+ [ -f "$DIST/manifest.json" ] && break
68
+ if [ $((i % 15)) -eq 0 ]; then
69
+ echo "[recipe-harness] waiting for dist manifest (~$((i * 2))s): $DIST/manifest.json" >&2
70
+ fi
71
+ sleep 2
72
+ done
73
+ test -f "$DIST/manifest.json" || { echo "snapshot-dist: no manifest at $DIST/manifest.json" >&2; exit 1; }
74
+
75
+ echo "[recipe-harness] snapshotting dist -> runtime-dist: $RUNTIME_DIST" >&2
76
+ rm -rf "$RUNTIME_DIST" && mkdir -p "$RUNTIME_DIST" \
77
+ && rsync -a --delete --exclude _metadata "$DIST/" "$RUNTIME_DIST/" || exit 1
78
+ echo "[recipe-harness] runtime-dist snapshot complete" >&2
79
+
80
+ # Freshness guard: the loaded runtime-dist must match dist/chrome's git id. A
81
+ # mismatch means the rsync caught a mid-rebuild dist; abort rather than load
82
+ # an inconsistent bundle (the "Element type is invalid: undefined" class of crash).
83
+ node -e 'const fs=require("fs");const id=p=>{try{return (JSON.parse(fs.readFileSync(p,"utf8")).description||"").match(/from git id: *([0-9a-f]+)/i)?.[1]||""}catch{return""}};const [distManifest,runtimeManifest]=process.argv.slice(-2);const d=id(distManifest),r=id(runtimeManifest);if(d&&d!==r){console.error("runtime-dist git id "+r+" != dist "+d+" (mid-rebuild?); aborting");process.exit(1)}' "$DIST/manifest.json" "$RUNTIME_DIST/manifest.json" || exit 1
84
+ status=pass