peachsim 0.1.630 → 0.1.632

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 (96) hide show
  1. package/README.md +9 -1
  2. package/cli/app-fonts.ts +23 -4
  3. package/cli/bridge-flow-runner.ts +35 -28
  4. package/cli/command-registry.ts +1 -0
  5. package/cli/commands/compat.ts +11 -60
  6. package/cli/commands/control.ts +12 -5
  7. package/cli/commands/detect.ts +289 -26
  8. package/cli/commands/inspect/core.ts +1 -1
  9. package/cli/commands/inspect.ts +4 -0
  10. package/cli/commands/maestro.ts +8 -2
  11. package/cli/commands/project-report.ts +386 -0
  12. package/cli/commands/projectReportContract.ts +42 -0
  13. package/cli/commands/report-issue.ts +38 -54
  14. package/cli/commands/setup.ts +6 -4
  15. package/cli/commands/telemetry.ts +8 -3
  16. package/cli/commands/test.ts +374 -133
  17. package/cli/drivers/playwright-core-pin.ts +10 -0
  18. package/cli/drivers/playwright-provisioning.ts +36 -1
  19. package/cli/drivers/playwright.ts +26 -19
  20. package/cli/main.ts +11 -1
  21. package/cli/outbound-endpoints.ts +2 -1
  22. package/cli/privacy.ts +1 -2
  23. package/cli/runtime-diagnostics.ts +51 -0
  24. package/cli/shell-init.ts +6 -1
  25. package/cli/telemetry.ts +78 -7
  26. package/cli/ws-bridge.ts +8 -2
  27. package/detox/index.ts +3 -8
  28. package/dist-lib/agent-daemon-client.cjs +1 -1
  29. package/dist-lib/agent-events.cjs +1 -1
  30. package/dist-lib/agent-identity.cjs +1 -1
  31. package/dist-lib/agent-sessions.cjs +1 -1
  32. package/dist-lib/attached-projects.cjs +1 -1
  33. package/dist-lib/auth/shared-session.cjs +1 -1
  34. package/dist-lib/backend-origin.cjs +1 -1
  35. package/dist-lib/beta.cjs +1 -1
  36. package/dist-lib/beta.mjs +1 -1
  37. package/dist-lib/bridge-constants.cjs +1 -1
  38. package/dist-lib/bridge-contract-input.cjs +1 -1
  39. package/dist-lib/bridge-contract-input.mjs +1 -1
  40. package/dist-lib/bridge-contract.cjs +1 -1
  41. package/dist-lib/bridge-contract.mjs +1 -1
  42. package/dist-lib/capture-contract.cjs +1 -1
  43. package/dist-lib/capture-contract.mjs +1 -1
  44. package/dist-lib/cli-constants.cjs +1 -1
  45. package/dist-lib/cloud-contract.cjs +1 -1
  46. package/dist-lib/cloud-contract.mjs +1 -1
  47. package/dist-lib/cloud-test-args.cjs +17 -5
  48. package/dist-lib/cloud-test-args.mjs +17 -5
  49. package/dist-lib/cloud.cjs +1 -1
  50. package/dist-lib/cloud.mjs +1 -1
  51. package/dist-lib/config.cjs +1 -1
  52. package/dist-lib/detox/index.cjs +2 -7
  53. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  54. package/dist-lib/home-paths.cjs +13 -5
  55. package/dist-lib/host/bridge-host.cjs +157 -113
  56. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  57. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  58. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  59. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  60. package/dist-lib/host/websocket-proxy.cjs +1 -1
  61. package/dist-lib/index.cjs +11 -2
  62. package/dist-lib/jump-to-source-babel.cjs +1 -1
  63. package/dist-lib/jump-to-source-native.cjs +1 -1
  64. package/dist-lib/kotlin-host.cjs +1 -1
  65. package/dist-lib/menu.cjs +11 -1
  66. package/dist-lib/menu.mjs +11 -1
  67. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  68. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  69. package/dist-lib/metro-production-bundle.cjs +1 -1
  70. package/dist-lib/metro-production-bundle.mjs +1 -1
  71. package/dist-lib/metro.cjs +1 -1
  72. package/dist-lib/native-app-identity.cjs +1 -1
  73. package/dist-lib/native-app-identity.mjs +1 -1
  74. package/dist-lib/profiles.cjs +1 -1
  75. package/dist-lib/public-brand.cjs +1 -1
  76. package/dist-lib/react-native-host-modules.cjs +1 -1
  77. package/dist-lib/react-native-host-modules.mjs +1 -1
  78. package/dist-lib/render-mode.cjs +1 -1
  79. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  80. package/dist-lib/sdk.cjs +3 -1
  81. package/dist-lib/sdk.mjs +2 -1
  82. package/dist-lib/skills.cjs +700 -329
  83. package/dist-lib/swift-compile.cjs +1 -1
  84. package/dist-lib/swift-compile.mjs +1 -1
  85. package/dist-lib/swift-package.cjs +1 -1
  86. package/dist-lib/swift-package.mjs +1 -1
  87. package/dist-lib/swift-submit.cjs +1 -1
  88. package/dist-lib/swift-submit.mjs +1 -1
  89. package/dist-lib/swift.cjs +443 -173
  90. package/dist-lib/vite-swift.cjs +1 -1
  91. package/dist-lib/vite.cjs +1 -1
  92. package/package.json +2 -1
  93. package/skills/contrast/SKILL.md +1 -0
  94. package/src/cloud-test-args.ts +18 -6
  95. package/src/home-paths.ts +19 -9
  96. package/src/menu.ts +11 -0
@@ -1,8 +1,8 @@
1
1
  // playwright driver — launches Playwright's Google Chrome for Testing via the
2
- // `playwright` package. availability depends on whether the package is
3
- // resolvable from the current working directory OR from the CLI's own
4
- // install tree (repo-local install is typical; the latter covers a global
5
- // CLI invoked from a directory with no node_modules).
2
+ // `playwright` package. the package resolves from the current working
3
+ // directory, then the CLI's own install tree, then the pinned playwright-core
4
+ // the CLI provisions into its data dir on first launch, so the driver is
5
+ // always available.
6
6
  //
7
7
  // the browser runs in a *detached* child process — this CLI re-invoked as
8
8
  // its own `__internal playwright-host` (see cli/internal-child.ts) — rather
@@ -22,7 +22,13 @@ import { ensureProfile, playwrightProfileUserDataDir } from '../../src/profiles'
22
22
  import { RNX_INTERNAL_CHILDREN, RNX_INTERNAL_COMMAND } from '../internal-child'
23
23
  import { getProcessComm } from '../parent-pid'
24
24
  import { rnxSelfInvocation } from '../self-invocation'
25
- import { ensurePlaywrightBrowser, resolvePlaywright } from './playwright-provisioning'
25
+ import { PINNED_PLAYWRIGHT_CORE } from './playwright-core-pin'
26
+ import {
27
+ ensurePlaywrightBrowser,
28
+ provisionPlaywright,
29
+ resolvePlaywright,
30
+ type ResolvedPlaywright,
31
+ } from './playwright-provisioning'
26
32
  import type {
27
33
  Driver,
28
34
  DriverAvailability,
@@ -32,16 +38,12 @@ import type {
32
38
 
33
39
  function probe(): DriverAvailability {
34
40
  const resolved = resolvePlaywright()
35
- if (!resolved) {
36
- return {
37
- available: false,
38
- reason: 'playwright not installed in the current workspace',
39
- }
40
- }
41
41
  return {
42
42
  available: true,
43
43
  reason: null,
44
- detail: `resolved via ${resolved.spec}@${resolved.version}`,
44
+ detail: resolved
45
+ ? `resolved via ${resolved.spec}@${resolved.version}`
46
+ : `installs playwright-core@${PINNED_PLAYWRIGHT_CORE.version} on first launch`,
45
47
  }
46
48
  }
47
49
 
@@ -75,13 +77,6 @@ function resolveUserDataDir(opts: DriverLaunchOptions): string {
75
77
  }
76
78
 
77
79
  async function launch(opts: DriverLaunchOptions): Promise<DriverLaunchResult> {
78
- const resolved = resolvePlaywright()
79
- if (!resolved) {
80
- return {
81
- launched: false,
82
- message: 'playwright not installed — run `bun add -D playwright` first',
83
- }
84
- }
85
80
  if (!opts.url) {
86
81
  return { launched: false, message: 'playwright driver requires a target url' }
87
82
  }
@@ -93,6 +88,18 @@ async function launch(opts: DriverLaunchOptions): Promise<DriverLaunchResult> {
93
88
  }
94
89
  }
95
90
 
91
+ let resolved: ResolvedPlaywright
92
+ try {
93
+ resolved = await provisionPlaywright()
94
+ } catch (error) {
95
+ return {
96
+ launched: false,
97
+ message: `playwright provisioning failed: ${
98
+ error instanceof Error ? error.message : String(error)
99
+ }`,
100
+ }
101
+ }
102
+
96
103
  // keep browser discovery, installation, and the detached host on one exact
97
104
  // cache contract even if another caller mutates process.env during launch.
98
105
  const playwrightEnv = { ...process.env }
package/cli/main.ts CHANGED
@@ -22,7 +22,10 @@ import { IS_STANDALONE } from './standalone'
22
22
  // "command timed out after 10s" message and is uncopyable for a bug report.
23
23
  // rejections that don't match this shape still get the verbose stack so we
24
24
  // don't paper over real bugs.
25
- function handleTopLevelError(err: unknown): never {
25
+ async function handleTopLevelError(err: unknown): Promise<never> {
26
+ const { trackCliError, flushCliTelemetry } = await import('./telemetry')
27
+ trackCliError(err)
28
+ await flushCliTelemetry()
26
29
  const msg = err instanceof Error ? err.message : String(err)
27
30
  // peach's known top-level errors are all single-line, narrow strings.
28
31
  // everything else gets the full stack so unexpected crashes still surface.
@@ -595,6 +598,11 @@ try {
595
598
  break
596
599
  }
597
600
 
601
+ case 'project-report': {
602
+ const { runProjectReport } = await import('./commands/project-report')
603
+ await exitWithFlush(runProjectReport(parsed.commandArgs))
604
+ }
605
+
598
606
  case 'report-issue': {
599
607
  const { runReportIssue } = await import('./commands/report-issue')
600
608
  const code = await runReportIssue(parsed.commandArgs)
@@ -714,5 +722,7 @@ try {
714
722
  throw error
715
723
  }
716
724
 
725
+ const { flushCliTelemetry } = await import('./telemetry')
726
+ await flushCliTelemetry()
717
727
  await scheduleAutomaticCliUpdate(0)
718
728
  await scheduleAutomaticCleanup()
@@ -38,7 +38,8 @@ export const OUTBOUND_ENDPOINTS: Record<string, OutboundEndpointDeclaration> = {
38
38
  category: 'bounded-diagnostics',
39
39
  destination: 'configured PostHog ingestion host',
40
40
  preConsent: false,
41
- payload: 'package name/version/unsupported API or error class only',
41
+ payload:
42
+ 'anonymous install id, CLI/runtime versions, OS/arch, command name, package compatibility fields, error class and bounded path-redacted first-line error/warning messages',
42
43
  },
43
44
  contrast_user_action: {
44
45
  category: 'explicit-user-action',
package/cli/privacy.ts CHANGED
@@ -23,7 +23,6 @@ export function diagnosticsAllowedForCurrentProcess(
23
23
  ): boolean {
24
24
  const doNotTrack = process.env.DO_NOT_TRACK
25
25
  if (doNotTrack === '1' || doNotTrack === 'true') return false
26
- if (!canPromptForPrivacyChoice()) return false
27
26
  return preferences.configured && preferences.productAnalytics
28
27
  }
29
28
 
@@ -42,7 +41,7 @@ export async function ensurePrivacyChoice(options: {
42
41
  }
43
42
 
44
43
  const allowed = await confirm(
45
- 'Share anonymous package incompatibilities and error classes to improve peach?',
44
+ 'Share anonymous diagnostics, including CI and agent runs: CLI/runtime versions, OS/arch, command name, random install ID, package incompatibilities, error class, and first-line error/warning messages (300 characters, paths redacted; no arguments, stacks, source or structured app data)?',
46
45
  false,
47
46
  )
48
47
  // the engine's historical analytics and crash transports carry broader
@@ -0,0 +1,51 @@
1
+ import { consoleEntriesEval, type InspectBridge } from './commands/inspect/core'
2
+ import { diagnosticsAllowedForCurrentProcess } from './privacy'
3
+ import { setDiagnosticRuntimeVersion, trackCliDiagnostic } from './telemetry'
4
+
5
+ // reuse the same local console/error stores the CLI already inspects. only
6
+ // message strings cross into the diagnostic queue, never stacks or objects.
7
+ export async function collectRuntimeDiagnostics(bridge: InspectBridge): Promise<void> {
8
+ if (!diagnosticsAllowedForCurrentProcess()) return
9
+ try {
10
+ const result: unknown = await bridge.send(
11
+ {
12
+ type: 'evaluate',
13
+ code: `(() => {
14
+ const messages = []
15
+ const add = (kind, message) => {
16
+ if (typeof message === 'string' && message) messages.push({ kind, message: message.slice(0, 2048) })
17
+ }
18
+ const obs = window.__sootsimObservability
19
+ for (const e of (obs?.reportedErrors?.getSnapshot?.() || []).slice(-20)) add('runtime_error', e.message)
20
+ for (const [kind, entries] of [
21
+ ['runtime_error', ${consoleEntriesEval('error', 20)}],
22
+ ['runtime_warning', ${consoleEntriesEval('warn', 20)}],
23
+ ]) {
24
+ for (const e of entries) {
25
+ const parts = e.args.map(a => typeof a === 'string' ? a : typeof a?.message === 'string' ? a.message : '').filter(Boolean)
26
+ add(kind, parts.join(' '))
27
+ }
28
+ }
29
+ return { runtimeVersion: window.__sootsimRuntimeVersion, messages: messages.slice(0, 50) }
30
+ })()`,
31
+ },
32
+ { timeoutMs: 1000 },
33
+ )
34
+ if (!result || typeof result !== 'object') return
35
+ if ('runtimeVersion' in result && typeof result.runtimeVersion === 'string') {
36
+ setDiagnosticRuntimeVersion(result.runtimeVersion)
37
+ }
38
+ if (!('messages' in result) || !Array.isArray(result.messages)) return
39
+ for (const entry of result.messages.slice(0, 50)) {
40
+ if (!entry || typeof entry !== 'object') continue
41
+ if (
42
+ (entry.kind === 'runtime_error' || entry.kind === 'runtime_warning') &&
43
+ typeof entry.message === 'string'
44
+ ) {
45
+ trackCliDiagnostic({ kind: entry.kind, message: entry.message })
46
+ }
47
+ }
48
+ } catch {
49
+ // diagnostics never delay a failed bridge beyond this bounded probe.
50
+ }
51
+ }
package/cli/shell-init.ts CHANGED
@@ -27,7 +27,12 @@ export function renderRnxShellInit(shell: string): string {
27
27
  esac
28
28
  done
29
29
  case "$_peach_shell_command" in
30
- claim|close) ;;
30
+ claim|close)
31
+ if [ -z "\${PEACH_CLOUD_SESSION:-}" ]; then
32
+ command peach "\${_peach_shell_args[@]}"
33
+ return $?
34
+ fi
35
+ ;;
31
36
  ios)
32
37
  for _peach_shell_session in "$@"; do
33
38
  if [ "$_peach_shell_session" = '--remote' ]; then
package/cli/telemetry.ts CHANGED
@@ -1,3 +1,7 @@
1
+ import { homedir } from 'node:os'
2
+ import { getCliVersion } from '../src/cli-version'
3
+ import { readActiveRuntime, readOrCreateAnonymousId } from '../src/home-paths'
4
+ import { parseArgs } from './parse-args'
1
5
  import { diagnosticsAllowedForCurrentProcess } from './privacy'
2
6
 
3
7
  type PackageIncompatibility = {
@@ -10,9 +14,15 @@ type PackageIncompatibility = {
10
14
  type ErrorShape = {
11
15
  kind: 'error_shape'
12
16
  errorClass: string
17
+ message: string
13
18
  }
14
19
 
15
- export type CliDiagnostic = PackageIncompatibility | ErrorShape
20
+ type RuntimeMessage = {
21
+ kind: 'runtime_error' | 'runtime_warning'
22
+ message: string
23
+ }
24
+
25
+ export type CliDiagnostic = PackageIncompatibility | ErrorShape | RuntimeMessage
16
26
 
17
27
  type Config = { apiKey: string; host: string }
18
28
 
@@ -32,6 +42,46 @@ function bounded(value: string, max: number): string {
32
42
  return value.trim().slice(0, max)
33
43
  }
34
44
 
45
+ export function sanitizeDiagnosticMessage(value: string): string {
46
+ const firstLine = value.slice(0, 2048).split(/[\r\n\u2028\u2029]/, 1)[0] ?? ''
47
+ // retain package names from dependency paths, never source filenames,
48
+ // URL paths/queries, local paths, stack frames, or serialized app objects.
49
+ return firstLine
50
+ .split(homedir())
51
+ .join('<home>')
52
+ .replace(
53
+ /<package:(?:@[\w.-]+\/)?[\w.-]+>|(?:https?:\/\/|file:\/\/|[A-Za-z]:[\\/]|<home>[\\/]|\/|\.{1,2}[\\/]|\b[\w.-]+[\\/])[^\s"'<>]+/g,
54
+ (location) => {
55
+ if (location.startsWith('<package:')) return location
56
+ const dependency = /(?:^|[\\/])node_modules[\\/]((?:@[^\\/]+[\\/])?[^\\/]+)/.exec(
57
+ location,
58
+ )
59
+ return dependency ? `<package:${dependency[1]?.replace(/\\/g, '/')}>` : '<path>'
60
+ },
61
+ )
62
+ .replace(/\b[\w.-]+\.(?:tsx?|jsx?|swift|m|mm|kt|java)(?::\d+(?::\d+)?)?/g, '<source>')
63
+ .replace(/\{.*$|\[(?:\s*["'{\d]|$).*$/g, '<data>')
64
+ .replace(
65
+ /\b(?:const|let|var)\s+\w+\s*=.*$|\b(?:function|class)\s+\w+.*$|\b(?:import|export)\s+.*$|\breturn\s*[({<].*$/g,
66
+ '<source>',
67
+ )
68
+ .replace(/^\s*at\s+.*$|\bat\s+[\w.$<>]+\s*\([^)]*\)/g, '<stack>')
69
+ .trim()
70
+ .slice(0, 300)
71
+ }
72
+
73
+ let observedRuntimeVersion: string | null = null
74
+
75
+ function diagnosticVersion(version: string | null): string | null {
76
+ return version && version.length <= 80 && /^\d+\.\d+\.\d+(?:-[\w.-]+)?$/.test(version)
77
+ ? version
78
+ : null
79
+ }
80
+
81
+ export function setDiagnosticRuntimeVersion(version: string): void {
82
+ observedRuntimeVersion = diagnosticVersion(version)
83
+ }
84
+
35
85
  export function buildCliDiagnosticEvent(diagnostic: CliDiagnostic) {
36
86
  const properties =
37
87
  diagnostic.kind === 'package_incompatibility'
@@ -42,12 +92,24 @@ export function buildCliDiagnosticEvent(diagnostic: CliDiagnostic) {
42
92
  ? { unsupported_api: bounded(diagnostic.unsupportedApi, 160) }
43
93
  : {}),
44
94
  }
45
- : { error_class: bounded(diagnostic.errorClass, 160) }
95
+ : diagnostic.kind === 'error_shape'
96
+ ? {
97
+ error_class: bounded(diagnostic.errorClass, 160),
98
+ message: sanitizeDiagnosticMessage(diagnostic.message),
99
+ }
100
+ : { message: sanitizeDiagnosticMessage(diagnostic.message) }
46
101
 
47
102
  return {
48
103
  event: diagnostic.kind,
49
- distinct_id: 'peach-cli-diagnostic',
50
- properties,
104
+ distinct_id: readOrCreateAnonymousId(),
105
+ properties: {
106
+ cli_version: diagnosticVersion(getCliVersion()),
107
+ runtime_version: observedRuntimeVersion ?? diagnosticVersion(readActiveRuntime()),
108
+ os: process.platform,
109
+ arch: process.arch,
110
+ command: parseArgs(process.argv).command ?? 'open',
111
+ ...properties,
112
+ },
51
113
  }
52
114
  }
53
115
 
@@ -55,12 +117,20 @@ const queue: CliDiagnostic[] = []
55
117
 
56
118
  export function trackCliDiagnostic(diagnostic: CliDiagnostic): void {
57
119
  if (!diagnosticsAllowedForCurrentProcess()) return
58
- queue.push(diagnostic)
120
+ if (queue.length >= 50) return
121
+ const entry =
122
+ diagnostic.kind === 'package_incompatibility'
123
+ ? diagnostic
124
+ : { ...diagnostic, message: sanitizeDiagnosticMessage(diagnostic.message) }
125
+ if (queue.some((queued) => JSON.stringify(queued) === JSON.stringify(entry))) return
126
+ queue.push(entry)
59
127
  }
60
128
 
61
129
  export function trackCliError(error: unknown): void {
62
130
  const errorClass = error instanceof Error ? error.constructor.name : typeof error
63
- trackCliDiagnostic({ kind: 'error_shape', errorClass })
131
+ const message =
132
+ error instanceof Error ? error.message : typeof error === 'string' ? error : ''
133
+ trackCliDiagnostic({ kind: 'error_shape', errorClass, message })
64
134
  }
65
135
 
66
136
  export async function flushCliTelemetry(): Promise<void> {
@@ -70,8 +140,9 @@ export async function flushCliTelemetry(): Promise<void> {
70
140
  queue.length = 0
71
141
  return
72
142
  }
73
- const batch = queue.splice(0, queue.length).map(buildCliDiagnosticEvent)
143
+ const diagnostics = queue.splice(0, queue.length)
74
144
  try {
145
+ const batch = diagnostics.map((diagnostic) => buildCliDiagnosticEvent(diagnostic))
75
146
  await fetch(`${config.host}/batch/`, {
76
147
  method: 'POST',
77
148
  headers: { 'content-type': 'application/json' },
package/cli/ws-bridge.ts CHANGED
@@ -148,7 +148,8 @@ function resolveDefaultDevBridgeLockfile(): DevBridgeLockfile | null {
148
148
  // another checkout's bridge on its own offset belongs to that checkout's
149
149
  // world; only the canonical bridge is shared, and past it the daemon serves.
150
150
  return (
151
- lockfiles.find((lockfile) => lockfile.bridgePort === DEFAULT_SOOTSIM_BRIDGE_PORT) ?? null
151
+ lockfiles.find((lockfile) => lockfile.bridgePort === DEFAULT_SOOTSIM_BRIDGE_PORT) ??
152
+ null
152
153
  )
153
154
  }
154
155
 
@@ -601,7 +602,12 @@ export function createBridge(wsPort: number, opts: WsBridgeOptions = {}): WsBrid
601
602
  // err.message is often empty (ECONNREFUSED surfaces with no message on
602
603
  // some node builds) — don't emit a dangling "...7668: " with no cause.
603
604
  const cause = err.message ? `: ${err.message}` : ''
604
- reject(new Error(`could not connect to ${bridgeWsUrl(wsPort)}${cause}`))
605
+ reject(
606
+ new Error(
607
+ `could not connect to ${bridgeWsUrl(wsPort)}${cause}\n` +
608
+ ` for sandboxed or agent runners, keep \`peach serve --port ${wsPort}\` running in a persistent terminal or background task.`,
609
+ ),
610
+ )
605
611
  })
606
612
  })
607
613
  // a bridge closed before its handshake finished rejects `ready` with nothing
package/detox/index.ts CHANGED
@@ -605,7 +605,7 @@ export async function waitForSootsimIdle(
605
605
  // find a node in the sootsim tree matching a matcher descriptor
606
606
  async function findNodeByMatcher(matcher: Matcher): Promise<any> {
607
607
  const page = getPage()
608
- if (matcher.index !== undefined) {
608
+ if (matcher.index !== undefined || matcher.type === 'text') {
609
609
  return page.evaluate(async (indexedMatcher: Matcher) => {
610
610
  const filter =
611
611
  indexedMatcher.type === 'id'
@@ -618,8 +618,8 @@ async function findNodeByMatcher(matcher: Matcher): Promise<any> {
618
618
  ? { hasRole: indexedMatcher.value }
619
619
  : { type: indexedMatcher.value }
620
620
  const results = await window.__sootsimTest!.queryAll(filter)
621
- // detox indexes by.text over the text elements that read exactly that
622
- // text. hasText also matches every ancestor, so index 0 would be the root.
621
+ // detox by.text matches exact text elements, with or without an index.
622
+ // hasText also matches ancestors and substrings, which are not targets.
623
623
  const indexed =
624
624
  indexedMatcher.type === 'text'
625
625
  ? results.filter(
@@ -634,11 +634,6 @@ async function findNodeByMatcher(matcher: Matcher): Promise<any> {
634
634
  (id: string) => window.__sootsimTest!.findByTestId(id),
635
635
  matcher.value,
636
636
  )
637
- } else if (matcher.type === 'text') {
638
- return page.evaluate(
639
- (text: string) => window.__sootsimTest!.findByText(text),
640
- matcher.value,
641
- )
642
637
  } else if (matcher.type === 'label') {
643
638
  return page.evaluate(
644
639
  (label: string) => window.__sootsimTest!.findByLabel(label),
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.630 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.632 | (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.630 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.632 | (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.630 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.632 | (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.630 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.632 | (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.630 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.632 | (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.630 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.632 | (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.630 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.632 | (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.630 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.632 | (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.630 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.632 | (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.630 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.632 | (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.630 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.632 | (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.630 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.632 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
 
3
3
  // src/bridge-contract.ts
4
4
  var SIM_LONG_PRESS_MAX_MS = 5e3;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.630 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.632 | (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.630 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.632 | (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.630 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.632 | (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.630 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.632 | (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.630 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.632 | (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.630 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.632 | (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.630 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.632 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
 
3
3
  // src/cloud-contract.ts
4
4
  var RNX_CLOUD_MAX_ARTIFACT_BYTES = 20 * 1024 * 1024;
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.630 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.632 | (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;
@@ -39,7 +39,9 @@ var CLOUD_TEST_VALUE_FLAGS = [
39
39
  "--testPathPattern",
40
40
  "--include-tags",
41
41
  "--exclude-tags",
42
- "--grep"
42
+ "--grep",
43
+ "--env",
44
+ "-e"
43
45
  ];
44
46
  var CLOUD_TEST_VALUE_FLAG_SET = new Set(CLOUD_TEST_VALUE_FLAGS);
45
47
  function isCloudTestValueFlag(name) {
@@ -64,7 +66,7 @@ function parseCloudTestArgs(value) {
64
66
  return { ok: false, message: "testArgs entries must be non-empty strings" };
65
67
  }
66
68
  if (entry.length > CLOUD_TEST_ARG_MAX_LENGTH || entry.includes("\0")) {
67
- return { ok: false, message: `testArgs entry is too long or carries a NUL byte: ${entry.slice(0, 60)}` };
69
+ return { ok: false, message: "testArgs entry is too long or carries a NUL byte" };
68
70
  }
69
71
  bytes += encoder.encode(entry).length;
70
72
  if (bytes > CLOUD_TEST_ARGS_MAX_BYTES) {
@@ -87,12 +89,19 @@ function parseCloudTestArgs(value) {
87
89
  const eq = entry.indexOf("=");
88
90
  const head = eq > 0 ? entry.slice(0, eq) : entry;
89
91
  if (!isCloudTestValueFlag(head)) {
90
- return { ok: false, message: `testArgs carries an unknown flag: ${head.slice(0, 60)}` };
92
+ return {
93
+ ok: false,
94
+ message: `testArgs carries an unknown flag: ${head.slice(0, 60)}`
95
+ };
91
96
  }
92
97
  if (eq > 0) {
93
- if (!entry.slice(eq + 1)) {
98
+ const value2 = entry.slice(eq + 1);
99
+ if (!value2) {
94
100
  return { ok: false, message: `${head} expects a value` };
95
101
  }
102
+ if ((head === "--env" || head === "-e") && value2.indexOf("=") < 1) {
103
+ return { ok: false, message: `${head} expects KEY=VALUE with a non-empty key` };
104
+ }
96
105
  args.push(entry);
97
106
  continue;
98
107
  }
@@ -100,6 +109,9 @@ function parseCloudTestArgs(value) {
100
109
  if (val === void 0 || val.startsWith("-")) {
101
110
  return { ok: false, message: `${head} expects a value` };
102
111
  }
112
+ if ((head === "--env" || head === "-e") && val.indexOf("=") < 1) {
113
+ return { ok: false, message: `${head} expects KEY=VALUE with a non-empty key` };
114
+ }
103
115
  args.push(entry, val);
104
116
  i += 1;
105
117
  }
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.630 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.632 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
 
3
3
  // src/cloud-test-args.ts
4
4
  var CLOUD_TEST_ARGS_MAX_COUNT = 32;
@@ -10,7 +10,9 @@ var CLOUD_TEST_VALUE_FLAGS = [
10
10
  "--testPathPattern",
11
11
  "--include-tags",
12
12
  "--exclude-tags",
13
- "--grep"
13
+ "--grep",
14
+ "--env",
15
+ "-e"
14
16
  ];
15
17
  var CLOUD_TEST_VALUE_FLAG_SET = new Set(CLOUD_TEST_VALUE_FLAGS);
16
18
  function isCloudTestValueFlag(name) {
@@ -35,7 +37,7 @@ function parseCloudTestArgs(value) {
35
37
  return { ok: false, message: "testArgs entries must be non-empty strings" };
36
38
  }
37
39
  if (entry.length > CLOUD_TEST_ARG_MAX_LENGTH || entry.includes("\0")) {
38
- return { ok: false, message: `testArgs entry is too long or carries a NUL byte: ${entry.slice(0, 60)}` };
40
+ return { ok: false, message: "testArgs entry is too long or carries a NUL byte" };
39
41
  }
40
42
  bytes += encoder.encode(entry).length;
41
43
  if (bytes > CLOUD_TEST_ARGS_MAX_BYTES) {
@@ -58,12 +60,19 @@ function parseCloudTestArgs(value) {
58
60
  const eq = entry.indexOf("=");
59
61
  const head = eq > 0 ? entry.slice(0, eq) : entry;
60
62
  if (!isCloudTestValueFlag(head)) {
61
- return { ok: false, message: `testArgs carries an unknown flag: ${head.slice(0, 60)}` };
63
+ return {
64
+ ok: false,
65
+ message: `testArgs carries an unknown flag: ${head.slice(0, 60)}`
66
+ };
62
67
  }
63
68
  if (eq > 0) {
64
- if (!entry.slice(eq + 1)) {
69
+ const value2 = entry.slice(eq + 1);
70
+ if (!value2) {
65
71
  return { ok: false, message: `${head} expects a value` };
66
72
  }
73
+ if ((head === "--env" || head === "-e") && value2.indexOf("=") < 1) {
74
+ return { ok: false, message: `${head} expects KEY=VALUE with a non-empty key` };
75
+ }
67
76
  args.push(entry);
68
77
  continue;
69
78
  }
@@ -71,6 +80,9 @@ function parseCloudTestArgs(value) {
71
80
  if (val === void 0 || val.startsWith("-")) {
72
81
  return { ok: false, message: `${head} expects a value` };
73
82
  }
83
+ if ((head === "--env" || head === "-e") && val.indexOf("=") < 1) {
84
+ return { ok: false, message: `${head} expects KEY=VALUE with a non-empty key` };
85
+ }
74
86
  args.push(entry, val);
75
87
  i += 1;
76
88
  }