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.
- package/LICENSE +21 -0
- package/README.md +512 -0
- package/detox/colors.ts +54 -0
- package/detox/config-loader.ts +135 -0
- package/detox/element-types.ts +39 -0
- package/detox/expectations.ts +537 -0
- package/detox/gestures.ts +480 -0
- package/detox/index.ts +2118 -0
- package/detox/jest-preset.cjs +51 -0
- package/detox/jest-setup-after-env.cjs +18 -0
- package/detox/matchers.ts +30 -0
- package/detox/motion-change.cjs +109 -0
- package/detox/motion-change.d.cts +35 -0
- package/detox/navigation.ts +43 -0
- package/detox/screenshots/animated-color-test-rest-norngh.png +0 -0
- package/detox/screenshots/color-test-after-drag-norngh.png +0 -0
- package/detox/screenshots/color-test-rest-norngh.png +0 -0
- package/detox/screenshots/theme-blue-toggle.png +0 -0
- package/detox/screenshots/theme-blue.png +0 -0
- package/detox/screenshots/theme-red-toggle.png +0 -0
- package/detox/screenshots/theme-red.png +0 -0
- package/dist-cli/bin.js +15 -0
- package/dist-cli/chunks/agent-XZ2KTPCU.js +48 -0
- package/dist-cli/chunks/agent-wrapper-JJYYW2WH.js +17 -0
- package/dist-cli/chunks/app-fonts-IXRNQG6B.js +4 -0
- package/dist-cli/chunks/assert-54T5SK5F.js +49 -0
- package/dist-cli/chunks/auth-FI5UDI45.js +12 -0
- package/dist-cli/chunks/beta-JV6UKADW.js +4 -0
- package/dist-cli/chunks/camera-VL73YIKP.js +22 -0
- package/dist-cli/chunks/chunk-2D2UPBBR.js +4 -0
- package/dist-cli/chunks/chunk-2YR5BGA5.js +5 -0
- package/dist-cli/chunks/chunk-3NV2NCNX.js +4 -0
- package/dist-cli/chunks/chunk-46EUUFJ5.js +4 -0
- package/dist-cli/chunks/chunk-4NPPOV2N.js +5 -0
- package/dist-cli/chunks/chunk-5DHC6KHQ.js +7 -0
- package/dist-cli/chunks/chunk-5TEF3ET3.js +74 -0
- package/dist-cli/chunks/chunk-5TPRP5QT.js +4 -0
- package/dist-cli/chunks/chunk-5YJCOWCH.js +4 -0
- package/dist-cli/chunks/chunk-7GN3LVWB.js +4 -0
- package/dist-cli/chunks/chunk-BBULZ7CG.js +568 -0
- package/dist-cli/chunks/chunk-BTWORNNG.js +71 -0
- package/dist-cli/chunks/chunk-D4FFVGI5.js +5 -0
- package/dist-cli/chunks/chunk-DCEMHR2Y.js +4 -0
- package/dist-cli/chunks/chunk-DZS6WPUI.js +4 -0
- package/dist-cli/chunks/chunk-F5ZRSS3C.js +3 -0
- package/dist-cli/chunks/chunk-FSUYIVJ6.js +9 -0
- package/dist-cli/chunks/chunk-G2WW6L2C.js +23 -0
- package/dist-cli/chunks/chunk-GASE6UBA.js +4 -0
- package/dist-cli/chunks/chunk-GGRX24GF.js +142 -0
- package/dist-cli/chunks/chunk-HI5TFJWN.js +5 -0
- package/dist-cli/chunks/chunk-IJ5CAZZC.js +5 -0
- package/dist-cli/chunks/chunk-IJO63TDP.js +4 -0
- package/dist-cli/chunks/chunk-KTHV3RUS.js +26 -0
- package/dist-cli/chunks/chunk-LF2ZVT7O.js +6 -0
- package/dist-cli/chunks/chunk-MJRLLB4R.js +38 -0
- package/dist-cli/chunks/chunk-NFK7T35W.js +4 -0
- package/dist-cli/chunks/chunk-NMF2ZMZQ.js +15 -0
- package/dist-cli/chunks/chunk-OHAZNXLK.js +4 -0
- package/dist-cli/chunks/chunk-OVFJFXUD.js +101 -0
- package/dist-cli/chunks/chunk-OZSSI4WN.js +28 -0
- package/dist-cli/chunks/chunk-QKDWYITG.js +25 -0
- package/dist-cli/chunks/chunk-RSZWCKNT.js +30 -0
- package/dist-cli/chunks/chunk-RTN5C5RL.js +4 -0
- package/dist-cli/chunks/chunk-TIVZIMMW.js +4 -0
- package/dist-cli/chunks/chunk-TZFFR3SD.js +7 -0
- package/dist-cli/chunks/chunk-UC6U3MML.js +5 -0
- package/dist-cli/chunks/chunk-VFCMSYZK.js +6 -0
- package/dist-cli/chunks/chunk-VNQEB4L7.js +5 -0
- package/dist-cli/chunks/chunk-VZXWHRUZ.js +983 -0
- package/dist-cli/chunks/chunk-W6K4EFPH.js +107 -0
- package/dist-cli/chunks/chunk-WEXDAC74.js +4 -0
- package/dist-cli/chunks/chunk-WF3T4SVI.js +14 -0
- package/dist-cli/chunks/chunk-WINYQ44O.js +4 -0
- package/dist-cli/chunks/chunk-WMIIKMGK.js +10 -0
- package/dist-cli/chunks/chunk-WUSWBCWA.js +507 -0
- package/dist-cli/chunks/chunk-WWZIXIRD.js +4 -0
- package/dist-cli/chunks/chunk-XEVZYVIW.js +766 -0
- package/dist-cli/chunks/chunk-YDGQTMQL.js +4 -0
- package/dist-cli/chunks/chunk-YIFT42WN.js +4 -0
- package/dist-cli/chunks/chunk-ZMJD5GEC.js +125 -0
- package/dist-cli/chunks/cleanup-P27PA6JI.js +25 -0
- package/dist-cli/chunks/cli-version-WWLPBDQ7.js +4 -0
- package/dist-cli/chunks/compat-ZD65FED3.js +20 -0
- package/dist-cli/chunks/config-XMJRNM2A.js +13 -0
- package/dist-cli/chunks/control-KMIQT3QP.js +4 -0
- package/dist-cli/chunks/daemon-G2ME7NLB.js +4 -0
- package/dist-cli/chunks/debug-PT4HOP7N.js +200 -0
- package/dist-cli/chunks/desktop-S3FG72AK.js +31 -0
- package/dist-cli/chunks/detox-B3D4IFCN.js +52 -0
- package/dist-cli/chunks/device-XBNDSB2R.js +18 -0
- package/dist-cli/chunks/diagnose-HMQXJE5N.js +43 -0
- package/dist-cli/chunks/disk-cleanup-BLCZ5BSZ.js +4 -0
- package/dist-cli/chunks/drivers-LDECZGP2.js +4 -0
- package/dist-cli/chunks/film-BJGTBYZB.js +74 -0
- package/dist-cli/chunks/flow-UEQNVTU7.js +4 -0
- package/dist-cli/chunks/help-T5FYSVGB.js +4 -0
- package/dist-cli/chunks/hidden-runtime-alias-ANOYADHM.js +4 -0
- package/dist-cli/chunks/home-paths-GT3LFNOR.js +4 -0
- package/dist-cli/chunks/inspect-ZA6XF5LD.js +4 -0
- package/dist-cli/chunks/install-desktop-TIMUDHPL.js +4 -0
- package/dist-cli/chunks/login-FJ737MWG.js +28 -0
- package/dist-cli/chunks/logout-ZCNMMHMY.js +4 -0
- package/dist-cli/chunks/maestro-SZTNKLDF.js +104 -0
- package/dist-cli/chunks/maestro-generate-DCFAIZ4H.js +36 -0
- package/dist-cli/chunks/mode-GRMQCRXR.js +26 -0
- package/dist-cli/chunks/optional-demo-registry-W36EWFFB.js +4 -0
- package/dist-cli/chunks/perf-QYBAAUZG.js +48 -0
- package/dist-cli/chunks/permissions-3QCQ6VF4.js +13 -0
- package/dist-cli/chunks/record-QPPC2S4E.js +72 -0
- package/dist-cli/chunks/report-issue-7NMFP4HK.js +29 -0
- package/dist-cli/chunks/runtime-XOAXMSTU.js +4 -0
- package/dist-cli/chunks/screenshot-command-67AECJFB.js +159 -0
- package/dist-cli/chunks/screenshot-layers-ASWBYPJL.js +20 -0
- package/dist-cli/chunks/screenshots-capture-PXA3HFQK.js +42 -0
- package/dist-cli/chunks/serve-BI2NBAXG.js +44 -0
- package/dist-cli/chunks/setup-7DWPMRSB.js +32 -0
- package/dist-cli/chunks/skills-S3Y22TUA.js +50 -0
- package/dist-cli/chunks/storage-XUIMJWAJ.js +37 -0
- package/dist-cli/chunks/store-JTHEJLAZ.js +4 -0
- package/dist-cli/chunks/telemetry-ZYJGD2DB.js +4 -0
- package/dist-cli/chunks/timeline-TMPLQPSP.js +24 -0
- package/dist-cli/chunks/upgrade-7HDSIM7K.js +7 -0
- package/dist-cli/chunks/upload-GMSZPWM6.js +4 -0
- package/dist-cli/chunks/version-HOCHZ37L.js +6 -0
- package/dist-cli/chunks/web-DG3WBYD3.js +4 -0
- package/dist-cli/chunks/what-happened-XFVUTZR7.js +25 -0
- package/dist-lib/agent-daemon-client.cjs +468 -0
- package/dist-lib/agent-events.cjs +48 -0
- package/dist-lib/agent-identity.cjs +53 -0
- package/dist-lib/agent-sessions.cjs +725 -0
- package/dist-lib/attached-projects.cjs +526 -0
- package/dist-lib/auth/shared-session.cjs +195 -0
- package/dist-lib/backend-origin.cjs +70 -0
- package/dist-lib/beta.cjs +41 -0
- package/dist-lib/beta.mjs +13 -0
- package/dist-lib/bridge-constants.cjs +161 -0
- package/dist-lib/cli-constants.cjs +32 -0
- package/dist-lib/config.cjs +96 -0
- package/dist-lib/detox/index.cjs +2863 -0
- package/dist-lib/detox/jest-preset.cjs +51 -0
- package/dist-lib/dev-bundle-resolution.cjs +223 -0
- package/dist-lib/home-paths.cjs +421 -0
- package/dist-lib/host/bridge-host.cjs +5600 -0
- package/dist-lib/host/fetch-proxy-handler.cjs +373 -0
- package/dist-lib/host/fetch-proxy-overrides.cjs +63 -0
- package/dist-lib/host/fetch-proxy-overrides.mjs +37 -0
- package/dist-lib/host/websocket-proxy.cjs +214 -0
- package/dist-lib/index.cjs +513 -0
- package/dist-lib/jump-to-source-babel.cjs +93 -0
- package/dist-lib/menu.cjs +620 -0
- package/dist-lib/menu.mjs +581 -0
- package/dist-lib/metro.cjs +297 -0
- package/dist-lib/profiles.cjs +246 -0
- package/dist-lib/public-brand.cjs +39 -0
- package/dist-lib/render-mode.cjs +54 -0
- package/dist-lib/scripts/dev-server-scanner.cjs +1034 -0
- package/dist-lib/sdk.cjs +2333 -0
- package/dist-lib/sdk.mjs +2240 -0
- package/dist-lib/skills.cjs +23922 -0
- package/dist-lib/vite.cjs +358 -0
- package/package.json +189 -18
- package/scripts/dev-server-scanner.ts +941 -0
- package/scripts/optional-demo-registry.ts +113 -0
- package/scripts/postinstall.cjs +3 -0
- package/scripts/preuninstall.cjs +118 -0
- package/skills/contrast/SKILL.md +171 -0
- package/skills/rnx-debug/SKILL.md +425 -0
- package/skills/rnx-perf/SKILL.md +295 -0
- package/skills/rnx-setup/SKILL.md +160 -0
- package/skills/rnx-test/SKILL.md +292 -0
- package/skills/rnx-visual/SKILL.md +122 -0
- package/src/agent-daemon-client.ts +410 -0
- package/src/agent-events.ts +71 -0
- package/src/agent-identity.ts +23 -0
- package/src/agent-prompt.ts +71 -0
- package/src/agent-sessions.ts +573 -0
- package/src/attached-projects.ts +539 -0
- package/src/auth/shared-session.ts +204 -0
- package/src/backend-origin.ts +49 -0
- package/src/beta.ts +19 -0
- package/src/bridge-constants.ts +171 -0
- package/src/browser-cache.ts +16 -0
- package/src/cli-constants.ts +1 -0
- package/src/cli-version.ts +32 -0
- package/src/codex-client.ts +215 -0
- package/src/config.ts +134 -0
- package/src/dev-bundle-resolution.ts +236 -0
- package/src/disk-cleanup.ts +466 -0
- package/src/home-paths.ts +552 -0
- package/src/host/agent-host.ts +666 -0
- package/src/host/bridge-host.ts +2626 -0
- package/src/host/fetch-proxy-handler.ts +397 -0
- package/src/host/fetch-proxy-overrides.ts +51 -0
- package/src/host/open-url.ts +89 -0
- package/src/host/websocket-proxy.ts +210 -0
- package/src/index.ts +12 -705
- package/src/jump-to-source-babel.ts +135 -0
- package/src/menu.ts +734 -0
- package/src/metro-plugin.ts +139 -0
- package/src/native-dev-bundle-url.ts +96 -0
- package/src/native-seam-manifest.ts +341 -0
- package/src/plist.ts +27 -0
- package/src/poll-until.ts +92 -0
- package/src/profiles.ts +179 -0
- package/src/public-brand.ts +9 -0
- package/src/render-mode.ts +27 -0
- package/src/runtime-assets.ts +228 -0
- package/src/runtime-delivery.ts +54 -0
- package/src/screenshots/compose.ts +422 -0
- package/src/screenshots/frame-compose.ts +454 -0
- package/src/screenshots/orchestrate.ts +256 -0
- package/src/screenshots/org-deck.ts +91 -0
- package/src/screenshots/registry.ts +58 -0
- package/src/screenshots/schema.ts +364 -0
- package/src/sdk.ts +7 -0
- package/src/settings-bundle.ts +418 -0
- package/src/skills/builtin/a11y-review.ts +132 -0
- package/src/skills/builtin/compat-check.ts +71 -0
- package/src/skills/builtin/maestro-test.ts +121 -0
- package/src/skills/builtin/perf-profile.ts +90 -0
- package/src/skills/builtin/screenshot-all.ts +52 -0
- package/src/skills/builtin/visual-diff.ts +100 -0
- package/src/skills/registry.ts +107 -0
- package/src/skills/types.ts +41 -0
- package/src/team-machine-prompt.ts +73 -0
- package/src/vite-plugin-one.ts +190 -0
- package/src/vite-plugin.ts +1750 -0
- 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
|
+
}
|