@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,317 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * open-debug.mjs — open human-facing debugger UIs for recipe runtimes.
4
+ *
5
+ * Mobile: Metro /open-debugger or /open-dev-menu HTTP endpoints.
6
+ * Extension: Chrome DevTools frontend for an extension page or service worker.
7
+ */
8
+ import { spawnSync } from 'node:child_process';
9
+
10
+ function usage() {
11
+ console.error(`Usage:
12
+ open-debug.mjs --adapter mobile [--port <watcher>] [--action debug|dev-menu] [--json] [--no-open]
13
+ open-debug.mjs --adapter extension [--cdp-port <port>] [--target page|worker] [--json] [--no-open]
14
+
15
+ Actions (mobile):
16
+ debug Open React Native DevTools / Hermes debugger (default)
17
+ dev-menu Open the React Native developer menu
18
+
19
+ Targets (extension):
20
+ page Extension UI page, preferring home.html (default)
21
+ worker Extension service worker background context`);
22
+ }
23
+
24
+ function parseArgs(argv) {
25
+ const args = {
26
+ adapter: null,
27
+ port: null,
28
+ cdpPort: null,
29
+ action: 'debug',
30
+ target: 'page',
31
+ json: false,
32
+ open: true,
33
+ help: false,
34
+ };
35
+ for (let i = 0; i < argv.length; i += 1) {
36
+ const arg = argv[i];
37
+ switch (arg) {
38
+ case '--adapter':
39
+ args.adapter = argv[++i];
40
+ break;
41
+ case '--port':
42
+ args.port = argv[++i];
43
+ break;
44
+ case '--cdp-port':
45
+ args.cdpPort = argv[++i];
46
+ break;
47
+ case '--action':
48
+ args.action = argv[++i];
49
+ break;
50
+ case '--target':
51
+ args.target = argv[++i];
52
+ break;
53
+ case '--json':
54
+ args.json = true;
55
+ break;
56
+ case '--no-open':
57
+ args.open = false;
58
+ break;
59
+ case '-h':
60
+ case '--help':
61
+ args.help = true;
62
+ break;
63
+ default:
64
+ throw new Error(`Unknown argument: ${arg}`);
65
+ }
66
+ }
67
+ return args;
68
+ }
69
+
70
+ function emit(result, json) {
71
+ if (json) {
72
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
73
+ return;
74
+ }
75
+ if (result.ok) {
76
+ const detail = result.method ? ` via ${result.method}` : '';
77
+ const where = result.port ? ` (port ${result.port})` : result.cdpPort ? ` (cdp ${result.cdpPort})` : '';
78
+ console.log(`Opened debugger${detail}${where}`);
79
+ if (result.target?.url) console.log(`Target: ${result.target.url}`);
80
+ if (result.configureHint) console.log(`Configure chrome://inspect target: ${result.configureHint}`);
81
+ return;
82
+ }
83
+ console.error(result.error || 'Failed to open debugger');
84
+ if (result.hint) console.error(result.hint);
85
+ process.exitCode = 1;
86
+ }
87
+
88
+ async function fetchOk(url, init) {
89
+ const response = await fetch(url, init);
90
+ return response.ok;
91
+ }
92
+
93
+ async function fetchJson(url) {
94
+ const response = await fetch(url);
95
+ if (!response.ok) {
96
+ throw new Error(`HTTP ${response.status} for ${url}`);
97
+ }
98
+ const body = await response.json();
99
+ return body;
100
+ }
101
+
102
+ function openUrl(url) {
103
+ if (process.platform === 'darwin') {
104
+ if (url.startsWith('devtools://') || url.startsWith('chrome://')) {
105
+ const chrome = spawnSync('open', ['-a', 'Google Chrome', url], { stdio: 'ignore' });
106
+ if (chrome.status === 0) return 'chrome';
107
+ }
108
+ spawnSync('open', [url], { stdio: 'ignore' });
109
+ return 'open';
110
+ }
111
+ if (process.platform === 'linux') {
112
+ spawnSync('xdg-open', [url], { stdio: 'ignore' });
113
+ return 'xdg-open';
114
+ }
115
+ return null;
116
+ }
117
+
118
+ async function openMobileDebugger(port, action, shouldOpen) {
119
+ const normalizedAction = action === 'dev-menu' ? 'dev-menu' : 'debug';
120
+ const path = normalizedAction === 'dev-menu' ? '/open-dev-menu' : '/open-debugger';
121
+ const numericPort = Number.parseInt(String(port), 10);
122
+ if (!Number.isInteger(numericPort) || numericPort <= 0) {
123
+ return { ok: false, adapter: 'mobile', action: normalizedAction, error: 'invalid-port', port };
124
+ }
125
+
126
+ try {
127
+ const opened = await fetchOk(`http://127.0.0.1:${numericPort}${path}`, { method: 'POST' });
128
+ if (opened) {
129
+ return {
130
+ ok: true,
131
+ adapter: 'mobile',
132
+ action: normalizedAction,
133
+ method: 'metro-http',
134
+ port: numericPort,
135
+ opened: shouldOpen,
136
+ };
137
+ }
138
+ } catch (error) {
139
+ return {
140
+ ok: false,
141
+ adapter: 'mobile',
142
+ action: normalizedAction,
143
+ method: 'metro-http',
144
+ port: numericPort,
145
+ error: error instanceof Error ? error.message : String(error),
146
+ hint: 'Metro may be down or the debug target is not ready. Try recipe refresh, then recipe runtime-status.',
147
+ fallback: 'cdp-eval',
148
+ };
149
+ }
150
+
151
+ return {
152
+ ok: false,
153
+ adapter: 'mobile',
154
+ action: normalizedAction,
155
+ method: 'metro-http',
156
+ port: numericPort,
157
+ error: 'metro-http-failed',
158
+ hint: 'Metro rejected the open-debugger request. Try recipe refresh, then recipe runtime-status.',
159
+ fallback: 'cdp-eval',
160
+ };
161
+ }
162
+
163
+ function pickExtensionTarget(targets, targetKind) {
164
+ const extensionTargets = targets.filter((target) => String(target.url || '').startsWith('chrome-extension://'));
165
+ if (targetKind === 'worker') {
166
+ return (
167
+ extensionTargets.find((target) => target.type === 'service_worker')
168
+ || targets.find((target) => target.type === 'service_worker' && String(target.url || '').startsWith('chrome-extension://'))
169
+ || null
170
+ );
171
+ }
172
+
173
+ const pages = extensionTargets.filter((target) => target.type === 'page');
174
+ return (
175
+ pages.find((target) => String(target.url || '').includes('/home.html'))
176
+ || pages.find((target) => !String(target.url || '').includes('/popup-init.html'))
177
+ || pages[0]
178
+ || null
179
+ );
180
+ }
181
+
182
+ async function openExtensionDevtools(cdpPort, targetKind, shouldOpen) {
183
+ const numericPort = Number.parseInt(String(cdpPort), 10);
184
+ if (!Number.isInteger(numericPort) || numericPort <= 0) {
185
+ return { ok: false, adapter: 'extension', error: 'invalid-cdp-port', cdpPort };
186
+ }
187
+
188
+ const inspectUrl = 'chrome://inspect/#devices';
189
+ const configureHint = `127.0.0.1:${numericPort}`;
190
+
191
+ try {
192
+ await fetchJson(`http://127.0.0.1:${numericPort}/json/version`);
193
+ } catch (error) {
194
+ return {
195
+ ok: false,
196
+ adapter: 'extension',
197
+ targetKind,
198
+ cdpPort: numericPort,
199
+ error: 'cdp-unreachable',
200
+ inspectUrl,
201
+ configureHint,
202
+ hint: `Start the extension runtime first (recipe reopen). Then open chrome://inspect and add ${configureHint}.`,
203
+ detail: error instanceof Error ? error.message : String(error),
204
+ };
205
+ }
206
+
207
+ let targets;
208
+ try {
209
+ targets = await fetchJson(`http://127.0.0.1:${numericPort}/json/list`);
210
+ } catch (error) {
211
+ return {
212
+ ok: false,
213
+ adapter: 'extension',
214
+ targetKind,
215
+ cdpPort: numericPort,
216
+ error: 'cdp-list-failed',
217
+ inspectUrl,
218
+ configureHint,
219
+ hint: `CDP is up but /json/list failed. Open chrome://inspect and add ${configureHint}.`,
220
+ detail: error instanceof Error ? error.message : String(error),
221
+ };
222
+ }
223
+
224
+ if (!Array.isArray(targets)) {
225
+ return {
226
+ ok: false,
227
+ adapter: 'extension',
228
+ targetKind,
229
+ cdpPort: numericPort,
230
+ error: 'cdp-list-invalid',
231
+ inspectUrl,
232
+ configureHint,
233
+ hint: `Open chrome://inspect and add ${configureHint}.`,
234
+ };
235
+ }
236
+
237
+ const picked = pickExtensionTarget(targets, targetKind);
238
+ if (picked?.devtoolsFrontendUrl && shouldOpen) {
239
+ openUrl(picked.devtoolsFrontendUrl);
240
+ return {
241
+ ok: true,
242
+ adapter: 'extension',
243
+ targetKind,
244
+ method: 'devtools-frontend',
245
+ cdpPort: numericPort,
246
+ target: {
247
+ type: picked.type,
248
+ url: picked.url,
249
+ title: picked.title || null,
250
+ },
251
+ devtoolsUrl: picked.devtoolsFrontendUrl,
252
+ inspectUrl,
253
+ configureHint,
254
+ opened: true,
255
+ };
256
+ }
257
+
258
+ if (shouldOpen) {
259
+ openUrl(inspectUrl);
260
+ }
261
+
262
+ return {
263
+ ok: true,
264
+ adapter: 'extension',
265
+ targetKind,
266
+ method: picked?.devtoolsFrontendUrl ? 'devtools-frontend' : 'chrome-inspect',
267
+ cdpPort: numericPort,
268
+ target: picked
269
+ ? { type: picked.type, url: picked.url, title: picked.title || null }
270
+ : null,
271
+ devtoolsUrl: picked?.devtoolsFrontendUrl || null,
272
+ inspectUrl,
273
+ configureHint,
274
+ opened: shouldOpen,
275
+ hint: picked
276
+ ? 'Opened chrome://inspect because no devtools frontend URL was available for the selected target.'
277
+ : `No extension targets are registered yet. Open chrome://inspect and add ${configureHint}.`,
278
+ };
279
+ }
280
+
281
+ async function main() {
282
+ const args = parseArgs(process.argv.slice(2));
283
+ if (args.help) {
284
+ usage();
285
+ process.exit(0);
286
+ }
287
+ if (!args.adapter) {
288
+ usage();
289
+ process.exit(2);
290
+ }
291
+
292
+ let result;
293
+ if (args.adapter === 'mobile') {
294
+ result = await openMobileDebugger(args.port || process.env.WATCHER_PORT || '8081', args.action, args.open);
295
+ } else if (args.adapter === 'extension') {
296
+ result = await openExtensionDevtools(
297
+ args.cdpPort || process.env.CDP_PORT || process.env.RECIPE_CDP_PORT,
298
+ args.target === 'worker' ? 'worker' : 'page',
299
+ args.open,
300
+ );
301
+ } else {
302
+ throw new Error(`Unsupported adapter: ${args.adapter}`);
303
+ }
304
+
305
+ emit(result, args.json);
306
+ if (!result.ok) process.exit(1);
307
+ }
308
+
309
+ main().catch((error) => {
310
+ const payload = { ok: false, error: error instanceof Error ? error.message : String(error) };
311
+ if (process.argv.includes('--json')) {
312
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
313
+ } else {
314
+ console.error(payload.error);
315
+ }
316
+ process.exit(1);
317
+ });
@@ -0,0 +1,4 @@
1
+ {
2
+ "recipeHarnessRoot": "temp/recipe/harness",
3
+ "recipeRuntimeDir": "temp/recipe/runtime"
4
+ }
@@ -0,0 +1,107 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * progress.mjs — structured progress events for long recipe CLI operations.
4
+ *
5
+ * Human mode: short lines on stderr.
6
+ * Machine mode: JSONL on stderr when --json or RECIPE_PROGRESS_JSON=1.
7
+ */
8
+ import { parseArgs } from 'node:util';
9
+ import { color } from './cli-color.mjs';
10
+
11
+ function shouldEmitJson(explicit) {
12
+ if (explicit) return true;
13
+ const env = process.env.RECIPE_PROGRESS_JSON;
14
+ return env === '1' || env === 'true';
15
+ }
16
+
17
+ export function emitProgress(event, { json = false } = {}) {
18
+ const payload = {
19
+ schemaVersion: 1,
20
+ type: 'progress',
21
+ ts: new Date().toISOString(),
22
+ ...event,
23
+ };
24
+ if (json || shouldEmitJson(false)) {
25
+ process.stderr.write(`${JSON.stringify(payload)}\n`);
26
+ return;
27
+ }
28
+ const stream = process.stderr;
29
+ const prefix = color('label', 'recipe', { stream });
30
+ if (payload.phase === 'done') {
31
+ const statusStyle = payload.status === 'pass' ? 'ok' : 'err';
32
+ const statusWord = payload.status === 'pass' ? 'done' : 'failed';
33
+ const verb = payload.verb ? color('cmd', `${payload.verb}`, { stream }) : '';
34
+ const elapsed = Number.isFinite(payload.elapsedMs)
35
+ ? color('comment', ` (${Math.round(payload.elapsedMs / 1000)}s)`, { stream })
36
+ : '';
37
+ process.stderr.write(`${prefix}: ${verb} ${color(statusStyle, statusWord, { stream })} ${payload.message || ''}${elapsed}\n`);
38
+ return;
39
+ }
40
+ const verb = payload.verb ? `${color('cmd', payload.verb, { stream })}: ` : '';
41
+ const phase = payload.phase ? `${color('accent', `[${payload.phase}]`, { stream })} ` : '';
42
+ const elapsed = Number.isFinite(payload.elapsedMs)
43
+ ? color('comment', ` (${Math.round(payload.elapsedMs / 1000)}s)`, { stream })
44
+ : '';
45
+ process.stderr.write(`${prefix}: ${verb}${phase}${payload.message || ''}${elapsed}\n`);
46
+ }
47
+
48
+ function usage() {
49
+ console.error(`Usage:
50
+ progress.mjs emit --verb <name> --phase <id> --message <text> [--elapsed-ms <n>] [--json]
51
+ progress.mjs done --verb <name> --status pass|fail --message <text> [--elapsed-ms <n>] [--json]`);
52
+ }
53
+
54
+ async function main() {
55
+ const [command, ...rest] = process.argv.slice(2);
56
+ if (!command || command === '--help' || command === '-h') {
57
+ usage();
58
+ process.exit(command ? 0 : 2);
59
+ }
60
+
61
+ const { values } = parseArgs({
62
+ args: rest,
63
+ options: {
64
+ verb: { type: 'string' },
65
+ phase: { type: 'string' },
66
+ message: { type: 'string' },
67
+ status: { type: 'string' },
68
+ 'elapsed-ms': { type: 'string' },
69
+ json: { type: 'boolean', default: false },
70
+ },
71
+ allowPositionals: false,
72
+ });
73
+
74
+ const elapsedMs = values['elapsed-ms'] ? Number.parseInt(values['elapsed-ms'], 10) : undefined;
75
+ const json = values.json || shouldEmitJson(false);
76
+
77
+ if (command === 'emit') {
78
+ emitProgress({
79
+ verb: values.verb || 'recipe',
80
+ phase: values.phase || 'working',
81
+ message: values.message || '',
82
+ elapsedMs: Number.isFinite(elapsedMs) ? elapsedMs : undefined,
83
+ }, { json });
84
+ return;
85
+ }
86
+
87
+ if (command === 'done') {
88
+ emitProgress({
89
+ verb: values.verb || 'recipe',
90
+ phase: 'done',
91
+ status: values.status || 'pass',
92
+ message: values.message || '',
93
+ elapsedMs: Number.isFinite(elapsedMs) ? elapsedMs : undefined,
94
+ }, { json });
95
+ return;
96
+ }
97
+
98
+ usage();
99
+ process.exit(2);
100
+ }
101
+
102
+ if (import.meta.url === new URL(process.argv[1], 'file:').href) {
103
+ main().catch((error) => {
104
+ console.error(error instanceof Error ? error.message : String(error));
105
+ process.exit(1);
106
+ });
107
+ }
@@ -0,0 +1,26 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ const defaultsPath = path.join(path.dirname(fileURLToPath(import.meta.url)), 'path-defaults.json');
6
+ const defaults = JSON.parse(fs.readFileSync(defaultsPath, 'utf8'));
7
+
8
+ export const DEFAULT_RECIPE_HARNESS_ROOT = defaults.recipeHarnessRoot;
9
+ export const DEFAULT_RECIPE_RUNTIME_DIR = defaults.recipeRuntimeDir;
10
+
11
+ export function validateRelativeRecipePath(name, value) {
12
+ if (!value || path.isAbsolute(value)) throw new Error(`${name} must be a non-empty relative path: ${value}`);
13
+ if (!/^[A-Za-z0-9._/-]+$/u.test(value)) throw new Error(`${name} contains unsupported characters: ${value}`);
14
+ for (const part of value.split('/')) {
15
+ if (!part || part === '.' || part === '..') throw new Error(`${name} contains unsafe path component: ${value}`);
16
+ }
17
+ return value;
18
+ }
19
+
20
+ export function recipeHarnessRoot(env = process.env) {
21
+ return validateRelativeRecipePath('RECIPE_HARNESS_ROOT', env.RECIPE_HARNESS_ROOT || DEFAULT_RECIPE_HARNESS_ROOT);
22
+ }
23
+
24
+ export function recipeRuntimeDir(env = process.env) {
25
+ return validateRelativeRecipePath('RECIPE_RUNTIME_DIR', env.RECIPE_RUNTIME_DIR || DEFAULT_RECIPE_RUNTIME_DIR);
26
+ }
@@ -0,0 +1,144 @@
1
+ # resolve-farmslot-ports.sh — resolve CDP + watcher ports for an extension checkout
2
+ #
3
+ # Farmslot pool JSON is authoritative when a slot repo matches; otherwise fall back
4
+ # to the local-extension-N formula (6660+N / 9010+N).
5
+ #
6
+ # Usage:
7
+ # . resolve-farmslot-ports.sh
8
+ # resolve_extension_runtime_ports /path/to/metamask-extension-N
9
+ # -> prints CDP_PORT=... WATCHER_PORT=... SLOT_ID=... lines
10
+
11
+ resolve_farmslot_ports_by_repo() {
12
+ local repo="$1"
13
+ local farmslot_root="${FARMSLOT_ROOT:-}"
14
+ [ -n "$repo" ] || return 1
15
+ repo="$(cd "$repo" && pwd -P)"
16
+
17
+ local root candidate match
18
+ for root in "$farmslot_root" "$HOME/dev/farmslot"; do
19
+ [ -n "$root" ] && [ -d "$root/pool" ] || continue
20
+ match="$(POOL_ROOT="$root" REPO_TARGET="$repo" python3 - <<'PY'
21
+ import glob, json, os, re, sys
22
+
23
+ pool_root = os.environ["POOL_ROOT"]
24
+ repo = os.path.realpath(os.environ["REPO_TARGET"])
25
+ base = os.path.basename(repo)
26
+ slot_suffix = None
27
+ m = re.search(r"-(\d+)$", base)
28
+ if m:
29
+ slot_suffix = m.group(1)
30
+
31
+ def score_slot(slot):
32
+ resources = slot.get("resources") or {}
33
+ cdp = (resources.get("browser") or {}).get("cdp_port")
34
+ port = (resources.get("dev-server") or {}).get("port")
35
+ session = slot.get("session") or ""
36
+ slot_id = slot.get("id") or ""
37
+ score = 0
38
+ if cdp is not None:
39
+ score += 100
40
+ if port is not None:
41
+ score += 10
42
+ if slot_suffix:
43
+ if session == f"mme-{slot_suffix}":
44
+ score += 50
45
+ if slot_id.endswith(f"mme-{slot_suffix}") or f"-mme-{slot_suffix}" in slot_id:
46
+ score += 40
47
+ if "demo" not in slot_id.lower():
48
+ score += 5
49
+ return score, cdp, port, slot_id
50
+
51
+ best = None
52
+ for path in sorted(glob.glob(os.path.join(pool_root, "pool", "*.json"))):
53
+ if ".bak." in os.path.basename(path):
54
+ continue
55
+ try:
56
+ data = json.load(open(path, encoding="utf-8"))
57
+ except Exception:
58
+ continue
59
+ for slot in data.get("slots") or []:
60
+ slot_repo = slot.get("repo") or ""
61
+ if not slot_repo:
62
+ continue
63
+ try:
64
+ if os.path.realpath(slot_repo) != repo:
65
+ continue
66
+ except Exception:
67
+ continue
68
+ scored = score_slot(slot)
69
+ if best is None or scored[0] > best[0]:
70
+ best = scored
71
+
72
+ if best is None:
73
+ sys.exit(1)
74
+
75
+ _, cdp, port, slot_id = best
76
+ if cdp is not None:
77
+ print(f"CDP_PORT={cdp}")
78
+ if port is not None:
79
+ print(f"WATCHER_PORT={port}")
80
+ if slot_id:
81
+ print(f"SLOT_ID={slot_id}")
82
+ sys.exit(0)
83
+ PY
84
+ )" || continue
85
+ [ -n "$match" ] || continue
86
+ printf '%s\n' "$match"
87
+ return 0
88
+ done
89
+ return 1
90
+ }
91
+
92
+ infer_extension_slot_suffix() {
93
+ local repo="$1" base
94
+ base="$(basename "$repo")"
95
+ if [[ "$base" =~ -([0-9]+)$ ]]; then
96
+ printf '%s' "${BASH_REMATCH[1]}"
97
+ return 0
98
+ fi
99
+ return 1
100
+ }
101
+
102
+ resolve_default_extension_ports() {
103
+ local repo="$1" n
104
+ n="$(infer_extension_slot_suffix "$repo" || true)"
105
+ [ -n "$n" ] || return 1
106
+ printf 'CDP_PORT=%s\n' "$((6660 + n))"
107
+ printf 'WATCHER_PORT=%s\n' "$((9010 + n))"
108
+ printf 'SLOT_ID=local-extension-%s\n' "$n"
109
+ }
110
+
111
+ resolve_extension_runtime_ports() {
112
+ local repo="${1:-.}"
113
+ if resolve_farmslot_ports_by_repo "$repo"; then
114
+ return 0
115
+ fi
116
+ resolve_default_extension_ports "$repo"
117
+ }
118
+
119
+ apply_resolved_extension_ports() {
120
+ local repo="${1:-.}" line key val from_pool=false resolved=""
121
+ if resolved="$(resolve_farmslot_ports_by_repo "$repo" 2>/dev/null)"; then
122
+ from_pool=true
123
+ elif resolved="$(resolve_default_extension_ports "$repo" 2>/dev/null)"; then
124
+ from_pool=false
125
+ else
126
+ return 0
127
+ fi
128
+ while IFS= read -r line; do
129
+ [ -n "$line" ] || continue
130
+ key="${line%%=*}"
131
+ val="${line#*=}"
132
+ case "$key" in
133
+ CDP_PORT)
134
+ if [ "$from_pool" = true ] || [ -z "${CDP_PORT:-}" ]; then CDP_PORT="$val"; fi
135
+ ;;
136
+ WATCHER_PORT)
137
+ if [ "$from_pool" = true ] || [ -z "${WATCHER_PORT:-}" ]; then WATCHER_PORT="$val"; fi
138
+ ;;
139
+ SLOT_ID)
140
+ if [ "$from_pool" = true ] || [ -z "${RECIPE_SLOT_ID:-}" ]; then RECIPE_SLOT_ID="$val"; fi
141
+ ;;
142
+ esac
143
+ done <<< "$resolved"
144
+ }