rnxsim 0.0.0 → 0.1.312

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 (228) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +512 -0
  3. package/detox/colors.ts +54 -0
  4. package/detox/config-loader.ts +135 -0
  5. package/detox/element-types.ts +39 -0
  6. package/detox/expectations.ts +537 -0
  7. package/detox/gestures.ts +480 -0
  8. package/detox/index.ts +2118 -0
  9. package/detox/jest-preset.cjs +51 -0
  10. package/detox/jest-setup-after-env.cjs +18 -0
  11. package/detox/matchers.ts +30 -0
  12. package/detox/motion-change.cjs +109 -0
  13. package/detox/motion-change.d.cts +35 -0
  14. package/detox/navigation.ts +43 -0
  15. package/detox/screenshots/animated-color-test-rest-norngh.png +0 -0
  16. package/detox/screenshots/color-test-after-drag-norngh.png +0 -0
  17. package/detox/screenshots/color-test-rest-norngh.png +0 -0
  18. package/detox/screenshots/theme-blue-toggle.png +0 -0
  19. package/detox/screenshots/theme-blue.png +0 -0
  20. package/detox/screenshots/theme-red-toggle.png +0 -0
  21. package/detox/screenshots/theme-red.png +0 -0
  22. package/dist-cli/bin.js +15 -0
  23. package/dist-cli/chunks/agent-NXVDDCFG.js +48 -0
  24. package/dist-cli/chunks/agent-wrapper-DXJCFKL3.js +17 -0
  25. package/dist-cli/chunks/app-fonts-W5AJZQJF.js +4 -0
  26. package/dist-cli/chunks/assert-3LOJEUDT.js +49 -0
  27. package/dist-cli/chunks/auth-MFTYYPNX.js +12 -0
  28. package/dist-cli/chunks/beta-GT3NN67B.js +4 -0
  29. package/dist-cli/chunks/camera-6EAK7CUL.js +22 -0
  30. package/dist-cli/chunks/chunk-274UD5FD.js +4 -0
  31. package/dist-cli/chunks/chunk-2SA2VTGP.js +5 -0
  32. package/dist-cli/chunks/chunk-33FKQH54.js +5 -0
  33. package/dist-cli/chunks/chunk-37I5AIS7.js +3 -0
  34. package/dist-cli/chunks/chunk-3D57IBPC.js +4 -0
  35. package/dist-cli/chunks/chunk-4BXCZHAU.js +4 -0
  36. package/dist-cli/chunks/chunk-5OX7ANRA.js +5 -0
  37. package/dist-cli/chunks/chunk-66NF2KNG.js +9 -0
  38. package/dist-cli/chunks/chunk-6UKJB2VC.js +125 -0
  39. package/dist-cli/chunks/chunk-7INBNA3U.js +26 -0
  40. package/dist-cli/chunks/chunk-7VWOSTD5.js +5 -0
  41. package/dist-cli/chunks/chunk-AY47WKS5.js +23 -0
  42. package/dist-cli/chunks/chunk-B7KMNHX7.js +4 -0
  43. package/dist-cli/chunks/chunk-BKNOLP3J.js +4 -0
  44. package/dist-cli/chunks/chunk-BRIZOR4K.js +28 -0
  45. package/dist-cli/chunks/chunk-DM6KUAOZ.js +4 -0
  46. package/dist-cli/chunks/chunk-EUAVBTKU.js +4 -0
  47. package/dist-cli/chunks/chunk-FTC4M26Q.js +4 -0
  48. package/dist-cli/chunks/chunk-FWYYCUAZ.js +4 -0
  49. package/dist-cli/chunks/chunk-G74YIEVS.js +7 -0
  50. package/dist-cli/chunks/chunk-GZDHEONW.js +507 -0
  51. package/dist-cli/chunks/chunk-HG7ORJMT.js +5 -0
  52. package/dist-cli/chunks/chunk-HMDOLCKG.js +15 -0
  53. package/dist-cli/chunks/chunk-JG6S4RSH.js +4 -0
  54. package/dist-cli/chunks/chunk-KR5BGMBZ.js +4 -0
  55. package/dist-cli/chunks/chunk-KZW5WBGU.js +983 -0
  56. package/dist-cli/chunks/chunk-L24Z3CRS.js +30 -0
  57. package/dist-cli/chunks/chunk-LDQ7HVCW.js +4 -0
  58. package/dist-cli/chunks/chunk-MENV7VLD.js +4 -0
  59. package/dist-cli/chunks/chunk-MNGQ42QE.js +568 -0
  60. package/dist-cli/chunks/chunk-N6A3SXMR.js +4 -0
  61. package/dist-cli/chunks/chunk-NLEMQ4PY.js +6 -0
  62. package/dist-cli/chunks/chunk-NPVGMH2N.js +101 -0
  63. package/dist-cli/chunks/chunk-OR3XTXEB.js +107 -0
  64. package/dist-cli/chunks/chunk-PEPUGJVI.js +4 -0
  65. package/dist-cli/chunks/chunk-Q3RGLERJ.js +142 -0
  66. package/dist-cli/chunks/chunk-SI3BUTQR.js +4 -0
  67. package/dist-cli/chunks/chunk-T4FT6CM7.js +4 -0
  68. package/dist-cli/chunks/chunk-TBMVBI26.js +7 -0
  69. package/dist-cli/chunks/chunk-TCWQJILS.js +5 -0
  70. package/dist-cli/chunks/chunk-TGQNQ6XT.js +766 -0
  71. package/dist-cli/chunks/chunk-TYE3WBSR.js +5 -0
  72. package/dist-cli/chunks/chunk-U4PN6JOT.js +6 -0
  73. package/dist-cli/chunks/chunk-VUFRB347.js +4 -0
  74. package/dist-cli/chunks/chunk-VZBLFMYC.js +10 -0
  75. package/dist-cli/chunks/chunk-WBT32R6N.js +25 -0
  76. package/dist-cli/chunks/chunk-XRGEEPJ5.js +38 -0
  77. package/dist-cli/chunks/chunk-Y2FSZSO7.js +71 -0
  78. package/dist-cli/chunks/chunk-YRV7HR3P.js +74 -0
  79. package/dist-cli/chunks/chunk-YZRFF4YA.js +14 -0
  80. package/dist-cli/chunks/chunk-ZMZTHU7V.js +4 -0
  81. package/dist-cli/chunks/cleanup-H3GWQB63.js +25 -0
  82. package/dist-cli/chunks/cli-version-3QPAJ3LC.js +4 -0
  83. package/dist-cli/chunks/compat-5WG2XJV7.js +20 -0
  84. package/dist-cli/chunks/config-ANMBVVLL.js +13 -0
  85. package/dist-cli/chunks/control-NSGNQFJG.js +4 -0
  86. package/dist-cli/chunks/daemon-JHF4BJ3U.js +4 -0
  87. package/dist-cli/chunks/debug-CHV2O2KF.js +200 -0
  88. package/dist-cli/chunks/desktop-FIPIK4CH.js +31 -0
  89. package/dist-cli/chunks/detox-CVUJP7K6.js +52 -0
  90. package/dist-cli/chunks/device-6WCUM4W6.js +18 -0
  91. package/dist-cli/chunks/diagnose-I6EMWEII.js +43 -0
  92. package/dist-cli/chunks/disk-cleanup-P27CYFTN.js +4 -0
  93. package/dist-cli/chunks/drivers-HQ6AKZT6.js +4 -0
  94. package/dist-cli/chunks/film-UOQFFYNF.js +74 -0
  95. package/dist-cli/chunks/flow-N27MRRNL.js +4 -0
  96. package/dist-cli/chunks/help-SF6FEGEE.js +4 -0
  97. package/dist-cli/chunks/hidden-runtime-alias-NJYFSJKZ.js +4 -0
  98. package/dist-cli/chunks/home-paths-XOV44JEF.js +4 -0
  99. package/dist-cli/chunks/inspect-CWFRFQTY.js +4 -0
  100. package/dist-cli/chunks/install-desktop-KLQDNXZQ.js +4 -0
  101. package/dist-cli/chunks/login-BPFQENCX.js +28 -0
  102. package/dist-cli/chunks/logout-5A45ZGY7.js +4 -0
  103. package/dist-cli/chunks/maestro-E4CSDMLM.js +104 -0
  104. package/dist-cli/chunks/maestro-generate-CKMOJQX2.js +36 -0
  105. package/dist-cli/chunks/mode-SZYNMIQF.js +26 -0
  106. package/dist-cli/chunks/optional-demo-registry-FRH4SFVG.js +4 -0
  107. package/dist-cli/chunks/perf-VXU2XJAX.js +48 -0
  108. package/dist-cli/chunks/permissions-IRIAOVND.js +13 -0
  109. package/dist-cli/chunks/record-RY7FPLLH.js +72 -0
  110. package/dist-cli/chunks/report-issue-KNWXOE53.js +29 -0
  111. package/dist-cli/chunks/runtime-AR3GJTGK.js +4 -0
  112. package/dist-cli/chunks/screenshot-command-QLALR6GY.js +159 -0
  113. package/dist-cli/chunks/screenshot-layers-OQFMFVRL.js +20 -0
  114. package/dist-cli/chunks/screenshots-capture-6TSRJFGB.js +42 -0
  115. package/dist-cli/chunks/serve-F54J2NQO.js +44 -0
  116. package/dist-cli/chunks/setup-7V6UKX4U.js +32 -0
  117. package/dist-cli/chunks/skills-QQBOSETH.js +50 -0
  118. package/dist-cli/chunks/storage-TAPMKK5O.js +37 -0
  119. package/dist-cli/chunks/store-7NX3S43H.js +4 -0
  120. package/dist-cli/chunks/telemetry-E7BUK6RA.js +4 -0
  121. package/dist-cli/chunks/timeline-IAM564NA.js +24 -0
  122. package/dist-cli/chunks/upgrade-CD55QXIX.js +7 -0
  123. package/dist-cli/chunks/upload-GZ6LIMYM.js +4 -0
  124. package/dist-cli/chunks/version-3DYJ5AS3.js +6 -0
  125. package/dist-cli/chunks/web-6NW6YLPI.js +4 -0
  126. package/dist-cli/chunks/what-happened-CZZDOOVG.js +25 -0
  127. package/dist-lib/agent-daemon-client.cjs +468 -0
  128. package/dist-lib/agent-events.cjs +48 -0
  129. package/dist-lib/agent-identity.cjs +53 -0
  130. package/dist-lib/agent-sessions.cjs +725 -0
  131. package/dist-lib/attached-projects.cjs +526 -0
  132. package/dist-lib/auth/shared-session.cjs +195 -0
  133. package/dist-lib/backend-origin.cjs +70 -0
  134. package/dist-lib/beta.cjs +41 -0
  135. package/dist-lib/beta.mjs +13 -0
  136. package/dist-lib/bridge-constants.cjs +161 -0
  137. package/dist-lib/cli-constants.cjs +32 -0
  138. package/dist-lib/config.cjs +96 -0
  139. package/dist-lib/detox/index.cjs +2863 -0
  140. package/dist-lib/detox/jest-preset.cjs +51 -0
  141. package/dist-lib/dev-bundle-resolution.cjs +223 -0
  142. package/dist-lib/home-paths.cjs +421 -0
  143. package/dist-lib/host/bridge-host.cjs +5600 -0
  144. package/dist-lib/host/fetch-proxy-handler.cjs +373 -0
  145. package/dist-lib/host/fetch-proxy-overrides.cjs +63 -0
  146. package/dist-lib/host/fetch-proxy-overrides.mjs +37 -0
  147. package/dist-lib/host/websocket-proxy.cjs +214 -0
  148. package/dist-lib/index.cjs +513 -0
  149. package/dist-lib/jump-to-source-babel.cjs +93 -0
  150. package/dist-lib/menu.cjs +620 -0
  151. package/dist-lib/menu.mjs +581 -0
  152. package/dist-lib/metro.cjs +297 -0
  153. package/dist-lib/profiles.cjs +246 -0
  154. package/dist-lib/public-brand.cjs +39 -0
  155. package/dist-lib/render-mode.cjs +54 -0
  156. package/dist-lib/scripts/dev-server-scanner.cjs +1034 -0
  157. package/dist-lib/sdk.cjs +2333 -0
  158. package/dist-lib/sdk.mjs +2240 -0
  159. package/dist-lib/skills.cjs +23911 -0
  160. package/dist-lib/vite.cjs +358 -0
  161. package/package.json +189 -18
  162. package/scripts/dev-server-scanner.ts +941 -0
  163. package/scripts/optional-demo-registry.ts +113 -0
  164. package/scripts/postinstall.cjs +3 -0
  165. package/scripts/preuninstall.cjs +118 -0
  166. package/skills/contrast/SKILL.md +171 -0
  167. package/skills/rnx-debug/SKILL.md +425 -0
  168. package/skills/rnx-perf/SKILL.md +295 -0
  169. package/skills/rnx-setup/SKILL.md +160 -0
  170. package/skills/rnx-test/SKILL.md +292 -0
  171. package/skills/rnx-visual/SKILL.md +122 -0
  172. package/src/agent-daemon-client.ts +410 -0
  173. package/src/agent-events.ts +71 -0
  174. package/src/agent-identity.ts +23 -0
  175. package/src/agent-prompt.ts +71 -0
  176. package/src/agent-sessions.ts +573 -0
  177. package/src/attached-projects.ts +539 -0
  178. package/src/auth/shared-session.ts +204 -0
  179. package/src/backend-origin.ts +49 -0
  180. package/src/beta.ts +19 -0
  181. package/src/bridge-constants.ts +171 -0
  182. package/src/browser-cache.ts +16 -0
  183. package/src/cli-constants.ts +1 -0
  184. package/src/cli-version.ts +32 -0
  185. package/src/codex-client.ts +215 -0
  186. package/src/config.ts +134 -0
  187. package/src/dev-bundle-resolution.ts +236 -0
  188. package/src/disk-cleanup.ts +466 -0
  189. package/src/home-paths.ts +552 -0
  190. package/src/host/agent-host.ts +666 -0
  191. package/src/host/bridge-host.ts +2626 -0
  192. package/src/host/fetch-proxy-handler.ts +397 -0
  193. package/src/host/fetch-proxy-overrides.ts +51 -0
  194. package/src/host/open-url.ts +89 -0
  195. package/src/host/websocket-proxy.ts +210 -0
  196. package/src/index.ts +12 -705
  197. package/src/jump-to-source-babel.ts +135 -0
  198. package/src/menu.ts +734 -0
  199. package/src/metro-plugin.ts +139 -0
  200. package/src/native-dev-bundle-url.ts +96 -0
  201. package/src/native-seam-manifest.ts +341 -0
  202. package/src/plist.ts +27 -0
  203. package/src/poll-until.ts +92 -0
  204. package/src/profiles.ts +179 -0
  205. package/src/public-brand.ts +9 -0
  206. package/src/render-mode.ts +27 -0
  207. package/src/runtime-assets.ts +228 -0
  208. package/src/runtime-delivery.ts +54 -0
  209. package/src/screenshots/compose.ts +422 -0
  210. package/src/screenshots/frame-compose.ts +454 -0
  211. package/src/screenshots/orchestrate.ts +256 -0
  212. package/src/screenshots/org-deck.ts +91 -0
  213. package/src/screenshots/registry.ts +58 -0
  214. package/src/screenshots/schema.ts +364 -0
  215. package/src/sdk.ts +7 -0
  216. package/src/settings-bundle.ts +418 -0
  217. package/src/skills/builtin/a11y-review.ts +132 -0
  218. package/src/skills/builtin/compat-check.ts +71 -0
  219. package/src/skills/builtin/maestro-test.ts +121 -0
  220. package/src/skills/builtin/perf-profile.ts +90 -0
  221. package/src/skills/builtin/screenshot-all.ts +52 -0
  222. package/src/skills/builtin/visual-diff.ts +100 -0
  223. package/src/skills/registry.ts +107 -0
  224. package/src/skills/types.ts +41 -0
  225. package/src/team-machine-prompt.ts +73 -0
  226. package/src/vite-plugin-one.ts +190 -0
  227. package/src/vite-plugin.ts +1750 -0
  228. package/src/worklets-babel.ts +134 -0
@@ -0,0 +1,454 @@
1
+ import {
2
+ LEGACY_BOTTOM_BEZEL,
3
+ LEGACY_FRAME_RADIUS,
4
+ LEGACY_HOME_BUTTON_RING,
5
+ LEGACY_HOME_BUTTON_SIZE,
6
+ LEGACY_SIDE_BEZEL,
7
+ LEGACY_TOP_BEZEL,
8
+ SIDE_BUTTON_RESERVE,
9
+ } from 'sootsim-engine/ios/chrome-metrics'
10
+ import {
11
+ getDevice,
12
+ resolveDeviceFrameGeometry,
13
+ type DeviceModel,
14
+ } from 'sootsim-engine/settings'
15
+ import { launchReapedChrome } from '../../../../scripts/lib/reap-browser'
16
+
17
+ const METALLIC_RING_SHADOW =
18
+ 'inset 0 0 0 0.5px #000, inset 0 0 0 2px #757575, inset 0 0 0 5px #212121'
19
+ const FRAME_OUTLINE_BLEED = 1
20
+
21
+ type FrameButtonLayout = {
22
+ side: 'left' | 'right'
23
+ top: number
24
+ height: number
25
+ width: number
26
+ }
27
+
28
+ type LegacyHomeButtonLayout = {
29
+ top: number
30
+ left: number
31
+ size: number
32
+ ring: number
33
+ }
34
+
35
+ export interface FramedScreenshotLayout {
36
+ model: DeviceModel
37
+ renderScale: number
38
+ outerWidth: number
39
+ outerHeight: number
40
+ logicalOuterWidth: number
41
+ logicalOuterHeight: number
42
+ logicalFrameWidth: number
43
+ logicalFrameHeight: number
44
+ logicalFrameLeft: number
45
+ logicalFrameTop: number
46
+ logicalScreenWidth: number
47
+ logicalScreenHeight: number
48
+ logicalScreenLeft: number
49
+ logicalScreenTop: number
50
+ logicalScreenRadius: number
51
+ logicalFrameRadius: number
52
+ frameBackground: string
53
+ frameOutline: string
54
+ metallicRingShadow: string | null
55
+ buttons: FrameButtonLayout[]
56
+ legacyHomeButton: LegacyHomeButtonLayout | null
57
+ showHomeIndicator: boolean
58
+ logicalHomeIndicatorStripHeight: number
59
+ }
60
+
61
+ // Exact shell device frame layout for still exports.
62
+ // Mirrors DeviceFrame's bezel/button geometry while intentionally excluding:
63
+ // - the electron simulator top bar
64
+ // - the browser rail / gutter
65
+ // The screen bitmap itself still comes from the raw bridge screenshot.
66
+ export function getFramedScreenshotLayout(model: DeviceModel): FramedScreenshotLayout {
67
+ const spec = getDevice(model)
68
+ const renderScale = spec.scale
69
+ // legacy = home-button iPhone (SE). the canvas workspace also has no island /
70
+ // home indicator / corner radius, but no status bar, so gate on statusBar > 0
71
+ // to keep it out of the SE shell (mirrors DeviceFrame.isLegacyHardware).
72
+ const isLegacyHardware =
73
+ spec.statusBarHeight > 0 &&
74
+ !spec.dynamicIsland &&
75
+ spec.homeIndicatorHeight === 0 &&
76
+ spec.cornerRadius === 0
77
+ const logicalScreenWidth = spec.width
78
+ const logicalScreenHeight = spec.height
79
+ const modernGeometry = resolveDeviceFrameGeometry(spec)
80
+ const logicalFrameWidth = isLegacyHardware
81
+ ? spec.width + LEGACY_SIDE_BEZEL * 2
82
+ : modernGeometry.frameWidth
83
+ const logicalFrameHeight = isLegacyHardware
84
+ ? spec.height + LEGACY_TOP_BEZEL + LEGACY_BOTTOM_BEZEL
85
+ : modernGeometry.frameHeight
86
+ const logicalFrameLeft = SIDE_BUTTON_RESERVE
87
+ const logicalFrameTop = FRAME_OUTLINE_BLEED
88
+ const logicalOuterWidth = logicalFrameWidth + logicalFrameLeft * 2
89
+ const logicalOuterHeight = logicalFrameHeight + FRAME_OUTLINE_BLEED * 2
90
+ const logicalScreenLeft =
91
+ logicalFrameLeft + (isLegacyHardware ? LEGACY_SIDE_BEZEL : modernGeometry.screenLeft)
92
+ const logicalScreenTop = isLegacyHardware ? LEGACY_TOP_BEZEL : modernGeometry.screenTop
93
+ const logicalFrameRadius = isLegacyHardware
94
+ ? LEGACY_FRAME_RADIUS
95
+ : modernGeometry.frameRadius
96
+ const buttons: FrameButtonLayout[] = [
97
+ {
98
+ side: spec.hardwareButtons.lock.side,
99
+ top: spec.hardwareButtons.lock.top,
100
+ height: spec.hardwareButtons.lock.height,
101
+ width: spec.hardwareButtons.width,
102
+ },
103
+ {
104
+ side: spec.hardwareButtons.ringToggle.side,
105
+ top: spec.hardwareButtons.ringToggle.top,
106
+ height: spec.hardwareButtons.ringToggle.height,
107
+ width: spec.hardwareButtons.width,
108
+ },
109
+ {
110
+ side: spec.hardwareButtons.volumeUp.side,
111
+ top: spec.hardwareButtons.volumeUp.top,
112
+ height: spec.hardwareButtons.volumeUp.height,
113
+ width: spec.hardwareButtons.width,
114
+ },
115
+ {
116
+ side: spec.hardwareButtons.volumeDown.side,
117
+ top: spec.hardwareButtons.volumeDown.top,
118
+ height: spec.hardwareButtons.volumeDown.height,
119
+ width: spec.hardwareButtons.width,
120
+ },
121
+ ]
122
+
123
+ return {
124
+ model,
125
+ renderScale,
126
+ outerWidth: Math.round(logicalOuterWidth * renderScale),
127
+ outerHeight: Math.round(logicalOuterHeight * renderScale),
128
+ logicalOuterWidth,
129
+ logicalOuterHeight,
130
+ logicalFrameWidth,
131
+ logicalFrameHeight,
132
+ logicalFrameLeft,
133
+ logicalFrameTop,
134
+ logicalScreenWidth,
135
+ logicalScreenHeight,
136
+ logicalScreenLeft,
137
+ logicalScreenTop,
138
+ logicalScreenRadius: isLegacyHardware ? 0 : modernGeometry.screenRadius,
139
+ logicalFrameRadius,
140
+ frameBackground: isLegacyHardware
141
+ ? 'linear-gradient(180deg, #1b1c20 0%, #0f1012 42%, #050608 100%)'
142
+ : '#000000',
143
+ frameOutline: isLegacyHardware ? '0 0 0 1px #1f2125' : '0 0 0 1px #333',
144
+ metallicRingShadow: isLegacyHardware ? null : METALLIC_RING_SHADOW,
145
+ buttons,
146
+ legacyHomeButton: isLegacyHardware
147
+ ? {
148
+ top:
149
+ LEGACY_TOP_BEZEL +
150
+ spec.height +
151
+ (LEGACY_BOTTOM_BEZEL - LEGACY_HOME_BUTTON_SIZE) / 2,
152
+ left: spec.width / 2 - LEGACY_HOME_BUTTON_SIZE / 2 + LEGACY_SIDE_BEZEL,
153
+ size: LEGACY_HOME_BUTTON_SIZE,
154
+ ring: LEGACY_HOME_BUTTON_RING,
155
+ }
156
+ : null,
157
+ showHomeIndicator: spec.homeIndicatorHeight > 0,
158
+ logicalHomeIndicatorStripHeight: spec.homeIndicatorHeight,
159
+ }
160
+ }
161
+
162
+ function renderSideButtonHtml(button: FrameButtonLayout): string {
163
+ const isRight = button.side === 'right'
164
+ const clip = isRight ? 'inset(-3px -3px -3px 0)' : 'inset(-3px 0 -3px -3px)'
165
+ return `
166
+ <div
167
+ aria-hidden="true"
168
+ style="
169
+ position:absolute;
170
+ ${isRight ? `right:-${button.width}px;` : `left:-${button.width}px;`}
171
+ top:${button.top}px;
172
+ width:${button.width}px;
173
+ height:${button.height}px;
174
+ background-color:#212121;
175
+ border-top-left-radius:${isRight ? 0 : 1.5}px;
176
+ border-bottom-left-radius:${isRight ? 0 : 1.5}px;
177
+ border-top-right-radius:${isRight ? 1.5 : 0}px;
178
+ border-bottom-right-radius:${isRight ? 1.5 : 0}px;
179
+ box-shadow:
180
+ inset 0 0.5px 0 #616161,
181
+ inset 0 -0.5px 0 #3F3F3F,
182
+ ${isRight ? 'inset -0.5px 0 0 #3F3F3F,' : 'inset 0.5px 0 0 #3F3F3F,'}
183
+ 0 0 0 0.5px #676767,
184
+ 0 0 0 1.5px #000;
185
+ clip-path:${clip};
186
+ pointer-events:none;
187
+ z-index:2;
188
+ "
189
+ ></div>
190
+ `
191
+ }
192
+
193
+ function renderFrameHtml(
194
+ layout: FramedScreenshotLayout,
195
+ rawImageDataUrl: string,
196
+ ): string {
197
+ const buttonsHtml = layout.buttons.map(renderSideButtonHtml).join('')
198
+
199
+ const ringHtml = layout.metallicRingShadow
200
+ ? `
201
+ <div
202
+ aria-hidden="true"
203
+ style="
204
+ position:absolute;
205
+ inset:0;
206
+ border-radius:${layout.logicalFrameRadius}px;
207
+ box-shadow:${layout.metallicRingShadow};
208
+ pointer-events:none;
209
+ z-index:10;
210
+ "
211
+ ></div>
212
+ `
213
+ : ''
214
+
215
+ const legacyHomeButtonHtml = layout.legacyHomeButton
216
+ ? `
217
+ <div
218
+ aria-hidden="true"
219
+ style="
220
+ position:absolute;
221
+ top:${layout.legacyHomeButton.top}px;
222
+ left:${layout.legacyHomeButton.left}px;
223
+ width:${layout.legacyHomeButton.size}px;
224
+ height:${layout.legacyHomeButton.size}px;
225
+ border-radius:50%;
226
+ background:
227
+ radial-gradient(circle at 35% 32%, rgba(62,64,68,0.35) 0%, rgba(17,18,20,0.98) 72%, rgba(8,9,11,1) 100%);
228
+ box-shadow:
229
+ inset 0 0 0 1px rgba(255,255,255,0.06),
230
+ inset 0 0 0 ${layout.legacyHomeButton.ring}px rgba(184,192,204,0.45),
231
+ 0 0 0 1px rgba(0,0,0,0.5);
232
+ z-index:3;
233
+ pointer-events:none;
234
+ display:flex;
235
+ align-items:center;
236
+ justify-content:center;
237
+ "
238
+ >
239
+ <div
240
+ style="
241
+ width:16px;
242
+ height:16px;
243
+ border-radius:4px;
244
+ box-shadow:inset 0 0 0 1.5px rgba(188,194,202,0.5);
245
+ "
246
+ ></div>
247
+ </div>
248
+ `
249
+ : ''
250
+
251
+ const homeIndicatorHtml = layout.showHomeIndicator
252
+ ? `
253
+ <div
254
+ aria-hidden="true"
255
+ style="
256
+ position:absolute;
257
+ left:0;
258
+ right:0;
259
+ bottom:4px;
260
+ height:${layout.logicalHomeIndicatorStripHeight}px;
261
+ display:flex;
262
+ align-items:center;
263
+ justify-content:center;
264
+ pointer-events:none;
265
+ z-index:10;
266
+ "
267
+ >
268
+ <div
269
+ style="
270
+ width:134px;
271
+ height:5px;
272
+ border-radius:2.5px;
273
+ background-color:rgb(150, 150, 150);
274
+ opacity:0.5;
275
+ "
276
+ ></div>
277
+ </div>
278
+ `
279
+ : ''
280
+
281
+ return `<!doctype html>
282
+ <html>
283
+ <head>
284
+ <meta charset="utf-8" />
285
+ <style>
286
+ html, body {
287
+ margin: 0;
288
+ width: ${layout.outerWidth}px;
289
+ height: ${layout.outerHeight}px;
290
+ background: transparent;
291
+ }
292
+ body {
293
+ overflow: hidden;
294
+ }
295
+ img {
296
+ display: block;
297
+ width: 100%;
298
+ height: 100%;
299
+ }
300
+ </style>
301
+ </head>
302
+ <body>
303
+ <div
304
+ id="frame-export-root"
305
+ style="
306
+ position:relative;
307
+ width:${layout.outerWidth}px;
308
+ height:${layout.outerHeight}px;
309
+ overflow:hidden;
310
+ background:transparent;
311
+ "
312
+ >
313
+ <div
314
+ id="frame-scale-layer"
315
+ style="
316
+ position:absolute;
317
+ top:0;
318
+ left:0;
319
+ width:${layout.logicalOuterWidth}px;
320
+ height:${layout.logicalOuterHeight}px;
321
+ transform:scale(${layout.renderScale});
322
+ transform-origin:top left;
323
+ overflow:visible;
324
+ "
325
+ >
326
+ <div
327
+ id="frame"
328
+ style="
329
+ position:absolute;
330
+ top:${layout.logicalFrameTop}px;
331
+ left:${layout.logicalFrameLeft}px;
332
+ width:${layout.logicalFrameWidth}px;
333
+ height:${layout.logicalFrameHeight}px;
334
+ border-radius:${layout.logicalFrameRadius}px;
335
+ box-sizing:border-box;
336
+ overflow:visible;
337
+ background:${layout.frameBackground};
338
+ box-shadow:${layout.frameOutline};
339
+ "
340
+ >
341
+ ${buttonsHtml}
342
+ ${ringHtml}
343
+ ${legacyHomeButtonHtml}
344
+ <div
345
+ id="screen"
346
+ style="
347
+ position:absolute;
348
+ top:${layout.logicalScreenTop}px;
349
+ left:${layout.logicalScreenLeft - layout.logicalFrameLeft}px;
350
+ width:${layout.logicalScreenWidth}px;
351
+ height:${layout.logicalScreenHeight}px;
352
+ border-radius:${layout.logicalScreenRadius}px;
353
+ overflow:hidden;
354
+ background:#000;
355
+ "
356
+ >
357
+ <img src="${rawImageDataUrl}" alt="" />
358
+ ${homeIndicatorHtml}
359
+ </div>
360
+ </div>
361
+ </div>
362
+ </div>
363
+ </body>
364
+ </html>`
365
+ }
366
+
367
+ export async function composeFramedScreenshot(
368
+ rawPng: Buffer,
369
+ model: DeviceModel,
370
+ ): Promise<Buffer> {
371
+ const { chromium } = await import('playwright')
372
+ const browser = await launchReapedChrome((options) => chromium.launch(options), {
373
+ headless: true,
374
+ })
375
+ try {
376
+ const composer = await createFramedScreenshotComposer(browser)
377
+ try {
378
+ return await composer.compose(rawPng, model)
379
+ } finally {
380
+ await composer.close()
381
+ }
382
+ } finally {
383
+ await browser.close()
384
+ }
385
+ }
386
+
387
+ type BrowserLike = {
388
+ newContext: (opts: {
389
+ viewport: { width: number; height: number }
390
+ deviceScaleFactor: number
391
+ }) => Promise<{
392
+ newPage: () => Promise<{
393
+ setContent: (html: string, opts: { waitUntil: 'load' }) => Promise<void>
394
+ waitForTimeout: (ms: number) => Promise<void>
395
+ screenshot: (opts: {
396
+ type: 'png'
397
+ clip: { x: number; y: number; width: number; height: number }
398
+ omitBackground: true
399
+ }) => Promise<Buffer>
400
+ }>
401
+ close: () => Promise<void>
402
+ }>
403
+ }
404
+
405
+ type CachedFramePage = {
406
+ layout: FramedScreenshotLayout
407
+ context: Awaited<ReturnType<BrowserLike['newContext']>>
408
+ page: Awaited<ReturnType<Awaited<ReturnType<BrowserLike['newContext']>>['newPage']>>
409
+ }
410
+
411
+ export async function createFramedScreenshotComposer(browser: BrowserLike) {
412
+ const cache = new Map<DeviceModel, CachedFramePage>()
413
+
414
+ async function getCachedPage(model: DeviceModel): Promise<CachedFramePage> {
415
+ const existing = cache.get(model)
416
+ if (existing) return existing
417
+ const layout = getFramedScreenshotLayout(model)
418
+ const context = await browser.newContext({
419
+ viewport: { width: layout.outerWidth, height: layout.outerHeight },
420
+ deviceScaleFactor: 1,
421
+ })
422
+ const page = await context.newPage()
423
+ const created = { layout, context, page }
424
+ cache.set(model, created)
425
+ return created
426
+ }
427
+
428
+ return {
429
+ async compose(rawPng: Buffer, model: DeviceModel): Promise<Buffer> {
430
+ const cached = await getCachedPage(model)
431
+ const rawImageDataUrl = `data:image/png;base64,${rawPng.toString('base64')}`
432
+ await cached.page.setContent(renderFrameHtml(cached.layout, rawImageDataUrl), {
433
+ waitUntil: 'load',
434
+ })
435
+ await cached.page.waitForTimeout(20)
436
+ return (await cached.page.screenshot({
437
+ type: 'png',
438
+ clip: {
439
+ x: 0,
440
+ y: 0,
441
+ width: cached.layout.outerWidth,
442
+ height: cached.layout.outerHeight,
443
+ },
444
+ omitBackground: true,
445
+ })) as Buffer
446
+ },
447
+ async close(): Promise<void> {
448
+ for (const entry of cache.values()) {
449
+ await entry.context.close()
450
+ }
451
+ cache.clear()
452
+ },
453
+ }
454
+ }
@@ -0,0 +1,256 @@
1
+ import { mkdirSync, readFileSync, writeFileSync } from 'fs'
2
+ import { tmpdir } from 'os'
3
+ import path from 'path'
4
+ import { launchReapedChrome } from '../../../../scripts/lib/reap-browser'
5
+ import { runFlowPlayback } from '../../cli/commands/flow'
6
+ import {
7
+ composeMarketingScreenshots,
8
+ type MarketingBrowserLike,
9
+ type ComposeResult,
10
+ type ComposeSlideSource,
11
+ } from './compose'
12
+ import { createFramedScreenshotComposer } from './frame-compose'
13
+ import { applyOrgDeckToPlan, type OrgDeckFrame } from './org-deck'
14
+ import { loadScreenshotsPlan, type NormalizedScreenshotsPlan } from './schema'
15
+
16
+ export interface RunScreenshotsOverrides {
17
+ appTarget?: string | null
18
+ deviceModel?: string | null
19
+ simId?: string | null
20
+ captureOnly?: boolean
21
+ composeOnly?: boolean
22
+ // copy/colors pulled from the team's org deck to overlay onto the plan's
23
+ // compose slides (sootsim screenshot appstore --org … --repo …).
24
+ orgDeckFrames?: OrgDeckFrame[] | null
25
+ }
26
+
27
+ export interface CaptureResult {
28
+ manifestPath: string
29
+ rawDir: string
30
+ framedDir: string
31
+ rawFiles: string[]
32
+ framedFiles: string[]
33
+ }
34
+
35
+ export interface ScreenshotsRunResult {
36
+ plan: NormalizedScreenshotsPlan
37
+ capture: CaptureResult
38
+ compose: ComposeResult | null
39
+ }
40
+
41
+ function resolveCapturePathMode(plan: NormalizedScreenshotsPlan): 'plan' | 'flow' {
42
+ if (plan.capture.pathMode === 'plan' || plan.capture.pathMode === 'flow') {
43
+ return plan.capture.pathMode
44
+ }
45
+ return plan.capture.fromDir ? 'flow' : 'plan'
46
+ }
47
+
48
+ export function buildCaptureFlowArgs(
49
+ plan: NormalizedScreenshotsPlan,
50
+ overrides: RunScreenshotsOverrides,
51
+ ): string[] {
52
+ if (!plan.capture.flowPath) {
53
+ throw new Error('capture flow path is required to build flow args')
54
+ }
55
+ const args = [plan.capture.flowPath]
56
+ const pathMode = resolveCapturePathMode(plan)
57
+ args.push('--screenshots', plan.capture.rawDir)
58
+ if (pathMode === 'flow') {
59
+ args.push('--screenshot-paths', 'flow')
60
+ }
61
+ const appTarget = overrides.appTarget ?? plan.appTarget
62
+ const deviceModel = overrides.deviceModel ?? plan.deviceModel
63
+ const simId = overrides.simId ?? plan.capture.simId
64
+ if (appTarget) args.push('--url', appTarget)
65
+ if (deviceModel) args.push('--device', deviceModel)
66
+ if (simId) args.push('--sim', simId)
67
+ if (plan.capture.openInNewSim) args.push('--new')
68
+ return args
69
+ }
70
+
71
+ function resolveSlideRawPath(
72
+ plan: NormalizedScreenshotsPlan,
73
+ screenshot: string,
74
+ ): string {
75
+ return path.isAbsolute(screenshot)
76
+ ? screenshot
77
+ : path.join(plan.capture.rawDir, screenshot)
78
+ }
79
+
80
+ function resolveCaptureManifestPath(plan: NormalizedScreenshotsPlan): string {
81
+ return path.join(plan.capture.outDir, 'manifest.json')
82
+ }
83
+
84
+ async function runCaptureStage(
85
+ plan: NormalizedScreenshotsPlan,
86
+ overrides: RunScreenshotsOverrides,
87
+ ): Promise<void> {
88
+ if (!plan.capture.flowPath) return
89
+ mkdirSync(plan.capture.rawDir, { recursive: true })
90
+ const args = buildCaptureFlowArgs(plan, overrides)
91
+ const exitCode = await runFlowPlayback(args)
92
+ if (exitCode !== 0) {
93
+ throw new Error(`flow capture failed with exit code ${exitCode}`)
94
+ }
95
+ }
96
+
97
+ async function buildFramedSources(
98
+ plan: NormalizedScreenshotsPlan,
99
+ browserOverride?: MarketingBrowserLike,
100
+ ): Promise<Map<string, string>> {
101
+ const sources = new Map<string, string>()
102
+ mkdirSync(plan.capture.framedDir, { recursive: true })
103
+ const browser =
104
+ browserOverride ??
105
+ (await (async () => {
106
+ const { chromium } = await import('playwright')
107
+ return launchReapedChrome((options) => chromium.launch(options), {
108
+ headless: true,
109
+ })
110
+ })())
111
+ try {
112
+ const composer = await createFramedScreenshotComposer(browser)
113
+ try {
114
+ for (const slide of plan.compose.slides) {
115
+ const rawPath = resolveSlideRawPath(plan, slide.screenshot)
116
+ const rawBuffer = readFileSync(rawPath)
117
+ const framedBuffer = await composer.compose(rawBuffer, plan.compose.frame.style)
118
+ const framedPath = path.join(plan.capture.framedDir, `${slide.assetKey}.png`)
119
+ writeFileSync(framedPath, framedBuffer)
120
+ sources.set(slide.id, framedPath)
121
+ }
122
+ } finally {
123
+ await composer.close()
124
+ }
125
+ } finally {
126
+ if (!browserOverride && typeof browser.close === 'function') {
127
+ await browser.close()
128
+ }
129
+ }
130
+ return sources
131
+ }
132
+
133
+ function writeCaptureManifest(
134
+ plan: NormalizedScreenshotsPlan,
135
+ framedSources: Map<string, string>,
136
+ effectiveDeviceModel: string | null,
137
+ ): CaptureResult {
138
+ const rawFiles: string[] = []
139
+ const framedFiles: string[] = []
140
+ mkdirSync(plan.capture.outDir, { recursive: true })
141
+ const manifestPath = resolveCaptureManifestPath(plan)
142
+ const slides = plan.compose.slides.map((slide) => {
143
+ const rawPath = resolveSlideRawPath(plan, slide.screenshot)
144
+ const framedPath = framedSources.get(slide.id) ?? null
145
+ rawFiles.push(rawPath)
146
+ if (framedPath) framedFiles.push(framedPath)
147
+ return {
148
+ id: slide.id,
149
+ screenshot: slide.screenshot,
150
+ rawPath,
151
+ framedPath,
152
+ }
153
+ })
154
+ writeFileSync(
155
+ manifestPath,
156
+ JSON.stringify(
157
+ {
158
+ generatedAt: new Date().toISOString(),
159
+ deviceModel: effectiveDeviceModel,
160
+ mode: plan.capture.mode,
161
+ rawDir: plan.capture.rawDir,
162
+ framedDir: plan.capture.framedDir,
163
+ slides,
164
+ },
165
+ null,
166
+ 2,
167
+ ),
168
+ )
169
+ return {
170
+ manifestPath,
171
+ rawDir: plan.capture.rawDir,
172
+ framedDir: plan.capture.framedDir,
173
+ rawFiles,
174
+ framedFiles,
175
+ }
176
+ }
177
+
178
+ function resolveComposeSources(
179
+ plan: NormalizedScreenshotsPlan,
180
+ framedSources: Map<string, string>,
181
+ ): ComposeSlideSource[] {
182
+ return plan.compose.slides.map((slide) => ({
183
+ slide,
184
+ imagePath:
185
+ plan.compose.frame.show === true
186
+ ? (framedSources.get(slide.id) ?? resolveSlideRawPath(plan, slide.screenshot))
187
+ : resolveSlideRawPath(plan, slide.screenshot),
188
+ }))
189
+ }
190
+
191
+ export async function runScreenshotsPlan(
192
+ planPath: string,
193
+ overrides: RunScreenshotsOverrides = {},
194
+ ): Promise<ScreenshotsRunResult> {
195
+ const plan = loadScreenshotsPlan(planPath)
196
+ if (overrides.orgDeckFrames && overrides.orgDeckFrames.length > 0) {
197
+ applyOrgDeckToPlan(plan, overrides.orgDeckFrames)
198
+ }
199
+ const effectiveDeviceModel = overrides.deviceModel ?? plan.deviceModel
200
+ if (!overrides.composeOnly) {
201
+ await runCaptureStage(plan, overrides)
202
+ }
203
+ const needsFramedSources =
204
+ plan.capture.mode !== 'raw' ||
205
+ (!overrides.captureOnly && plan.compose.frame.show === true)
206
+ const needsSharedBrowser = needsFramedSources || !overrides.captureOnly
207
+ const sharedBrowser = needsSharedBrowser
208
+ ? await (async () => {
209
+ const { chromium } = await import('playwright')
210
+ return launchReapedChrome((options) => chromium.launch(options), {
211
+ headless: true,
212
+ })
213
+ })()
214
+ : null
215
+ try {
216
+ const framedSources = needsFramedSources
217
+ ? await buildFramedSources(plan, sharedBrowser ?? undefined)
218
+ : new Map()
219
+ const capture = writeCaptureManifest(plan, framedSources, effectiveDeviceModel)
220
+
221
+ if (overrides.captureOnly) {
222
+ return { plan, capture, compose: null }
223
+ }
224
+
225
+ const composeSources = resolveComposeSources(plan, framedSources)
226
+ const compose = await composeMarketingScreenshots(
227
+ plan.compose,
228
+ composeSources,
229
+ sharedBrowser ?? undefined,
230
+ )
231
+ return { plan, capture, compose }
232
+ } finally {
233
+ if (sharedBrowser && typeof sharedBrowser.close === 'function') {
234
+ await sharedBrowser.close()
235
+ }
236
+ }
237
+ }
238
+
239
+ export async function runScreenshotsPlanFromExistingRaw(
240
+ planPath: string,
241
+ rawDir: string,
242
+ ): Promise<ScreenshotsRunResult> {
243
+ const plan = loadScreenshotsPlan(planPath)
244
+ plan.capture.rawDir = rawDir
245
+ plan.capture.flowPath = null
246
+ plan.capture.fromDir = rawDir
247
+ const framedDir = path.join(tmpdir(), 'sootsim-screenshots-framed')
248
+ plan.capture.framedDir = framedDir
249
+ const framedSources = await buildFramedSources(plan)
250
+ const capture = writeCaptureManifest(plan, framedSources, plan.deviceModel)
251
+ const compose = await composeMarketingScreenshots(
252
+ plan.compose,
253
+ resolveComposeSources(plan, framedSources),
254
+ )
255
+ return { plan, capture, compose }
256
+ }