rnxsim 0.1.381 → 0.1.383

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 (138) hide show
  1. package/README.md +19 -6
  2. package/cli/app-fonts.ts +1 -1
  3. package/cli/bin.ts +118 -26
  4. package/cli/bridge-diagnostics.ts +1 -1
  5. package/cli/bridge-flow-runner.ts +9 -9
  6. package/cli/browser-evals.ts +3 -3
  7. package/cli/cli-update.ts +261 -0
  8. package/cli/cloud-client.ts +407 -0
  9. package/cli/cloud-dispatch.ts +165 -0
  10. package/cli/cloud-session.ts +156 -0
  11. package/cli/command-contract.ts +2 -2
  12. package/cli/command-registry.ts +9 -0
  13. package/cli/commands/agent-wrapper.ts +4 -4
  14. package/cli/commands/agent.ts +3 -3
  15. package/cli/commands/box/checkout-plane.ts +16 -8
  16. package/cli/commands/box/rnx-command.ts +2 -2
  17. package/cli/commands/box/search-command.ts +2 -2
  18. package/cli/commands/box.ts +123 -70
  19. package/cli/commands/config.ts +3 -2
  20. package/cli/commands/control.ts +6 -6
  21. package/cli/commands/cpu-profile.ts +2 -2
  22. package/cli/commands/daemon.ts +7 -7
  23. package/cli/commands/debug.ts +2 -2
  24. package/cli/commands/detox.ts +7 -7
  25. package/cli/commands/diagnose.ts +1 -1
  26. package/cli/commands/electron.ts +1 -1
  27. package/cli/commands/film.ts +1 -1
  28. package/cli/commands/flow.ts +4 -4
  29. package/cli/commands/inspect/core.ts +58 -7
  30. package/cli/commands/inspect/env.ts +5 -3
  31. package/cli/commands/inspect/shared.ts +2 -1
  32. package/cli/commands/inspect/wait-event.ts +1 -1
  33. package/cli/commands/inspect/wait-ready.ts +15 -5
  34. package/cli/commands/inspect.ts +31 -9
  35. package/cli/commands/install-desktop.ts +2 -2
  36. package/cli/commands/maestro.ts +3 -3
  37. package/cli/commands/permissions.ts +1 -1
  38. package/cli/commands/platform.ts +91 -0
  39. package/cli/commands/record.ts +3 -3
  40. package/cli/commands/report-issue.ts +1 -1
  41. package/cli/commands/runtime.ts +1 -1
  42. package/cli/commands/screenshot-mode.ts +1 -1
  43. package/cli/commands/screenshot.ts +25 -0
  44. package/cli/commands/serve.ts +1 -1
  45. package/cli/commands/skills.ts +1 -1
  46. package/cli/commands/timeline.ts +1 -1
  47. package/cli/commands/upgrade.ts +8 -34
  48. package/cli/commands/upload.ts +3 -3
  49. package/cli/commands/version.ts +14 -3
  50. package/cli/commands/what-happened.ts +2 -2
  51. package/cli/current-sim.ts +43 -30
  52. package/cli/desktop-companion.ts +1 -1
  53. package/cli/drivers/electron.ts +1 -1
  54. package/cli/drivers/playwright.ts +3 -3
  55. package/cli/drivers/types.ts +4 -4
  56. package/cli/flow-file.ts +3 -3
  57. package/cli/help-core.ts +18 -11
  58. package/cli/hidden-runtime-alias.ts +1 -1
  59. package/cli/hints.ts +2 -2
  60. package/cli/maestro-js.ts +2 -2
  61. package/cli/outbound-endpoints.ts +40 -6
  62. package/cli/parse-args.ts +1 -1
  63. package/cli/registry.ts +1 -1
  64. package/cli/resolve-assets.ts +2 -2
  65. package/cli/secure-file.ts +31 -0
  66. package/cli/shell-init.ts +98 -0
  67. package/cli/ws-bridge.ts +18 -8
  68. package/detox/expectations.ts +1 -1
  69. package/detox/gestures.ts +3 -3
  70. package/detox/index.ts +3 -3
  71. package/dist-lib/agent-daemon-client.cjs +1 -1
  72. package/dist-lib/agent-events.cjs +1 -1
  73. package/dist-lib/agent-identity.cjs +1 -1
  74. package/dist-lib/agent-sessions.cjs +1 -1
  75. package/dist-lib/attached-projects.cjs +1 -1
  76. package/dist-lib/auth/shared-session.cjs +1 -1
  77. package/dist-lib/backend-origin.cjs +1 -1
  78. package/dist-lib/beta.cjs +1 -1
  79. package/dist-lib/beta.mjs +1 -1
  80. package/dist-lib/bridge-constants.cjs +1 -1
  81. package/dist-lib/bridge-contract-input.cjs +20 -8
  82. package/dist-lib/bridge-contract-input.mjs +20 -8
  83. package/dist-lib/bridge-contract.cjs +1 -1
  84. package/dist-lib/bridge-contract.mjs +1 -1
  85. package/dist-lib/capture-contract.cjs +217 -0
  86. package/dist-lib/capture-contract.mjs +187 -0
  87. package/dist-lib/cli-constants.cjs +1 -1
  88. package/dist-lib/cloud-contract.cjs +41 -0
  89. package/dist-lib/cloud-contract.mjs +13 -0
  90. package/dist-lib/config.cjs +1 -1
  91. package/dist-lib/detox/index.cjs +4 -4
  92. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  93. package/dist-lib/home-paths.cjs +9 -1
  94. package/dist-lib/host/bridge-host.cjs +15 -10
  95. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  96. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  97. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  98. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  99. package/dist-lib/host/websocket-proxy.cjs +1 -1
  100. package/dist-lib/index.cjs +361 -75
  101. package/dist-lib/jump-to-source-babel.cjs +1 -1
  102. package/dist-lib/menu.cjs +1 -1
  103. package/dist-lib/menu.mjs +1 -1
  104. package/dist-lib/metro-production-bundle.cjs +1 -1
  105. package/dist-lib/metro-production-bundle.mjs +1 -1
  106. package/dist-lib/metro.cjs +1 -1
  107. package/dist-lib/profiles.cjs +1 -1
  108. package/dist-lib/public-brand.cjs +1 -1
  109. package/dist-lib/react-native-host-modules.cjs +1 -1
  110. package/dist-lib/react-native-host-modules.mjs +1 -1
  111. package/dist-lib/render-mode.cjs +1 -1
  112. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  113. package/dist-lib/sdk.cjs +44 -6
  114. package/dist-lib/sdk.mjs +44 -6
  115. package/dist-lib/skills.cjs +436 -155
  116. package/dist-lib/vite.cjs +1 -1
  117. package/package.json +20 -4
  118. package/scripts/dev-server-scanner.ts +3 -3
  119. package/skills/contrast/SKILL.md +1 -1
  120. package/skills/rnx-perf/SKILL.md +68 -52
  121. package/src/app-url.ts +2 -3
  122. package/src/bridge-contract-input.ts +25 -6
  123. package/src/bridge-contract.ts +35 -5
  124. package/src/capture-contract.ts +576 -0
  125. package/src/cloud-contract.ts +18 -0
  126. package/src/home-paths.ts +9 -3
  127. package/src/host/agent-host.ts +1 -1
  128. package/src/host/bridge-host.ts +13 -11
  129. package/src/host/plane-host.ts +2 -2
  130. package/src/index.ts +25 -0
  131. package/src/native-dev-bundle-url.ts +2 -2
  132. package/src/screenshots/orchestrate.ts +1 -1
  133. package/src/screenshots/org-deck.ts +1 -1
  134. package/src/settings-bundle.ts +1 -1
  135. package/src/sim-client.ts +4 -0
  136. package/src/vite-plugin-one.ts +2 -2
  137. package/src/vite-plugin.ts +1 -1
  138. package/cli/commands/box/cloud-box.ts +0 -294
@@ -0,0 +1,576 @@
1
+ // public, runtime-neutral screen capture contract. semantic evidence and pixel
2
+ // evidence are separate because neither one proves the other.
3
+
4
+ export const RNX_SCREEN_CAPTURE_VERSION = 1 as const
5
+
6
+ export interface RnxCaptureRect {
7
+ x: number
8
+ y: number
9
+ width: number
10
+ height: number
11
+ }
12
+
13
+ export interface RnxCaptureAccessibilityState {
14
+ disabled: boolean
15
+ selected: boolean
16
+ checked: boolean | 'mixed'
17
+ busy: boolean
18
+ expanded: boolean
19
+ }
20
+
21
+ export interface RnxCaptureAccessibilityValue {
22
+ min?: number
23
+ max?: number
24
+ now?: number
25
+ text?: string
26
+ }
27
+
28
+ export interface RnxCaptureAccessibility {
29
+ accessible: boolean
30
+ label: string | null
31
+ role: string | null
32
+ hint: string | null
33
+ liveRegion: 'none' | 'polite' | 'assertive' | null
34
+ state: RnxCaptureAccessibilityState | null
35
+ value: RnxCaptureAccessibilityValue | null
36
+ }
37
+
38
+ export interface RnxCaptureInteractionState {
39
+ pressed: boolean
40
+ pressable: boolean
41
+ textInput: boolean
42
+ textSelection: { start: number | null; end: number | null } | null
43
+ secureTextEntry: boolean
44
+ placeholder: string | null
45
+ scrollOffset: { x: number; y: number }
46
+ zoomScale: number | null
47
+ nativePinchEnabled: boolean
48
+ contentOffsetProp: { x?: number; y?: number } | null
49
+ decelerationRateProp: unknown
50
+ strokeDashoffsetProp: number | null
51
+ }
52
+
53
+ export interface RnxCaptureStyleTokenBinding {
54
+ token: string
55
+ theme: string
56
+ }
57
+
58
+ export interface RnxCaptureTextSegment {
59
+ text: string
60
+ color?: string | number
61
+ fontFamily?: string
62
+ fontSize?: number
63
+ fontStyle?: string
64
+ fontWeight?: string | number
65
+ letterSpacing?: number
66
+ textDecorationLine?: string
67
+ }
68
+
69
+ export interface RnxCaptureTextLine {
70
+ text: string
71
+ x: number
72
+ y: number
73
+ width: number
74
+ height: number
75
+ baseline: number
76
+ segments?: RnxCaptureTextSegment[]
77
+ }
78
+
79
+ export interface RnxCaptureSource {
80
+ location?: string
81
+ file?: string
82
+ componentName?: string
83
+ componentStack?: string[]
84
+ react?: {
85
+ component?: string
86
+ owners?: string[]
87
+ source?: string
88
+ key?: string
89
+ hasRef?: boolean
90
+ hooks?: {
91
+ total: number
92
+ counts: Record<string, number>
93
+ }
94
+ contexts?: string[]
95
+ lastCommit?: {
96
+ ageMs: number
97
+ causes: string[]
98
+ propDiffs?: string[]
99
+ stateChanged?: boolean
100
+ actualDurationMs?: number
101
+ }
102
+ }
103
+ }
104
+
105
+ export interface RnxCaptureNode {
106
+ nodeId: number
107
+ parentNodeId: number | null
108
+ childNodeIds: number[]
109
+ paintIndex: number
110
+ type: string
111
+ id: string | null
112
+ testID: string | null
113
+ text: string | null
114
+ childCount: number
115
+ accessibility: RnxCaptureAccessibility
116
+ interaction: RnxCaptureInteractionState
117
+ layout: {
118
+ local: RnxCaptureRect
119
+ frame: RnxCaptureRect | null
120
+ absolutePosition: { x: number; y: number }
121
+ visibleFrame: RnxCaptureRect | null
122
+ }
123
+ style: {
124
+ resolved: Record<string, unknown>
125
+ authored: Record<string, unknown>
126
+ computed: Record<string, unknown>
127
+ tokenBindings: Record<string, RnxCaptureStyleTokenBinding>
128
+ }
129
+ source: RnxCaptureSource
130
+ props: Record<string, unknown>
131
+ textLines?: RnxCaptureTextLine[]
132
+ /** additional generic inspect fields preserved for tooling adapters */
133
+ inspection: Record<string, unknown>
134
+ }
135
+
136
+ export interface RnxCaptureTree {
137
+ viewport: { width: number; height: number }
138
+ rootNodeId: number
139
+ nodes: RnxCaptureNode[]
140
+ }
141
+
142
+ export interface RnxCaptureVisualEvidence {
143
+ kind: 'png'
144
+ /** names the runtime that produced these pixels without claiming visibility */
145
+ producer: 'headless-cpu-app-surface' | 'browser-compositor-app-surface'
146
+ dataUri: string
147
+ pixelWidth: number
148
+ pixelHeight: number
149
+ }
150
+
151
+ export interface RnxCaptureRegionRequest {
152
+ key: string
153
+ /** host node whose own subtree is rendered into this transparent atlas region */
154
+ nodeId: number
155
+ frame: RnxCaptureRect
156
+ }
157
+
158
+ export interface RnxRasterRegionTree {
159
+ width: number
160
+ height: number
161
+ nodes: readonly {
162
+ nodeId: number
163
+ parentNodeId: number | null
164
+ type: string
165
+ style: Record<string, unknown>
166
+ layout: RnxCaptureRect
167
+ frame: RnxCaptureRect | null
168
+ absolutePosition: { x: number; y: number }
169
+ visibleFrame?: RnxCaptureRect | null
170
+ }[]
171
+ }
172
+
173
+ export function selectRnxRasterNodeIds(tree: RnxRasterRegionTree): Set<number> {
174
+ const nodesById = new Map(tree.nodes.map((node) => [node.nodeId, node]))
175
+ const unsupported = new Set<number>()
176
+ for (const node of tree.nodes) {
177
+ const visibleFrame = node.visibleFrame ?? node.frame
178
+ if (!visibleFrame || visibleFrame.width <= 0 || visibleFrame.height <= 0) continue
179
+ if (
180
+ node.type === 'masked-view' ||
181
+ node.style._liquidGlass !== undefined ||
182
+ node.style._liquidGlassEdge !== undefined
183
+ ) {
184
+ unsupported.add(node.nodeId)
185
+ }
186
+ }
187
+
188
+ for (const nodeId of [...unsupported]) {
189
+ let parentNodeId = nodesById.get(nodeId)?.parentNodeId ?? null
190
+ while (parentNodeId !== null) {
191
+ if (unsupported.has(parentNodeId)) {
192
+ unsupported.delete(nodeId)
193
+ break
194
+ }
195
+ parentNodeId = nodesById.get(parentNodeId)?.parentNodeId ?? null
196
+ }
197
+ }
198
+
199
+ const screenArea = tree.width * tree.height
200
+ const maxLayerArea = screenArea * 0.25
201
+ const maxAtlasArea = screenArea * 0.5
202
+ const candidates = [...unsupported]
203
+ .map((nodeId) => {
204
+ const node = nodesById.get(nodeId)
205
+ const frame = node?.visibleFrame ?? node?.frame
206
+ return frame ? { nodeId, area: frame.width * frame.height } : null
207
+ })
208
+ .filter(
209
+ (candidate): candidate is { nodeId: number; area: number } =>
210
+ candidate !== null && candidate.area <= maxLayerArea,
211
+ )
212
+ .sort((left, right) => left.area - right.area || left.nodeId - right.nodeId)
213
+ const selected = new Set<number>()
214
+ let atlasArea = 0
215
+ for (const candidate of candidates) {
216
+ if (atlasArea + candidate.area > maxAtlasArea) continue
217
+ selected.add(candidate.nodeId)
218
+ atlasArea += candidate.area
219
+ }
220
+ return selected
221
+ }
222
+
223
+ export function rnxRasterFrames(
224
+ tree: RnxRasterRegionTree,
225
+ nodeIds: ReadonlySet<number>,
226
+ ): Map<number, RnxCaptureRect> {
227
+ const frames = new Map<number, RnxCaptureRect>()
228
+ for (const node of tree.nodes) {
229
+ if (!nodeIds.has(node.nodeId)) continue
230
+ const frame = node.frame ?? {
231
+ x: node.absolutePosition.x,
232
+ y: node.absolutePosition.y,
233
+ width: node.layout.width,
234
+ height: node.layout.height,
235
+ }
236
+ const liquidGlass = node.style._liquidGlass
237
+ const capturePadding =
238
+ liquidGlass &&
239
+ typeof liquidGlass === 'object' &&
240
+ typeof Reflect.get(liquidGlass, 'capturePadding') === 'number'
241
+ ? Reflect.get(liquidGlass, 'capturePadding')
242
+ : 0
243
+ const shadowRadius =
244
+ typeof node.style.shadowRadius === 'number' ? node.style.shadowRadius : 0
245
+ const shadowOffset = node.style.shadowOffset
246
+ const shadowOffsetX =
247
+ shadowOffset &&
248
+ typeof shadowOffset === 'object' &&
249
+ typeof Reflect.get(shadowOffset, 'width') === 'number'
250
+ ? Reflect.get(shadowOffset, 'width')
251
+ : 0
252
+ const shadowOffsetY =
253
+ shadowOffset &&
254
+ typeof shadowOffset === 'object' &&
255
+ typeof Reflect.get(shadowOffset, 'height') === 'number'
256
+ ? Reflect.get(shadowOffset, 'height')
257
+ : 0
258
+ const left = Math.max(
259
+ 0,
260
+ frame.x - Math.max(capturePadding, shadowRadius - shadowOffsetX),
261
+ )
262
+ const top = Math.max(
263
+ 0,
264
+ frame.y - Math.max(capturePadding, shadowRadius - shadowOffsetY),
265
+ )
266
+ const right = Math.min(
267
+ tree.width,
268
+ frame.x + frame.width + Math.max(capturePadding, shadowRadius + shadowOffsetX),
269
+ )
270
+ const bottom = Math.min(
271
+ tree.height,
272
+ frame.y + frame.height + Math.max(capturePadding, shadowRadius + shadowOffsetY),
273
+ )
274
+ if (right > left && bottom > top) {
275
+ frames.set(node.nodeId, {
276
+ x: left,
277
+ y: top,
278
+ width: right - left,
279
+ height: bottom - top,
280
+ })
281
+ }
282
+ }
283
+ return frames
284
+ }
285
+
286
+ export function rnxCaptureRasterRegions(
287
+ capture: RnxScreenCapture,
288
+ ): RnxCaptureRegionRequest[] {
289
+ const tree: RnxRasterRegionTree = {
290
+ width: capture.tree.viewport.width,
291
+ height: capture.tree.viewport.height,
292
+ nodes: capture.tree.nodes.map((node) => ({
293
+ nodeId: node.nodeId,
294
+ parentNodeId: node.parentNodeId,
295
+ type: node.type,
296
+ style: node.style.resolved,
297
+ layout: node.layout.local,
298
+ frame: node.layout.frame,
299
+ absolutePosition: node.layout.absolutePosition,
300
+ visibleFrame: node.layout.visibleFrame,
301
+ })),
302
+ }
303
+ const nodeIds = selectRnxRasterNodeIds(tree)
304
+ const frames = rnxRasterFrames(tree, nodeIds)
305
+ return [...nodeIds].flatMap((nodeId) => {
306
+ const frame = frames.get(nodeId)
307
+ return frame ? [{ key: String(nodeId), nodeId, frame }] : []
308
+ })
309
+ }
310
+
311
+ export interface RnxCaptureAtlasRegion extends RnxCaptureRegionRequest {
312
+ /** source rectangle inside the packed atlas, in logical atlas coordinates */
313
+ source: RnxCaptureRect
314
+ }
315
+
316
+ export interface RnxCaptureRegionAtlas {
317
+ kind: 'png-atlas'
318
+ producer: 'headless-cpu-app-surface'
319
+ dataUri: string
320
+ /** logical atlas dimensions; the encoded PNG uses pixelWidth × pixelHeight */
321
+ width: number
322
+ height: number
323
+ pixelWidth: number
324
+ pixelHeight: number
325
+ regions: RnxCaptureAtlasRegion[]
326
+ }
327
+
328
+ export interface RnxCaptureFontRegistration {
329
+ url?: string
330
+ family: string
331
+ sourceLabel?: string
332
+ parsedFamily?: string | null
333
+ parsedPostScript?: string | null
334
+ fullName?: string | null
335
+ }
336
+
337
+ export interface RnxScreenCapture {
338
+ contractVersion: typeof RNX_SCREEN_CAPTURE_VERSION
339
+ capturedAt: number
340
+ tree: RnxCaptureTree
341
+ /** app fonts registered in the captured tenant, included only when requested */
342
+ fonts?: RnxCaptureFontRegistration[]
343
+ /** optional full-frame diagnostic evidence, never required by tree consumers */
344
+ visual?: RnxCaptureVisualEvidence
345
+ }
346
+
347
+ function captureRecord(value: unknown): value is Record<string, unknown> {
348
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
349
+ }
350
+
351
+ function captureInteger(value: unknown): value is number {
352
+ return typeof value === 'number' && Number.isSafeInteger(value)
353
+ }
354
+
355
+ function captureNumber(value: unknown): value is number {
356
+ return typeof value === 'number' && Number.isFinite(value)
357
+ }
358
+
359
+ function captureNullableString(value: unknown): value is string | null {
360
+ return value === null || typeof value === 'string'
361
+ }
362
+
363
+ function captureRect(value: unknown): value is RnxCaptureRect {
364
+ return (
365
+ captureRecord(value) &&
366
+ captureNumber(value.x) &&
367
+ captureNumber(value.y) &&
368
+ captureNumber(value.width) &&
369
+ captureNumber(value.height)
370
+ )
371
+ }
372
+
373
+ function captureOptionalString(value: unknown): value is string | undefined {
374
+ return value === undefined || typeof value === 'string'
375
+ }
376
+
377
+ function captureOptionalNullableString(
378
+ value: unknown,
379
+ ): value is string | null | undefined {
380
+ return value === undefined || captureNullableString(value)
381
+ }
382
+
383
+ function captureFontRegistration(value: unknown): value is RnxCaptureFontRegistration {
384
+ return (
385
+ captureRecord(value) &&
386
+ typeof value.family === 'string' &&
387
+ value.family.length > 0 &&
388
+ captureOptionalString(value.url) &&
389
+ captureOptionalString(value.sourceLabel) &&
390
+ captureOptionalNullableString(value.parsedFamily) &&
391
+ captureOptionalNullableString(value.parsedPostScript) &&
392
+ captureOptionalNullableString(value.fullName)
393
+ )
394
+ }
395
+
396
+ function capturePoint(value: unknown): value is { x: number; y: number } {
397
+ return captureRecord(value) && captureNumber(value.x) && captureNumber(value.y)
398
+ }
399
+
400
+ function captureNode(value: unknown): value is RnxCaptureNode {
401
+ if (
402
+ !captureRecord(value) ||
403
+ !captureInteger(value.nodeId) ||
404
+ !(value.parentNodeId === null || captureInteger(value.parentNodeId)) ||
405
+ !Array.isArray(value.childNodeIds) ||
406
+ !value.childNodeIds.every(captureInteger) ||
407
+ !captureInteger(value.paintIndex) ||
408
+ typeof value.type !== 'string' ||
409
+ !captureNullableString(value.id) ||
410
+ !captureNullableString(value.testID) ||
411
+ !captureNullableString(value.text) ||
412
+ !captureInteger(value.childCount) ||
413
+ !captureRecord(value.accessibility) ||
414
+ !captureRecord(value.interaction) ||
415
+ !captureRecord(value.layout) ||
416
+ !captureRecord(value.style) ||
417
+ !captureRecord(value.source) ||
418
+ !captureRecord(value.props) ||
419
+ !captureRecord(value.inspection)
420
+ ) {
421
+ return false
422
+ }
423
+ const accessibility = value.accessibility
424
+ if (
425
+ typeof accessibility.accessible !== 'boolean' ||
426
+ !captureNullableString(accessibility.label) ||
427
+ !captureNullableString(accessibility.role) ||
428
+ !captureNullableString(accessibility.hint) ||
429
+ !(
430
+ accessibility.liveRegion === null ||
431
+ accessibility.liveRegion === 'none' ||
432
+ accessibility.liveRegion === 'polite' ||
433
+ accessibility.liveRegion === 'assertive'
434
+ ) ||
435
+ !(accessibility.state === null || captureRecord(accessibility.state)) ||
436
+ !(accessibility.value === null || captureRecord(accessibility.value))
437
+ ) {
438
+ return false
439
+ }
440
+ const interaction = value.interaction
441
+ if (
442
+ typeof interaction.pressed !== 'boolean' ||
443
+ typeof interaction.pressable !== 'boolean' ||
444
+ typeof interaction.textInput !== 'boolean' ||
445
+ !(interaction.textSelection === null || captureRecord(interaction.textSelection)) ||
446
+ typeof interaction.secureTextEntry !== 'boolean' ||
447
+ !captureNullableString(interaction.placeholder) ||
448
+ !capturePoint(interaction.scrollOffset) ||
449
+ !(interaction.zoomScale === null || captureNumber(interaction.zoomScale)) ||
450
+ typeof interaction.nativePinchEnabled !== 'boolean' ||
451
+ !(
452
+ interaction.contentOffsetProp === null ||
453
+ captureRecord(interaction.contentOffsetProp)
454
+ ) ||
455
+ !(
456
+ interaction.strokeDashoffsetProp === null ||
457
+ captureNumber(interaction.strokeDashoffsetProp)
458
+ )
459
+ ) {
460
+ return false
461
+ }
462
+ const layout = value.layout
463
+ if (
464
+ !captureRect(layout.local) ||
465
+ !(layout.frame === null || captureRect(layout.frame)) ||
466
+ !capturePoint(layout.absolutePosition) ||
467
+ !(layout.visibleFrame === null || captureRect(layout.visibleFrame))
468
+ ) {
469
+ return false
470
+ }
471
+ const style = value.style
472
+ return (
473
+ captureRecord(style.resolved) &&
474
+ captureRecord(style.authored) &&
475
+ captureRecord(style.computed) &&
476
+ captureRecord(style.tokenBindings) &&
477
+ (value.textLines === undefined || Array.isArray(value.textLines))
478
+ )
479
+ }
480
+
481
+ export function isRnxScreenCapture(value: unknown): value is RnxScreenCapture {
482
+ if (
483
+ !captureRecord(value) ||
484
+ value.contractVersion !== RNX_SCREEN_CAPTURE_VERSION ||
485
+ !captureInteger(value.capturedAt) ||
486
+ !captureRecord(value.tree) ||
487
+ !(
488
+ value.fonts === undefined ||
489
+ (Array.isArray(value.fonts) &&
490
+ value.fonts.length <= 200 &&
491
+ value.fonts.every(captureFontRegistration))
492
+ ) ||
493
+ !(value.visual === undefined || captureRecord(value.visual))
494
+ ) {
495
+ return false
496
+ }
497
+ const tree = value.tree
498
+ const visual = value.visual
499
+ return (
500
+ captureRecord(tree.viewport) &&
501
+ captureInteger(tree.viewport.width) &&
502
+ tree.viewport.width > 0 &&
503
+ captureInteger(tree.viewport.height) &&
504
+ tree.viewport.height > 0 &&
505
+ captureInteger(tree.rootNodeId) &&
506
+ Array.isArray(tree.nodes) &&
507
+ tree.nodes.every(captureNode) &&
508
+ (visual === undefined ||
509
+ (visual.kind === 'png' &&
510
+ (visual.producer === 'headless-cpu-app-surface' ||
511
+ visual.producer === 'browser-compositor-app-surface') &&
512
+ typeof visual.dataUri === 'string' &&
513
+ visual.dataUri.startsWith('data:image/png;base64,') &&
514
+ captureInteger(visual.pixelWidth) &&
515
+ visual.pixelWidth > 0 &&
516
+ captureInteger(visual.pixelHeight) &&
517
+ visual.pixelHeight > 0))
518
+ )
519
+ }
520
+
521
+ export function isRnxCaptureRegionAtlas(value: unknown): value is RnxCaptureRegionAtlas {
522
+ if (
523
+ !captureRecord(value) ||
524
+ value.kind !== 'png-atlas' ||
525
+ value.producer !== 'headless-cpu-app-surface' ||
526
+ typeof value.dataUri !== 'string' ||
527
+ !value.dataUri.startsWith('data:image/png;base64,') ||
528
+ !captureNumber(value.width) ||
529
+ value.width <= 0 ||
530
+ !captureNumber(value.height) ||
531
+ value.height <= 0 ||
532
+ !captureInteger(value.pixelWidth) ||
533
+ value.pixelWidth <= 0 ||
534
+ !captureInteger(value.pixelHeight) ||
535
+ value.pixelHeight <= 0 ||
536
+ !Array.isArray(value.regions) ||
537
+ value.regions.length === 0 ||
538
+ value.regions.length > 256
539
+ ) {
540
+ return false
541
+ }
542
+ const scale = value.pixelWidth / value.width
543
+ if (
544
+ !Number.isInteger(scale) ||
545
+ scale < 1 ||
546
+ scale > 4 ||
547
+ Math.abs(value.height * scale - value.pixelHeight) > 0.000_001
548
+ ) {
549
+ return false
550
+ }
551
+ const keys = new Set<string>()
552
+ for (const region of value.regions) {
553
+ if (
554
+ !captureRecord(region) ||
555
+ typeof region.key !== 'string' ||
556
+ region.key.length === 0 ||
557
+ keys.has(region.key) ||
558
+ !captureInteger(region.nodeId) ||
559
+ region.nodeId <= 0 ||
560
+ !captureRect(region.frame) ||
561
+ region.frame.width <= 0 ||
562
+ region.frame.height <= 0 ||
563
+ !captureRect(region.source) ||
564
+ region.source.x < 0 ||
565
+ region.source.y < 0 ||
566
+ region.source.width !== region.frame.width ||
567
+ region.source.height !== region.frame.height ||
568
+ region.source.x + region.source.width > value.width ||
569
+ region.source.y + region.source.height > value.height
570
+ ) {
571
+ return false
572
+ }
573
+ keys.add(region.key)
574
+ }
575
+ return true
576
+ }
@@ -0,0 +1,18 @@
1
+ export const RNX_CLOUD_MAX_ARTIFACT_BYTES = 20 * 1024 * 1024
2
+ export const RNX_CLOUD_DESIGN_PARALLEL_JOB_LIMIT = 16
3
+ export const RNX_CLOUD_DESIGN_MAX_STORAGE_ENTRIES = 256
4
+ export const RNX_CLOUD_DESIGN_MAX_STORAGE_BYTES = 1024 * 1024
5
+
6
+ export interface RnxCloudClaim {
7
+ id: string
8
+ expiresAt: number
9
+ }
10
+
11
+ export interface RnxCloudCreateReceipt {
12
+ simId: string
13
+ artifact: {
14
+ id: `sha256:${string}`
15
+ bytes: number
16
+ }
17
+ claim: RnxCloudClaim
18
+ }
package/src/home-paths.ts CHANGED
@@ -18,6 +18,7 @@
18
18
  // ├── runtime-upgraded.json one-shot notice written by the daemon's
19
19
  // │ background updater, consumed (printed + deleted)
20
20
  // │ by the next interactive CLI run
21
+ // ├── cli-update.json cached standalone CLI release check + notice state
21
22
  // ├── automatic-cleanup-v1.json completion marker for the one-time safe
22
23
  // │ cleanup introduced for legacy unbounded data
23
24
  // └── config.json user prefs: update channel, cdn origin override,
@@ -33,6 +34,7 @@ export const ACTIVE_RUNTIME_FILE = 'active'
33
34
  export const DAEMON_LOCKFILE = 'daemon.json'
34
35
  export const DEV_BRIDGES_DIR = 'dev-bridges'
35
36
  export const CONFIG_FILE = 'config.json'
37
+ export const CLI_UPDATE_FILE = 'cli-update.json'
36
38
  export const DAEMON_HEARTBEAT_STALE_MS = 30_000
37
39
 
38
40
  export function rnxHomeDir(): string {
@@ -93,8 +95,8 @@ export function isDevWorkstation(): boolean {
93
95
  // the persistent launchd / systemd daemon must never auto-install or run on a
94
96
  // dev checkout or a dev workstation. either makes the background agent point at
95
97
  // moving workspace artifacts / dev env and serve stale assets that shadow the
96
- // live dev stack. setup and the launchd-spawned `sootsim serve` boot consult
97
- // this; a foreground `sootsim serve` the dev starts
98
+ // live dev stack. setup and the launchd-spawned `rnx serve` boot consult
99
+ // this; a foreground `rnx serve` the dev starts
98
100
  // by hand is intentional and stays allowed.
99
101
  export function shouldSkipPersistentDaemon(): boolean {
100
102
  return isSootsimDevCheckout() || isDevWorkstation()
@@ -176,6 +178,10 @@ export function configFilePath(): string {
176
178
  return path.join(rnxHomeDir(), CONFIG_FILE)
177
179
  }
178
180
 
181
+ export function cliUpdateFilePath(): string {
182
+ return path.join(rnxHomeDir(), CLI_UPDATE_FILE)
183
+ }
184
+
179
185
  // --- shared rnx config ---------------------------------------------------
180
186
  //
181
187
  // ~/.rnx/config.json is the single user-level config file every rnx
@@ -291,7 +297,7 @@ export function readOrCreateAnonymousId(): string {
291
297
  // the only place its "updated to vX" line lands is the daemon's own stderr,
292
298
  // which nobody reads. it also drops a one-shot notice file here; the next
293
299
  // interactive CLI invocation prints it once and deletes it, so users learn
294
- // their engine moved without having to run `sootsim version`.
300
+ // their engine moved without having to run `rnx version`.
295
301
 
296
302
  export interface RuntimeUpgradeNotice {
297
303
  from: string | null
@@ -67,7 +67,7 @@ interface PendingPromptEcho {
67
67
  export interface AgentHostOptions {
68
68
  /** callback so hosts with a Contrast-env context (electron, dev middleware)
69
69
  * can inject their own exclude list. defaults to the env-derived list
70
- * the rest of the repo uses, so `sootsim serve` standalone does the
70
+ * the rest of the repo uses, so `rnx serve` standalone does the
71
71
  * right thing. */
72
72
  getExcludePorts?: () => number[]
73
73
  /** returns the active CLI lease for a sim. used only for prompt routing to