sootsim 0.1.82 → 0.1.84

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 (209) hide show
  1. package/README.md +0 -1
  2. package/detox/colors.ts +54 -0
  3. package/detox/config-loader.ts +135 -0
  4. package/detox/expectations.ts +477 -0
  5. package/detox/gestures.ts +442 -0
  6. package/detox/index.ts +1436 -0
  7. package/detox/jest-environment.ts +86 -0
  8. package/detox/jest-preset.cjs +50 -0
  9. package/detox/matchers.ts +29 -0
  10. package/detox/navigation.ts +43 -0
  11. package/detox/run-test.ts +113 -0
  12. package/detox/screenshots/animated-color-test-rest-norngh.png +0 -0
  13. package/detox/screenshots/color-test-after-drag-norngh.png +0 -0
  14. package/detox/screenshots/color-test-rest-norngh.png +0 -0
  15. package/detox/screenshots/theme-blue-toggle.png +0 -0
  16. package/detox/screenshots/theme-blue.png +0 -0
  17. package/detox/screenshots/theme-red-toggle.png +0 -0
  18. package/detox/screenshots/theme-red.png +0 -0
  19. package/dist-cli/bin.js +3 -3
  20. package/dist-cli/chunks/{agent-3C6Z6YXA.js → agent-2CWD6W6P.js} +2 -2
  21. package/dist-cli/chunks/{agent-wrapper-7Z4UFACX.js → agent-wrapper-5W3LOX6S.js} +2 -2
  22. package/dist-cli/chunks/{assert-XYBIZRDK.js → assert-ZOMAMKRT.js} +2 -2
  23. package/dist-cli/chunks/auto-bootstrap-NYYSMTIM.js +2 -0
  24. package/dist-cli/chunks/beta-4K2SQACK.js +2 -0
  25. package/dist-cli/chunks/chunk-3HXQ7MJK.js +79 -0
  26. package/dist-cli/chunks/{chunk-EJGEDUOC.js → chunk-4K7BH2D4.js} +3 -3
  27. package/dist-cli/chunks/{chunk-2EFQQWEC.js → chunk-4OPRODFA.js} +2 -2
  28. package/dist-cli/chunks/{chunk-Z6G5SDG7.js → chunk-4OWVPRZV.js} +2 -2
  29. package/dist-cli/chunks/{chunk-DCFGNIJC.js → chunk-5XCXOLG2.js} +2 -2
  30. package/dist-cli/chunks/chunk-67ZZ2CM5.js +1 -0
  31. package/dist-cli/chunks/{chunk-M3OULYY3.js → chunk-73UZXB4B.js} +2 -2
  32. package/dist-cli/chunks/{chunk-QPDWMYCA.js → chunk-7NWNTUJF.js} +1 -1
  33. package/dist-cli/chunks/chunk-7YHDJLO2.js +119 -0
  34. package/dist-cli/chunks/{chunk-EX6IOT23.js → chunk-AJVTY6KY.js} +2 -2
  35. package/dist-cli/chunks/chunk-AWSQUOAS.js +67 -0
  36. package/dist-cli/chunks/{chunk-JVNGH5S7.js → chunk-BCBNVJVG.js} +1 -1
  37. package/dist-cli/chunks/{chunk-WZLKUS54.js → chunk-BKBL6K2G.js} +1 -1
  38. package/dist-cli/chunks/{chunk-DSYW2NOW.js → chunk-C3DPQZ4J.js} +2 -2
  39. package/dist-cli/chunks/chunk-D3ZSBIIY.js +2 -0
  40. package/dist-cli/chunks/{chunk-PYDAVGCZ.js → chunk-D4HUVLZR.js} +1 -1
  41. package/dist-cli/chunks/{chunk-H6NBDJIO.js → chunk-DUUSJDES.js} +1 -1
  42. package/dist-cli/chunks/{chunk-BVXP2GDN.js → chunk-ELJLF4SG.js} +3 -3
  43. package/dist-cli/chunks/{chunk-TR7NIFSL.js → chunk-EQ7TFQ2F.js} +1 -1
  44. package/dist-cli/chunks/{chunk-UOWBKSSI.js → chunk-EQCKGC4B.js} +1 -1
  45. package/dist-cli/chunks/chunk-FUCGLWNN.js +1 -0
  46. package/dist-cli/chunks/{chunk-BISEHRNE.js → chunk-HYPJW65U.js} +2 -2
  47. package/dist-cli/chunks/chunk-IILJQCZA.js +2 -0
  48. package/dist-cli/chunks/{chunk-2XULSYS6.js → chunk-KU6MSPAH.js} +2 -2
  49. package/dist-cli/chunks/{chunk-QTJJHBCI.js → chunk-OOOR7NT2.js} +1 -1
  50. package/dist-cli/chunks/{chunk-U3XCDQRH.js → chunk-P7WDNKOS.js} +3 -3
  51. package/dist-cli/chunks/{chunk-C7JOLDDQ.js → chunk-PPKKA5VW.js} +2 -2
  52. package/dist-cli/chunks/{chunk-JUCV3VHM.js → chunk-PS2G44GT.js} +2 -2
  53. package/dist-cli/chunks/{chunk-PO64TMRT.js → chunk-QMSJR5R2.js} +2 -2
  54. package/dist-cli/chunks/{chunk-4QUAOBUB.js → chunk-RF4R2U46.js} +2 -2
  55. package/dist-cli/chunks/{chunk-D3SM2JYB.js → chunk-RIXUH3NK.js} +2 -2
  56. package/dist-cli/chunks/{chunk-2JQIKL3B.js → chunk-SFGUPL2X.js} +2 -2
  57. package/dist-cli/chunks/{chunk-GI5MF6LP.js → chunk-SQX5CAYG.js} +1 -1
  58. package/dist-cli/chunks/{chunk-Q4JNA5VO.js → chunk-SQZAC7C4.js} +1 -1
  59. package/dist-cli/chunks/{chunk-M4ERVRM4.js → chunk-SV7FOGJ3.js} +2 -2
  60. package/dist-cli/chunks/{chunk-ZN2C7V5R.js → chunk-TK3OJSEO.js} +2 -2
  61. package/dist-cli/chunks/{chunk-7SCQEPXK.js → chunk-TL7SIZ7S.js} +1 -1
  62. package/dist-cli/chunks/{chunk-IZ2OO47Y.js → chunk-V2GQ4WXJ.js} +2 -2
  63. package/dist-cli/chunks/{chunk-JUDJXJSE.js → chunk-VH7F45CN.js} +1 -1
  64. package/dist-cli/chunks/chunk-WNVNU2OW.js +4 -0
  65. package/dist-cli/chunks/{chunk-O3AOQP3V.js → chunk-XQ2OBHBE.js} +2 -2
  66. package/dist-cli/chunks/{chunk-MQXYJTXM.js → chunk-YCIA4BHJ.js} +2 -2
  67. package/dist-cli/chunks/chunk-ZSMMJMPA.js +1 -0
  68. package/dist-cli/chunks/cli-version-QB4VH24H.js +2 -0
  69. package/dist-cli/chunks/{compat-2DVSCCR7.js → compat-FWSEEGEH.js} +3 -3
  70. package/dist-cli/chunks/{config-YDX4Q4XM.js → config-CYI2WAGP.js} +2 -2
  71. package/dist-cli/chunks/control-UXY7YQVX.js +2 -0
  72. package/dist-cli/chunks/{cpu-profile-GU62WVZZ.js → cpu-profile-IKAE3KTY.js} +2 -2
  73. package/dist-cli/chunks/{daemon-V5NLDTSB.js → daemon-ZUMF53YB.js} +2 -2
  74. package/dist-cli/chunks/{debug-HAOCONNB.js → debug-P6KULKKS.js} +3 -3
  75. package/dist-cli/chunks/{detox-YLC4DLXB.js → detox-SPWAZCYG.js} +2 -2
  76. package/dist-cli/chunks/{device-ZQ4DN4H6.js → device-JWEPK6I2.js} +2 -2
  77. package/dist-cli/chunks/{diagnose-HNUO3Z5F.js → diagnose-IZODTXV2.js} +2 -2
  78. package/dist-cli/chunks/drivers-MK6WJKBC.js +2 -0
  79. package/dist-cli/chunks/{electron-ZAASAHSW.js → electron-R5GP6RVB.js} +3 -3
  80. package/dist-cli/chunks/flow-6O4GEOPJ.js +2 -0
  81. package/dist-cli/chunks/{hints-BA3GE5W5.js → hints-DYDNYX7N.js} +2 -2
  82. package/dist-cli/chunks/{home-paths-MQXRHBTW.js → home-paths-GLMX5OKL.js} +2 -2
  83. package/dist-cli/chunks/{inspect-T4RMS5KX.js → inspect-FJOPCTY2.js} +3 -3
  84. package/dist-cli/chunks/install-A3TUGGHN.js +2 -0
  85. package/dist-cli/chunks/{install-desktop-MH26VONS.js → install-desktop-YPJZMZM5.js} +3 -3
  86. package/dist-cli/chunks/{keys-IELIDRGB.js → keys-GSYPHWNY.js} +2 -2
  87. package/dist-cli/chunks/{launch-VMT3OWOB.js → launch-4G2PKW5X.js} +3 -3
  88. package/dist-cli/chunks/{login-VZBANVLU.js → login-KJQGHA64.js} +4 -4
  89. package/dist-cli/chunks/{logout-GWXBTQ4H.js → logout-XM2SYH5C.js} +2 -2
  90. package/dist-cli/chunks/{maestro-JYHR4HFR.js → maestro-EOWGI7DG.js} +2 -2
  91. package/dist-cli/chunks/{preview-RPZ4UQ2B.js → preview-F73TKK37.js} +2 -2
  92. package/dist-cli/chunks/{profile-7FLDF2AP.js → profile-22FDKBUO.js} +2 -2
  93. package/dist-cli/chunks/{react-3RC4CNDZ.js → react-5L6VPFUP.js} +2 -2
  94. package/dist-cli/chunks/record-JZXCQ4IN.js +70 -0
  95. package/dist-cli/chunks/runtime-EEBX7CFV.js +2 -0
  96. package/dist-cli/chunks/{runtime-delivery-Z7I2KIRB.js → runtime-delivery-LXUM3R4A.js} +2 -2
  97. package/dist-cli/chunks/{screenshot-GRCZ6AM4.js → screenshot-HDRRG33Q.js} +2 -2
  98. package/dist-cli/chunks/{screenshot-mode-E4YHXHH5.js → screenshot-mode-WY63LZIX.js} +2 -2
  99. package/dist-cli/chunks/{screenshots-7SXMX2AY.js → screenshots-MPV2ENL5.js} +2 -2
  100. package/dist-cli/chunks/{server-GDJ2TCRV.js → server-5LBMCJ3G.js} +2 -2
  101. package/dist-cli/chunks/setup-repo-SZSYNKNI.js +2 -0
  102. package/dist-cli/chunks/{skills-62E7NDRC.js → skills-BQ73YOBF.js} +2 -2
  103. package/dist-cli/chunks/{start-Y7KR5ZQ3.js → start-2WU4W6ZU.js} +4 -4
  104. package/dist-cli/chunks/store-RE45SUBF.js +2 -0
  105. package/dist-cli/chunks/telemetry-DG6GJLCP.js +2 -0
  106. package/dist-cli/chunks/{test-IYMSUPVC.js → test-OVO4CQTG.js} +3 -3
  107. package/dist-cli/chunks/{three-mode-QKKXCCC2.js → three-mode-BKM3KFM7.js} +2 -2
  108. package/dist-cli/chunks/{timeline-PF6NQ7RT.js → timeline-MDXGEDQL.js} +2 -2
  109. package/dist-cli/chunks/{upgrade-CE2Y3TAN.js → upgrade-JGQABWVF.js} +2 -2
  110. package/dist-cli/chunks/upload-UJNUA4ZV.js +2 -0
  111. package/dist-cli/chunks/{web-XEO3ZCPF.js → web-WYFAYQ72.js} +2 -2
  112. package/dist-cli/chunks/{what-happened-372J7YF7.js → what-happened-PZW2KW6A.js} +2 -2
  113. package/dist-cli/chunks/{whoami-B4E7KCT5.js → whoami-7ATWJQS6.js} +2 -2
  114. package/dist-lib/agent-daemon-client.cjs +1 -1
  115. package/dist-lib/agent-events.cjs +1 -1
  116. package/dist-lib/agent-sessions.cjs +1 -1
  117. package/dist-lib/attached-projects.cjs +1 -1
  118. package/dist-lib/auth/shared-session.cjs +1 -1
  119. package/dist-lib/backend-origin.cjs +1 -1
  120. package/dist-lib/beta.cjs +44 -0
  121. package/dist-lib/bridge-constants.cjs +1 -1
  122. package/dist-lib/cli-constants.cjs +1 -1
  123. package/dist-lib/config.cjs +1 -1
  124. package/dist-lib/detox/index.cjs +1770 -0
  125. package/dist-lib/detox/jest-preset.cjs +50 -0
  126. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  127. package/dist-lib/home-paths.cjs +1 -1
  128. package/dist-lib/host/bridge-host.cjs +1 -1
  129. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  130. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  131. package/dist-lib/index.cjs +1 -1
  132. package/dist-lib/metro.cjs +1 -1
  133. package/dist-lib/profiles.cjs +1 -1
  134. package/dist-lib/render-mode.cjs +1 -1
  135. package/dist-lib/scripts/demo-app-registry.cjs +809 -0
  136. package/dist-lib/scripts/dev-server-scanner.cjs +1269 -0
  137. package/dist-lib/skills.cjs +8322 -0
  138. package/dist-lib/vite-base.cjs +3 -3
  139. package/dist-lib/vite.cjs +1 -1
  140. package/package.json +39 -10
  141. package/scripts/demo-app-registry.ts +989 -0
  142. package/scripts/dev-server-scanner.ts +674 -0
  143. package/src/agent-daemon-client.ts +390 -0
  144. package/src/agent-events.ts +71 -0
  145. package/src/agent-prompt.ts +71 -0
  146. package/src/agent-sessions.ts +572 -0
  147. package/src/attached-projects.ts +536 -0
  148. package/src/auth/shared-session.ts +199 -0
  149. package/src/backend-origin.ts +49 -0
  150. package/src/beta.ts +21 -0
  151. package/src/bridge-constants.ts +10 -0
  152. package/src/cli-constants.ts +1 -0
  153. package/src/cli-version.ts +30 -0
  154. package/src/codex-client.ts +215 -0
  155. package/src/config.ts +110 -0
  156. package/src/dev-bundle-resolution.ts +180 -0
  157. package/src/home-paths.ts +382 -0
  158. package/src/host/agent-host.ts +576 -0
  159. package/src/host/bridge-host.ts +2293 -0
  160. package/src/host/fetch-proxy-handler.ts +288 -0
  161. package/src/host/fetch-proxy-overrides.ts +39 -0
  162. package/src/host/open-url.ts +234 -0
  163. package/src/index.ts +9 -0
  164. package/src/metro-plugin.ts +207 -0
  165. package/src/native-dev-bundle-url.ts +62 -0
  166. package/src/native-seam-manifest.ts +313 -0
  167. package/src/profiles.ts +179 -0
  168. package/src/render-mode.ts +27 -0
  169. package/src/runtime-delivery.ts +334 -0
  170. package/src/screenshots/compose.ts +422 -0
  171. package/src/screenshots/frame-compose.ts +438 -0
  172. package/src/screenshots/orchestrate.ts +244 -0
  173. package/src/screenshots/registry.ts +58 -0
  174. package/src/screenshots/schema.ts +364 -0
  175. package/src/skills/builtin/a11y-review.ts +126 -0
  176. package/src/skills/builtin/compat-check.ts +104 -0
  177. package/src/skills/builtin/perf-profile.ts +84 -0
  178. package/src/skills/builtin/screenshot-all.ts +46 -0
  179. package/src/skills/builtin/test-flow.ts +118 -0
  180. package/src/skills/builtin/visual-diff.ts +94 -0
  181. package/src/skills/registry.ts +107 -0
  182. package/src/skills/types.ts +41 -0
  183. package/src/vite-plugin-one.ts +187 -0
  184. package/src/vite-plugin.ts +1381 -0
  185. package/src/worklets-babel.ts +132 -0
  186. package/dist-cli/chunks/auto-bootstrap-D2EQVL7R.js +0 -2
  187. package/dist-cli/chunks/beta-VHPXECZY.js +0 -2
  188. package/dist-cli/chunks/chunk-27HBWBE6.js +0 -4
  189. package/dist-cli/chunks/chunk-2W5C5J4O.js +0 -64
  190. package/dist-cli/chunks/chunk-3OH4VCJA.js +0 -1
  191. package/dist-cli/chunks/chunk-45HLFQRI.js +0 -2
  192. package/dist-cli/chunks/chunk-7YLCK5HG.js +0 -5
  193. package/dist-cli/chunks/chunk-BRDUKIZI.js +0 -119
  194. package/dist-cli/chunks/chunk-GADW2Q5S.js +0 -1
  195. package/dist-cli/chunks/chunk-HST43CVE.js +0 -2
  196. package/dist-cli/chunks/chunk-QJBQOGTK.js +0 -73
  197. package/dist-cli/chunks/chunk-V26REV7G.js +0 -1
  198. package/dist-cli/chunks/cli-version-WF7T6IKI.js +0 -2
  199. package/dist-cli/chunks/control-EAK2OPGB.js +0 -2
  200. package/dist-cli/chunks/demo-app-registry-52A2MI72.js +0 -2
  201. package/dist-cli/chunks/drivers-B4QPIZ4B.js +0 -2
  202. package/dist-cli/chunks/flow-PFLHFNVM.js +0 -2
  203. package/dist-cli/chunks/install-ZCPEMK6U.js +0 -2
  204. package/dist-cli/chunks/record-CZ33G5FT.js +0 -70
  205. package/dist-cli/chunks/runtime-AZKHZHJ4.js +0 -2
  206. package/dist-cli/chunks/setup-repo-3Y2QAZRK.js +0 -2
  207. package/dist-cli/chunks/store-TDTFZMGA.js +0 -2
  208. package/dist-cli/chunks/telemetry-G3NIU5NP.js +0 -2
  209. package/dist-cli/chunks/upload-CLWFS7IL.js +0 -2
@@ -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
+ }
@@ -0,0 +1,126 @@
1
+ // built-in skill: accessibility audit
2
+
3
+ import type { SootSimSkill } from '../types'
4
+
5
+ export const skill: SootSimSkill = {
6
+ name: 'a11y-review',
7
+ description: 'Check the app for accessibility issues',
8
+ type: 'review',
9
+ version: '1.0.0',
10
+ triggers: ['accessibility', 'a11y', 'check accessibility', 'audit accessibility'],
11
+ tools: [
12
+ {
13
+ name: 'audit_accessibility',
14
+ description: 'Run an accessibility audit on the current screen',
15
+ parameters: {
16
+ screen: { type: 'string', description: 'Screen name or URL to audit' },
17
+ },
18
+ async execute(params, context) {
19
+ const { chromium } = await import('playwright')
20
+ const path = await import('path')
21
+ const fs = await import('fs')
22
+
23
+ const browser = await chromium.launch({ headless: true })
24
+ const page = await browser.newPage({ viewport: { width: 500, height: 900 } })
25
+
26
+ try {
27
+ await page.goto(context.url, { waitUntil: 'networkidle' })
28
+ await page.waitForFunction('window.__sootsimTest', { timeout: 10000 })
29
+ await page.waitForTimeout(2000)
30
+
31
+ // get accessibility tree
32
+ const tree = await page.evaluate(
33
+ 'window.__sootsimTest.dumpAccessibilityTree(10)',
34
+ )
35
+ const allNodes = await page.evaluate('window.__sootsimTest.queryAll({})')
36
+
37
+ // analyze for common issues
38
+ const issues: {
39
+ severity: 'error' | 'warning' | 'info'
40
+ message: string
41
+ node?: string
42
+ }[] = []
43
+
44
+ if (Array.isArray(allNodes)) {
45
+ for (const node of allNodes) {
46
+ // check for missing accessibility labels on interactive elements
47
+ if (
48
+ (node.accessibilityRole === 'button' ||
49
+ node.accessibilityRole === 'link' ||
50
+ node.type === 'TouchableOpacity' ||
51
+ node.type === 'Pressable') &&
52
+ !node.accessibilityLabel &&
53
+ !node.text
54
+ ) {
55
+ issues.push({
56
+ severity: 'error',
57
+ message: `Interactive element missing accessibility label`,
58
+ node: node.testID || node.id || node.type,
59
+ })
60
+ }
61
+
62
+ // check for missing roles
63
+ if (
64
+ (node.type === 'TouchableOpacity' || node.type === 'Pressable') &&
65
+ !node.accessibilityRole
66
+ ) {
67
+ issues.push({
68
+ severity: 'warning',
69
+ message: `Touchable element missing accessibility role`,
70
+ node: node.testID || node.id || node.type,
71
+ })
72
+ }
73
+
74
+ // check for images without alt text
75
+ if (node.type === 'Image' && !node.accessibilityLabel && !node.accessible) {
76
+ issues.push({
77
+ severity: 'warning',
78
+ message: `Image missing accessibility label`,
79
+ node: node.testID || node.id || 'Image',
80
+ })
81
+ }
82
+
83
+ // check for small touch targets
84
+ if (
85
+ node.layout &&
86
+ (node.accessibilityRole === 'button' ||
87
+ node.type === 'TouchableOpacity') &&
88
+ (node.layout.width < 44 || node.layout.height < 44)
89
+ ) {
90
+ issues.push({
91
+ severity: 'warning',
92
+ message: `Touch target too small (${Math.round(node.layout.width)}x${Math.round(node.layout.height)}pt, minimum 44x44pt)`,
93
+ node: node.testID || node.text || node.type,
94
+ })
95
+ }
96
+ }
97
+ }
98
+
99
+ // write report
100
+ const report = {
101
+ timestamp: new Date().toISOString(),
102
+ screen: params.screen || 'current',
103
+ nodeCount: Array.isArray(allNodes) ? allNodes.length : 0,
104
+ issues,
105
+ errors: issues.filter((i) => i.severity === 'error').length,
106
+ warnings: issues.filter((i) => i.severity === 'warning').length,
107
+ tree: typeof tree === 'string' ? tree : JSON.stringify(tree),
108
+ }
109
+
110
+ const reportPath = path.join(context.outputDir, 'a11y-report.json')
111
+ fs.mkdirSync(path.dirname(reportPath), { recursive: true })
112
+ fs.writeFileSync(reportPath, JSON.stringify(report, null, 2))
113
+
114
+ return {
115
+ success: issues.filter((i) => i.severity === 'error').length === 0,
116
+ message: `${report.errors} errors, ${report.warnings} warnings across ${report.nodeCount} nodes`,
117
+ artifacts: [{ type: 'report', name: 'a11y-report', path: reportPath }],
118
+ data: report,
119
+ }
120
+ } finally {
121
+ await browser.close()
122
+ }
123
+ },
124
+ },
125
+ ],
126
+ }
@@ -0,0 +1,104 @@
1
+ // built-in skill: package compatibility check
2
+
3
+ import type { SootSimSkill } from '../types'
4
+
5
+ function getCompatStatus(entry: {
6
+ stubType: 'native' | 'works' | 'build-only'
7
+ versions: Array<{ coverage: number }>
8
+ }): 'full' | 'partial' | 'auto-stub' {
9
+ if (entry.stubType === 'works') return 'full'
10
+ if (entry.stubType === 'build-only') return 'full'
11
+ return entry.versions.some((version) => version.coverage >= 1) ? 'full' : 'auto-stub'
12
+ }
13
+
14
+ export const skill: SootSimSkill = {
15
+ name: 'compat-check',
16
+ description: 'Check package compatibility with sootsim',
17
+ type: 'review',
18
+ version: '1.0.0',
19
+ triggers: ['compatibility', 'compat', 'check package', 'supported packages'],
20
+ tools: [
21
+ {
22
+ name: 'check_compatibility',
23
+ description: 'Scan project dependencies for sootsim compatibility',
24
+ parameters: {
25
+ packageName: {
26
+ type: 'string',
27
+ description: 'Specific package to check (optional)',
28
+ },
29
+ },
30
+ async execute(params, context) {
31
+ const fs = await import('fs')
32
+ const path = await import('path')
33
+
34
+ const { POLYFILL_REGISTRY } = await import('@soot/compat/web')
35
+
36
+ if (params.packageName) {
37
+ const entry = POLYFILL_REGISTRY[params.packageName]
38
+ if (!entry) {
39
+ return {
40
+ success: true,
41
+ message: `${params.packageName}: not in registry (may be auto-stubbed if native)`,
42
+ data: { package: params.packageName, status: 'unknown' },
43
+ }
44
+ }
45
+ const status = getCompatStatus(entry)
46
+ return {
47
+ success: true,
48
+ message: `${params.packageName}: ${status}`,
49
+ data: { package: params.packageName, status, ...entry },
50
+ }
51
+ }
52
+
53
+ // scan project deps
54
+ const pkgPath = path.join(context.projectDir, 'package.json')
55
+ if (!fs.existsSync(pkgPath)) {
56
+ return { success: false, message: 'No package.json found' }
57
+ }
58
+
59
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
60
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }
61
+
62
+ const results: Record<string, string[]> = {
63
+ full: [],
64
+ partial: [],
65
+ 'auto-stub': [],
66
+ unsupported: [],
67
+ unknown: [],
68
+ }
69
+
70
+ for (const dep of Object.keys(allDeps)) {
71
+ const entry = POLYFILL_REGISTRY[dep]
72
+ if (entry) {
73
+ const status = getCompatStatus(entry)
74
+ if (results[status]) results[status].push(dep)
75
+ } else if (isNative(dep)) {
76
+ results.unknown.push(dep)
77
+ }
78
+ }
79
+
80
+ const reportPath = path.join(context.outputDir, 'compat-report.json')
81
+ fs.mkdirSync(path.dirname(reportPath), { recursive: true })
82
+ fs.writeFileSync(reportPath, JSON.stringify(results, null, 2))
83
+
84
+ return {
85
+ success: results.unsupported.length === 0,
86
+ message: `${results.full.length} full, ${results.partial.length} partial, ${results.unsupported.length} unsupported, ${results.unknown.length} unknown`,
87
+ artifacts: [{ type: 'report', name: 'compat-report', path: reportPath }],
88
+ data: results,
89
+ }
90
+ },
91
+ },
92
+ ],
93
+ }
94
+
95
+ function isNative(name: string): boolean {
96
+ return (
97
+ name.startsWith('expo-') ||
98
+ name.startsWith('react-native-') ||
99
+ name.startsWith('@react-native/') ||
100
+ name.startsWith('@react-native-community/') ||
101
+ name.startsWith('@expo/') ||
102
+ name === 'expo'
103
+ )
104
+ }