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,92 @@
1
+ export interface PollUntilOptions {
2
+ timeoutMs: number
3
+ intervalMs?: number
4
+ label: string
5
+ signal?: AbortSignal
6
+ }
7
+
8
+ type PollReadOutcome<T> = { kind: 'value'; value: T } | { kind: 'error'; error: unknown }
9
+
10
+ export async function pollUntil<T>(
11
+ read: () => Promise<T>,
12
+ ready: (value: T) => boolean,
13
+ options: PollUntilOptions,
14
+ ): Promise<T> {
15
+ if (!Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0) {
16
+ throw new Error('poll timeout must be a positive finite number')
17
+ }
18
+ const intervalMs = options.intervalMs ?? 100
19
+ if (!Number.isFinite(intervalMs) || intervalMs < 0) {
20
+ throw new Error('poll interval must be a non-negative finite number')
21
+ }
22
+
23
+ const deadline = Date.now() + options.timeoutMs
24
+ let lastError: unknown
25
+ const timeoutError = () =>
26
+ new Error(`${options.label} timed out after ${options.timeoutMs}ms`, {
27
+ cause: lastError,
28
+ })
29
+
30
+ options.signal?.throwIfAborted()
31
+ let rejectBoundary: (reason: unknown) => void = () => {}
32
+ const boundary = new Promise<never>((_, reject) => {
33
+ rejectBoundary = reject
34
+ })
35
+ const onAbort = () =>
36
+ rejectBoundary(
37
+ options.signal?.reason ??
38
+ new DOMException('The operation was aborted', 'AbortError'),
39
+ )
40
+ options.signal?.addEventListener('abort', onAbort, { once: true })
41
+ const deadlineTimer = setTimeout(
42
+ () => rejectBoundary(timeoutError()),
43
+ options.timeoutMs,
44
+ )
45
+
46
+ try {
47
+ while (true) {
48
+ if (Date.now() >= deadline) throw timeoutError()
49
+
50
+ const outcome = await Promise.race([
51
+ Promise.resolve()
52
+ .then(read)
53
+ .then(
54
+ (value): PollReadOutcome<T> => ({ kind: 'value', value }),
55
+ (error: unknown): PollReadOutcome<T> => ({ kind: 'error', error }),
56
+ ),
57
+ boundary,
58
+ ])
59
+
60
+ options.signal?.throwIfAborted()
61
+ if (outcome.kind === 'error') {
62
+ lastError = outcome.error
63
+ } else {
64
+ if (Date.now() >= deadline) throw timeoutError()
65
+ try {
66
+ if (ready(outcome.value)) return outcome.value
67
+ } catch (error) {
68
+ lastError = error
69
+ }
70
+ }
71
+
72
+ const remaining = deadline - Date.now()
73
+ if (remaining <= 0) throw timeoutError()
74
+ if (intervalMs > 0) {
75
+ let intervalTimer: ReturnType<typeof setTimeout> | undefined
76
+ try {
77
+ await Promise.race([
78
+ new Promise<void>((resolve) => {
79
+ intervalTimer = setTimeout(resolve, Math.min(intervalMs, remaining))
80
+ }),
81
+ boundary,
82
+ ])
83
+ } finally {
84
+ clearTimeout(intervalTimer)
85
+ }
86
+ }
87
+ }
88
+ } finally {
89
+ clearTimeout(deadlineTimer)
90
+ options.signal?.removeEventListener('abort', onAbort)
91
+ }
92
+ }
@@ -0,0 +1,179 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import { electronUserDataDir, ensureSootsimHome, profilesDir } from './home-paths'
4
+
5
+ export const DEFAULT_PROFILE_ID = 'default'
6
+ const PROFILE_INDEX_FILE = 'profiles.json'
7
+ const PROFILE_INDEX_VERSION = 1 as const
8
+
9
+ export interface StorageProfile {
10
+ id: string
11
+ createdAt: string
12
+ updatedAt: string
13
+ }
14
+
15
+ interface ProfileIndex {
16
+ version: 1
17
+ profiles: StorageProfile[]
18
+ }
19
+
20
+ export function normalizeProfileId(input: string): string {
21
+ const id = input.trim()
22
+ if (!id) throw new Error('profile id is required')
23
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(id)) {
24
+ throw new Error(
25
+ 'profile ids must start with a letter or number and contain only letters, numbers, dot, dash, or underscore',
26
+ )
27
+ }
28
+ if (id === '.' || id === '..') throw new Error(`invalid profile id: ${id}`)
29
+ return id
30
+ }
31
+
32
+ export function profileIndexPath(): string {
33
+ return path.join(profilesDir(), PROFILE_INDEX_FILE)
34
+ }
35
+
36
+ export function electronProfilePartitionName(profileId: string): string {
37
+ return `sootsim-profile-${normalizeProfileId(profileId)}`
38
+ }
39
+
40
+ export function electronProfilePartition(profileId: string): string {
41
+ return `persist:${electronProfilePartitionName(profileId)}`
42
+ }
43
+
44
+ export function electronProfilePartitionDir(profileId: string): string {
45
+ return path.join(
46
+ electronUserDataDir(),
47
+ 'Partitions',
48
+ electronProfilePartitionName(profileId),
49
+ )
50
+ }
51
+
52
+ export function playwrightProfileUserDataDir(profileId: string): string {
53
+ return path.join(profilesDir(), 'playwright', normalizeProfileId(profileId))
54
+ }
55
+
56
+ function readIndexRaw(): ProfileIndex {
57
+ try {
58
+ const parsed = JSON.parse(
59
+ fs.readFileSync(profileIndexPath(), 'utf8'),
60
+ ) as Partial<ProfileIndex> | null
61
+ if (!parsed || parsed.version !== PROFILE_INDEX_VERSION) {
62
+ return { version: PROFILE_INDEX_VERSION, profiles: [] }
63
+ }
64
+ const profiles = Array.isArray(parsed.profiles)
65
+ ? parsed.profiles
66
+ .filter(
67
+ (profile): profile is StorageProfile =>
68
+ !!profile &&
69
+ typeof profile.id === 'string' &&
70
+ typeof profile.createdAt === 'string' &&
71
+ typeof profile.updatedAt === 'string',
72
+ )
73
+ .map((profile) => ({
74
+ id: normalizeProfileId(profile.id),
75
+ createdAt: profile.createdAt,
76
+ updatedAt: profile.updatedAt,
77
+ }))
78
+ : []
79
+ return { version: PROFILE_INDEX_VERSION, profiles }
80
+ } catch {
81
+ return { version: PROFILE_INDEX_VERSION, profiles: [] }
82
+ }
83
+ }
84
+
85
+ function sortProfiles(profiles: StorageProfile[]): StorageProfile[] {
86
+ return [...profiles].sort((a, b) => {
87
+ if (a.id === DEFAULT_PROFILE_ID) return -1
88
+ if (b.id === DEFAULT_PROFILE_ID) return 1
89
+ return a.id.localeCompare(b.id)
90
+ })
91
+ }
92
+
93
+ function withDefault(index: ProfileIndex): ProfileIndex {
94
+ if (index.profiles.some((profile) => profile.id === DEFAULT_PROFILE_ID)) {
95
+ return { ...index, profiles: sortProfiles(index.profiles) }
96
+ }
97
+ const now = new Date().toISOString()
98
+ return {
99
+ version: PROFILE_INDEX_VERSION,
100
+ profiles: sortProfiles([
101
+ { id: DEFAULT_PROFILE_ID, createdAt: now, updatedAt: now },
102
+ ...index.profiles,
103
+ ]),
104
+ }
105
+ }
106
+
107
+ function writeIndex(index: ProfileIndex): ProfileIndex {
108
+ ensureSootsimHome()
109
+ const next = withDefault(index)
110
+ const tmp = `${profileIndexPath()}.tmp`
111
+ fs.writeFileSync(tmp, `${JSON.stringify(next, null, 2)}\n`, 'utf8')
112
+ fs.renameSync(tmp, profileIndexPath())
113
+ return next
114
+ }
115
+
116
+ export function listProfiles(): StorageProfile[] {
117
+ return writeIndex(readIndexRaw()).profiles
118
+ }
119
+
120
+ export function getProfile(id: string): StorageProfile | null {
121
+ const normalized = normalizeProfileId(id)
122
+ return listProfiles().find((profile) => profile.id === normalized) ?? null
123
+ }
124
+
125
+ export function ensureProfile(id = DEFAULT_PROFILE_ID): StorageProfile {
126
+ const normalized = normalizeProfileId(id)
127
+ const existing = getProfile(normalized)
128
+ if (existing) return existing
129
+ return createProfile(normalized)
130
+ }
131
+
132
+ export function createProfile(id: string): StorageProfile {
133
+ const normalized = normalizeProfileId(id)
134
+ const index = withDefault(readIndexRaw())
135
+ if (index.profiles.some((profile) => profile.id === normalized)) {
136
+ throw new Error(`profile already exists: ${normalized}`)
137
+ }
138
+ const now = new Date().toISOString()
139
+ const profile = { id: normalized, createdAt: now, updatedAt: now }
140
+ writeIndex({ version: PROFILE_INDEX_VERSION, profiles: [...index.profiles, profile] })
141
+ return profile
142
+ }
143
+
144
+ export function nextGeneratedProfileId(): string {
145
+ const existing = new Set(listProfiles().map((profile) => profile.id))
146
+ for (let i = 1; i < 10_000; i++) {
147
+ const id = `profile-${i}`
148
+ if (!existing.has(id)) return id
149
+ }
150
+ throw new Error('could not allocate a new profile id')
151
+ }
152
+
153
+ export function deleteProfile(id: string): StorageProfile {
154
+ const normalized = normalizeProfileId(id)
155
+ if (normalized === DEFAULT_PROFILE_ID) {
156
+ throw new Error('the default profile cannot be deleted; clear it instead')
157
+ }
158
+ const index = withDefault(readIndexRaw())
159
+ const profile = index.profiles.find((entry) => entry.id === normalized)
160
+ if (!profile) throw new Error(`profile not found: ${normalized}`)
161
+ writeIndex({
162
+ version: PROFILE_INDEX_VERSION,
163
+ profiles: index.profiles.filter((entry) => entry.id !== normalized),
164
+ })
165
+ clearProfileStorage(normalized)
166
+ return profile
167
+ }
168
+
169
+ export function clearProfileStorage(id: string): void {
170
+ const normalized = normalizeProfileId(id)
171
+ for (const dir of [
172
+ electronProfilePartitionDir(normalized),
173
+ playwrightProfileUserDataDir(normalized),
174
+ ]) {
175
+ try {
176
+ fs.rmSync(dir, { recursive: true, force: true })
177
+ } catch {}
178
+ }
179
+ }
@@ -0,0 +1,9 @@
1
+ const name = 'rnx'
2
+
3
+ export const rnxPublicBrand = Object.freeze({
4
+ name,
5
+ packageName: 'rnxsim',
6
+ commandName: 'rnxsim',
7
+ domain: 'rnxsim.com',
8
+ origin: 'https://rnxsim.com',
9
+ })
@@ -0,0 +1,27 @@
1
+ export type RenderMode = 'worker'
2
+
3
+ // worker rendering is now the only supported mode.
4
+ export function getRenderMode(_searchParams: URLSearchParams): RenderMode {
5
+ return 'worker'
6
+ }
7
+
8
+ export function isRenderWorkerMode(_searchParams: URLSearchParams): boolean {
9
+ return true
10
+ }
11
+
12
+ export function isRenderMainMode(_searchParams: URLSearchParams): boolean {
13
+ return false
14
+ }
15
+
16
+ export function hasRenderWorkerBootstrap(
17
+ workerModuleBaseUrl: string | null | undefined,
18
+ ): boolean {
19
+ return typeof workerModuleBaseUrl === 'string' && workerModuleBaseUrl.length > 0
20
+ }
21
+
22
+ export function shouldEnableRenderWorker(
23
+ _searchParams: URLSearchParams,
24
+ workerModuleBaseUrl: string | null | undefined,
25
+ ): boolean {
26
+ return hasRenderWorkerBootstrap(workerModuleBaseUrl)
27
+ }
@@ -0,0 +1,228 @@
1
+ // helpers for serving the installed sootsim engine runtime from framework
2
+ // dev servers. the runtime is managed by `sootsim runtime install` and lives
3
+ // under ~/.sootsim/runtimes/<version>.
4
+
5
+ import fs from 'fs'
6
+ import path from 'path'
7
+ import { fileURLToPath } from 'url'
8
+ import { isValidRuntimeVersion } from '@contrast/runtime-delivery'
9
+ import { readActiveRuntime, runtimeDir } from './home-paths.ts'
10
+
11
+ export const SOOTSIM_RUNTIME_MISSING_MESSAGE =
12
+ '[rnx] no engine runtime installed — run `rnxsim setup`, or `rnxsim runtime install`'
13
+
14
+ // --- dev-checkout mode boundary --------------------------------------------
15
+ //
16
+ // there is exactly one rule for which engine gets served: INSIDE the Contrast
17
+ // monorepo dev checkout the fresh dev-stack build (public/sootsim, kept current
18
+ // by `bun dev`'s watch:sootsim) IS the engine; OUTSIDE it (a published CLI in a
19
+ // user's project, or an isolated home) the installed runtime IS. this is a mode
20
+ // boundary, not a preference or a runtime fallback chain — the check is
21
+ // structural (the monorepo layout is present next to this module) and
22
+ // deterministic (the dev build's files exist). it exists because the daemon and
23
+ // the /__soot plugin otherwise serve ~/.sootsim/runtimes/<active>, a PUBLISHED
24
+ // runtime that `bun dev` never refreshes, so local engine edits are invisible
25
+ // until a manual publish — the silent trap that cost a full night of
26
+ // already-fixed bugs re-reproducing.
27
+ //
28
+ // the isolated CI headless path (scripts/local-publish-runtime.sh) sets
29
+ // SOOTSIM_HOME to a dedicated dir precisely to validate the published-runtime
30
+ // code path; keying off that EXISTING isolation env keeps it (and packaged
31
+ // electron with a custom home) on the installed runtime.
32
+ let _monorepoRootCache: string | null | undefined
33
+
34
+ function findMonorepoRoot(): string | null {
35
+ if (_monorepoRootCache !== undefined) return _monorepoRootCache
36
+ let dir = path.dirname(fileURLToPath(import.meta.url))
37
+ // walk up until we find the Contrast monorepo: it has packages/sootsim-engine and
38
+ // a public/sootsim dev-build output slot. bounded by the filesystem root.
39
+ for (let i = 0; i < 12 && dir !== path.dirname(dir); i++) {
40
+ if (
41
+ fs.existsSync(path.join(dir, 'packages/sootsim-engine/package.json')) &&
42
+ fs.existsSync(path.join(dir, 'public/sootsim'))
43
+ ) {
44
+ _monorepoRootCache = dir
45
+ return dir
46
+ }
47
+ dir = path.dirname(dir)
48
+ }
49
+ _monorepoRootCache = null
50
+ return null
51
+ }
52
+
53
+ /** the fresh dev-stack engine build to serve when running inside the Contrast
54
+ * monorepo dev checkout, or null when the installed runtime should serve
55
+ * (published CLI, packaged electron, or an isolated SOOTSIM_HOME). */
56
+ export function resolveDevCheckoutRuntimeRoot(): string | null {
57
+ // an explicit home that has a STAGED/active runtime is the isolation the
58
+ // CI/local-publish path sets up to validate the installed-runtime code path —
59
+ // honor that staged runtime. an explicit home with NO active runtime is just
60
+ // an isolated dev daemon (own lockfile, doesn't clobber ~/.sootsim), which
61
+ // still wants the dev build; and the default home (SOOTSIM_HOME unset) always
62
+ // does. keying on "explicit home + a runtime deliberately staged there" keeps
63
+ // it structural, not a bespoke toggle.
64
+ if (process.env.SOOTSIM_HOME && readActiveRuntime()) return null
65
+ const root = findMonorepoRoot()
66
+ if (!root) return null
67
+ const devBuild = path.join(root, 'public/sootsim')
68
+ // require the runtime shape the installed runtime also has, so serving it is
69
+ // a drop-in (index.html shell + engine/ + engine-tenant/worker.js tenant).
70
+ if (
71
+ fs.existsSync(path.join(devBuild, 'index.html')) &&
72
+ fs.existsSync(path.join(devBuild, 'engine/index.html')) &&
73
+ fs.existsSync(path.join(devBuild, 'engine-tenant/worker.js'))
74
+ ) {
75
+ return devBuild
76
+ }
77
+ return null
78
+ }
79
+
80
+ const MIME_TYPES: Record<string, string> = {
81
+ '.js': 'application/javascript',
82
+ '.mjs': 'application/javascript',
83
+ '.css': 'text/css',
84
+ '.html': 'text/html',
85
+ '.wasm': 'application/wasm',
86
+ '.json': 'application/json',
87
+ '.jpg': 'image/jpeg',
88
+ '.jpeg': 'image/jpeg',
89
+ '.png': 'image/png',
90
+ '.svg': 'image/svg+xml',
91
+ '.webp': 'image/webp',
92
+ '.glb': 'model/gltf-binary',
93
+ '.ttf': 'font/ttf',
94
+ '.otf': 'font/otf',
95
+ '.woff': 'font/woff',
96
+ '.woff2': 'font/woff2',
97
+ '.mp3': 'audio/mpeg',
98
+ '.wav': 'audio/wav',
99
+ }
100
+
101
+ const ROOT_RUNTIME_PATHS = [
102
+ '/assets/',
103
+ '/engine/',
104
+ '/engine-tenant/',
105
+ '/photos/',
106
+ '/three-mode/',
107
+ '/fonts/',
108
+ '/icons/',
109
+ '/sounds/',
110
+ '/spike/',
111
+ '/test-wallpaper.jpg',
112
+ '/preview-sw.js',
113
+ '/sootsim.svg',
114
+ ]
115
+
116
+ export function resolveActiveRuntimeRoot(): string | null {
117
+ // mode boundary: inside the dev checkout the dev-stack build is the engine.
118
+ const devRoot = resolveDevCheckoutRuntimeRoot()
119
+ if (devRoot) return devRoot
120
+ const active = readActiveRuntime()
121
+ if (!active) return null
122
+ const dir = runtimeDir(active)
123
+ if (!fs.existsSync(path.join(dir, 'index.html'))) return null
124
+ return dir
125
+ }
126
+
127
+ const RUNTIME_VERSION_HOSTNAME_SUFFIX = '.runtime.localhost'
128
+
129
+ export function runtimeVersionHostname(version: string): string {
130
+ const trimmed = version.trim()
131
+ if (!isValidRuntimeVersion(trimmed)) {
132
+ throw new Error(`invalid rnx runtime version: ${version}`)
133
+ }
134
+ const encoded = Buffer.from(trimmed, 'utf8').toString('hex')
135
+ const labels = encoded.match(/.{1,60}/g)
136
+ if (!labels) throw new Error(`invalid rnx runtime version: ${version}`)
137
+ return `${labels.join('.')}${RUNTIME_VERSION_HOSTNAME_SUFFIX}`
138
+ }
139
+
140
+ export function isRuntimeVersionHostname(hostname: string): boolean {
141
+ return hostname.toLowerCase().endsWith(RUNTIME_VERSION_HOSTNAME_SUFFIX)
142
+ }
143
+
144
+ export function runtimeVersionFromHostname(hostname: string): string | null {
145
+ if (!isRuntimeVersionHostname(hostname)) return null
146
+ const labels = hostname.slice(0, -RUNTIME_VERSION_HOSTNAME_SUFFIX.length).split('.')
147
+ if (labels.length === 0 || labels.some((label) => !/^[0-9a-f]{1,60}$/i.test(label))) {
148
+ return null
149
+ }
150
+ const encoded = labels.join('')
151
+ if (encoded.length % 2 !== 0) return null
152
+ try {
153
+ const version = Buffer.from(encoded, 'hex').toString('utf8')
154
+ return isValidRuntimeVersion(version) ? version : null
155
+ } catch {
156
+ return null
157
+ }
158
+ }
159
+
160
+ export function resolveInstalledRuntimeRoot(version: string): string | null {
161
+ const dir = runtimeDir(version)
162
+ if (!fs.existsSync(path.join(dir, 'index.html'))) return null
163
+ return dir
164
+ }
165
+
166
+ /** select one immutable installed runtime for a version-owned origin. ordinary
167
+ * localhost requests follow the active runtime; a malformed or unavailable
168
+ * version origin fails closed instead of borrowing active assets. */
169
+ export function resolveRuntimeRootForHostname(hostname: string): string | null {
170
+ if (!isRuntimeVersionHostname(hostname)) return resolveActiveRuntimeRoot()
171
+ const version = runtimeVersionFromHostname(hostname)
172
+ return version ? resolveInstalledRuntimeRoot(version) : null
173
+ }
174
+
175
+ export function resolveRuntimeRootForRequestHost(
176
+ host: string | undefined,
177
+ ): string | null {
178
+ if (!host) return resolveActiveRuntimeRoot()
179
+ try {
180
+ return resolveRuntimeRootForHostname(new URL(`http://${host}`).hostname)
181
+ } catch {
182
+ return null
183
+ }
184
+ }
185
+
186
+ export function isRootRuntimeAssetPath(pathname: string): boolean {
187
+ return ROOT_RUNTIME_PATHS.some((p) => pathname === p || pathname.startsWith(p))
188
+ }
189
+
190
+ export function resolveRuntimeFilePath(
191
+ runtimeRoot: string,
192
+ pathname: string,
193
+ ): string | null {
194
+ if (!pathname.startsWith('/')) return null
195
+ if (pathname.includes('\0') || pathname.includes('\\')) return null
196
+ for (const segment of pathname.split('/')) {
197
+ if (segment === '..') return null
198
+ }
199
+
200
+ const fullPath = path.resolve(runtimeRoot, pathname.replace(/^\/+/, ''))
201
+ const rootWithSep = runtimeRoot.endsWith(path.sep)
202
+ ? runtimeRoot
203
+ : runtimeRoot + path.sep
204
+ if (!fullPath.startsWith(rootWithSep) && fullPath !== runtimeRoot) return null
205
+ if (!fs.existsSync(fullPath) || !fs.statSync(fullPath).isFile()) return null
206
+ return fullPath
207
+ }
208
+
209
+ export function serveRuntimeFile(req: any, res: any, fullPath: string) {
210
+ const ext = path.extname(fullPath)
211
+ const stat = fs.statSync(fullPath)
212
+ const lastModified = stat.mtime.toUTCString()
213
+ res.setHeader('content-type', MIME_TYPES[ext] || 'application/octet-stream')
214
+ // these urls are STABLE across dev rebuilds and runtime version switches
215
+ // (worker.js, canvaskit-graphite.wasm, the html shells are unhashed), so the
216
+ // browser must revalidate every load — the old year-long immutable here made every
217
+ // new tab run a stale cached engine-tenant worker.js against a fresh engine,
218
+ // which also broke `do reload` ("bridge never reconnected"). no-cache +
219
+ // last-modified keeps repeat loads cheap 304s on localhost.
220
+ res.setHeader('cache-control', 'no-cache')
221
+ res.setHeader('last-modified', lastModified)
222
+ if (req?.headers?.['if-modified-since'] === lastModified) {
223
+ res.statusCode = 304
224
+ res.end()
225
+ return
226
+ }
227
+ fs.createReadStream(fullPath).pipe(res)
228
+ }
@@ -0,0 +1,54 @@
1
+ // sootsim's binding of the shared runtime-delivery machinery.
2
+ //
3
+ // the machinery itself (hosted manifest, sha256-verified tarballs, channels,
4
+ // channel resolution, and auto-update) lives in @contrast/runtime-delivery and
5
+ // knows nothing about sootsim. this file supplies sootsim's identity: ~/.sootsim, the
6
+ // SOOTSIM_* env overrides, and the contrast.dev/runtimes hosting layout.
7
+ //
8
+ // @contrast/runtime-delivery is an internal workspace package that is never
9
+ // published, so both the CLI bundle and dist-lib inline it (see the `alias`
10
+ // entry in scripts/build-cli.ts). pack-smoke fails the build if it ever leaks
11
+ // out as a bare require.
12
+
13
+ import { createRuntimeDelivery, type RuntimeProduct } from '@contrast/runtime-delivery'
14
+ import {
15
+ cacheDir,
16
+ configFilePath,
17
+ ensureSootsimHome,
18
+ readActiveRuntime,
19
+ runtimeDir,
20
+ runtimesDir,
21
+ writeActiveRuntime,
22
+ } from './home-paths.ts'
23
+ import { rnxPublicBrand } from './public-brand.ts'
24
+
25
+ export const DEFAULT_RUNTIME_CDN_ORIGIN = 'https://contrast.dev'
26
+
27
+ /** the official curated changelog and the machine-readable release feed. the
28
+ * SootSim site is static, so its /api traffic is owned by contrast.dev. */
29
+ export const SOOTSIM_CHANGELOG_URL = `${rnxPublicBrand.origin}/changelog`
30
+ export const SOOTSIM_CHANGELOG_API_URL = 'https://contrast.dev/api/changelog/sootsim'
31
+
32
+ export const SOOTSIM_RUNTIME_PRODUCT: RuntimeProduct = {
33
+ id: 'sootsim',
34
+ defaultCdnOrigin: DEFAULT_RUNTIME_CDN_ORIGIN,
35
+ manifestPath: '/runtimes/manifest.json',
36
+ tarballPath: (version) => `/runtimes/sootsim-runtime-${version}.tar.gz`,
37
+ // the engine runtime is a static SPA: shell at the root, engine/ and
38
+ // engine-tenant/ beneath it. index.html is what the daemon and the
39
+ // vite/metro plugins serve, so its absence means the tarball is not one.
40
+ installedMarker: 'index.html',
41
+ env: {
42
+ cdnOrigin: 'SOOTSIM_CDN_ORIGIN',
43
+ channel: 'SOOTSIM_RUNTIME_CHANNEL',
44
+ },
45
+ ensureHome: ensureSootsimHome,
46
+ configFile: configFilePath,
47
+ runtimesDir,
48
+ runtimeDir,
49
+ cacheDir,
50
+ readActiveRuntime,
51
+ writeActiveRuntime,
52
+ }
53
+
54
+ export const sootsimRuntime = createRuntimeDelivery(SOOTSIM_RUNTIME_PRODUCT)