peachsim 0.1.656 → 0.1.658

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 (111) hide show
  1. package/README.md +22 -3
  2. package/cli/app-config.ts +34 -15
  3. package/cli/app-project.ts +6 -6
  4. package/cli/bridge-flow-runner.ts +10 -23
  5. package/cli/commands/compat.ts +234 -10
  6. package/cli/commands/control.ts +20 -20
  7. package/cli/commands/do-chain.ts +5 -1
  8. package/cli/commands/flow.ts +3 -3
  9. package/cli/commands/inspect/actions.ts +33 -76
  10. package/cli/commands/inspect/resolve-target.ts +67 -2
  11. package/cli/commands/inspect.ts +16 -1
  12. package/cli/drivers/playwright-provisioning.ts +95 -0
  13. package/cli/ws-bridge.ts +7 -7
  14. package/detox/config-loader.ts +1 -1
  15. package/dist-lib/agent-daemon-client.cjs +1 -1
  16. package/dist-lib/agent-events.cjs +1 -1
  17. package/dist-lib/agent-identity.cjs +1 -1
  18. package/dist-lib/agent-sessions.cjs +1 -1
  19. package/dist-lib/attached-projects.cjs +1 -1
  20. package/dist-lib/auth/shared-session.cjs +1 -1
  21. package/dist-lib/backend-origin.cjs +1 -1
  22. package/dist-lib/beta.cjs +1 -1
  23. package/dist-lib/beta.mjs +1 -1
  24. package/dist-lib/bridge-constants.cjs +1 -1
  25. package/dist-lib/bridge-contract-input.cjs +9 -1
  26. package/dist-lib/bridge-contract-input.mjs +9 -1
  27. package/dist-lib/bridge-contract.cjs +1 -1
  28. package/dist-lib/bridge-contract.mjs +1 -1
  29. package/dist-lib/capture-contract.cjs +1 -1
  30. package/dist-lib/capture-contract.mjs +1 -1
  31. package/dist-lib/cli-constants.cjs +1 -1
  32. package/dist-lib/cloud-contract.cjs +1 -1
  33. package/dist-lib/cloud-contract.mjs +1 -1
  34. package/dist-lib/cloud-test-args.cjs +1 -1
  35. package/dist-lib/cloud-test-args.mjs +1 -1
  36. package/dist-lib/cloud.cjs +1 -1
  37. package/dist-lib/cloud.mjs +1 -1
  38. package/dist-lib/config.cjs +27 -26
  39. package/dist-lib/detox/index.cjs +1 -1
  40. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  41. package/dist-lib/home-paths.cjs +1 -1
  42. package/dist-lib/host/bridge-host.cjs +90 -47
  43. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  44. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  45. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  46. package/dist-lib/host/replacement-module-handler.cjs +51 -8
  47. package/dist-lib/host/websocket-proxy.cjs +1 -1
  48. package/dist-lib/index.cjs +317 -70
  49. package/dist-lib/jump-to-source-babel.cjs +1 -1
  50. package/dist-lib/jump-to-source-native.cjs +1 -1
  51. package/dist-lib/kotlin-host.cjs +8 -4
  52. package/dist-lib/menu.cjs +1 -1
  53. package/dist-lib/menu.mjs +1 -1
  54. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  55. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  56. package/dist-lib/metro-production-bundle.cjs +1 -1
  57. package/dist-lib/metro-production-bundle.mjs +1 -1
  58. package/dist-lib/metro.cjs +300 -29
  59. package/dist-lib/native-app-identity.cjs +58 -25
  60. package/dist-lib/native-app-identity.mjs +57 -25
  61. package/dist-lib/plist.cjs +64 -0
  62. package/dist-lib/plist.mjs +29 -0
  63. package/dist-lib/profiles.cjs +1 -1
  64. package/dist-lib/public-brand.cjs +1 -1
  65. package/dist-lib/react-native-host-modules.cjs +1 -1
  66. package/dist-lib/react-native-host-modules.mjs +1 -1
  67. package/dist-lib/render-mode.cjs +1 -1
  68. package/dist-lib/scripts/dev-server-scanner.cjs +14 -16
  69. package/dist-lib/sdk.cjs +36 -49
  70. package/dist-lib/sdk.mjs +36 -49
  71. package/dist-lib/skills.cjs +341 -198
  72. package/dist-lib/swift-compile.cjs +1 -1
  73. package/dist-lib/swift-compile.mjs +1 -1
  74. package/dist-lib/swift-package.cjs +1 -1
  75. package/dist-lib/swift-package.mjs +1 -1
  76. package/dist-lib/swift-submit.cjs +1 -1
  77. package/dist-lib/swift-submit.mjs +1 -1
  78. package/dist-lib/swift.cjs +1 -1
  79. package/dist-lib/vite-swift.cjs +1 -1
  80. package/dist-lib/vite.cjs +51 -8
  81. package/kotlin/AppSources/android/app/UiModeManager.kt +9 -0
  82. package/kotlin/AppSources/android/content/Context.kt +9 -2
  83. package/kotlin/AppSources/android/content/res/Configuration.kt +16 -0
  84. package/kotlin/AppSources/android/content/res/Resources.kt +7 -0
  85. package/kotlin/AppSources/android/graphics/Color.kt +13 -0
  86. package/kotlin/AppSources/android/view/View.kt +2 -0
  87. package/kotlin/AppSources/android/view/Window.kt +99 -8
  88. package/kotlin/AppSources/android/view/WindowInsetsController.kt +12 -0
  89. package/kotlin/AppSources/android/view/WindowManager.kt +16 -0
  90. package/kotlin/AppSources/androidx/activity/EdgeToEdge.kt +71 -0
  91. package/kotlin/AppSources/androidx/core/view/WindowCompat.kt +32 -0
  92. package/kotlin/AppSources/dev/peach/app/PeachApp.kt +16 -11
  93. package/kotlin/AppSources/dev/peach/app/PeachWindowHost.kt +2 -1
  94. package/kotlin/ComposeSources/PeachComposeHost.kt +11 -1
  95. package/package.json +8 -1
  96. package/scripts/dev-server-scanner.ts +3 -3
  97. package/scripts/optional-demo-registry.ts +1 -1
  98. package/skills/peach-setup/SKILL.md +27 -0
  99. package/src/android-theme.ts +167 -0
  100. package/src/android-xml.ts +70 -0
  101. package/src/bridge-contract-input.ts +10 -1
  102. package/src/bridge-contract.ts +7 -1
  103. package/src/config.ts +34 -26
  104. package/src/generated/android-framework-themes.json +6303 -0
  105. package/src/host/replacement-module-handler.ts +33 -0
  106. package/src/index.ts +1 -0
  107. package/src/kotlin-app.ts +75 -61
  108. package/src/kotlin-host.ts +7 -3
  109. package/src/metro-plugin.ts +46 -3
  110. package/src/native-app-identity.ts +75 -26
  111. package/src/sim-client.ts +2 -4
@@ -313,7 +313,7 @@ export async function runDoChain(
313
313
  const { SimPerformError } = simClientModule
314
314
  const { checkSimHealth, parseBridgeCliArgs } = bridgeModule
315
315
  const { isAgentEnv } = envModule
316
- const { resolveTargetCoords } = targetModule
316
+ const { offscreenTargetRefusal, resolveTargetCoords } = targetModule
317
317
  const wantsJson = args.includes('--json')
318
318
  const skipSettle = args.includes('--no-wait') || process.env.PEACH_NO_AUTO_WAIT === '1'
319
319
  const parsed = parseBridgeCliArgs(args, {
@@ -355,6 +355,10 @@ export async function runDoChain(
355
355
  `do chain could not find testID ${JSON.stringify(action.id)}`,
356
356
  )
357
357
  }
358
+ const refusal = offscreenTargetRefusal(sim.plane, target)
359
+ if (refusal) {
360
+ throw new BridgeInputError(`do chain #${action.id}: ${refusal}`)
361
+ }
358
362
  steps.push(
359
363
  action.type === 'targetedDoubleTap'
360
364
  ? {
@@ -7,7 +7,7 @@ import { tmpdir } from 'os'
7
7
  import * as path from 'path'
8
8
  import { scanDevServers } from '../../scripts/dev-server-scanner'
9
9
  import { loadOptionalDemoApps } from '../../scripts/optional-demo-registry'
10
- import { applyRNXConfigToUrl, type RNXConfig } from '../../src/config'
10
+ import { applyPeachConfigToUrl, type PeachConfig } from '../../src/config'
11
11
  import { printBridgeFailureDiagnostics } from '../bridge-diagnostics'
12
12
  import {
13
13
  SootSimBridgeFlowRunner,
@@ -176,7 +176,7 @@ export function getLastFlowTraceSteps(): SootSimFlowTraceStep[] {
176
176
  // port and runtime config into the open command.
177
177
  export async function resolveAppFrontmatterTarget(
178
178
  raw: string,
179
- ): Promise<{ target: string; runtimeConfig?: RNXConfig }> {
179
+ ): Promise<{ target: string; runtimeConfig?: PeachConfig }> {
180
180
  if (!raw) return { target: '' }
181
181
  const trimmed = raw.trim()
182
182
  if (!trimmed) return { target: '' }
@@ -531,7 +531,7 @@ flow extension:
531
531
  }
532
532
 
533
533
  if (openTarget && targetDevice) {
534
- const shellBaseUrl = applyRNXConfigToUrl(
534
+ const shellBaseUrl = applyPeachConfigToUrl(
535
535
  pinnedShellBaseUrl || resolveShellBaseUrlForBridgePort(parsedBridgeArgs.wsPort),
536
536
  targetAppResolution.runtimeConfig,
537
537
  )
@@ -1,5 +1,11 @@
1
1
  import { inspectTree, type InspectBridge } from './core'
2
- import { resolveTargetCoords, type ResolvedTargetCoords } from './resolve-target'
2
+ import {
3
+ isOffscreen,
4
+ isScreenSize,
5
+ resolveTargetCoords,
6
+ UNREPORTED_SCREEN_ERROR,
7
+ type ResolvedTargetCoords,
8
+ } from './resolve-target'
3
9
  import { waitForSootsimIdle } from './settling'
4
10
  import type { SimSemanticNode } from '../../../src/bridge-contract'
5
11
  import type {
@@ -39,54 +45,6 @@ export interface TapTimingOptions {
39
45
  retryWaitMs?: number
40
46
  }
41
47
 
42
- type ScreenSize = { width: number; height: number }
43
-
44
- function isScreenSize(value: unknown): value is ScreenSize {
45
- if (value === null || typeof value !== 'object') return false
46
- const width = Reflect.get(value, 'width')
47
- const height = Reflect.get(value, 'height')
48
- return (
49
- typeof width === 'number' &&
50
- Number.isFinite(width) &&
51
- width > 0 &&
52
- typeof height === 'number' &&
53
- Number.isFinite(height) &&
54
- height > 0
55
- )
56
- }
57
-
58
- function viewportFromCapture(value: unknown): unknown {
59
- if (value === null || typeof value !== 'object') return undefined
60
- const tree = Reflect.get(value, 'tree')
61
- return tree !== null && typeof tree === 'object'
62
- ? Reflect.get(tree, 'viewport')
63
- : undefined
64
- }
65
-
66
- // the semantic tree has no covering root, so its first node is often a 64x64
67
- // icon. capture.tree.viewport is the device layout size on local and cloud.
68
- //
69
- // a whole screen capture is the only way to ask for it today, and it is not
70
- // cheap: the headless runtime forces a full redraw of the tree before it walks
71
- // it, which measured 3.9-5.5s on a real app in a cloud box against 120-290ms
72
- // for every other command. that is why this runs on the failure path only.
73
- async function readScreen(bridge: InspectBridge): Promise<ScreenSize | null> {
74
- try {
75
- const viewport = viewportFromCapture(
76
- await bridge.send({ type: 'capture', includeVisual: false }),
77
- )
78
- return isScreenSize(viewport)
79
- ? { width: viewport.width, height: viewport.height }
80
- : null
81
- } catch {
82
- return null
83
- }
84
- }
85
-
86
- function isOffscreen(cx: number, cy: number, screen: ScreenSize): boolean {
87
- return !(cx >= 0 && cy >= 0 && cx <= screen.width && cy <= screen.height)
88
- }
89
-
90
48
  const DEFAULT_AGENT_TIMING: Required<TapTimingOptions> = {
91
49
  initialWaitMs: 3000,
92
50
  deadlineMs: 5000,
@@ -185,19 +143,24 @@ export async function tapResolvedTarget(
185
143
 
186
144
  const deadline = Date.now() + timing.deadlineMs
187
145
  let lastOffscreenAt: string | null = null
188
- // a cloud sim refuses a point outside its screen itself, and says so, so
189
- // asking it for a capture first would buy nothing. every other plane still
190
- // presses whatever the hit test finds at tree coordinates no finger can
191
- // reach, so there the screen has to be read before dispatching.
192
- const screen = bridge.plane === 'cloud' ? null : await readScreen(bridge)
193
146
  while (Date.now() <= deadline || attempts === 0) {
194
147
  attempts++
195
- const resolved = await args.resolve()
148
+ const { screen, ...resolved } = (await args.resolve()) ?? {}
149
+ // a cloud sim refuses a point outside its screen itself, and says so. every
150
+ // other plane still presses whatever the hit test finds at tree
151
+ // coordinates no finger can reach, so the resolved target's screen decides
152
+ // before dispatching. a runtime too old to report it cannot be checked; a
153
+ // zero screen is one not laid out yet, so the attempt waits and retries.
154
+ const checksScreen = bridge.plane !== 'cloud' && typeof resolved.cx === 'number'
155
+ if (checksScreen && screen === undefined) {
156
+ throw new Error(UNREPORTED_SCREEN_ERROR)
157
+ }
158
+ const unsized = checksScreen && !isScreenSize(screen)
196
159
  const payload =
197
- resolved &&
160
+ checksScreen &&
198
161
  typeof resolved.cx === 'number' &&
199
162
  typeof resolved.cy === 'number' &&
200
- screen &&
163
+ isScreenSize(screen) &&
201
164
  isOffscreen(resolved.cx, resolved.cy, screen)
202
165
  ? { ...resolved, offscreen: true, screen }
203
166
  : resolved
@@ -215,7 +178,10 @@ export async function tapResolvedTarget(
215
178
  // don't dispatch a point outside the device screen: the press would do
216
179
  // nothing a user could reproduce. report the offscreen miss instead so
217
180
  // drivers scroll first rather than getting stuck trusting a phantom hit.
218
- if (payload.offscreen) {
181
+ if (unsized) {
182
+ lastResult = { hit: false, reason: 'screen-not-laid-out' }
183
+ lastOffscreenAt = null
184
+ } else if (payload.offscreen) {
219
185
  lastResult = {
220
186
  hit: false,
221
187
  reason: 'offscreen',
@@ -310,6 +276,7 @@ function payloadFromResolved(
310
276
  return {
311
277
  cx: resolved.x,
312
278
  cy: resolved.y,
279
+ screen: resolved.viewport,
313
280
  match: {
314
281
  nodeId: match.nodeId ?? null,
315
282
  id: match.testID ?? null,
@@ -489,25 +456,14 @@ async function resolveTextTapTarget(
489
456
  if (!picked) return { matched: 0, total }
490
457
  const node = picked.node
491
458
  // keep the selected handle: re-searching its text or testID can pick a
492
- // different control, including a covered tenant behind native UI.
493
- const tap: unknown = await bridge.send({
494
- type: 'call',
495
- path: 'SootSim.bridges.test.resolveTapTarget',
496
- args: [node.nodeId],
459
+ // different control, including a covered tenant behind native UI. resolve by
460
+ // node is a read, so a remote simulator's driving token can send it.
461
+ const resolved = await resolveTargetCoords(bridge, {
462
+ mode: 'node',
463
+ nodeId: node.nodeId,
497
464
  })
498
- if (!isTapTargetInfo(tap)) return { matched: 0, total }
499
- return payloadFromResolved(
500
- {
501
- x: tap.cx,
502
- y: tap.cy,
503
- nodeId: tap.target.nodeId,
504
- testID: tap.target.testID,
505
- text: tap.target.text ?? tap.target.accessibilityLabel,
506
- type: tap.target.type,
507
- },
508
- node,
509
- { total, idx },
510
- )
465
+ if (!resolved) return { matched: 0, total }
466
+ return payloadFromResolved(resolved, node, { total, idx })
511
467
  }
512
468
 
513
469
  export type TapQuery =
@@ -567,6 +523,7 @@ export async function tapByQuery(
567
523
  testID: tap.target.testID,
568
524
  text: tap.target.text ?? tap.target.accessibilityLabel,
569
525
  type: tap.target.type,
526
+ viewport: tap.viewport,
570
527
  },
571
528
  {
572
529
  nodeId: node.nodeId,
@@ -35,16 +35,69 @@ export interface ResolvedTargetCoords {
35
35
  testID?: string | null
36
36
  text?: string | null
37
37
  type?: string | null
38
+ // absent from runtimes older than the field; coordinate-only callers do not
39
+ // need it, and the tap boundary refuses to guess without it.
40
+ viewport?: { width: number; height: number }
41
+ }
42
+
43
+ export type ScreenSize = { width: number; height: number }
44
+
45
+ // a reported screen with a zero or non-finite side has not been laid out yet.
46
+ export function isScreenSize(value: unknown): value is ScreenSize {
47
+ if (value === null || typeof value !== 'object') return false
48
+ const width = Reflect.get(value, 'width')
49
+ const height = Reflect.get(value, 'height')
50
+ return (
51
+ typeof width === 'number' &&
52
+ Number.isFinite(width) &&
53
+ width > 0 &&
54
+ typeof height === 'number' &&
55
+ Number.isFinite(height) &&
56
+ height > 0
57
+ )
58
+ }
59
+
60
+ export function isOffscreen(
61
+ x: number,
62
+ y: number,
63
+ screen: { width: number; height: number },
64
+ ): boolean {
65
+ return !(x >= 0 && y >= 0 && x <= screen.width && y <= screen.height)
66
+ }
67
+
68
+ export const UNREPORTED_SCREEN_ERROR =
69
+ 'this runtime does not report the screen size a targeted gesture needs; ' +
70
+ 'update it with `peach runtime install`, or use coordinates'
71
+
72
+ // a gesture at a resolved point outside the device screen presses whatever the
73
+ // hit test finds where no finger can reach. a cloud sim refuses such a point
74
+ // itself; every other plane checks here. returns the refusal, or null to send.
75
+ export function offscreenTargetRefusal(
76
+ plane: InspectBridge['plane'],
77
+ target: ResolvedTargetCoords,
78
+ ): string | null {
79
+ if (plane === 'cloud') return null
80
+ const screen = target.viewport
81
+ if (!screen) return UNREPORTED_SCREEN_ERROR
82
+ if (!isScreenSize(screen)) {
83
+ return 'the screen is not laid out yet, so the target cannot be checked; run the command again'
84
+ }
85
+ if (!isOffscreen(target.x, target.y, screen)) return null
86
+ return `the target at (${Math.round(target.x)},${Math.round(target.y)}) is outside the ${screen.width}x${screen.height} screen; scroll it into view first`
38
87
  }
39
88
 
40
89
  export async function resolveTargetCoords(
41
90
  bridge: InspectBridge,
42
- target: { mode: 'testid' | 'text'; value: string },
91
+ target: { mode: 'testid' | 'text'; value: string } | { mode: 'node'; nodeId: number },
43
92
  ): Promise<ResolvedTargetCoords | null> {
44
93
  const result: unknown = await bridge.send({
45
94
  type: 'resolve',
46
95
  selector:
47
- target.mode === 'testid' ? { testID: target.value } : { text: target.value },
96
+ target.mode === 'node'
97
+ ? { nodeId: target.nodeId }
98
+ : target.mode === 'testid'
99
+ ? { testID: target.value }
100
+ : { text: target.value },
48
101
  })
49
102
  if (result === null) return null
50
103
  if (typeof result !== 'object') throw new Error('resolve returned an invalid result')
@@ -52,6 +105,15 @@ export async function resolveTargetCoords(
52
105
  const resolvedTarget = Reflect.get(result, 'target')
53
106
  const semanticNodes = [match, resolvedTarget]
54
107
  const point = Reflect.get(result, 'point')
108
+ const viewport = Reflect.get(result, 'viewport')
109
+ const width =
110
+ viewport !== null && typeof viewport === 'object'
111
+ ? Reflect.get(viewport, 'width')
112
+ : undefined
113
+ const height =
114
+ viewport !== null && typeof viewport === 'object'
115
+ ? Reflect.get(viewport, 'height')
116
+ : undefined
55
117
  const x =
56
118
  point !== null && typeof point === 'object' ? Reflect.get(point, 'x') : undefined
57
119
  const y =
@@ -75,5 +137,8 @@ export async function resolveTargetCoords(
75
137
  text:
76
138
  semanticMatch.text ?? (target.mode === 'text' ? target.value : semanticMatch.label),
77
139
  type: semanticMatch.type,
140
+ ...(typeof width === 'number' && typeof height === 'number'
141
+ ? { viewport: { width, height } }
142
+ : {}),
78
143
  }
79
144
  }
@@ -75,7 +75,11 @@ import {
75
75
  redactKeyboardStateForOutput,
76
76
  redactTextForKeyboardState,
77
77
  } from './inspect/redaction'
78
- import { readTargetFlag, resolveTargetCoords } from './inspect/resolve-target'
78
+ import {
79
+ offscreenTargetRefusal,
80
+ readTargetFlag,
81
+ resolveTargetCoords,
82
+ } from './inspect/resolve-target'
79
83
  import { runScreensSubcommand } from './inspect/screens'
80
84
  import { runSettleSubcommand } from './inspect/settle'
81
85
  import { waitForSootsimIdle } from './inspect/settling'
@@ -2006,6 +2010,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
2006
2010
  return {
2007
2011
  cx: resolved.x,
2008
2012
  cy: resolved.y,
2013
+ screen: resolved.viewport,
2009
2014
  match: {
2010
2015
  nodeId: resolved.nodeId,
2011
2016
  testID:
@@ -2798,6 +2803,11 @@ export async function runInspect(args: string[], opts: InspectOptions) {
2798
2803
  await reportMissingTarget(bridge, 'double-tap', target)
2799
2804
  rnxExit(1)
2800
2805
  }
2806
+ const refusal = offscreenTargetRefusal(bridge.plane, resolved)
2807
+ if (refusal) {
2808
+ console.error(` double-tap: ${refusal}`)
2809
+ rnxExit(1)
2810
+ }
2801
2811
  x = resolved.x
2802
2812
  y = resolved.y
2803
2813
  }
@@ -2838,6 +2848,11 @@ export async function runInspect(args: string[], opts: InspectOptions) {
2838
2848
  await reportMissingTarget(bridge, 'long-press', target)
2839
2849
  rnxExit(1)
2840
2850
  }
2851
+ const refusal = offscreenTargetRefusal(bridge.plane, resolved)
2852
+ if (refusal) {
2853
+ console.error(` long-press: ${refusal}`)
2854
+ rnxExit(1)
2855
+ }
2841
2856
  x = resolved.x
2842
2857
  y = resolved.y
2843
2858
  resolvedTarget = {
@@ -161,6 +161,8 @@ export function ensurePlaywrightBrowser(
161
161
  const before = probeBrowser(resolved, probeEnv)
162
162
  if (!before.ok) return before
163
163
  if (before.exists) {
164
+ const missing = missingLibrariesMessage(before.executablePath, browser)
165
+ if (missing) return { ok: false, message: missing }
164
166
  return { ok: true, executablePath: before.executablePath, installed: false }
165
167
  }
166
168
 
@@ -209,5 +211,98 @@ export function ensurePlaywrightBrowser(
209
211
  `${browser} executable is still missing at ${after.executablePath}`,
210
212
  }
211
213
  }
214
+ const missing = missingLibrariesMessage(after.executablePath, browser)
215
+ if (missing) return { ok: false, message: missing }
212
216
  return { ok: true, executablePath: after.executablePath, installed: true }
213
217
  }
218
+
219
+ // the debian package providing each shared library chromium and its headless
220
+ // shell link (readelf NEEDED, linux x64 and arm64), apart from glibc's own.
221
+ const LIBRARY_PACKAGES: Record<string, string> = {
222
+ 'libasound.so.2': 'libasound2',
223
+ 'libatk-1.0.so.0': 'libatk1.0-0',
224
+ 'libatk-bridge-2.0.so.0': 'libatk-bridge2.0-0',
225
+ 'libatspi.so.0': 'libatspi2.0-0',
226
+ 'libcairo.so.2': 'libcairo2',
227
+ 'libcups.so.2': 'libcups2',
228
+ 'libdbus-1.so.3': 'libdbus-1-3',
229
+ 'libexpat.so.1': 'libexpat1',
230
+ 'libgbm.so.1': 'libgbm1',
231
+ 'libgio-2.0.so.0': 'libglib2.0-0',
232
+ 'libglib-2.0.so.0': 'libglib2.0-0',
233
+ 'libgobject-2.0.so.0': 'libglib2.0-0',
234
+ 'libnspr4.so': 'libnspr4',
235
+ 'libnss3.so': 'libnss3',
236
+ 'libnssutil3.so': 'libnss3',
237
+ 'libpango-1.0.so.0': 'libpango-1.0-0',
238
+ 'libsmime3.so': 'libnss3',
239
+ 'libudev.so.1': 'libudev1',
240
+ 'libX11.so.6': 'libx11-6',
241
+ 'libxcb.so.1': 'libxcb1',
242
+ 'libXcomposite.so.1': 'libxcomposite1',
243
+ 'libXdamage.so.1': 'libxdamage1',
244
+ 'libXext.so.6': 'libxext6',
245
+ 'libXfixes.so.3': 'libxfixes3',
246
+ 'libxkbcommon.so.0': 'libxkbcommon0',
247
+ 'libXrandr.so.2': 'libxrandr2',
248
+ }
249
+
250
+ // packages renamed by the 64-bit time_t transition in ubuntu 24.04 and debian 13.
251
+ const TIME64_PACKAGES = new Set([
252
+ 'libasound2',
253
+ 'libatk1.0-0',
254
+ 'libatk-bridge2.0-0',
255
+ 'libatspi2.0-0',
256
+ 'libcups2',
257
+ 'libglib2.0-0',
258
+ ])
259
+
260
+ function isTime64Distro(): boolean {
261
+ if (!existsSync('/etc/os-release')) return false
262
+ const fields = Object.fromEntries(
263
+ readFileSync('/etc/os-release', 'utf8')
264
+ .split('\n')
265
+ .map((line) => line.match(/^(\w+)="?([^"]*)"?$/))
266
+ .filter((match) => match !== null)
267
+ .map((match) => [match[1], match[2]]),
268
+ )
269
+ const version = Number.parseFloat(fields.VERSION_ID ?? '')
270
+ return (
271
+ (fields.ID === 'ubuntu' && version >= 24.04) ||
272
+ (fields.ID === 'debian' && version >= 13)
273
+ )
274
+ }
275
+
276
+ /** a linux browser whose shared libraries are missing exits 127 before
277
+ * playwright can say why, so name them and their packages up front, as
278
+ * playwright's own host validation does. */
279
+ function missingLibrariesMessage(executablePath: string, browser: string): string | null {
280
+ if (process.platform !== 'linux') return null
281
+ const ldd = spawnSync('ldd', [executablePath], { encoding: 'utf8', timeout: 30_000 })
282
+ const libraries = [...(ldd.stdout ?? '').matchAll(/^\s*(\S+) => not found$/gm)].map(
283
+ (match) => match[1]!,
284
+ )
285
+ if (!libraries.length) return null
286
+ const time64 = isTime64Distro()
287
+ const packages = new Set<string>()
288
+ const unknown: string[] = []
289
+ for (const library of libraries) {
290
+ const name = LIBRARY_PACKAGES[library]
291
+ if (!name) unknown.push(library)
292
+ else packages.add(time64 && TIME64_PACKAGES.has(name) ? `${name}t64` : name)
293
+ }
294
+ const sudo = process.getuid?.() === 0 ? '' : 'sudo '
295
+ return [
296
+ `${browser} at ${executablePath} cannot start: this system is missing shared libraries it links (${libraries.join(', ')}).`,
297
+ ...(packages.size
298
+ ? [
299
+ 'On Debian or Ubuntu, install them and run the command again:',
300
+ '',
301
+ ` ${sudo}apt-get update && ${sudo}apt-get install -y --no-install-recommends ${[...packages].join(' ')}`,
302
+ ]
303
+ : []),
304
+ ...(unknown.length
305
+ ? ['', `No known Debian package provides: ${unknown.join(', ')}`]
306
+ : []),
307
+ ].join('\n')
308
+ }
package/cli/ws-bridge.ts CHANGED
@@ -24,7 +24,6 @@ import {
24
24
  } from '../src/home-paths'
25
25
  import { createCloudBridgeForParsed } from './cloud-client'
26
26
  import {
27
- clearCurrentSimId,
28
27
  getCliIdentity,
29
28
  readCurrentSim,
30
29
  readCurrentSimId,
@@ -825,13 +824,14 @@ export function createBridge(wsPort: number, opts: WsBridgeOptions = {}): WsBrid
825
824
  )
826
825
  return sendOnce(successor)
827
826
  }
828
- clearCurrentSimId()
829
- // the saved sim is gone and nothing proves what replaced it. fail
830
- // closed instead of driving whichever sim is primary; that is how
831
- // agents end up recording the wrong app.
827
+ // the saved sim is not connected and nothing proves what replaced
828
+ // it. fail closed instead of driving whichever sim is primary; that
829
+ // is how agents end up recording the wrong app. the pin stays: a
830
+ // page whose socket dropped reconnects under the same id, and
831
+ // clearing the pin then would hand every later command to primary.
832
832
  throw new Error(
833
- `saved sim ${sentSimId} is gone${message.slice(missingSimMessage.length)}; ` +
834
- 'run `peach list` and ' +
833
+ `saved sim ${sentSimId} is not connected${message.slice(missingSimMessage.length)}; ` +
834
+ 'commands resume when it reconnects, or run `peach list` and ' +
835
835
  `\`peach use <sim>\`, or open a fresh sim`,
836
836
  )
837
837
  }
@@ -121,7 +121,7 @@ export function mapDetoxDevice(config: DetoxConfig | null): string {
121
121
  }
122
122
 
123
123
  // generate a sootsim-compatible config from detox config
124
- export function toRNXConfig(
124
+ export function toPeachConfig(
125
125
  projectDir: string,
126
126
  config: DetoxConfig | null,
127
127
  ): SootSimDetoxConfig {
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.656 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.658 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.656 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.658 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.656 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.658 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.656 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.658 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.656 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.658 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.656 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.658 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __create = Object.create;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.656 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.658 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
package/dist-lib/beta.cjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.656 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.658 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
package/dist-lib/beta.mjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.656 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.658 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
 
3
3
  // src/beta.ts
4
4
  var IS_BETA = true;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.656 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.658 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.656 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.658 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -358,6 +358,14 @@ function semanticResolveSelector(value, path) {
358
358
  return validationFailure(path, "must select testID or text");
359
359
  }
360
360
  function parseSemanticResolveSelector(value) {
361
+ const selector = recordValue(value, "selector");
362
+ if (selector.nodeId !== void 0) {
363
+ const nodeId = numberValue(selector.nodeId, "selector.nodeId");
364
+ if (!Number.isSafeInteger(nodeId)) {
365
+ return validationFailure("selector.nodeId", "must be an integer");
366
+ }
367
+ return { nodeId };
368
+ }
361
369
  return semanticResolveSelector(value, "selector");
362
370
  }
363
371
  function parseSemanticWaitOptions(value) {
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.656 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.658 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
 
3
3
  // src/bridge-contract.ts
4
4
  var SIM_LONG_PRESS_MAX_MS = 5e3;
@@ -322,6 +322,14 @@ function semanticResolveSelector(value, path) {
322
322
  return validationFailure(path, "must select testID or text");
323
323
  }
324
324
  function parseSemanticResolveSelector(value) {
325
+ const selector = recordValue(value, "selector");
326
+ if (selector.nodeId !== void 0) {
327
+ const nodeId = numberValue(selector.nodeId, "selector.nodeId");
328
+ if (!Number.isSafeInteger(nodeId)) {
329
+ return validationFailure("selector.nodeId", "must be an integer");
330
+ }
331
+ return { nodeId };
332
+ }
325
333
  return semanticResolveSelector(value, "selector");
326
334
  }
327
335
  function parseSemanticWaitOptions(value) {
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.656 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.658 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.656 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.658 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
 
3
3
  // src/bridge-contract.ts
4
4
  var REFUSED_PERFORM_STEP_TYPES = [
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.656 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.658 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.656 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.658 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
 
3
3
  // src/capture-contract.ts
4
4
  var RNX_SCREEN_CAPTURE_VERSION = 1;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.656 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.658 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.656 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.658 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
  let __sootsim_import_meta_url = ''; try { __sootsim_import_meta_url = require('url').pathToFileURL(__filename).href; } catch {}
3
3
  "use strict";
4
4
  var __defProp = Object.defineProperty;