@deeeed/metamask-harness 0.79.0 → 0.81.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 (252) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/README.md +1 -1
  3. package/adapters/extension/ensure-browser.sh +2 -2
  4. package/adapters/extension/inject.mjs +0 -1
  5. package/adapters/extension/launch-browser.cjs +39 -613
  6. package/adapters/extension/readiness.mjs +2 -5
  7. package/adapters/extension/reattach.sh +2 -2
  8. package/adapters/extension/stamp-runtime-title.cjs +1 -1
  9. package/adapters/manifest.json +16 -64
  10. package/adapters/mobile/bridge-runtime/cdp-bridge.cjs +57 -1358
  11. package/adapters/mobile/bridge-runtime/teaching.cjs +20 -0
  12. package/adapters/mobile/inject.sh +16 -0
  13. package/adapters/mobile/launch-console-forwarder.cjs +1 -2
  14. package/adapters/mobile/open-device.sh +2 -2
  15. package/adapters/mobile/reload-app.mjs +4 -4
  16. package/adapters/mobile/reset-app-data.sh +1 -1
  17. package/adapters/mobile/start-console-forwarder.sh +3 -2
  18. package/adapters/mobile/start-metro.sh +17 -12
  19. package/adapters/mobile/wait-for-bridge.cjs +2 -2
  20. package/adapters/shared/harness-source-fingerprint.mjs +13 -8
  21. package/adapters/shared/open-debug.mjs +2 -2
  22. package/adapters/shared/sync-wallet-fixture.sh +8 -5
  23. package/adapters/terminal/cleanup.mjs +2 -1
  24. package/adapters/terminal/inject.mjs +2 -1
  25. package/adapters/terminal/launch.mjs +17 -13
  26. package/adapters/terminal/lib/mainnet-guard.mjs +9 -0
  27. package/adapters/terminal/lib/readiness.mjs +20 -12
  28. package/adapters/terminal/policy.mjs +35 -0
  29. package/adapters/terminal/verify.mjs +10 -3
  30. package/adapters/terminal/wallet-host.mjs +43 -225
  31. package/bin/mm-harness +9 -28
  32. package/dist/adapters/checkout-files.js +16 -0
  33. package/dist/adapters/console-capture.js +14 -1
  34. package/dist/adapters/core/actions.js +8 -4
  35. package/dist/adapters/core/overlay.js +1 -1
  36. package/dist/adapters/core/readiness.js +28 -1
  37. package/dist/adapters/core/run.js +2 -2
  38. package/dist/adapters/core/surface.js +16 -1
  39. package/dist/adapters/extension/actions.js +6 -2
  40. package/dist/adapters/extension/browser-pid.js +56 -0
  41. package/dist/adapters/extension/console-capture.js +1 -1
  42. package/dist/adapters/extension/doctor-checks.js +1 -1
  43. package/dist/adapters/extension/ensure-ready.js +5 -4
  44. package/dist/adapters/extension/extension-id.js +1 -1
  45. package/dist/adapters/extension/feature-flags.js +1 -1
  46. package/dist/adapters/extension/fixtures.js +1 -2
  47. package/dist/adapters/extension/launch.js +4 -5
  48. package/dist/adapters/extension/launched-browser.js +1 -2
  49. package/dist/adapters/extension/network-observer.js +14 -1
  50. package/dist/adapters/extension/overlay.js +1 -1
  51. package/dist/adapters/extension/product-config.js +1 -1
  52. package/dist/adapters/extension/readiness.js +1 -1
  53. package/dist/adapters/extension/reload.js +1 -1
  54. package/dist/adapters/extension/run.js +135 -14
  55. package/dist/adapters/extension/runtime-decision.js +2 -1
  56. package/dist/adapters/extension/runtime.js +9 -5
  57. package/dist/adapters/extension/surface.js +15 -2
  58. package/dist/adapters/extension/verify.js +2 -2
  59. package/dist/adapters/harness-freshness.js +2 -1
  60. package/dist/adapters/mobile/actions.js +4 -1
  61. package/dist/adapters/mobile/android-gfxinfo-performance.js +1 -3
  62. package/dist/adapters/mobile/app-identity.js +10 -0
  63. package/dist/adapters/mobile/doctor-checks.js +1 -1
  64. package/dist/adapters/mobile/fixtures.js +1 -1
  65. package/dist/adapters/mobile/launch.js +27 -28
  66. package/dist/adapters/mobile/lifecycle.js +1 -1
  67. package/dist/adapters/mobile/metro-env.js +9 -59
  68. package/dist/adapters/mobile/network-observer.js +68 -0
  69. package/dist/adapters/mobile/performance-observer.js +2 -4
  70. package/dist/adapters/mobile/prepare-target.js +1 -1
  71. package/dist/adapters/mobile/prepare.js +3 -4
  72. package/dist/adapters/mobile/provision.js +1 -1
  73. package/dist/adapters/mobile/readiness.js +1 -1
  74. package/dist/adapters/mobile/recording-target.js +15 -0
  75. package/dist/adapters/mobile/release-artifact-state.js +1 -1
  76. package/dist/adapters/mobile/reload.js +1 -1
  77. package/dist/adapters/mobile/run.js +39 -15
  78. package/dist/adapters/mobile/runtime-decision.js +2 -4
  79. package/dist/adapters/mobile/source-freshness.js +22 -126
  80. package/dist/adapters/mobile/surface.js +26 -3
  81. package/dist/adapters/mobile/ui.js +16 -5
  82. package/dist/adapters/mobile/verify.js +2 -3
  83. package/dist/adapters/performance/cdp-trace.js +1 -1
  84. package/dist/adapters/performance/device-platform.js +1 -1
  85. package/dist/adapters/perps-order-inputs.js +36 -0
  86. package/dist/adapters/proof-failure.js +11 -0
  87. package/dist/adapters/slot-ports.js +1 -1
  88. package/dist/adapters/surface.js +1 -0
  89. package/dist/adapters/terminal/actions.js +4 -1
  90. package/dist/adapters/terminal/console-capture.js +1 -1
  91. package/dist/adapters/terminal/doctor-checks.js +2 -0
  92. package/dist/adapters/terminal/launch.js +4 -1
  93. package/dist/adapters/terminal/overlay.js +8 -1
  94. package/dist/adapters/terminal/run.js +33 -5
  95. package/dist/adapters/terminal/surface.js +9 -1
  96. package/dist/adapters.js +27 -7
  97. package/dist/advisor-policy.js +1 -1
  98. package/dist/checkout-view.js +1 -2
  99. package/dist/cli.js +59 -21
  100. package/dist/command-contract.js +1 -26
  101. package/dist/commands/check.js +33 -10
  102. package/dist/commands/checklist.js +1 -1
  103. package/dist/commands/completion-candidates.js +6 -14
  104. package/dist/commands/config.js +2 -2
  105. package/dist/commands/core-readiness.js +12 -7
  106. package/dist/commands/debug.js +3 -3
  107. package/dist/commands/device-target.js +5 -5
  108. package/dist/commands/doctor.js +2 -14
  109. package/dist/commands/domain.js +2 -2
  110. package/dist/commands/ensure-ready.js +1 -7
  111. package/dist/commands/fixtures.js +5 -6
  112. package/dist/commands/help.js +3 -4
  113. package/dist/commands/launch/device.js +10 -17
  114. package/dist/commands/launch/extension.js +2 -8
  115. package/dist/commands/launch/mobile.js +1 -1
  116. package/dist/commands/logs.js +3 -3
  117. package/dist/commands/mobile-device-view.js +4 -2
  118. package/dist/commands/overlay.js +110 -0
  119. package/dist/commands/pin.js +1 -1
  120. package/dist/commands/pr-body.js +2 -2
  121. package/dist/commands/prepare.js +4 -9
  122. package/dist/commands/provision.js +3 -12
  123. package/dist/commands/recipe-advice.js +1 -2
  124. package/dist/commands/recipe-quality.js +2 -3
  125. package/dist/commands/resolve-extension.js +1 -7
  126. package/dist/commands/review.js +4 -4
  127. package/dist/commands/runtime-decision.js +1 -6
  128. package/dist/commands/runtime-health.js +1 -7
  129. package/dist/commands/runtime-launch.js +1 -13
  130. package/dist/commands/self-test.js +5 -8
  131. package/dist/commands/status-watch.js +1 -2
  132. package/dist/commands/status.js +2 -5
  133. package/dist/commands/task-init.js +2 -3
  134. package/dist/commands/tutorial.js +2 -2
  135. package/dist/commands/update.js +1 -1
  136. package/dist/completions-cache.js +1 -1
  137. package/dist/console-classifier.js +19 -0
  138. package/dist/doctor.js +2 -3
  139. package/dist/engine.js +57 -0
  140. package/dist/funding-execution-context.js +96 -1
  141. package/dist/host.js +32 -0
  142. package/dist/index.js +1 -6
  143. package/dist/live-tsx.js +49 -0
  144. package/dist/mm-harness-cli.js +4 -6
  145. package/dist/paths.js +34 -64
  146. package/dist/performance-observation.js +3 -5
  147. package/dist/progress.js +1 -1
  148. package/dist/recipe-assistance-context.js +6 -9
  149. package/dist/recipe-security.js +2 -59
  150. package/dist/review/knowledge.js +2 -2
  151. package/dist/runner.js +7 -4
  152. package/dist/runtime-context.js +1 -1
  153. package/dist/task-view.js +2 -3
  154. package/docs/CONTRIBUTING.md +5 -3
  155. package/docs/RECIPES.md +31 -3
  156. package/docs/cli-contract.md +2 -2
  157. package/docs/feature-guard.md +90 -0
  158. package/library/README.md +3 -1
  159. package/library/actions/core/perps/_controller.mjs +153 -134
  160. package/library/actions/core/perps/_rewards.mjs +186 -0
  161. package/library/actions/core/perps/close_positions.mjs +1 -1
  162. package/library/actions/core/perps/place_order.mjs +167 -45
  163. package/library/actions/core/perps/quote_fees.mjs +61 -0
  164. package/library/actions/core/wallet/list_accounts.mjs +14 -57
  165. package/library/actions/extension/feature_flags/state.mjs +1 -1
  166. package/library/actions/extension/perps/close_visible_position.mjs +5 -0
  167. package/library/actions/extension/perps/edit_margin.mjs +4 -0
  168. package/library/actions/extension/perps/mutation-receipt.mjs +95 -3
  169. package/library/actions/extension/perps/perps.mjs +320 -3
  170. package/library/actions/extension/perps/place_order.mjs +3 -1
  171. package/library/actions/extension/perps/update_position_tpsl.mjs +4 -0
  172. package/library/actions/extension/platform/cdp.mjs +73 -11
  173. package/library/actions/harness-exports.mjs +1 -6
  174. package/library/actions/mobile/app/network-control.mjs +6 -8
  175. package/library/actions/mobile/perps/perps.mjs +216 -11
  176. package/library/actions/mobile/perps/place_order.mjs +3 -1
  177. package/library/actions/mobile/perps/prepare_local_snapshot_endpoint.mjs +1 -1
  178. package/library/actions/mobile/platform/bridge.mjs +8 -8
  179. package/library/actions/mobile/platform/native-session-name.mjs +21 -1
  180. package/library/actions/mobile/platform/native-session.mjs +3 -4
  181. package/library/actions/mobile/platform/observe-ui.mjs +12 -6
  182. package/library/actions/mobile/ui/native-navigation.mjs +4 -3
  183. package/library/actions/mobile/wallet/native-ui.mjs +3 -2
  184. package/library/actions/shared/analytics/_adapter.mjs +1 -1
  185. package/library/actions/shared/console-findings.mjs +1 -1
  186. package/library/actions/shared/perps/order-policy.mjs +177 -0
  187. package/library/actions/shared/wallet/fixture-accounts.mjs +84 -0
  188. package/library/actions/terminal/app/launch.mjs +3 -2
  189. package/library/actions/terminal/platform/hud.mjs +2 -1
  190. package/library/actions/terminal/platform/page.mjs +2 -1
  191. package/library/actions/terminal/platform/runtime.mjs +2 -1
  192. package/library/actions/terminal/wallet/_signature-log.mjs +40 -161
  193. package/library/actions/terminal/wallet/assert_signatures.mjs +6 -12
  194. package/library/actions/terminal/wallet/read_signatures.mjs +10 -9
  195. package/library/manifests/core.action-manifest.json +87 -13
  196. package/library/manifests/extension.action-manifest.json +4 -0
  197. package/library/manifests/mobile.action-manifest.json +12 -0
  198. package/package.json +8 -9
  199. package/site/assets/help-recipes.json +1 -0
  200. package/adapters/extension/lib/slot-title.cjs +0 -292
  201. package/adapters/mobile/bridge-runtime/console-forwarder.cjs +0 -472
  202. package/adapters/mobile/bridge-runtime/lib/bridge-errors.cjs +0 -125
  203. package/adapters/mobile/bridge-runtime/lib/cdp-broker.cjs +0 -1055
  204. package/adapters/mobile/bridge-runtime/lib/cdp-eval.cjs +0 -110
  205. package/adapters/mobile/bridge-runtime/lib/config.cjs +0 -61
  206. package/adapters/mobile/bridge-runtime/lib/console-format.cjs +0 -53
  207. package/adapters/mobile/bridge-runtime/lib/devtools-proxy.cjs +0 -177
  208. package/adapters/mobile/bridge-runtime/lib/issue-capture.cjs +0 -446
  209. package/adapters/mobile/bridge-runtime/lib/match-bridge-target.cjs +0 -84
  210. package/adapters/mobile/bridge-runtime/lib/target-discovery.cjs +0 -403
  211. package/adapters/mobile/bridge-runtime/lib/ws-client.cjs +0 -116
  212. package/adapters/mobile/coalesce-metro-log.cjs +0 -24
  213. package/adapters/mobile/launch-metro.cjs +0 -77
  214. package/adapters/mobile/metro-config.cjs +0 -98
  215. package/adapters/mobile/metro-log-generation.cjs +0 -106
  216. package/adapters/terminal/lib/origin.mjs +0 -30
  217. package/adapters/terminal/lib/page-script.mjs +0 -246
  218. package/adapters/terminal/lib/strict-wallet.mjs +0 -138
  219. package/dist/adapters/mobile/frame-metrics.js +0 -118
  220. package/dist/adapters/mobile/video-recorder.js +0 -289
  221. package/dist/artifact-files.js +0 -92
  222. package/dist/checkout-lock.js +0 -149
  223. package/dist/cli-color.js +0 -98
  224. package/dist/command-journal.js +0 -308
  225. package/dist/commands/call.js +0 -685
  226. package/dist/commands/last.js +0 -60
  227. package/dist/commands/launch/index.js +0 -109
  228. package/dist/commands/list-executables.js +0 -247
  229. package/dist/commands/manifest.js +0 -488
  230. package/dist/commands/parse-args.js +0 -279
  231. package/dist/commands/reload.js +0 -41
  232. package/dist/commands/run-engine.js +0 -1340
  233. package/dist/commands/run-report.js +0 -241
  234. package/dist/commands/run.js +0 -751
  235. package/dist/commands/shared.js +0 -323
  236. package/dist/commands/stop.js +0 -60
  237. package/dist/devices.js +0 -100
  238. package/dist/discovery.js +0 -15
  239. package/dist/execution-provenance.js +0 -332
  240. package/dist/harness.js +0 -451
  241. package/dist/heal-bounds.js +0 -137
  242. package/dist/json-stream.js +0 -60
  243. package/dist/leaf-invoke.js +0 -19
  244. package/dist/library-provenance.js +0 -33
  245. package/dist/live-adapter-contract.js +0 -380
  246. package/dist/metamask-action-validation.js +0 -68
  247. package/dist/network-observation.js +0 -290
  248. package/dist/recording-target.js +0 -120
  249. package/dist/run-diagnostics.js +0 -364
  250. package/dist/run-recording.js +0 -481
  251. package/dist/runtime-proof.js +0 -90
  252. package/library/actions/mobile/platform/tool-paths.mjs +0 -123
@@ -1,118 +0,0 @@
1
- const MIN_ACTIVE_INTERVALS = 5;
2
- function summarizeFrames(samples) {
3
- const allValidSamples = samples.filter(
4
- (sample) => Number.isFinite(sample.durationMs) && sample.durationMs > 0
5
- );
6
- const frameBudgetMs = commonFrameBudget(allValidSamples);
7
- const cadenceSamples = allValidSamples.length > 0 && allValidSamples.every((sample) => sample.cadenceInterval === true);
8
- const cadenceGaps = cadenceSamples && frameBudgetMs ? allValidSamples.filter(
9
- (sample) => sample.durationMs > frameBudgetMs * 3.01
10
- ) : [];
11
- const validSamples = cadenceGaps.length > 0 ? allValidSamples.filter(
12
- (sample) => sample.durationMs <= (frameBudgetMs ?? 0) * 3.01
13
- ) : allValidSamples;
14
- const durations = validSamples.map((sample) => sample.durationMs).sort((first, second) => first - second);
15
- if (durations.length === 0) {
16
- return {
17
- fpsUnavailableReason: frameBudgetMs ? "insufficient_active_frames" : "refresh_rate_unavailable",
18
- ...cadenceGaps.length > 0 ? {
19
- cadenceGapCount: cadenceGaps.length,
20
- longestCadenceGapMs: round(
21
- Math.max(...cadenceGaps.map((sample) => sample.durationMs))
22
- )
23
- } : {},
24
- ...frameBudgetMs ? {
25
- frameBudgetMs: round(frameBudgetMs),
26
- refreshRateHz: round(1e3 / frameBudgetMs)
27
- } : {},
28
- frameCount: 0
29
- };
30
- }
31
- const frameBudgetOutcomes = validSamples.map((sample) => sample.overBudget).filter((value) => value !== void 0);
32
- const missedFrameBudgetCount = frameBudgetOutcomes.filter(Boolean).length;
33
- const activeIntervals = frameBudgetMs ? activeRenderingIntervals(validSamples, frameBudgetMs) : [];
34
- const activeElapsedMs = activeIntervals.reduce(
35
- (total, interval) => total + interval,
36
- 0
37
- );
38
- const hasActiveFps = cadenceGaps.length === 0 && activeIntervals.length >= MIN_ACTIVE_INTERVALS && activeElapsedMs > 0;
39
- const refreshRateHz = frameBudgetMs ? 1e3 / frameBudgetMs : void 0;
40
- return {
41
- ...hasActiveFps ? {
42
- activeFps: round(
43
- Math.min(
44
- refreshRateHz ?? Number.POSITIVE_INFINITY,
45
- activeIntervals.length * 1e3 / activeElapsedMs
46
- )
47
- ),
48
- fpsIntervalCount: activeIntervals.length
49
- } : {
50
- fpsUnavailableReason: frameBudgetMs ? cadenceGaps.length > 0 ? "unclassified_cadence_gap" : "insufficient_active_frames" : "refresh_rate_unavailable"
51
- },
52
- ...cadenceGaps.length > 0 ? {
53
- cadenceGapCount: cadenceGaps.length,
54
- longestCadenceGapMs: round(
55
- Math.max(...cadenceGaps.map((sample) => sample.durationMs))
56
- )
57
- } : {},
58
- ...frameBudgetMs ? {
59
- frameBudgetMs: round(frameBudgetMs),
60
- refreshRateHz: round(1e3 / frameBudgetMs)
61
- } : {},
62
- frameCount: durations.length,
63
- ...frameBudgetOutcomes.length === validSamples.length ? {
64
- overBudgetFrameCount: missedFrameBudgetCount,
65
- overBudgetFramePercent: round(
66
- missedFrameBudgetCount / validSamples.length * 100
67
- )
68
- } : {},
69
- longestFrameMs: round(durations.at(-1) ?? 0),
70
- p50FrameMs: round(percentile(durations, 0.5)),
71
- p95FrameMs: round(percentile(durations, 0.95)),
72
- p99FrameMs: round(percentile(durations, 0.99))
73
- };
74
- }
75
- function commonFrameBudget(samples) {
76
- const budgets = samples.map((sample) => sample.frameBudgetMs).filter(
77
- (value) => value !== void 0 && Number.isFinite(value) && value > 0
78
- ).sort((first, second) => first - second);
79
- return budgets.length > 0 ? percentile(budgets, 0.5) : void 0;
80
- }
81
- function activeRenderingIntervals(samples, frameBudgetMs) {
82
- if (samples.every((sample) => sample.cadenceInterval === true)) {
83
- return samples.map((sample) => sample.durationMs).filter(
84
- (durationMs) => Number.isFinite(durationMs) && durationMs > 0 && durationMs <= frameBudgetMs * 3.01
85
- );
86
- }
87
- const ordered = [...samples].map((sample) => ({
88
- ...sample,
89
- startedAtEpochMs: sample.startedAtEpochMs ?? sample.completedAtEpochMs - sample.durationMs
90
- })).sort((first, second) => first.startedAtEpochMs - second.startedAtEpochMs);
91
- const intervals = [];
92
- for (let index = 1; index < ordered.length; index += 1) {
93
- const previous = ordered[index - 1];
94
- const current = ordered[index];
95
- if (!previous || !current) continue;
96
- const interval = current.startedAtEpochMs - previous.startedAtEpochMs;
97
- if (!Number.isFinite(interval) || interval <= 0) continue;
98
- const continuous = interval <= frameBudgetMs * 3.01;
99
- const explainedBySlowFrame = Math.max(previous.durationMs, current.durationMs) >= interval - frameBudgetMs * 1.01;
100
- if (continuous || explainedBySlowFrame) intervals.push(interval);
101
- }
102
- return intervals;
103
- }
104
- function percentile(sorted, quantile) {
105
- if (sorted.length === 1) return sorted[0] ?? 0;
106
- const position = (sorted.length - 1) * quantile;
107
- const lower = Math.floor(position);
108
- const upper = Math.ceil(position);
109
- const lowerValue = sorted[lower] ?? 0;
110
- const upperValue = sorted[upper] ?? lowerValue;
111
- return lowerValue + (upperValue - lowerValue) * (position - lower);
112
- }
113
- function round(value) {
114
- return Math.round(value * 1e3) / 1e3;
115
- }
116
- export {
117
- summarizeFrames
118
- };
@@ -1,289 +0,0 @@
1
- import { spawn, spawnSync } from "node:child_process";
2
- import fs from "node:fs";
3
- import path from "node:path";
4
- import { optionalVideoTiming } from "@farmslot/recipe-runner";
5
- import {
6
- ANDROID_RECORDING_TARGET_PREFIX,
7
- IOS_SIMULATOR_RECORDING_TARGET_PREFIX
8
- } from "../../recording-target.js";
9
- const RECORDING_START_TIMEOUT_MS = 5e3;
10
- const RECORDING_STOP_TIMEOUT_MS = 1e4;
11
- const RECORDING_TIME_LIMIT_SECONDS = 0;
12
- function androidScreenrecordArgs(remotePath) {
13
- return [
14
- "shell",
15
- "screenrecord",
16
- "--time-limit",
17
- String(RECORDING_TIME_LIMIT_SECONDS),
18
- remotePath
19
- ];
20
- }
21
- function createAndroidVideoRecorder(serial) {
22
- return {
23
- name: "adb-screenrecord",
24
- platform: "android",
25
- doctor: async () => doctorAndroidRecorder(serial),
26
- start: (request) => startAndroidRecording(serial, request)
27
- };
28
- }
29
- function createIosSimulatorVideoRecorder(simulator) {
30
- return {
31
- name: "simctl-recordVideo",
32
- platform: "ios",
33
- doctor: async () => doctorIosSimulatorRecorder(simulator),
34
- start: (request) => startIosSimulatorRecording(simulator, request)
35
- };
36
- }
37
- function doctorIosSimulatorRecorder(simulator) {
38
- const state = runSimctl(["getenv", simulator, "SIMULATOR_UDID"]);
39
- if (state.status !== 0 || !state.stdout.trim()) {
40
- return {
41
- ok: false,
42
- code: "ios_simulator_unavailable",
43
- message: `iOS simulator ${simulator} is not booted: ${commandFailure(state)}`,
44
- suggestedFix: `Boot ${simulator} and run xcrun simctl getenv ${simulator} SIMULATOR_UDID.`
45
- };
46
- }
47
- return {
48
- ok: true,
49
- code: "ok",
50
- message: `iOS simulator recording is ready on ${simulator}.`
51
- };
52
- }
53
- async function startIosSimulatorRecording(simulator, request) {
54
- const targetSimulator = iosTargetSimulator(request);
55
- if (targetSimulator !== simulator) {
56
- throw new Error(
57
- `iOS recording target ${targetSimulator ?? "<missing>"} does not match selected simulator ${simulator}.`
58
- );
59
- }
60
- fs.mkdirSync(path.dirname(request.outputPath), { recursive: true });
61
- const child = spawn(
62
- "xcrun",
63
- [
64
- "simctl",
65
- "io",
66
- simulator,
67
- "recordVideo",
68
- "--codec=h264",
69
- "--force",
70
- request.outputPath
71
- ],
72
- { stdio: ["ignore", "pipe", "pipe"] }
73
- );
74
- const output = [];
75
- child.stdout?.setEncoding("utf8");
76
- child.stderr?.setEncoding("utf8");
77
- child.stdout?.on("data", (chunk) => output.push(String(chunk)));
78
- child.stderr?.on("data", (chunk) => output.push(String(chunk)));
79
- await waitForIosRecordingStart(child, output);
80
- let stopped = false;
81
- return {
82
- async stop() {
83
- if (stopped) {
84
- throw new Error(
85
- `iOS simulator recording on ${simulator} was already stopped.`
86
- );
87
- }
88
- stopped = true;
89
- child.kill("SIGINT");
90
- await waitForChildExit(
91
- child,
92
- RECORDING_STOP_TIMEOUT_MS,
93
- "iOS simulator recordVideo"
94
- );
95
- const recorded = fs.statSync(request.outputPath);
96
- if (!recorded.isFile() || recorded.size <= 0) {
97
- throw new Error(
98
- `iOS simulator recording output is empty: ${request.outputPath}`
99
- );
100
- }
101
- return {
102
- recorder: {
103
- name: "simctl-recordVideo",
104
- platform: "ios",
105
- target: { selector: "simulator", value: simulator }
106
- }
107
- };
108
- }
109
- };
110
- }
111
- function doctorAndroidRecorder(serial) {
112
- const state = runAdb(serial, ["get-state"]);
113
- if (state.status !== 0 || state.stdout.trim() !== "device") {
114
- return {
115
- ok: false,
116
- code: "android_device_unavailable",
117
- message: `Android device ${serial} is not ready: ${commandFailure(state)}`,
118
- suggestedFix: `Run adb -s ${serial} get-state and reconnect or unlock the device.`
119
- };
120
- }
121
- const screenrecord = runAdb(serial, ["shell", "screenrecord", "--help"]);
122
- if (screenrecord.status !== 0) {
123
- return {
124
- ok: false,
125
- code: "android_screenrecord_unavailable",
126
- message: `Android screenrecord is unavailable on ${serial}: ${commandFailure(screenrecord)}`,
127
- suggestedFix: `Run adb -s ${serial} shell screenrecord --help.`
128
- };
129
- }
130
- return {
131
- ok: true,
132
- code: "ok",
133
- message: `Android screen recording is ready on ${serial}.`
134
- };
135
- }
136
- async function startAndroidRecording(serial, request) {
137
- const targetSerial = androidTargetSerial(request);
138
- if (targetSerial !== serial) {
139
- throw new Error(
140
- `Android recording target ${targetSerial ?? "<missing>"} does not match selected device ${serial}.`
141
- );
142
- }
143
- const existingPid = screenrecordPid(serial);
144
- if (existingPid) {
145
- throw new Error(
146
- `Android screenrecord is already running on ${serial} (pid ${existingPid}). Stop that recording before retrying.`
147
- );
148
- }
149
- const remotePath = `/sdcard/Download/mm-harness-recipe-${Date.now()}-${process.pid}.mp4`;
150
- const startedAtUnixMs = Date.now();
151
- const child = spawn(
152
- "adb",
153
- ["-s", serial, ...androidScreenrecordArgs(remotePath)],
154
- { stdio: ["ignore", "pipe", "pipe"] }
155
- );
156
- const stderr = [];
157
- child.stderr?.setEncoding("utf8");
158
- child.stderr?.on("data", (chunk) => stderr.push(String(chunk)));
159
- child.stdout?.resume();
160
- const recorderPid = await waitForScreenrecordStart(serial, child, stderr);
161
- let stopped = false;
162
- return {
163
- async stop() {
164
- if (stopped) {
165
- throw new Error(`Android screen recording on ${serial} was already stopped.`);
166
- }
167
- stopped = true;
168
- try {
169
- stopRemoteScreenrecord(serial, recorderPid);
170
- await waitForChildExit(
171
- child,
172
- RECORDING_STOP_TIMEOUT_MS,
173
- "Android screenrecord"
174
- );
175
- const stoppedAtUnixMs = Date.now();
176
- fs.mkdirSync(path.dirname(request.outputPath), { recursive: true });
177
- const pull = runAdb(serial, ["pull", remotePath, request.outputPath]);
178
- if (pull.status !== 0) {
179
- throw new Error(
180
- `Could not pull Android recording from ${serial}: ${commandFailure(pull)}`
181
- );
182
- }
183
- const output = fs.statSync(request.outputPath);
184
- if (!output.isFile() || output.size <= 0) {
185
- throw new Error(`Android recording output is empty: ${request.outputPath}`);
186
- }
187
- return {
188
- ...await optionalVideoTiming(request.outputPath, { startedAtUnixMs, stoppedAtUnixMs }),
189
- recorder: {
190
- name: "adb-screenrecord",
191
- platform: "android",
192
- target: { selector: "adb-serial", value: serial }
193
- }
194
- };
195
- } finally {
196
- runAdb(serial, ["shell", "rm", "-f", remotePath]);
197
- }
198
- }
199
- };
200
- }
201
- function androidTargetSerial(request) {
202
- if (request.target.kind === "android-device") return request.target.serial;
203
- if (request.target.kind !== "window-id") return void 0;
204
- if (!request.target.windowId.startsWith(ANDROID_RECORDING_TARGET_PREFIX)) return void 0;
205
- return request.target.windowId.slice(ANDROID_RECORDING_TARGET_PREFIX.length) || void 0;
206
- }
207
- async function waitForScreenrecordStart(serial, child, stderr) {
208
- const deadline = Date.now() + RECORDING_START_TIMEOUT_MS;
209
- while (Date.now() < deadline) {
210
- if (child.exitCode !== null) {
211
- throw new Error(
212
- `Android screenrecord exited before capture started (exit ${child.exitCode}): ${stderr.join("").trim() || "no recorder output"}`
213
- );
214
- }
215
- const pid = screenrecordPid(serial);
216
- if (pid) return pid;
217
- await sleep(100);
218
- }
219
- child.kill("SIGTERM");
220
- throw new Error(
221
- `Android screenrecord did not start on ${serial} within ${RECORDING_START_TIMEOUT_MS}ms.`
222
- );
223
- }
224
- function stopRemoteScreenrecord(serial, pid) {
225
- const stop = runAdb(serial, ["shell", "kill", "-2", pid]);
226
- if (stop.status !== 0 && screenrecordPid(serial) === pid) {
227
- throw new Error(`Could not stop Android screenrecord pid ${pid}: ${commandFailure(stop)}`);
228
- }
229
- }
230
- async function waitForChildExit(child, timeoutMs, recorderName) {
231
- if (child.exitCode !== null) return;
232
- await new Promise((resolve, reject) => {
233
- const timeout = setTimeout(() => {
234
- child.kill("SIGTERM");
235
- reject(new Error(`${recorderName} did not stop within ${timeoutMs}ms.`));
236
- }, timeoutMs);
237
- child.once("exit", () => {
238
- clearTimeout(timeout);
239
- resolve();
240
- });
241
- });
242
- }
243
- async function waitForIosRecordingStart(child, output) {
244
- const deadline = Date.now() + RECORDING_START_TIMEOUT_MS;
245
- while (Date.now() < deadline) {
246
- if (child.exitCode !== null) {
247
- throw new Error(
248
- `iOS simulator recordVideo exited before capture started (exit ${child.exitCode}): ${output.join("").trim() || "no recorder output"}`
249
- );
250
- }
251
- if (output.join("").includes("Recording started")) return;
252
- await sleep(100);
253
- }
254
- if (child.exitCode === null) return;
255
- throw new Error(
256
- `iOS simulator recordVideo did not start within ${RECORDING_START_TIMEOUT_MS}ms.`
257
- );
258
- }
259
- function iosTargetSimulator(request) {
260
- if (request.target.kind !== "window-id") return void 0;
261
- if (!request.target.windowId.startsWith(
262
- IOS_SIMULATOR_RECORDING_TARGET_PREFIX
263
- )) {
264
- return void 0;
265
- }
266
- return request.target.windowId.slice(IOS_SIMULATOR_RECORDING_TARGET_PREFIX.length) || void 0;
267
- }
268
- function screenrecordPid(serial) {
269
- const result = runAdb(serial, ["shell", "pidof", "screenrecord"]);
270
- if (result.status !== 0) return void 0;
271
- return result.stdout.trim().split(/\s+/u).find(Boolean);
272
- }
273
- function runAdb(serial, args) {
274
- return spawnSync("adb", ["-s", serial, ...args], { encoding: "utf8" });
275
- }
276
- function runSimctl(args) {
277
- return spawnSync("xcrun", ["simctl", ...args], { encoding: "utf8" });
278
- }
279
- function commandFailure(result) {
280
- return result.stderr.trim() || result.stdout.trim() || `exit ${result.status ?? "unknown"}`;
281
- }
282
- function sleep(durationMs) {
283
- return new Promise((resolve) => setTimeout(resolve, durationMs));
284
- }
285
- export {
286
- androidScreenrecordArgs,
287
- createAndroidVideoRecorder,
288
- createIosSimulatorVideoRecorder
289
- };
@@ -1,92 +0,0 @@
1
- import { constants as fsConstants } from "node:fs";
2
- import {
3
- lstat,
4
- mkdir,
5
- open,
6
- readFile,
7
- realpath,
8
- writeFile
9
- } from "node:fs/promises";
10
- import path from "node:path";
11
- async function writeContainedArtifact(artifactsDir, relativePath, value, label) {
12
- const file = resolveContainedArtifact(artifactsDir, relativePath, label);
13
- await mkdir(path.dirname(file), { recursive: true });
14
- await assertContainedParent(artifactsDir, file, label);
15
- await assertNotSymlink(file, label);
16
- const handle = await open(
17
- file,
18
- fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_TRUNC | fsConstants.O_NOFOLLOW,
19
- 384
20
- );
21
- try {
22
- await handle.writeFile(value);
23
- } finally {
24
- await handle.close();
25
- }
26
- }
27
- async function readContainedJsonArtifact(artifactsDir, relativePath, maxBytes, label) {
28
- const file = resolveContainedArtifact(artifactsDir, relativePath, label);
29
- await assertContainedParent(artifactsDir, file, label);
30
- await assertNotSymlink(file, label);
31
- const handle = await open(file, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
32
- try {
33
- const info = await handle.stat();
34
- if (!info.isFile() || info.size > maxBytes) {
35
- throw new Error(`${label} is not a bounded regular file.`);
36
- }
37
- return JSON.parse(await handle.readFile("utf8"));
38
- } finally {
39
- await handle.close();
40
- }
41
- }
42
- async function indexArtifactManifest(manifestPath, entries) {
43
- const parsed = JSON.parse(await readFile(manifestPath, "utf8"));
44
- const manifest = asRecord(parsed);
45
- const replacementPaths = new Set(entries.map((entry) => entry.path));
46
- const current = Array.isArray(manifest.artifacts) ? manifest.artifacts.filter(
47
- (artifact) => !replacementPaths.has(String(asRecord(artifact).path ?? ""))
48
- ) : [];
49
- await writeFile(
50
- manifestPath,
51
- `${JSON.stringify(
52
- { ...manifest, artifacts: [...current, ...entries] },
53
- null,
54
- 2
55
- )}
56
- `
57
- );
58
- }
59
- function resolveContainedArtifact(artifactsDir, relativePath, label) {
60
- const root = path.resolve(artifactsDir);
61
- const file = path.resolve(root, relativePath);
62
- const relative = path.relative(root, file);
63
- if (relative.startsWith("..") || path.isAbsolute(relative)) {
64
- throw new Error(`${label} path must stay inside artifactsDir.`);
65
- }
66
- return file;
67
- }
68
- async function assertContainedParent(artifactsDir, file, label) {
69
- const root = await realpath(artifactsDir);
70
- const parent = await realpath(path.dirname(file));
71
- const relative = path.relative(root, parent);
72
- if (relative.startsWith("..") || path.isAbsolute(relative)) {
73
- throw new Error(`${label} parent must stay inside artifactsDir.`);
74
- }
75
- }
76
- async function assertNotSymlink(file, label) {
77
- try {
78
- if ((await lstat(file)).isSymbolicLink()) {
79
- throw new Error(`${label} must not be a symbolic link.`);
80
- }
81
- } catch (error) {
82
- if (error.code !== "ENOENT") throw error;
83
- }
84
- }
85
- function asRecord(value) {
86
- return value && typeof value === "object" && !Array.isArray(value) ? value : {};
87
- }
88
- export {
89
- indexArtifactManifest,
90
- readContainedJsonArtifact,
91
- writeContainedArtifact
92
- };
@@ -1,149 +0,0 @@
1
- import { ensureRuntimeDirectory, processIdentity } from "@farmslot/recipe-runner/runtime/operation";
2
- import { randomUUID } from "node:crypto";
3
- import fs from "node:fs";
4
- import path from "node:path";
5
- import { recipeRuntimeDir } from "./paths.js";
6
- function acquireCheckoutLock(target, operation) {
7
- const repoRoot = fs.realpathSync(target);
8
- const lockPath = path.join(repoRoot, recipeRuntimeDir(), "sandbox.lock");
9
- ensureRuntimeDirectory(path.dirname(lockPath));
10
- const inheritedToken = process.env.MM_HARNESS_CHECKOUT_LOCK_TOKEN;
11
- for (let attempt = 0; attempt < 4; attempt += 1) {
12
- try {
13
- const identity = processIdentity(process.pid);
14
- if (!identity) throw new Error("Cannot identify checkout lock owner process");
15
- const fd = fs.openSync(lockPath, "wx", 384);
16
- fs.fchmodSync(fd, 384);
17
- const token = randomUUID();
18
- const owner = {
19
- schemaVersion: 1,
20
- pid: process.pid,
21
- processStartedAt: identity,
22
- operation,
23
- repoRoot,
24
- token,
25
- startedAt: (/* @__PURE__ */ new Date()).toISOString()
26
- };
27
- fs.writeFileSync(fd, `${JSON.stringify(owner, null, 2)}
28
- `);
29
- fs.closeSync(fd);
30
- process.env.MM_HARNESS_CHECKOUT_LOCK_TOKEN = token;
31
- return {
32
- path: lockPath,
33
- release: () => {
34
- const current = readOwner(lockPath);
35
- if (current?.pid === process.pid && current.token === token && !childrenAlive(current)) {
36
- fs.rmSync(lockPath, { force: true });
37
- fs.rmSync(path.join(path.dirname(lockPath), "lock-members", token), { recursive: true, force: true });
38
- }
39
- if (process.env.MM_HARNESS_CHECKOUT_LOCK_TOKEN === token) {
40
- if (inheritedToken) process.env.MM_HARNESS_CHECKOUT_LOCK_TOKEN = inheritedToken;
41
- else delete process.env.MM_HARNESS_CHECKOUT_LOCK_TOKEN;
42
- }
43
- }
44
- };
45
- } catch (error) {
46
- if (error.code !== "EEXIST") throw error;
47
- const owner = readOwner(lockPath);
48
- if (inheritedToken && owner?.token === inheritedToken && owner.repoRoot === repoRoot && typeof owner.pid === "number" && ownerAlive(owner)) {
49
- trackCheckoutChild(repoRoot, process.pid);
50
- return { path: lockPath, release: () => void 0 };
51
- }
52
- if (owner && typeof owner.pid === "number" && ownerAlive(owner)) {
53
- return {
54
- path: lockPath,
55
- owner,
56
- message: `checkout sandbox is busy with ${String(owner.operation ?? "another operation")} (pid ${owner.pid}).`
57
- };
58
- }
59
- if (!owner || childrenAlive(owner)) {
60
- return { path: lockPath, owner, message: `checkout sandbox owner is unreadable or has surviving child processes. Inspect ${lockPath} and its lock-members directory; remove the lock only after verifying all recorded processes have exited.` };
61
- }
62
- claimStaleLock(lockPath, owner);
63
- }
64
- }
65
- return { path: lockPath, owner: readOwner(lockPath), message: "checkout sandbox lock could not be acquired." };
66
- }
67
- function claimStaleLock(lockPath, expectedOwner) {
68
- const claimedPath = `${lockPath}.stale-${process.pid}-${randomUUID()}`;
69
- try {
70
- fs.renameSync(lockPath, claimedPath);
71
- } catch (error) {
72
- if (error.code === "ENOENT") return;
73
- throw error;
74
- }
75
- try {
76
- const claimedOwner = readOwner(claimedPath);
77
- if (!sameOwner(claimedOwner, expectedOwner) || claimedOwner && typeof claimedOwner.pid === "number" && (ownerAlive(claimedOwner) || childrenAlive(claimedOwner))) {
78
- try {
79
- fs.linkSync(claimedPath, lockPath);
80
- } catch (error) {
81
- if (error.code !== "EEXIST") throw error;
82
- }
83
- } else if (typeof claimedOwner?.token === "string" && /^[a-f0-9-]{36}$/.test(claimedOwner.token)) {
84
- fs.rmSync(path.join(path.dirname(lockPath), "lock-members", claimedOwner.token), { recursive: true, force: true });
85
- }
86
- } finally {
87
- fs.rmSync(claimedPath, { force: true });
88
- }
89
- }
90
- function sameOwner(left, right) {
91
- if (!left || !right) return left === right;
92
- return left.pid === right.pid && left.token === right.token && left.startedAt === right.startedAt && left.repoRoot === right.repoRoot;
93
- }
94
- function readOwner(lockPath) {
95
- try {
96
- const value = JSON.parse(fs.readFileSync(lockPath, "utf8"));
97
- return value && typeof value === "object" && !Array.isArray(value) ? value : null;
98
- } catch {
99
- return null;
100
- }
101
- }
102
- function ownerAlive(owner) {
103
- if (typeof owner.pid !== "number") return true;
104
- if (typeof owner.processStartedAt === "string") return processIdentity(owner.pid) === owner.processStartedAt;
105
- return processAlive(owner.pid);
106
- }
107
- function childrenAlive(owner) {
108
- if (typeof owner.repoRoot !== "string" || typeof owner.token !== "string" || !/^[a-f0-9-]{36}$/.test(owner.token)) return false;
109
- const directory = path.join(owner.repoRoot, recipeRuntimeDir(), "lock-members", owner.token);
110
- if (!fs.existsSync(directory)) return false;
111
- return fs.readdirSync(directory).filter((name) => name.endsWith(".json")).some((name) => {
112
- const child = readOwner(path.join(directory, name));
113
- if (child && typeof child.processGroupId === "number" && child.processGroupId > 0) return processAlive(-child.processGroupId);
114
- return !child || typeof child.pid !== "number" || typeof child.processStartedAt !== "string" || child.pid !== owner.pid && processIdentity(child.pid) === child.processStartedAt;
115
- });
116
- }
117
- function trackCheckoutChild(target, pid, ownsProcessGroup = false) {
118
- const repoRoot = fs.realpathSync(target);
119
- const lockPath = path.join(repoRoot, recipeRuntimeDir(), "sandbox.lock");
120
- const owner = readOwner(lockPath);
121
- if (!owner || owner.token !== process.env.MM_HARNESS_CHECKOUT_LOCK_TOKEN || typeof owner.token !== "string" || !/^[a-f0-9-]{36}$/.test(owner.token)) return () => void 0;
122
- const identity = processIdentity(pid);
123
- if (!identity && !ownsProcessGroup) return () => void 0;
124
- const directory = path.join(repoRoot, recipeRuntimeDir(), "lock-members", owner.token);
125
- ensureRuntimeDirectory(directory);
126
- const file = path.join(directory, `${pid}.json`);
127
- const previous = readOwner(file);
128
- const group = ownsProcessGroup || previous?.processStartedAt === identity && previous?.processGroupId === pid;
129
- const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
130
- fs.writeFileSync(temporary, JSON.stringify({ pid, processStartedAt: identity, ...group ? { processGroupId: pid } : {} }), { mode: 384 });
131
- fs.chmodSync(temporary, 384);
132
- fs.renameSync(temporary, file);
133
- return () => {
134
- const current = readOwner(file);
135
- if (current?.pid === pid && current.processStartedAt === identity) fs.rmSync(file, { force: true });
136
- };
137
- }
138
- function processAlive(pid) {
139
- try {
140
- process.kill(pid, 0);
141
- return true;
142
- } catch (error) {
143
- return error.code === "EPERM";
144
- }
145
- }
146
- export {
147
- acquireCheckoutLock,
148
- trackCheckoutChild
149
- };