rnxsim 0.0.0 → 0.1.313

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-XZ2KTPCU.js +48 -0
  24. package/dist-cli/chunks/agent-wrapper-JJYYW2WH.js +17 -0
  25. package/dist-cli/chunks/app-fonts-IXRNQG6B.js +4 -0
  26. package/dist-cli/chunks/assert-54T5SK5F.js +49 -0
  27. package/dist-cli/chunks/auth-FI5UDI45.js +12 -0
  28. package/dist-cli/chunks/beta-JV6UKADW.js +4 -0
  29. package/dist-cli/chunks/camera-VL73YIKP.js +22 -0
  30. package/dist-cli/chunks/chunk-2D2UPBBR.js +4 -0
  31. package/dist-cli/chunks/chunk-2YR5BGA5.js +5 -0
  32. package/dist-cli/chunks/chunk-3NV2NCNX.js +4 -0
  33. package/dist-cli/chunks/chunk-46EUUFJ5.js +4 -0
  34. package/dist-cli/chunks/chunk-4NPPOV2N.js +5 -0
  35. package/dist-cli/chunks/chunk-5DHC6KHQ.js +7 -0
  36. package/dist-cli/chunks/chunk-5TEF3ET3.js +74 -0
  37. package/dist-cli/chunks/chunk-5TPRP5QT.js +4 -0
  38. package/dist-cli/chunks/chunk-5YJCOWCH.js +4 -0
  39. package/dist-cli/chunks/chunk-7GN3LVWB.js +4 -0
  40. package/dist-cli/chunks/chunk-BBULZ7CG.js +568 -0
  41. package/dist-cli/chunks/chunk-BTWORNNG.js +71 -0
  42. package/dist-cli/chunks/chunk-D4FFVGI5.js +5 -0
  43. package/dist-cli/chunks/chunk-DCEMHR2Y.js +4 -0
  44. package/dist-cli/chunks/chunk-DZS6WPUI.js +4 -0
  45. package/dist-cli/chunks/chunk-F5ZRSS3C.js +3 -0
  46. package/dist-cli/chunks/chunk-FSUYIVJ6.js +9 -0
  47. package/dist-cli/chunks/chunk-G2WW6L2C.js +23 -0
  48. package/dist-cli/chunks/chunk-GASE6UBA.js +4 -0
  49. package/dist-cli/chunks/chunk-GGRX24GF.js +142 -0
  50. package/dist-cli/chunks/chunk-HI5TFJWN.js +5 -0
  51. package/dist-cli/chunks/chunk-IJ5CAZZC.js +5 -0
  52. package/dist-cli/chunks/chunk-IJO63TDP.js +4 -0
  53. package/dist-cli/chunks/chunk-KTHV3RUS.js +26 -0
  54. package/dist-cli/chunks/chunk-LF2ZVT7O.js +6 -0
  55. package/dist-cli/chunks/chunk-MJRLLB4R.js +38 -0
  56. package/dist-cli/chunks/chunk-NFK7T35W.js +4 -0
  57. package/dist-cli/chunks/chunk-NMF2ZMZQ.js +15 -0
  58. package/dist-cli/chunks/chunk-OHAZNXLK.js +4 -0
  59. package/dist-cli/chunks/chunk-OVFJFXUD.js +101 -0
  60. package/dist-cli/chunks/chunk-OZSSI4WN.js +28 -0
  61. package/dist-cli/chunks/chunk-QKDWYITG.js +25 -0
  62. package/dist-cli/chunks/chunk-RSZWCKNT.js +30 -0
  63. package/dist-cli/chunks/chunk-RTN5C5RL.js +4 -0
  64. package/dist-cli/chunks/chunk-TIVZIMMW.js +4 -0
  65. package/dist-cli/chunks/chunk-TZFFR3SD.js +7 -0
  66. package/dist-cli/chunks/chunk-UC6U3MML.js +5 -0
  67. package/dist-cli/chunks/chunk-VFCMSYZK.js +6 -0
  68. package/dist-cli/chunks/chunk-VNQEB4L7.js +5 -0
  69. package/dist-cli/chunks/chunk-VZXWHRUZ.js +983 -0
  70. package/dist-cli/chunks/chunk-W6K4EFPH.js +107 -0
  71. package/dist-cli/chunks/chunk-WEXDAC74.js +4 -0
  72. package/dist-cli/chunks/chunk-WF3T4SVI.js +14 -0
  73. package/dist-cli/chunks/chunk-WINYQ44O.js +4 -0
  74. package/dist-cli/chunks/chunk-WMIIKMGK.js +10 -0
  75. package/dist-cli/chunks/chunk-WUSWBCWA.js +507 -0
  76. package/dist-cli/chunks/chunk-WWZIXIRD.js +4 -0
  77. package/dist-cli/chunks/chunk-XEVZYVIW.js +766 -0
  78. package/dist-cli/chunks/chunk-YDGQTMQL.js +4 -0
  79. package/dist-cli/chunks/chunk-YIFT42WN.js +4 -0
  80. package/dist-cli/chunks/chunk-ZMJD5GEC.js +125 -0
  81. package/dist-cli/chunks/cleanup-P27PA6JI.js +25 -0
  82. package/dist-cli/chunks/cli-version-WWLPBDQ7.js +4 -0
  83. package/dist-cli/chunks/compat-ZD65FED3.js +20 -0
  84. package/dist-cli/chunks/config-XMJRNM2A.js +13 -0
  85. package/dist-cli/chunks/control-KMIQT3QP.js +4 -0
  86. package/dist-cli/chunks/daemon-G2ME7NLB.js +4 -0
  87. package/dist-cli/chunks/debug-PT4HOP7N.js +200 -0
  88. package/dist-cli/chunks/desktop-S3FG72AK.js +31 -0
  89. package/dist-cli/chunks/detox-B3D4IFCN.js +52 -0
  90. package/dist-cli/chunks/device-XBNDSB2R.js +18 -0
  91. package/dist-cli/chunks/diagnose-HMQXJE5N.js +43 -0
  92. package/dist-cli/chunks/disk-cleanup-BLCZ5BSZ.js +4 -0
  93. package/dist-cli/chunks/drivers-LDECZGP2.js +4 -0
  94. package/dist-cli/chunks/film-BJGTBYZB.js +74 -0
  95. package/dist-cli/chunks/flow-UEQNVTU7.js +4 -0
  96. package/dist-cli/chunks/help-T5FYSVGB.js +4 -0
  97. package/dist-cli/chunks/hidden-runtime-alias-ANOYADHM.js +4 -0
  98. package/dist-cli/chunks/home-paths-GT3LFNOR.js +4 -0
  99. package/dist-cli/chunks/inspect-ZA6XF5LD.js +4 -0
  100. package/dist-cli/chunks/install-desktop-TIMUDHPL.js +4 -0
  101. package/dist-cli/chunks/login-FJ737MWG.js +28 -0
  102. package/dist-cli/chunks/logout-ZCNMMHMY.js +4 -0
  103. package/dist-cli/chunks/maestro-SZTNKLDF.js +104 -0
  104. package/dist-cli/chunks/maestro-generate-DCFAIZ4H.js +36 -0
  105. package/dist-cli/chunks/mode-GRMQCRXR.js +26 -0
  106. package/dist-cli/chunks/optional-demo-registry-W36EWFFB.js +4 -0
  107. package/dist-cli/chunks/perf-QYBAAUZG.js +48 -0
  108. package/dist-cli/chunks/permissions-3QCQ6VF4.js +13 -0
  109. package/dist-cli/chunks/record-QPPC2S4E.js +72 -0
  110. package/dist-cli/chunks/report-issue-7NMFP4HK.js +29 -0
  111. package/dist-cli/chunks/runtime-XOAXMSTU.js +4 -0
  112. package/dist-cli/chunks/screenshot-command-67AECJFB.js +159 -0
  113. package/dist-cli/chunks/screenshot-layers-ASWBYPJL.js +20 -0
  114. package/dist-cli/chunks/screenshots-capture-PXA3HFQK.js +42 -0
  115. package/dist-cli/chunks/serve-BI2NBAXG.js +44 -0
  116. package/dist-cli/chunks/setup-7DWPMRSB.js +32 -0
  117. package/dist-cli/chunks/skills-S3Y22TUA.js +50 -0
  118. package/dist-cli/chunks/storage-XUIMJWAJ.js +37 -0
  119. package/dist-cli/chunks/store-JTHEJLAZ.js +4 -0
  120. package/dist-cli/chunks/telemetry-ZYJGD2DB.js +4 -0
  121. package/dist-cli/chunks/timeline-TMPLQPSP.js +24 -0
  122. package/dist-cli/chunks/upgrade-7HDSIM7K.js +7 -0
  123. package/dist-cli/chunks/upload-GMSZPWM6.js +4 -0
  124. package/dist-cli/chunks/version-HOCHZ37L.js +6 -0
  125. package/dist-cli/chunks/web-DG3WBYD3.js +4 -0
  126. package/dist-cli/chunks/what-happened-XFVUTZR7.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 +23922 -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,418 @@
1
+ import { readFile, readdir, stat } from 'node:fs/promises'
2
+ import { join } from 'node:path'
3
+ import { parsePlistSource } from './plist.ts'
4
+ import type { AppPreferenceValue, AppSettingsSpecifier } from '@sootsim/globals'
5
+
6
+ const MAX_PLIST_BYTES = 1_048_576
7
+ const MAX_CHILD_DEPTH = 8
8
+ const MAX_PLIST_FILES = 32
9
+ const MAX_SPECIFIERS = 512
10
+
11
+ type PlistRecord = Record<string, unknown>
12
+
13
+ interface ParseBudget {
14
+ files: number
15
+ specifiers: number
16
+ }
17
+
18
+ interface CachedSettingsBundle {
19
+ fingerprint: string
20
+ specifiers: readonly AppSettingsSpecifier[] | null
21
+ }
22
+
23
+ const settingsBundleCache = new Map<string, CachedSettingsBundle>()
24
+
25
+ function isRecord(value: unknown): value is PlistRecord {
26
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
27
+ }
28
+
29
+ function optionalString(value: unknown): string | undefined {
30
+ return typeof value === 'string' ? value : undefined
31
+ }
32
+
33
+ function requiredString(value: unknown): string | null {
34
+ if (typeof value !== 'string') return null
35
+ const normalized = value.trim()
36
+ return normalized ? normalized : null
37
+ }
38
+
39
+ function finiteNumber(value: unknown): number | null {
40
+ return typeof value === 'number' && Number.isFinite(value) ? value : null
41
+ }
42
+
43
+ function preferenceValue(value: unknown): AppPreferenceValue | null {
44
+ if (typeof value === 'string' || typeof value === 'boolean') return value
45
+ return finiteNumber(value)
46
+ }
47
+
48
+ function keyboardType(
49
+ value: unknown,
50
+ ): Extract<AppSettingsSpecifier, { type: 'text' }>['keyboardType'] {
51
+ if (typeof value !== 'string') return undefined
52
+ switch (value.replaceAll(/[-_\s]/g, '').toLowerCase()) {
53
+ case 'alphabet':
54
+ return 'alphabet'
55
+ case 'numbersandpunctuation':
56
+ return 'numbers-and-punctuation'
57
+ case 'numberpad':
58
+ return 'number-pad'
59
+ case 'url':
60
+ return 'url'
61
+ case 'emailaddress':
62
+ return 'email-address'
63
+ case 'namephonepad':
64
+ return 'name-phone-pad'
65
+ case 'asciicapable':
66
+ return 'ascii-capable'
67
+ default:
68
+ return undefined
69
+ }
70
+ }
71
+
72
+ function autocapitalization(
73
+ value: unknown,
74
+ ): Extract<AppSettingsSpecifier, { type: 'text' }>['autocapitalization'] {
75
+ if (typeof value !== 'string') return undefined
76
+ switch (value.replaceAll(/[-_\s]/g, '').toLowerCase()) {
77
+ case 'none':
78
+ return 'none'
79
+ case 'sentences':
80
+ return 'sentences'
81
+ case 'words':
82
+ return 'words'
83
+ case 'allcharacters':
84
+ return 'all-characters'
85
+ default:
86
+ return undefined
87
+ }
88
+ }
89
+
90
+ function autocorrection(value: unknown): boolean | undefined {
91
+ if (typeof value === 'boolean') return value
92
+ if (typeof value !== 'string') return undefined
93
+ switch (value.trim().toLowerCase()) {
94
+ case 'yes':
95
+ return true
96
+ case 'no':
97
+ return false
98
+ default:
99
+ return undefined
100
+ }
101
+ }
102
+
103
+ function safeChildPlistName(value: unknown): { file: string; name: string } | null {
104
+ const file = requiredString(value)
105
+ if (!file || file.includes('/') || file.includes('\\') || file.includes('..')) {
106
+ return null
107
+ }
108
+ return {
109
+ file,
110
+ name: file.toLowerCase().endsWith('.plist') ? file : `${file}.plist`,
111
+ }
112
+ }
113
+
114
+ async function readableSettingsBundle(path: string): Promise<boolean> {
115
+ try {
116
+ const root = await stat(join(path, 'Root.plist'))
117
+ return root.isFile() && root.size <= MAX_PLIST_BYTES
118
+ } catch {
119
+ return false
120
+ }
121
+ }
122
+
123
+ async function findSettingsBundle(projectRoot: string): Promise<string | null> {
124
+ const directCandidates = [
125
+ join(projectRoot, 'Settings.bundle'),
126
+ join(projectRoot, 'ios', 'Settings.bundle'),
127
+ ]
128
+ for (const candidate of directCandidates) {
129
+ if (await readableSettingsBundle(candidate)) return candidate
130
+ }
131
+
132
+ try {
133
+ const iosRoot = join(projectRoot, 'ios')
134
+ const children = await readdir(iosRoot, { withFileTypes: true })
135
+ const candidates = children
136
+ .filter(
137
+ (entry) =>
138
+ entry.isDirectory() &&
139
+ entry.name !== 'Pods' &&
140
+ entry.name !== 'build' &&
141
+ !entry.name.startsWith('.'),
142
+ )
143
+ .map((entry) => join(iosRoot, entry.name, 'Settings.bundle'))
144
+ .sort()
145
+ for (const candidate of candidates) {
146
+ if (await readableSettingsBundle(candidate)) return candidate
147
+ }
148
+ } catch {}
149
+
150
+ return null
151
+ }
152
+
153
+ async function settingsBundleFingerprint(bundlePath: string): Promise<string | null> {
154
+ try {
155
+ const entries = await readdir(bundlePath, { withFileTypes: true })
156
+ const names = entries
157
+ .filter((entry) => entry.isFile() && entry.name.toLowerCase().endsWith('.plist'))
158
+ .map((entry) => entry.name)
159
+ .sort()
160
+ .slice(0, MAX_PLIST_FILES + 1)
161
+ if (names.length === 0 || names.length > MAX_PLIST_FILES) return null
162
+
163
+ const parts = await Promise.all(
164
+ names.map(async (name) => {
165
+ const metadata = await stat(join(bundlePath, name))
166
+ if (!metadata.isFile() || metadata.size > MAX_PLIST_BYTES) return null
167
+ return `${name}:${metadata.size}:${metadata.mtimeMs}:${metadata.ctimeMs}`
168
+ }),
169
+ )
170
+ if (parts.some((part) => part === null)) return null
171
+ return parts.join('|')
172
+ } catch {
173
+ return null
174
+ }
175
+ }
176
+
177
+ async function parsePlistRecord(path: string): Promise<PlistRecord | null> {
178
+ try {
179
+ const metadata = await stat(path)
180
+ if (!metadata.isFile() || metadata.size > MAX_PLIST_BYTES) return null
181
+ const source = await readFile(path, 'utf8')
182
+ const parsed = parsePlistSource(source)
183
+ return isRecord(parsed) ? parsed : null
184
+ } catch {
185
+ return null
186
+ }
187
+ }
188
+
189
+ function normalizeMultiValue(
190
+ raw: PlistRecord,
191
+ key: string,
192
+ title: string,
193
+ ): AppSettingsSpecifier | null {
194
+ if (!Array.isArray(raw.Values) || !Array.isArray(raw.Titles)) return null
195
+ const values: AppPreferenceValue[] = []
196
+ const titles: string[] = []
197
+ let optionType: 'string' | 'number' | 'boolean' | undefined
198
+ const pairCount = Math.min(raw.Values.length, raw.Titles.length)
199
+ for (let index = 0; index < pairCount; index++) {
200
+ const value = preferenceValue(raw.Values[index])
201
+ const optionTitle = optionalString(raw.Titles[index])
202
+ if (value === null || optionTitle === undefined) continue
203
+ const valueType: 'string' | 'number' | 'boolean' =
204
+ typeof value === 'string'
205
+ ? 'string'
206
+ : typeof value === 'number'
207
+ ? 'number'
208
+ : 'boolean'
209
+ if (optionType !== undefined && valueType !== optionType) continue
210
+ optionType = valueType
211
+ values.push(value)
212
+ titles.push(optionTitle)
213
+ }
214
+ const firstValue = values[0]
215
+ if (firstValue === undefined) return null
216
+ const declaredDefault = preferenceValue(raw.DefaultValue)
217
+ const defaultValue =
218
+ declaredDefault !== null && values.includes(declaredDefault)
219
+ ? declaredDefault
220
+ : firstValue
221
+ return {
222
+ type: 'multi-value',
223
+ key,
224
+ title,
225
+ defaultValue,
226
+ currentValue: defaultValue,
227
+ values,
228
+ titles,
229
+ }
230
+ }
231
+
232
+ async function normalizeSpecifier(
233
+ raw: unknown,
234
+ context: {
235
+ bundlePath: string
236
+ depth: number
237
+ budget: ParseBudget
238
+ stack: ReadonlySet<string>
239
+ fileCache: Map<string, readonly AppSettingsSpecifier[] | null>
240
+ },
241
+ ): Promise<AppSettingsSpecifier | null> {
242
+ if (!isRecord(raw)) return null
243
+ if (context.budget.specifiers >= MAX_SPECIFIERS) return null
244
+ const type = requiredString(raw.Type)
245
+ if (!type) return null
246
+ context.budget.specifiers += 1
247
+
248
+ if (type === 'PSGroupSpecifier') {
249
+ const title = optionalString(raw.Title)
250
+ const footer = optionalString(raw.FooterText)
251
+ return {
252
+ type: 'group',
253
+ ...(title !== undefined ? { title } : null),
254
+ ...(footer !== undefined ? { footer } : null),
255
+ }
256
+ }
257
+
258
+ const key = requiredString(raw.Key)
259
+ if (type === 'PSSliderSpecifier') {
260
+ if (!key) return null
261
+ const minimumValue = finiteNumber(raw.MinimumValue)
262
+ const maximumValue = finiteNumber(raw.MaximumValue)
263
+ if (minimumValue === null || maximumValue === null || maximumValue <= minimumValue) {
264
+ return null
265
+ }
266
+ const declaredDefault = finiteNumber(raw.DefaultValue)
267
+ const defaultValue = Math.min(
268
+ maximumValue,
269
+ Math.max(minimumValue, declaredDefault ?? minimumValue),
270
+ )
271
+ const title = optionalString(raw.Title)
272
+ const minimumValueImage = optionalString(raw.MinimumValueImage)
273
+ const maximumValueImage = optionalString(raw.MaximumValueImage)
274
+ return {
275
+ type: 'slider',
276
+ key,
277
+ defaultValue,
278
+ currentValue: defaultValue,
279
+ minimumValue,
280
+ maximumValue,
281
+ ...(title !== undefined ? { title } : null),
282
+ ...(minimumValueImage !== undefined ? { minimumValueImage } : null),
283
+ ...(maximumValueImage !== undefined ? { maximumValueImage } : null),
284
+ }
285
+ }
286
+
287
+ if (type === 'PSChildPaneSpecifier') {
288
+ const title = requiredString(raw.Title)
289
+ const child = safeChildPlistName(raw.File)
290
+ if (!title || !child || context.depth >= MAX_CHILD_DEPTH) return null
291
+ const childSpecifiers = await parseSpecifierFile(child.name, {
292
+ ...context,
293
+ depth: context.depth + 1,
294
+ })
295
+ if (!childSpecifiers || childSpecifiers.length === 0) return null
296
+ return {
297
+ type: 'child-pane',
298
+ title,
299
+ file: child.file,
300
+ specifiers: childSpecifiers,
301
+ }
302
+ }
303
+
304
+ if (!key) return null
305
+ const title = requiredString(raw.Title) ?? key
306
+
307
+ if (type === 'PSToggleSwitchSpecifier') {
308
+ const defaultValue = typeof raw.DefaultValue === 'boolean' ? raw.DefaultValue : false
309
+ return {
310
+ type: 'toggle',
311
+ key,
312
+ title,
313
+ defaultValue,
314
+ currentValue: defaultValue,
315
+ }
316
+ }
317
+
318
+ if (type === 'PSTextFieldSpecifier') {
319
+ const defaultValue = optionalString(raw.DefaultValue) ?? ''
320
+ const placeholder = optionalString(raw.Placeholder)
321
+ const resolvedKeyboardType = keyboardType(raw.KeyboardType)
322
+ const resolvedAutocapitalization = autocapitalization(raw.AutocapitalizationType)
323
+ const resolvedAutocorrection = autocorrection(raw.AutocorrectionType)
324
+ return {
325
+ type: 'text',
326
+ key,
327
+ title,
328
+ defaultValue,
329
+ currentValue: defaultValue,
330
+ ...(placeholder !== undefined ? { placeholder } : null),
331
+ ...(typeof raw.IsSecure === 'boolean' ? { secure: raw.IsSecure } : null),
332
+ ...(resolvedKeyboardType ? { keyboardType: resolvedKeyboardType } : null),
333
+ ...(resolvedAutocapitalization
334
+ ? { autocapitalization: resolvedAutocapitalization }
335
+ : null),
336
+ ...(resolvedAutocorrection !== undefined
337
+ ? { autocorrection: resolvedAutocorrection }
338
+ : null),
339
+ }
340
+ }
341
+
342
+ if (type === 'PSMultiValueSpecifier' || type === 'PSRadioGroupSpecifier') {
343
+ return normalizeMultiValue(raw, key, title)
344
+ }
345
+
346
+ if (type === 'PSTitleValueSpecifier') {
347
+ const defaultValue = preferenceValue(raw.DefaultValue) ?? ''
348
+ return {
349
+ type: 'title-value',
350
+ key,
351
+ title,
352
+ defaultValue,
353
+ currentValue: defaultValue,
354
+ }
355
+ }
356
+
357
+ return null
358
+ }
359
+
360
+ async function parseSpecifierFile(
361
+ fileName: string,
362
+ context: {
363
+ bundlePath: string
364
+ depth: number
365
+ budget: ParseBudget
366
+ stack: ReadonlySet<string>
367
+ fileCache: Map<string, readonly AppSettingsSpecifier[] | null>
368
+ },
369
+ ): Promise<readonly AppSettingsSpecifier[] | null> {
370
+ const cached = context.fileCache.get(fileName)
371
+ if (cached !== undefined) return cached
372
+ if (context.stack.has(fileName) || context.budget.files >= MAX_PLIST_FILES) {
373
+ return null
374
+ }
375
+ context.budget.files += 1
376
+ const root = await parsePlistRecord(join(context.bundlePath, fileName))
377
+ if (!root || !Array.isArray(root.PreferenceSpecifiers)) {
378
+ context.fileCache.set(fileName, null)
379
+ return null
380
+ }
381
+
382
+ const stack = new Set(context.stack)
383
+ stack.add(fileName)
384
+ const specifiers: AppSettingsSpecifier[] = []
385
+ for (const raw of root.PreferenceSpecifiers) {
386
+ if (context.budget.specifiers >= MAX_SPECIFIERS) break
387
+ const specifier = await normalizeSpecifier(raw, { ...context, stack })
388
+ if (specifier) specifiers.push(specifier)
389
+ }
390
+ context.fileCache.set(fileName, specifiers)
391
+ return specifiers
392
+ }
393
+
394
+ export async function readAppSettingsBundle(
395
+ projectRoot: string,
396
+ ): Promise<readonly AppSettingsSpecifier[] | null> {
397
+ const bundlePath = await findSettingsBundle(projectRoot)
398
+ if (!bundlePath) return null
399
+ const fingerprint = await settingsBundleFingerprint(bundlePath)
400
+ if (!fingerprint) return null
401
+ const cached = settingsBundleCache.get(bundlePath)
402
+ if (cached?.fingerprint === fingerprint) return cached.specifiers
403
+
404
+ const specifiers = await parseSpecifierFile('Root.plist', {
405
+ bundlePath,
406
+ depth: 0,
407
+ budget: { files: 0, specifiers: 0 },
408
+ stack: new Set(),
409
+ fileCache: new Map(),
410
+ })
411
+ const normalized = specifiers && specifiers.length > 0 ? specifiers : null
412
+ settingsBundleCache.set(bundlePath, { fingerprint, specifiers: normalized })
413
+ return normalized
414
+ }
415
+
416
+ export function __resetAppSettingsBundleCacheForTests(): void {
417
+ settingsBundleCache.clear()
418
+ }
@@ -0,0 +1,132 @@
1
+ // built-in skill: accessibility audit
2
+
3
+ import { launchReapedChrome } from '../../../../../scripts/lib/reap-browser'
4
+ import type { SootSimSkill } from '../types'
5
+
6
+ export const skill: SootSimSkill = {
7
+ name: 'a11y-review',
8
+ description: 'Check the app for accessibility issues',
9
+ type: 'review',
10
+ version: '1.0.0',
11
+ triggers: ['accessibility', 'a11y', 'check accessibility', 'audit accessibility'],
12
+ tools: [
13
+ {
14
+ name: 'audit_accessibility',
15
+ description: 'Run an accessibility audit on the current screen',
16
+ parameters: {
17
+ screen: { type: 'string', description: 'Screen name or URL to audit' },
18
+ },
19
+ async execute(params, context) {
20
+ const { chromium } = await import('playwright')
21
+ const path = await import('path')
22
+ const fs = await import('fs')
23
+
24
+ // sootsim renders CanvasKit — require GPU-backed Chrome (Metal). the
25
+ // bundled swiftshader shell software-renders the canvas and pegs every
26
+ // core; never use it for sootsim.
27
+ const browser = await launchReapedChrome((options) => chromium.launch(options), {
28
+ headless: true,
29
+ })
30
+ const page = await browser.newPage({ viewport: { width: 500, height: 900 } })
31
+
32
+ try {
33
+ await page.goto(context.url, { waitUntil: 'networkidle' })
34
+ await page.waitForFunction('window.__sootsimTest', { timeout: 10000 })
35
+ await page.waitForTimeout(2000)
36
+
37
+ // get accessibility tree
38
+ const tree = await page.evaluate(
39
+ 'window.__sootsimTest.dumpAccessibilityTree(10)',
40
+ )
41
+ const allNodes = await page.evaluate('window.__sootsimTest.queryAll({})')
42
+
43
+ // analyze for common issues
44
+ const issues: {
45
+ severity: 'error' | 'warning' | 'info'
46
+ message: string
47
+ node?: string
48
+ }[] = []
49
+
50
+ if (Array.isArray(allNodes)) {
51
+ for (const node of allNodes) {
52
+ // check for missing accessibility labels on interactive elements
53
+ if (
54
+ (node.accessibilityRole === 'button' ||
55
+ node.accessibilityRole === 'link' ||
56
+ node.type === 'TouchableOpacity' ||
57
+ node.type === 'Pressable') &&
58
+ !node.accessibilityLabel &&
59
+ !node.text
60
+ ) {
61
+ issues.push({
62
+ severity: 'error',
63
+ message: `Interactive element missing accessibility label`,
64
+ node: node.testID || node.id || node.type,
65
+ })
66
+ }
67
+
68
+ // check for missing roles
69
+ if (
70
+ (node.type === 'TouchableOpacity' || node.type === 'Pressable') &&
71
+ !node.accessibilityRole
72
+ ) {
73
+ issues.push({
74
+ severity: 'warning',
75
+ message: `Touchable element missing accessibility role`,
76
+ node: node.testID || node.id || node.type,
77
+ })
78
+ }
79
+
80
+ // check for images without alt text
81
+ if (node.type === 'Image' && !node.accessibilityLabel && !node.accessible) {
82
+ issues.push({
83
+ severity: 'warning',
84
+ message: `Image missing accessibility label`,
85
+ node: node.testID || node.id || 'Image',
86
+ })
87
+ }
88
+
89
+ // check for small touch targets
90
+ if (
91
+ node.layout &&
92
+ (node.accessibilityRole === 'button' ||
93
+ node.type === 'TouchableOpacity') &&
94
+ (node.layout.width < 44 || node.layout.height < 44)
95
+ ) {
96
+ issues.push({
97
+ severity: 'warning',
98
+ message: `Touch target too small (${Math.round(node.layout.width)}x${Math.round(node.layout.height)}pt, minimum 44x44pt)`,
99
+ node: node.testID || node.text || node.type,
100
+ })
101
+ }
102
+ }
103
+ }
104
+
105
+ // write report
106
+ const report = {
107
+ timestamp: new Date().toISOString(),
108
+ screen: params.screen || 'current',
109
+ nodeCount: Array.isArray(allNodes) ? allNodes.length : 0,
110
+ issues,
111
+ errors: issues.filter((i) => i.severity === 'error').length,
112
+ warnings: issues.filter((i) => i.severity === 'warning').length,
113
+ tree: typeof tree === 'string' ? tree : JSON.stringify(tree),
114
+ }
115
+
116
+ const reportPath = path.join(context.outputDir, 'a11y-report.json')
117
+ fs.mkdirSync(path.dirname(reportPath), { recursive: true })
118
+ fs.writeFileSync(reportPath, JSON.stringify(report, null, 2))
119
+
120
+ return {
121
+ success: issues.filter((i) => i.severity === 'error').length === 0,
122
+ message: `${report.errors} errors, ${report.warnings} warnings across ${report.nodeCount} nodes`,
123
+ artifacts: [{ type: 'report', name: 'a11y-report', path: reportPath }],
124
+ data: report,
125
+ }
126
+ } finally {
127
+ await browser.close()
128
+ }
129
+ },
130
+ },
131
+ ],
132
+ }
@@ -0,0 +1,71 @@
1
+ // built-in skill: package compatibility check
2
+
3
+ import type { SootSimSkill } from '../types'
4
+
5
+ export const skill: SootSimSkill = {
6
+ name: 'compat-check',
7
+ description: 'Check package compatibility with rnx',
8
+ type: 'review',
9
+ version: '1.0.0',
10
+ triggers: ['compatibility', 'compat', 'check package', 'supported packages'],
11
+ tools: [
12
+ {
13
+ name: 'check_compatibility',
14
+ description: 'Scan project dependencies for rnx compatibility',
15
+ parameters: {
16
+ packageName: {
17
+ type: 'string',
18
+ description: 'Specific package to check (optional)',
19
+ },
20
+ },
21
+ async execute(params, context) {
22
+ const fs = await import('node:fs')
23
+ const path = await import('node:path')
24
+ const { COMPAT_RESULT_CAVEAT, formatPackageStatus, formatScanCounts, scanDeps } =
25
+ await import('../../../../compat/src/web.ts')
26
+
27
+ const packagePath = path.join(context.projectDir, 'package.json')
28
+ if (!fs.existsSync(packagePath)) {
29
+ return { success: false, message: 'No package.json found' }
30
+ }
31
+ const parsed: unknown = JSON.parse(fs.readFileSync(packagePath, 'utf8'))
32
+ if (!parsed || typeof parsed !== 'object') {
33
+ return { success: false, message: 'package.json is not an object' }
34
+ }
35
+
36
+ const dependencies = readDependencies(parsed)
37
+ const scan = params.packageName
38
+ ? scanDeps({
39
+ dependencies: {
40
+ [params.packageName]: dependencies[params.packageName] ?? '*',
41
+ },
42
+ })
43
+ : scanDeps(parsed)
44
+ const packageResult = params.packageName ? scan.packages[0] : undefined
45
+ const message = packageResult
46
+ ? `${params.packageName}: ${formatPackageStatus(packageResult)}. ${COMPAT_RESULT_CAVEAT}`
47
+ : `${formatScanCounts(scan)}. ${COMPAT_RESULT_CAVEAT}`
48
+
49
+ const reportPath = path.join(context.outputDir, 'compat-report.json')
50
+ fs.mkdirSync(path.dirname(reportPath), { recursive: true })
51
+ fs.writeFileSync(reportPath, `${JSON.stringify(scan, null, 2)}\n`)
52
+
53
+ return {
54
+ success: true,
55
+ message,
56
+ artifacts: [{ type: 'report', name: 'compat-report', path: reportPath }],
57
+ data: scan,
58
+ }
59
+ },
60
+ },
61
+ ],
62
+ }
63
+
64
+ function readDependencies(value: object): Record<string, string> {
65
+ const dependencies = Reflect.get(value, 'dependencies')
66
+ const devDependencies = Reflect.get(value, 'devDependencies')
67
+ return {
68
+ ...(dependencies && typeof dependencies === 'object' ? dependencies : {}),
69
+ ...(devDependencies && typeof devDependencies === 'object' ? devDependencies : {}),
70
+ }
71
+ }