rnxsim 0.1.381 → 0.1.382
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/README.md +19 -6
- package/cli/app-fonts.ts +1 -1
- package/cli/bin.ts +118 -26
- package/cli/bridge-diagnostics.ts +1 -1
- package/cli/bridge-flow-runner.ts +9 -9
- package/cli/browser-evals.ts +3 -3
- package/cli/cli-update.ts +261 -0
- package/cli/cloud-client.ts +407 -0
- package/cli/cloud-dispatch.ts +165 -0
- package/cli/cloud-session.ts +156 -0
- package/cli/command-contract.ts +2 -2
- package/cli/command-registry.ts +9 -0
- package/cli/commands/agent-wrapper.ts +4 -4
- package/cli/commands/agent.ts +3 -3
- package/cli/commands/box/checkout-plane.ts +16 -8
- package/cli/commands/box/rnx-command.ts +2 -2
- package/cli/commands/box/search-command.ts +2 -2
- package/cli/commands/box.ts +123 -70
- package/cli/commands/config.ts +3 -2
- package/cli/commands/control.ts +6 -6
- package/cli/commands/cpu-profile.ts +2 -2
- package/cli/commands/daemon.ts +7 -7
- package/cli/commands/debug.ts +2 -2
- package/cli/commands/detox.ts +7 -7
- package/cli/commands/diagnose.ts +1 -1
- package/cli/commands/electron.ts +1 -1
- package/cli/commands/film.ts +1 -1
- package/cli/commands/flow.ts +4 -4
- package/cli/commands/inspect/core.ts +58 -7
- package/cli/commands/inspect/env.ts +5 -3
- package/cli/commands/inspect/shared.ts +2 -1
- package/cli/commands/inspect/wait-event.ts +1 -1
- package/cli/commands/inspect/wait-ready.ts +15 -5
- package/cli/commands/inspect.ts +31 -9
- package/cli/commands/install-desktop.ts +2 -2
- package/cli/commands/maestro.ts +3 -3
- package/cli/commands/permissions.ts +1 -1
- package/cli/commands/platform.ts +91 -0
- package/cli/commands/record.ts +3 -3
- package/cli/commands/report-issue.ts +1 -1
- package/cli/commands/runtime.ts +1 -1
- package/cli/commands/screenshot-mode.ts +1 -1
- package/cli/commands/screenshot.ts +25 -0
- package/cli/commands/serve.ts +1 -1
- package/cli/commands/skills.ts +1 -1
- package/cli/commands/timeline.ts +1 -1
- package/cli/commands/upgrade.ts +8 -34
- package/cli/commands/upload.ts +3 -3
- package/cli/commands/version.ts +14 -3
- package/cli/commands/what-happened.ts +2 -2
- package/cli/current-sim.ts +43 -30
- package/cli/desktop-companion.ts +1 -1
- package/cli/drivers/electron.ts +1 -1
- package/cli/drivers/playwright.ts +3 -3
- package/cli/drivers/types.ts +4 -4
- package/cli/flow-file.ts +3 -3
- package/cli/help-core.ts +18 -11
- package/cli/hidden-runtime-alias.ts +1 -1
- package/cli/hints.ts +2 -2
- package/cli/maestro-js.ts +2 -2
- package/cli/outbound-endpoints.ts +40 -6
- package/cli/parse-args.ts +1 -1
- package/cli/registry.ts +1 -1
- package/cli/resolve-assets.ts +2 -2
- package/cli/secure-file.ts +31 -0
- package/cli/shell-init.ts +98 -0
- package/cli/ws-bridge.ts +18 -8
- package/detox/expectations.ts +1 -1
- package/detox/gestures.ts +3 -3
- package/detox/index.ts +3 -3
- package/dist-lib/agent-daemon-client.cjs +1 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-identity.cjs +1 -1
- package/dist-lib/agent-sessions.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +1 -1
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +1 -1
- package/dist-lib/bridge-contract-input.cjs +15 -8
- package/dist-lib/bridge-contract-input.mjs +15 -8
- package/dist-lib/bridge-contract.cjs +1 -1
- package/dist-lib/bridge-contract.mjs +1 -1
- package/dist-lib/capture-contract.cjs +107 -0
- package/dist-lib/capture-contract.mjs +80 -0
- package/dist-lib/cli-constants.cjs +1 -1
- package/dist-lib/cloud-contract.cjs +32 -0
- package/dist-lib/cloud-contract.mjs +7 -0
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +4 -4
- package/dist-lib/dev-bundle-resolution.cjs +1 -1
- package/dist-lib/home-paths.cjs +9 -1
- package/dist-lib/host/bridge-host.cjs +15 -10
- package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
- package/dist-lib/host/replacement-module-handler.cjs +1 -1
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +248 -73
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/menu.cjs +1 -1
- package/dist-lib/menu.mjs +1 -1
- package/dist-lib/metro-production-bundle.cjs +1 -1
- package/dist-lib/metro-production-bundle.mjs +1 -1
- package/dist-lib/metro.cjs +1 -1
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +1 -1
- package/dist-lib/react-native-host-modules.cjs +1 -1
- package/dist-lib/react-native-host-modules.mjs +1 -1
- package/dist-lib/render-mode.cjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
- package/dist-lib/sdk.cjs +44 -6
- package/dist-lib/sdk.mjs +44 -6
- package/dist-lib/skills.cjs +435 -155
- package/dist-lib/vite.cjs +1 -1
- package/package.json +20 -4
- package/scripts/dev-server-scanner.ts +3 -3
- package/skills/contrast/SKILL.md +1 -1
- package/skills/rnx-perf/SKILL.md +68 -52
- package/src/app-url.ts +2 -3
- package/src/bridge-contract-input.ts +17 -6
- package/src/bridge-contract.ts +25 -4
- package/src/capture-contract.ts +423 -0
- package/src/cloud-contract.ts +15 -0
- package/src/home-paths.ts +9 -3
- package/src/host/agent-host.ts +1 -1
- package/src/host/bridge-host.ts +13 -11
- package/src/host/plane-host.ts +2 -2
- package/src/index.ts +21 -0
- package/src/native-dev-bundle-url.ts +2 -2
- package/src/screenshots/orchestrate.ts +1 -1
- package/src/screenshots/org-deck.ts +1 -1
- package/src/settings-bundle.ts +1 -1
- package/src/sim-client.ts +4 -0
- package/src/vite-plugin-one.ts +2 -2
- package/src/vite-plugin.ts +1 -1
- package/cli/commands/box/cloud-box.ts +0 -294
|
@@ -0,0 +1,423 @@
|
|
|
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 RnxCaptureAtlasRegion extends RnxCaptureRegionRequest {
|
|
159
|
+
/** source rectangle inside the packed atlas, in logical atlas coordinates */
|
|
160
|
+
source: RnxCaptureRect
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export interface RnxCaptureRegionAtlas {
|
|
164
|
+
kind: 'png-atlas'
|
|
165
|
+
producer: 'headless-cpu-app-surface'
|
|
166
|
+
dataUri: string
|
|
167
|
+
/** logical atlas dimensions; the encoded PNG uses pixelWidth × pixelHeight */
|
|
168
|
+
width: number
|
|
169
|
+
height: number
|
|
170
|
+
pixelWidth: number
|
|
171
|
+
pixelHeight: number
|
|
172
|
+
regions: RnxCaptureAtlasRegion[]
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export interface RnxCaptureFontRegistration {
|
|
176
|
+
url?: string
|
|
177
|
+
family: string
|
|
178
|
+
sourceLabel?: string
|
|
179
|
+
parsedFamily?: string | null
|
|
180
|
+
parsedPostScript?: string | null
|
|
181
|
+
fullName?: string | null
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export interface RnxScreenCapture {
|
|
185
|
+
contractVersion: typeof RNX_SCREEN_CAPTURE_VERSION
|
|
186
|
+
capturedAt: number
|
|
187
|
+
tree: RnxCaptureTree
|
|
188
|
+
/** app fonts registered in the captured tenant, included only when requested */
|
|
189
|
+
fonts?: RnxCaptureFontRegistration[]
|
|
190
|
+
/** optional full-frame diagnostic evidence, never required by tree consumers */
|
|
191
|
+
visual?: RnxCaptureVisualEvidence
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function captureRecord(value: unknown): value is Record<string, unknown> {
|
|
195
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function captureInteger(value: unknown): value is number {
|
|
199
|
+
return typeof value === 'number' && Number.isSafeInteger(value)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function captureNumber(value: unknown): value is number {
|
|
203
|
+
return typeof value === 'number' && Number.isFinite(value)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function captureNullableString(value: unknown): value is string | null {
|
|
207
|
+
return value === null || typeof value === 'string'
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function captureRect(value: unknown): value is RnxCaptureRect {
|
|
211
|
+
return (
|
|
212
|
+
captureRecord(value) &&
|
|
213
|
+
captureNumber(value.x) &&
|
|
214
|
+
captureNumber(value.y) &&
|
|
215
|
+
captureNumber(value.width) &&
|
|
216
|
+
captureNumber(value.height)
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function captureOptionalString(value: unknown): value is string | undefined {
|
|
221
|
+
return value === undefined || typeof value === 'string'
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function captureOptionalNullableString(
|
|
225
|
+
value: unknown,
|
|
226
|
+
): value is string | null | undefined {
|
|
227
|
+
return value === undefined || captureNullableString(value)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function captureFontRegistration(value: unknown): value is RnxCaptureFontRegistration {
|
|
231
|
+
return (
|
|
232
|
+
captureRecord(value) &&
|
|
233
|
+
typeof value.family === 'string' &&
|
|
234
|
+
value.family.length > 0 &&
|
|
235
|
+
captureOptionalString(value.url) &&
|
|
236
|
+
captureOptionalString(value.sourceLabel) &&
|
|
237
|
+
captureOptionalNullableString(value.parsedFamily) &&
|
|
238
|
+
captureOptionalNullableString(value.parsedPostScript) &&
|
|
239
|
+
captureOptionalNullableString(value.fullName)
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function capturePoint(value: unknown): value is { x: number; y: number } {
|
|
244
|
+
return captureRecord(value) && captureNumber(value.x) && captureNumber(value.y)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function captureNode(value: unknown): value is RnxCaptureNode {
|
|
248
|
+
if (
|
|
249
|
+
!captureRecord(value) ||
|
|
250
|
+
!captureInteger(value.nodeId) ||
|
|
251
|
+
!(value.parentNodeId === null || captureInteger(value.parentNodeId)) ||
|
|
252
|
+
!Array.isArray(value.childNodeIds) ||
|
|
253
|
+
!value.childNodeIds.every(captureInteger) ||
|
|
254
|
+
!captureInteger(value.paintIndex) ||
|
|
255
|
+
typeof value.type !== 'string' ||
|
|
256
|
+
!captureNullableString(value.id) ||
|
|
257
|
+
!captureNullableString(value.testID) ||
|
|
258
|
+
!captureNullableString(value.text) ||
|
|
259
|
+
!captureInteger(value.childCount) ||
|
|
260
|
+
!captureRecord(value.accessibility) ||
|
|
261
|
+
!captureRecord(value.interaction) ||
|
|
262
|
+
!captureRecord(value.layout) ||
|
|
263
|
+
!captureRecord(value.style) ||
|
|
264
|
+
!captureRecord(value.source) ||
|
|
265
|
+
!captureRecord(value.props) ||
|
|
266
|
+
!captureRecord(value.inspection)
|
|
267
|
+
) {
|
|
268
|
+
return false
|
|
269
|
+
}
|
|
270
|
+
const accessibility = value.accessibility
|
|
271
|
+
if (
|
|
272
|
+
typeof accessibility.accessible !== 'boolean' ||
|
|
273
|
+
!captureNullableString(accessibility.label) ||
|
|
274
|
+
!captureNullableString(accessibility.role) ||
|
|
275
|
+
!captureNullableString(accessibility.hint) ||
|
|
276
|
+
!(
|
|
277
|
+
accessibility.liveRegion === null ||
|
|
278
|
+
accessibility.liveRegion === 'none' ||
|
|
279
|
+
accessibility.liveRegion === 'polite' ||
|
|
280
|
+
accessibility.liveRegion === 'assertive'
|
|
281
|
+
) ||
|
|
282
|
+
!(accessibility.state === null || captureRecord(accessibility.state)) ||
|
|
283
|
+
!(accessibility.value === null || captureRecord(accessibility.value))
|
|
284
|
+
) {
|
|
285
|
+
return false
|
|
286
|
+
}
|
|
287
|
+
const interaction = value.interaction
|
|
288
|
+
if (
|
|
289
|
+
typeof interaction.pressed !== 'boolean' ||
|
|
290
|
+
typeof interaction.pressable !== 'boolean' ||
|
|
291
|
+
typeof interaction.textInput !== 'boolean' ||
|
|
292
|
+
!(interaction.textSelection === null || captureRecord(interaction.textSelection)) ||
|
|
293
|
+
typeof interaction.secureTextEntry !== 'boolean' ||
|
|
294
|
+
!captureNullableString(interaction.placeholder) ||
|
|
295
|
+
!capturePoint(interaction.scrollOffset) ||
|
|
296
|
+
!(interaction.zoomScale === null || captureNumber(interaction.zoomScale)) ||
|
|
297
|
+
typeof interaction.nativePinchEnabled !== 'boolean' ||
|
|
298
|
+
!(
|
|
299
|
+
interaction.contentOffsetProp === null ||
|
|
300
|
+
captureRecord(interaction.contentOffsetProp)
|
|
301
|
+
) ||
|
|
302
|
+
!(
|
|
303
|
+
interaction.strokeDashoffsetProp === null ||
|
|
304
|
+
captureNumber(interaction.strokeDashoffsetProp)
|
|
305
|
+
)
|
|
306
|
+
) {
|
|
307
|
+
return false
|
|
308
|
+
}
|
|
309
|
+
const layout = value.layout
|
|
310
|
+
if (
|
|
311
|
+
!captureRect(layout.local) ||
|
|
312
|
+
!(layout.frame === null || captureRect(layout.frame)) ||
|
|
313
|
+
!capturePoint(layout.absolutePosition) ||
|
|
314
|
+
!(layout.visibleFrame === null || captureRect(layout.visibleFrame))
|
|
315
|
+
) {
|
|
316
|
+
return false
|
|
317
|
+
}
|
|
318
|
+
const style = value.style
|
|
319
|
+
return (
|
|
320
|
+
captureRecord(style.resolved) &&
|
|
321
|
+
captureRecord(style.authored) &&
|
|
322
|
+
captureRecord(style.computed) &&
|
|
323
|
+
captureRecord(style.tokenBindings) &&
|
|
324
|
+
(value.textLines === undefined || Array.isArray(value.textLines))
|
|
325
|
+
)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export function isRnxScreenCapture(value: unknown): value is RnxScreenCapture {
|
|
329
|
+
if (
|
|
330
|
+
!captureRecord(value) ||
|
|
331
|
+
value.contractVersion !== RNX_SCREEN_CAPTURE_VERSION ||
|
|
332
|
+
!captureInteger(value.capturedAt) ||
|
|
333
|
+
!captureRecord(value.tree) ||
|
|
334
|
+
!(
|
|
335
|
+
value.fonts === undefined ||
|
|
336
|
+
(Array.isArray(value.fonts) &&
|
|
337
|
+
value.fonts.length <= 200 &&
|
|
338
|
+
value.fonts.every(captureFontRegistration))
|
|
339
|
+
) ||
|
|
340
|
+
!(value.visual === undefined || captureRecord(value.visual))
|
|
341
|
+
) {
|
|
342
|
+
return false
|
|
343
|
+
}
|
|
344
|
+
const tree = value.tree
|
|
345
|
+
const visual = value.visual
|
|
346
|
+
return (
|
|
347
|
+
captureRecord(tree.viewport) &&
|
|
348
|
+
captureInteger(tree.viewport.width) &&
|
|
349
|
+
tree.viewport.width > 0 &&
|
|
350
|
+
captureInteger(tree.viewport.height) &&
|
|
351
|
+
tree.viewport.height > 0 &&
|
|
352
|
+
captureInteger(tree.rootNodeId) &&
|
|
353
|
+
Array.isArray(tree.nodes) &&
|
|
354
|
+
tree.nodes.every(captureNode) &&
|
|
355
|
+
(visual === undefined ||
|
|
356
|
+
(visual.kind === 'png' &&
|
|
357
|
+
(visual.producer === 'headless-cpu-app-surface' ||
|
|
358
|
+
visual.producer === 'browser-compositor-app-surface') &&
|
|
359
|
+
typeof visual.dataUri === 'string' &&
|
|
360
|
+
visual.dataUri.startsWith('data:image/png;base64,') &&
|
|
361
|
+
captureInteger(visual.pixelWidth) &&
|
|
362
|
+
visual.pixelWidth > 0 &&
|
|
363
|
+
captureInteger(visual.pixelHeight) &&
|
|
364
|
+
visual.pixelHeight > 0))
|
|
365
|
+
)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export function isRnxCaptureRegionAtlas(value: unknown): value is RnxCaptureRegionAtlas {
|
|
369
|
+
if (
|
|
370
|
+
!captureRecord(value) ||
|
|
371
|
+
value.kind !== 'png-atlas' ||
|
|
372
|
+
value.producer !== 'headless-cpu-app-surface' ||
|
|
373
|
+
typeof value.dataUri !== 'string' ||
|
|
374
|
+
!value.dataUri.startsWith('data:image/png;base64,') ||
|
|
375
|
+
!captureNumber(value.width) ||
|
|
376
|
+
value.width <= 0 ||
|
|
377
|
+
!captureNumber(value.height) ||
|
|
378
|
+
value.height <= 0 ||
|
|
379
|
+
!captureInteger(value.pixelWidth) ||
|
|
380
|
+
value.pixelWidth <= 0 ||
|
|
381
|
+
!captureInteger(value.pixelHeight) ||
|
|
382
|
+
value.pixelHeight <= 0 ||
|
|
383
|
+
!Array.isArray(value.regions) ||
|
|
384
|
+
value.regions.length === 0 ||
|
|
385
|
+
value.regions.length > 256
|
|
386
|
+
) {
|
|
387
|
+
return false
|
|
388
|
+
}
|
|
389
|
+
const scale = value.pixelWidth / value.width
|
|
390
|
+
if (
|
|
391
|
+
!Number.isInteger(scale) ||
|
|
392
|
+
scale < 1 ||
|
|
393
|
+
scale > 4 ||
|
|
394
|
+
Math.abs(value.height * scale - value.pixelHeight) > 0.000_001
|
|
395
|
+
) {
|
|
396
|
+
return false
|
|
397
|
+
}
|
|
398
|
+
const keys = new Set<string>()
|
|
399
|
+
for (const region of value.regions) {
|
|
400
|
+
if (
|
|
401
|
+
!captureRecord(region) ||
|
|
402
|
+
typeof region.key !== 'string' ||
|
|
403
|
+
region.key.length === 0 ||
|
|
404
|
+
keys.has(region.key) ||
|
|
405
|
+
!captureInteger(region.nodeId) ||
|
|
406
|
+
region.nodeId <= 0 ||
|
|
407
|
+
!captureRect(region.frame) ||
|
|
408
|
+
region.frame.width <= 0 ||
|
|
409
|
+
region.frame.height <= 0 ||
|
|
410
|
+
!captureRect(region.source) ||
|
|
411
|
+
region.source.x < 0 ||
|
|
412
|
+
region.source.y < 0 ||
|
|
413
|
+
region.source.width !== region.frame.width ||
|
|
414
|
+
region.source.height !== region.frame.height ||
|
|
415
|
+
region.source.x + region.source.width > value.width ||
|
|
416
|
+
region.source.y + region.source.height > value.height
|
|
417
|
+
) {
|
|
418
|
+
return false
|
|
419
|
+
}
|
|
420
|
+
keys.add(region.key)
|
|
421
|
+
}
|
|
422
|
+
return true
|
|
423
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const RNX_CLOUD_MAX_ARTIFACT_BYTES = 20 * 1024 * 1024
|
|
2
|
+
|
|
3
|
+
export interface RnxCloudClaim {
|
|
4
|
+
id: string
|
|
5
|
+
expiresAt: number
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface RnxCloudCreateReceipt {
|
|
9
|
+
simId: string
|
|
10
|
+
artifact: {
|
|
11
|
+
id: `sha256:${string}`
|
|
12
|
+
bytes: number
|
|
13
|
+
}
|
|
14
|
+
claim: RnxCloudClaim
|
|
15
|
+
}
|
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 `
|
|
97
|
-
// this; a foreground `
|
|
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 `
|
|
300
|
+
// their engine moved without having to run `rnx version`.
|
|
295
301
|
|
|
296
302
|
export interface RuntimeUpgradeNotice {
|
|
297
303
|
from: string | null
|
package/src/host/agent-host.ts
CHANGED
|
@@ -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 `
|
|
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
|
package/src/host/bridge-host.ts
CHANGED
|
@@ -165,7 +165,7 @@ interface BridgeCliLease {
|
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
// interactive commands change app state and must respect the lease.
|
|
168
|
-
// observational/lifecycle commands (evaluate/tree/query/resolve/screenshot/focus/call/close)
|
|
168
|
+
// observational/lifecycle commands (evaluate/tree/query/resolve/screenshot/capture/focus/call/close)
|
|
169
169
|
// pass through without taking or checking a lease so cleanup never gets stuck
|
|
170
170
|
// behind a stale cli owner.
|
|
171
171
|
// `call` used to acquire a lease too, but most call paths are queries
|
|
@@ -228,7 +228,7 @@ export interface BridgeHostOptions {
|
|
|
228
228
|
portFallbackCount?: number
|
|
229
229
|
/** when true, write ~/.rnx/daemon.json on successful bind + update
|
|
230
230
|
* it on a heartbeat interval + remove it on close. only the standalone
|
|
231
|
-
* `
|
|
231
|
+
* `rnx serve` process should set this — tests, vite plugin, and
|
|
232
232
|
* anything embedded should leave it off to avoid clobbering a real
|
|
233
233
|
* daemon's lockfile. defaults to false. */
|
|
234
234
|
writeLockfile?: boolean
|
|
@@ -249,7 +249,7 @@ export interface BridgeHostOptions {
|
|
|
249
249
|
* resolves the daemon's own loopback host (localhost:<runtimePort>) to
|
|
250
250
|
* the dev `contrast.localhost:3000` stack, so a *prod* CLI user's preview
|
|
251
251
|
* recording would upload to a localhost stack that doesn't exist. the
|
|
252
|
-
* `
|
|
252
|
+
* `rnx serve` command resolves this with the same probe the
|
|
253
253
|
* upload itself uses (`resolveDefaultUploadOrigin`). */
|
|
254
254
|
contrastOrigin?: string
|
|
255
255
|
/** override the abandoned-sim GC TTL (ms). defaults to
|
|
@@ -325,7 +325,7 @@ const CAMERA_FIXTURE_NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/
|
|
|
325
325
|
* the same server on the same port, but a daemon whose preferred 7668
|
|
326
326
|
* was taken falls back to 7669+. without this the runtime page would
|
|
327
327
|
* hardcode the 7668 default in `resolveBridgePort` and register on a
|
|
328
|
-
* *different* daemon (or none), so `
|
|
328
|
+
* *different* daemon (or none), so `rnx open` times out waiting for
|
|
329
329
|
* a sim that connected to the wrong bridge.
|
|
330
330
|
* - `window.__sootsimContrastOrigin` — the contrast origin for auth /
|
|
331
331
|
* billing / preview uploads. the daemon serves runtimes from its own
|
|
@@ -400,8 +400,8 @@ export class SootSimBridgeHost {
|
|
|
400
400
|
private static USER_BOOT_LEASE_TTL_MS = 60_000
|
|
401
401
|
private static SIM_RECONNECT_TTL_MS = 30_000
|
|
402
402
|
// abandoned-tab GC. the ws heartbeat only reaps sims whose socket went
|
|
403
|
-
// dead — but a background tab from a prior `
|
|
404
|
-
// keeps its socket alive (the page still pongs) forever, so `
|
|
403
|
+
// dead — but a background tab from a prior `rnx open` / QA / test run
|
|
404
|
+
// keeps its socket alive (the page still pongs) forever, so `rnx list`
|
|
405
405
|
// accretes dozens of zombie sims that all time out on every command (QA
|
|
406
406
|
// F21-3, carried F19-2). reap a sim only when it is provably nobody's:
|
|
407
407
|
// not primary, not user-focused, no CLI attached, no active lease, and no
|
|
@@ -409,7 +409,7 @@ export class SootSimBridgeHost {
|
|
|
409
409
|
// terminal 4001 code, so the client closes the abandoned window instead
|
|
410
410
|
// of reconnecting the zombie straight back in.
|
|
411
411
|
private static SIM_IDLE_REAP_TTL_MS = 30 * 60_000
|
|
412
|
-
// automation sims (spawned by the playwright driver: `
|
|
412
|
+
// automation sims (spawned by the playwright driver: `rnx open --driver
|
|
413
413
|
// playwright`, the factory/screenshot capture path — `meta.sootsimHostDriver
|
|
414
414
|
// === 'playwright'`) are unattended one-shot browsers, never a human's tab. a
|
|
415
415
|
// capture run opens one per slide/app/retry and the 30-min human/dev TTL lets
|
|
@@ -440,7 +440,7 @@ export class SootSimBridgeHost {
|
|
|
440
440
|
// dropped, sim crashed) leave their server-side WebSocket sitting "open"
|
|
441
441
|
// forever. ping every WS_HEARTBEAT_INTERVAL_MS; if the previous round's
|
|
442
442
|
// ping was never answered, terminate(). that fires 'close' which runs the
|
|
443
|
-
// sim-cleanup path and stops `
|
|
443
|
+
// sim-cleanup path and stops `rnx list` from showing 8 zombie
|
|
444
444
|
// sims that all time out on every command.
|
|
445
445
|
private wsHeartbeatTimer: NodeJS.Timeout | null = null
|
|
446
446
|
private wsIsAlive = new WeakMap<WebSocket, boolean>()
|
|
@@ -462,7 +462,7 @@ export class SootSimBridgeHost {
|
|
|
462
462
|
constructor(opts: BridgeHostOptions = {}) {
|
|
463
463
|
this.preferredPort = opts.port || DEFAULT_SOOTSIM_BRIDGE_PORT
|
|
464
464
|
this.port = this.preferredPort
|
|
465
|
-
// default off — callers that want the lockfile (
|
|
465
|
+
// default off — callers that want the lockfile (rnx serve) opt in.
|
|
466
466
|
this.shouldWriteLockfile = opts.writeLockfile === true
|
|
467
467
|
this.shouldWriteDevLockfile = opts.writeDevLockfile === true
|
|
468
468
|
this.getShellPort = opts.getShellPort ?? null
|
|
@@ -1920,7 +1920,7 @@ export class SootSimBridgeHost {
|
|
|
1920
1920
|
this.reapIdleSims(now)
|
|
1921
1921
|
}
|
|
1922
1922
|
|
|
1923
|
-
// GC abandoned sims so `
|
|
1923
|
+
// GC abandoned sims so `rnx list` stays usable across long dev / QA /
|
|
1924
1924
|
// test sessions (F21-3). conservative on purpose: a sim is only reaped
|
|
1925
1925
|
// when every "someone cares about this" signal is absent.
|
|
1926
1926
|
//
|
|
@@ -1936,7 +1936,7 @@ export class SootSimBridgeHost {
|
|
|
1936
1936
|
// using is the primary and/or holds a fresh user-active lease; one that is
|
|
1937
1937
|
// none of those and idle past the TTL is abandoned regardless of a stale
|
|
1938
1938
|
// focus flag.
|
|
1939
|
-
// automation sims are spawned by the playwright driver (`
|
|
1939
|
+
// automation sims are spawned by the playwright driver (`rnx open
|
|
1940
1940
|
// --driver playwright`, the factory/screenshot capture path), which tags them
|
|
1941
1941
|
// with `meta.sootsimHostDriver === 'playwright'` via the host's addInitScript.
|
|
1942
1942
|
// they are unattended one-shot browsers, never a human's tab, so they get a
|
|
@@ -2551,6 +2551,8 @@ export class SootSimBridgeHost {
|
|
|
2551
2551
|
return 'evaluated page state'
|
|
2552
2552
|
case 'screenshot':
|
|
2553
2553
|
return 'captured screenshot'
|
|
2554
|
+
case 'capture':
|
|
2555
|
+
return 'captured app screen'
|
|
2554
2556
|
case 'tap':
|
|
2555
2557
|
return 'sent tap event'
|
|
2556
2558
|
case 'keyboard':
|
package/src/host/plane-host.ts
CHANGED
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
|
|
18
18
|
import fs from 'node:fs'
|
|
19
19
|
import path from 'node:path'
|
|
20
|
-
import { dispatchPlaneCommand, indexVersion } from '@
|
|
20
|
+
import { dispatchPlaneCommand, indexVersion } from '@rnx/box/plane'
|
|
21
21
|
import { CheckoutFilePlane } from '../../cli/commands/box/checkout-plane'
|
|
22
|
-
import type { ProjectFilePlane } from '@
|
|
22
|
+
import type { ProjectFilePlane } from '@rnx/box/plane'
|
|
23
23
|
import type { WebSocket } from 'ws'
|
|
24
24
|
|
|
25
25
|
const WS_OPEN = 1
|
package/src/index.ts
CHANGED
|
@@ -35,6 +35,27 @@ export type {
|
|
|
35
35
|
SimSemanticResolveSelector,
|
|
36
36
|
SimSemanticSelector,
|
|
37
37
|
} from './bridge-contract'
|
|
38
|
+
export {
|
|
39
|
+
isRnxCaptureRegionAtlas,
|
|
40
|
+
isRnxScreenCapture,
|
|
41
|
+
RNX_SCREEN_CAPTURE_VERSION,
|
|
42
|
+
type RnxCaptureAccessibility,
|
|
43
|
+
type RnxCaptureAccessibilityState,
|
|
44
|
+
type RnxCaptureAccessibilityValue,
|
|
45
|
+
type RnxCaptureAtlasRegion,
|
|
46
|
+
type RnxCaptureInteractionState,
|
|
47
|
+
type RnxCaptureNode,
|
|
48
|
+
type RnxCaptureRect,
|
|
49
|
+
type RnxCaptureRegionAtlas,
|
|
50
|
+
type RnxCaptureRegionRequest,
|
|
51
|
+
type RnxCaptureSource,
|
|
52
|
+
type RnxCaptureStyleTokenBinding,
|
|
53
|
+
type RnxCaptureTextLine,
|
|
54
|
+
type RnxCaptureTextSegment,
|
|
55
|
+
type RnxCaptureTree,
|
|
56
|
+
type RnxCaptureVisualEvidence,
|
|
57
|
+
type RnxScreenCapture,
|
|
58
|
+
} from './capture-contract'
|
|
38
59
|
|
|
39
60
|
export { rnxPlugin } from './vite-plugin-one'
|
|
40
61
|
export { default as metroPlugin } from './metro-plugin'
|
|
@@ -42,7 +42,7 @@ export function alignLoopbackBundleUrlWithProbe(
|
|
|
42
42
|
// unreliably on small CI runners (tens of seconds when metro is warm,
|
|
43
43
|
// minutes cold, sometimes past any wait-ready budget). a minified bundle
|
|
44
44
|
// parses/executes far faster and reboots reliably. gated on
|
|
45
|
-
// SOOTSIM_PREVIEW_PROD_BUNDLE so normal interactive `
|
|
45
|
+
// SOOTSIM_PREVIEW_PROD_BUNDLE so normal interactive `rnx open` keeps
|
|
46
46
|
// dev=true. app-agnostic: rewrites whatever metro/launchAsset advertised.
|
|
47
47
|
function applyPreviewProdBundle(parsed: URL): void {
|
|
48
48
|
if (!process.env.SOOTSIM_PREVIEW_PROD_BUNDLE) return
|
|
@@ -95,7 +95,7 @@ export function normalizeNativeDevBundleUrl(bundleUrl: string): string {
|
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
// explicit override used by `
|
|
98
|
+
// explicit override used by `rnx open` to flip metro's `hot` query when
|
|
99
99
|
// the caller knows their context: interactive opens want `hot=true` so metro
|
|
100
100
|
// includes its HMR/react-refresh runtime in the bundle prelude and pushes
|
|
101
101
|
// edits as `update` events; `--driver playwright` / agent-test runs want
|
|
@@ -20,7 +20,7 @@ export interface RunScreenshotsOverrides {
|
|
|
20
20
|
captureOnly?: boolean
|
|
21
21
|
composeOnly?: boolean
|
|
22
22
|
// copy/colors pulled from the team's org deck to overlay onto the plan's
|
|
23
|
-
// compose slides (
|
|
23
|
+
// compose slides (rnx screenshot appstore --org … --repo …).
|
|
24
24
|
orgDeckFrames?: OrgDeckFrame[] | null
|
|
25
25
|
}
|
|
26
26
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// fetch the team's screenshot deck (the copy / colors founders set up in the
|
|
2
2
|
// org dashboard) and overlay it onto a screenshots plan, so the programmatic
|
|
3
|
-
// `
|
|
3
|
+
// `rnx screenshot appstore --plan … --org … --repo …` flow renders the org's
|
|
4
4
|
// words + brand gradient instead of the local yaml's. the plan still owns
|
|
5
5
|
// which screen each slide captures and the device frame; the deck owns copy +
|
|
6
6
|
// colors. 3d scenes don't translate to the cli's 2d composer, so only copy and
|
package/src/settings-bundle.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFile, readdir, stat } from 'node:fs/promises'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
3
|
import { parsePlistSource } from './plist.ts'
|
|
4
|
-
import type { AppPreferenceValue, AppSettingsSpecifier } from '@
|
|
4
|
+
import type { AppPreferenceValue, AppSettingsSpecifier } from '@rnx/globals'
|
|
5
5
|
|
|
6
6
|
const MAX_PLIST_BYTES = 1_048_576
|
|
7
7
|
const MAX_CHILD_DEPTH = 8
|