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,91 @@
1
+ // fetch the team's screenshot deck (the copy / colors founders set up in the
2
+ // org dashboard) and overlay it onto a screenshots plan, so the programmatic
3
+ // `sootsim screenshot appstore --plan … --org … --repo …` flow renders the org's
4
+ // words + brand gradient instead of the local yaml's. the plan still owns
5
+ // which screen each slide captures and the device frame; the deck owns copy +
6
+ // colors. 3d scenes don't translate to the cli's 2d composer, so only copy and
7
+ // colors carry over (the 3d hero look stays the in-browser screenshot mode).
8
+
9
+ import { resolveBgColorValue } from 'sootsim-engine/screenshots/tokens'
10
+ import type { BackgroundSpec } from './registry'
11
+ import type { NormalizedScreenshotsPlan } from './schema'
12
+
13
+ export interface OrgDeckFetchOptions {
14
+ org: string
15
+ repo: string
16
+ origin: string
17
+ authHeader: string
18
+ }
19
+
20
+ export interface OrgDeckFrame {
21
+ title?: string
22
+ subtitle?: string
23
+ bgTop?: string
24
+ bgBottom?: string
25
+ titleColor?: string
26
+ }
27
+
28
+ function readString(value: unknown): string | undefined {
29
+ return typeof value === 'string' && value.length > 0 ? value : undefined
30
+ }
31
+
32
+ // GET the team deck and return its first deck's frames, or null when the team
33
+ // has no deck yet. throws on auth / network failure so the caller can surface a
34
+ // clear message rather than silently falling back to the local copy.
35
+ export async function fetchOrgDeckFrames(
36
+ opts: OrgDeckFetchOptions,
37
+ ): Promise<OrgDeckFrame[] | null> {
38
+ const base = opts.origin.replace(/\/$/, '')
39
+ const url = `${base}/api/sootsim/screenshot-decks?org=${encodeURIComponent(
40
+ opts.org,
41
+ )}&repo=${encodeURIComponent(opts.repo)}`
42
+ const res = await fetch(url, { headers: { Authorization: opts.authHeader } })
43
+ if (!res.ok) {
44
+ const detail = await res.text().catch(() => '')
45
+ throw new Error(
46
+ `org deck fetch failed (${res.status} ${res.statusText}) for ${opts.org}/${opts.repo}${
47
+ detail ? ` — ${detail.slice(0, 200)}` : ''
48
+ }`,
49
+ )
50
+ }
51
+ const body = (await res.json()) as { decks?: Array<{ config?: unknown }> }
52
+ const deck = body.decks?.[0]
53
+ if (!deck || !deck.config || typeof deck.config !== 'object') return null
54
+ const frames = (deck.config as { frames?: unknown }).frames
55
+ if (!Array.isArray(frames)) return null
56
+ return frames.map((entry) => {
57
+ const frame = (entry ?? {}) as Record<string, unknown>
58
+ return {
59
+ title: readString(frame.title),
60
+ subtitle: readString(frame.subtitle),
61
+ bgTop: readString(frame.bgTop),
62
+ bgBottom: readString(frame.bgBottom),
63
+ titleColor: readString(frame.titleColor),
64
+ }
65
+ })
66
+ }
67
+
68
+ // overlay deck frames onto the plan's compose slides, matched by order. a deck
69
+ // with fewer frames than the plan leaves the trailing slides on their yaml copy.
70
+ export function applyOrgDeckToPlan(
71
+ plan: NormalizedScreenshotsPlan,
72
+ frames: OrgDeckFrame[],
73
+ ): void {
74
+ plan.compose.slides.forEach((slide, index) => {
75
+ const frame = frames[index]
76
+ if (!frame) return
77
+ if (frame.title != null) slide.headline = frame.title
78
+ if (frame.subtitle != null) slide.subheadline = frame.subtitle
79
+ if (frame.bgTop && frame.bgBottom) {
80
+ const background: BackgroundSpec = {
81
+ type: 'gradient',
82
+ direction: 180,
83
+ stops: [
84
+ { offset: 0, color: resolveBgColorValue(frame.bgTop) },
85
+ { offset: 1, color: resolveBgColorValue(frame.bgBottom) },
86
+ ],
87
+ }
88
+ slide.background = background
89
+ }
90
+ })
91
+ }
@@ -0,0 +1,58 @@
1
+ // composer-facing registry. shared tokens live in
2
+ // `sootsim-engine/src/screenshots/tokens.ts` so the live browser
3
+ // screenshot mode and the post-process composer render from the same
4
+ // canvas / background / text / pose / animation / clamp vocabulary.
5
+ //
6
+ // this module re-exports those tokens plus the composer-specific
7
+ // `FrameComposeSpec` shape.
8
+
9
+ import type { DeviceModel } from 'sootsim-engine/settings'
10
+
11
+ export {
12
+ BACKGROUND_PRESETS,
13
+ DEFAULT_FRAME_SHADOW,
14
+ DEFAULT_FRAME_STYLE,
15
+ SCREENSHOT_CANVASES,
16
+ SCREENSHOT_MODE_ANIM,
17
+ SCREENSHOT_TEXT_CLAMP,
18
+ SCREENSHOT_TEXT_SAFE_AREA,
19
+ cloneBackgroundSpec,
20
+ resolveBackgroundPreset,
21
+ resolveCanvasPreset,
22
+ } from 'sootsim-engine/screenshots/tokens'
23
+
24
+ export type {
25
+ BackgroundPresetName,
26
+ BackgroundSpec,
27
+ CanvasPreset,
28
+ FrameShadowSpec,
29
+ GradientStop,
30
+ PosePresetName,
31
+ ScreenshotCanvasName,
32
+ TextPresetName,
33
+ } from 'sootsim-engine/screenshots/tokens'
34
+
35
+ import type { FrameShadowSpec, PosePresetName } from 'sootsim-engine/screenshots/tokens'
36
+
37
+ export interface FrameComposeSpec {
38
+ show: boolean
39
+ style: DeviceModel
40
+ pose: PosePresetName
41
+ scale: number
42
+ offsetY: number
43
+ shadow: FrameShadowSpec
44
+ }
45
+
46
+ export function toAssetKey(value: string): string {
47
+ const normalized = value
48
+ .replace(/\\/g, '/')
49
+ .replace(/\.png$/i, '')
50
+ .trim()
51
+ .split('/')
52
+ .filter(Boolean)
53
+ .join('--')
54
+ .replace(/[^A-Za-z0-9-]+/g, '-')
55
+ .replace(/-+/g, '-')
56
+ .replace(/^-|-$/g, '')
57
+ return normalized || 'slide'
58
+ }
@@ -0,0 +1,364 @@
1
+ import { readFileSync } from 'fs'
2
+ import path from 'path'
3
+ import { devices, type DeviceModel } from 'sootsim-engine/settings'
4
+ import yaml from 'yaml'
5
+ import {
6
+ cloneBackgroundSpec,
7
+ DEFAULT_FRAME_SHADOW,
8
+ DEFAULT_FRAME_STYLE,
9
+ resolveBackgroundPreset,
10
+ resolveCanvasPreset,
11
+ toAssetKey,
12
+ type BackgroundPresetName,
13
+ type BackgroundSpec,
14
+ type CanvasPreset,
15
+ type FrameComposeSpec,
16
+ type PosePresetName,
17
+ type TextPresetName,
18
+ } from './registry'
19
+
20
+ export type CaptureMode = 'raw' | 'framed' | 'raw+framed'
21
+ export type CapturePathMode = 'auto' | 'plan' | 'flow'
22
+
23
+ export interface NormalizedScreenshotSlide {
24
+ id: string
25
+ assetKey: string
26
+ screenshot: string
27
+ headline: string
28
+ subheadline: string
29
+ eyebrow: string
30
+ pose?: PosePresetName
31
+ scale?: number
32
+ offsetY?: number
33
+ background?: BackgroundSpec
34
+ }
35
+
36
+ export interface NormalizedCapturePlan {
37
+ flowPath: string | null
38
+ fromDir: string | null
39
+ outDir: string
40
+ rawDir: string
41
+ framedDir: string
42
+ mode: CaptureMode
43
+ pathMode: CapturePathMode
44
+ simId: string | null
45
+ openInNewSim: boolean
46
+ }
47
+
48
+ export interface NormalizedComposePlan {
49
+ outDir: string
50
+ locale: string
51
+ canvases: CanvasPreset[]
52
+ frame: FrameComposeSpec
53
+ background: BackgroundSpec
54
+ text: {
55
+ preset: TextPresetName
56
+ color: string
57
+ subColor: string
58
+ eyebrowColor: string
59
+ }
60
+ slides: NormalizedScreenshotSlide[]
61
+ }
62
+
63
+ export interface NormalizedScreenshotsPlan {
64
+ planPath: string
65
+ planDir: string
66
+ appTarget: string | null
67
+ deviceModel: DeviceModel | null
68
+ capture: NormalizedCapturePlan
69
+ compose: NormalizedComposePlan
70
+ }
71
+
72
+ function isRecord(value: unknown): value is Record<string, unknown> {
73
+ return !!value && typeof value === 'object' && !Array.isArray(value)
74
+ }
75
+
76
+ function expectRecord(value: unknown, label: string): Record<string, unknown> {
77
+ if (!isRecord(value)) throw new Error(`${label} must be an object`)
78
+ return value
79
+ }
80
+
81
+ function readString(value: unknown, fallback = ''): string {
82
+ return typeof value === 'string' ? value : fallback
83
+ }
84
+
85
+ function readOptionalString(value: unknown): string | null {
86
+ return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null
87
+ }
88
+
89
+ function readBoolean(value: unknown, fallback: boolean): boolean {
90
+ return typeof value === 'boolean' ? value : fallback
91
+ }
92
+
93
+ function readNumber(value: unknown, fallback: number): number {
94
+ return typeof value === 'number' && Number.isFinite(value) ? value : fallback
95
+ }
96
+
97
+ function readDeviceModel(
98
+ value: unknown,
99
+ fallback: DeviceModel | null,
100
+ ): DeviceModel | null {
101
+ if (typeof value !== 'string' || value.trim().length === 0) return fallback
102
+ return Object.prototype.hasOwnProperty.call(devices, value)
103
+ ? (value as DeviceModel)
104
+ : null
105
+ }
106
+
107
+ function normalizeBackground(value: unknown, fallback: BackgroundSpec): BackgroundSpec {
108
+ if (typeof value === 'string') {
109
+ const preset = resolveBackgroundPreset(value)
110
+ if (!preset) throw new Error(`unknown background preset: ${value}`)
111
+ return preset
112
+ }
113
+ if (!isRecord(value)) return cloneBackgroundSpec(fallback)
114
+ const type =
115
+ value.type === 'solid' || value.type === 'gradient' ? value.type : fallback.type
116
+ if (type === 'solid') {
117
+ return {
118
+ type,
119
+ color: readString(value.color, fallback.color ?? '#000000'),
120
+ glow: readOptionalString(value.glow) ?? fallback.glow,
121
+ }
122
+ }
123
+ const stops = Array.isArray(value.stops)
124
+ ? value.stops
125
+ .map((stop) => {
126
+ if (!isRecord(stop)) return null
127
+ const offset = readNumber(stop.offset, Number.NaN)
128
+ const color = readOptionalString(stop.color)
129
+ if (!Number.isFinite(offset) || !color) return null
130
+ return { offset, color }
131
+ })
132
+ .filter((stop): stop is { offset: number; color: string } => !!stop)
133
+ : (fallback.stops?.map((stop) => ({ ...stop })) ?? [])
134
+ return {
135
+ type,
136
+ direction: readNumber(value.direction, fallback.direction ?? 180),
137
+ glow: readOptionalString(value.glow) ?? fallback.glow,
138
+ stops:
139
+ stops.length > 0 ? stops : (fallback.stops?.map((stop) => ({ ...stop })) ?? []),
140
+ }
141
+ }
142
+
143
+ function normalizeCanvasList(value: unknown): CanvasPreset[] {
144
+ const names = Array.isArray(value) ? value : ['iphone-6-9']
145
+ const presets = names.map((entry) => {
146
+ if (typeof entry !== 'string')
147
+ throw new Error('compose.canvases entries must be strings')
148
+ const preset = resolveCanvasPreset(entry)
149
+ if (!preset) throw new Error(`unknown canvas preset: ${entry}`)
150
+ return preset
151
+ })
152
+ if (presets.length === 0)
153
+ throw new Error('compose.canvases must include at least one preset')
154
+ return presets
155
+ }
156
+
157
+ function normalizeTextPreset(value: unknown): TextPresetName {
158
+ switch (value) {
159
+ case 'editorial-left':
160
+ case 'minimal-bottom':
161
+ case 'none':
162
+ case 'bold-top':
163
+ return value
164
+ default:
165
+ return 'bold-top'
166
+ }
167
+ }
168
+
169
+ function normalizeCapturePathMode(value: unknown): CapturePathMode {
170
+ switch (value) {
171
+ case 'plan':
172
+ case 'flow':
173
+ case 'auto':
174
+ return value
175
+ default:
176
+ return 'auto'
177
+ }
178
+ }
179
+
180
+ function normalizePose(value: unknown): PosePresetName | undefined {
181
+ switch (value) {
182
+ case 'straight':
183
+ case 'tilted-left':
184
+ case 'tilted-right':
185
+ case 'cut-bottom':
186
+ case 'cut-top':
187
+ return value
188
+ default:
189
+ return undefined
190
+ }
191
+ }
192
+
193
+ function resolvePlanPath(planDir: string, value: string): string {
194
+ return path.isAbsolute(value) ? value : path.resolve(planDir, value)
195
+ }
196
+
197
+ function normalizeSlides(
198
+ value: unknown,
199
+ fallbackBackground: BackgroundSpec,
200
+ ): NormalizedScreenshotSlide[] {
201
+ if (!Array.isArray(value) || value.length === 0) {
202
+ throw new Error('compose.slides must be a non-empty array')
203
+ }
204
+ return value.map((entry, index) => {
205
+ const slide = expectRecord(entry, `compose.slides[${index}]`)
206
+ const screenshot = readOptionalString(slide.screenshot)
207
+ if (!screenshot) {
208
+ throw new Error(`compose.slides[${index}].screenshot is required`)
209
+ }
210
+ const explicitId = readOptionalString(slide.id)
211
+ const assetKey = toAssetKey(explicitId || screenshot)
212
+ return {
213
+ id: explicitId || assetKey,
214
+ assetKey,
215
+ screenshot,
216
+ headline: readString(slide.headline),
217
+ subheadline: readString(slide.subheadline),
218
+ eyebrow: readString(slide.eyebrow),
219
+ pose: normalizePose(slide.pose),
220
+ scale:
221
+ typeof slide.scale === 'number' && Number.isFinite(slide.scale)
222
+ ? slide.scale
223
+ : undefined,
224
+ offsetY:
225
+ typeof slide.offsetY === 'number' && Number.isFinite(slide.offsetY)
226
+ ? slide.offsetY
227
+ : undefined,
228
+ background:
229
+ slide.theme || slide.background
230
+ ? normalizeBackground(slide.theme ?? slide.background, fallbackBackground)
231
+ : undefined,
232
+ }
233
+ })
234
+ }
235
+
236
+ export function loadScreenshotsPlan(planPath: string): NormalizedScreenshotsPlan {
237
+ const resolvedPlanPath = path.resolve(planPath)
238
+ const planDir = path.dirname(resolvedPlanPath)
239
+ const raw = yaml.parse(readFileSync(resolvedPlanPath, 'utf8'))
240
+ const root = expectRecord(raw, 'screenshots plan')
241
+ const capture = expectRecord(root.capture ?? {}, 'capture')
242
+ const compose = expectRecord(root.compose ?? {}, 'compose')
243
+
244
+ const appTarget =
245
+ typeof root.app === 'number' ? String(root.app) : readOptionalString(root.app)
246
+ const deviceModel = readDeviceModel(root.device, null)
247
+ const captureOutDir = resolvePlanPath(
248
+ planDir,
249
+ readString(capture.out, path.join('.sootsim', 'screenshots', 'capture')),
250
+ )
251
+ const captureFrom = readOptionalString(capture.from)
252
+ const flowPath = readOptionalString(capture.flow)
253
+ if (!captureFrom && !flowPath) {
254
+ throw new Error('capture.flow or capture.from is required')
255
+ }
256
+ const captureMode =
257
+ capture.mode === 'raw' || capture.mode === 'framed' || capture.mode === 'raw+framed'
258
+ ? capture.mode
259
+ : ('raw+framed' as CaptureMode)
260
+ const capturePathMode = normalizeCapturePathMode(capture.pathMode)
261
+ const defaultBackground = normalizeBackground(
262
+ compose.background ?? ('cyan' as BackgroundPresetName),
263
+ resolveBackgroundPreset('cyan')!,
264
+ )
265
+ const frameConfig: FrameComposeSpec = {
266
+ show: readBoolean(
267
+ compose.frame && isRecord(compose.frame) ? compose.frame.show : undefined,
268
+ true,
269
+ ),
270
+ style:
271
+ readDeviceModel(
272
+ compose.frame && isRecord(compose.frame) ? compose.frame.style : null,
273
+ null,
274
+ ) ||
275
+ deviceModel ||
276
+ DEFAULT_FRAME_STYLE,
277
+ pose:
278
+ normalizePose(
279
+ compose.frame && isRecord(compose.frame) ? compose.frame.pose : undefined,
280
+ ) || 'straight',
281
+ scale: readNumber(
282
+ compose.frame && isRecord(compose.frame) ? compose.frame.scale : undefined,
283
+ 1,
284
+ ),
285
+ offsetY: readNumber(
286
+ compose.frame && isRecord(compose.frame) ? compose.frame.offsetY : undefined,
287
+ 0,
288
+ ),
289
+ shadow: {
290
+ color: readString(
291
+ compose.frame && isRecord(compose.frame) && isRecord(compose.frame.shadow)
292
+ ? compose.frame.shadow.color
293
+ : undefined,
294
+ DEFAULT_FRAME_SHADOW.color,
295
+ ),
296
+ blur: readNumber(
297
+ compose.frame && isRecord(compose.frame) && isRecord(compose.frame.shadow)
298
+ ? compose.frame.shadow.blur
299
+ : undefined,
300
+ DEFAULT_FRAME_SHADOW.blur,
301
+ ),
302
+ spread: readNumber(
303
+ compose.frame && isRecord(compose.frame) && isRecord(compose.frame.shadow)
304
+ ? compose.frame.shadow.spread
305
+ : undefined,
306
+ DEFAULT_FRAME_SHADOW.spread,
307
+ ),
308
+ opacity: readNumber(
309
+ compose.frame && isRecord(compose.frame) && isRecord(compose.frame.shadow)
310
+ ? compose.frame.shadow.opacity
311
+ : undefined,
312
+ DEFAULT_FRAME_SHADOW.opacity,
313
+ ),
314
+ },
315
+ }
316
+
317
+ return {
318
+ planPath: resolvedPlanPath,
319
+ planDir,
320
+ appTarget,
321
+ deviceModel,
322
+ capture: {
323
+ flowPath: flowPath ? resolvePlanPath(planDir, flowPath) : null,
324
+ fromDir: captureFrom ? resolvePlanPath(planDir, captureFrom) : null,
325
+ outDir: captureOutDir,
326
+ rawDir: captureFrom
327
+ ? resolvePlanPath(planDir, captureFrom)
328
+ : path.join(captureOutDir, 'raw'),
329
+ framedDir: path.join(captureOutDir, 'framed'),
330
+ mode: captureMode,
331
+ pathMode: capturePathMode,
332
+ simId: readOptionalString(capture.sim),
333
+ openInNewSim: readBoolean(capture.new, false),
334
+ },
335
+ compose: {
336
+ outDir: resolvePlanPath(
337
+ planDir,
338
+ readString(compose.out, path.join('.sootsim', 'screenshots', 'exports')),
339
+ ),
340
+ locale: readString(compose.locale, 'en'),
341
+ canvases: normalizeCanvasList(compose.canvases),
342
+ frame: frameConfig,
343
+ background: defaultBackground,
344
+ text: {
345
+ preset: normalizeTextPreset(
346
+ compose.text && isRecord(compose.text) ? compose.text.preset : undefined,
347
+ ),
348
+ color: readString(
349
+ compose.text && isRecord(compose.text) ? compose.text.color : undefined,
350
+ '#ffffff',
351
+ ),
352
+ subColor: readString(
353
+ compose.text && isRecord(compose.text) ? compose.text.subColor : undefined,
354
+ 'rgba(232,240,247,0.86)',
355
+ ),
356
+ eyebrowColor: readString(
357
+ compose.text && isRecord(compose.text) ? compose.text.eyebrowColor : undefined,
358
+ 'rgba(229,242,255,0.72)',
359
+ ),
360
+ },
361
+ slides: normalizeSlides(compose.slides, defaultBackground),
362
+ },
363
+ }
364
+ }
package/src/sdk.ts ADDED
@@ -0,0 +1,7 @@
1
+ export * from '../cli/commands/inspect/core'
2
+ export * from '../cli/commands/inspect/actions'
3
+ export { waitForSootsimIdle } from '../cli/commands/inspect/settling'
4
+ export type {
5
+ WaitForSootsimIdleOptions,
6
+ WaitForSootsimIdleResult,
7
+ } from '../cli/commands/inspect/settling'