peachsim 0.1.578 → 0.1.579

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 (178) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +11 -9
  3. package/cli/app-catalog.ts +77 -0
  4. package/cli/app-fonts.ts +4 -20
  5. package/cli/app-splash.ts +2 -18
  6. package/cli/bridge-flow-runner.ts +505 -139
  7. package/cli/cloud-client.ts +1 -4
  8. package/cli/cloud-session.ts +1 -4
  9. package/cli/cloud-test-result-files.ts +3 -0
  10. package/cli/cloud-test-tunnel.ts +2 -5
  11. package/cli/command-registry.ts +2 -0
  12. package/cli/commands/agent.ts +149 -23
  13. package/cli/commands/assert.ts +1 -4
  14. package/cli/commands/box/peach-command.ts +62 -0
  15. package/cli/commands/box/plane-share.ts +0 -70
  16. package/cli/commands/box/rnx-command.ts +1 -60
  17. package/cli/commands/box/search-command.ts +14 -0
  18. package/cli/commands/box.ts +2 -2
  19. package/cli/commands/control.ts +121 -27
  20. package/cli/commands/cpu-profile.ts +23 -10
  21. package/cli/commands/daemon.ts +1 -9
  22. package/cli/commands/detox.ts +26 -18
  23. package/cli/commands/device.ts +129 -1
  24. package/cli/commands/film.ts +25 -16
  25. package/cli/commands/flow.ts +9 -2
  26. package/cli/commands/inspect/actions.ts +75 -1
  27. package/cli/commands/inspect/core.ts +81 -2
  28. package/cli/commands/inspect/describe.ts +2 -6
  29. package/cli/commands/inspect/env.ts +1 -1
  30. package/cli/commands/inspect/settling.ts +20 -3
  31. package/cli/commands/inspect/shared.ts +1 -4
  32. package/cli/commands/inspect/sleep.ts +1 -1
  33. package/cli/commands/inspect.ts +16 -49
  34. package/cli/commands/install-desktop.ts +8 -6
  35. package/cli/commands/login.ts +3 -5
  36. package/cli/commands/maestro-generate.ts +1 -4
  37. package/cli/commands/maestro.ts +38 -21
  38. package/cli/commands/no-bridge-hint.ts +0 -14
  39. package/cli/commands/permissions.ts +2 -2
  40. package/cli/commands/platform.ts +13 -6
  41. package/cli/commands/screenshot.ts +6 -1
  42. package/cli/commands/skills.ts +1 -1
  43. package/cli/commands/test.ts +6 -2
  44. package/cli/commands/three-mode.ts +162 -19
  45. package/cli/commands/upload.ts +3 -18
  46. package/cli/current-sim.ts +0 -1
  47. package/cli/desktop-companion.ts +40 -8
  48. package/cli/drivers/playwright-provisioning.ts +1 -4
  49. package/cli/flow-export.ts +1 -8
  50. package/cli/flow-session.ts +0 -4
  51. package/cli/hints.ts +1 -16
  52. package/cli/inspect-notice-state.ts +0 -4
  53. package/cli/main.ts +2 -4
  54. package/cli/outbound-endpoints.ts +1 -24
  55. package/cli/parse-args.ts +1 -2
  56. package/cli/rnx-in-process.ts +3 -1
  57. package/cli/run-rnx-core.ts +4 -2
  58. package/cli/run-rnx.ts +18 -10
  59. package/cli/send-to-box.ts +1 -4
  60. package/cli/telemetry.ts +1 -1
  61. package/cli/ws-bridge.ts +1 -5
  62. package/detox/gestures.ts +20 -6
  63. package/detox/index.ts +379 -123
  64. package/detox/navigation-lifecycle.ts +22 -13
  65. package/dist-lib/agent-daemon-client.cjs +21 -15
  66. package/dist-lib/agent-events.cjs +1 -1
  67. package/dist-lib/agent-identity.cjs +3 -17
  68. package/dist-lib/agent-sessions.cjs +5 -4
  69. package/dist-lib/attached-projects.cjs +1 -77
  70. package/dist-lib/auth/shared-session.cjs +1 -1
  71. package/dist-lib/backend-origin.cjs +11 -1
  72. package/dist-lib/beta.cjs +1 -1
  73. package/dist-lib/beta.mjs +1 -1
  74. package/dist-lib/bridge-constants.cjs +1 -1
  75. package/dist-lib/bridge-contract-input.cjs +7 -5
  76. package/dist-lib/bridge-contract-input.mjs +7 -5
  77. package/dist-lib/bridge-contract.cjs +1 -1
  78. package/dist-lib/bridge-contract.mjs +1 -1
  79. package/dist-lib/capture-contract.cjs +30 -7
  80. package/dist-lib/capture-contract.mjs +30 -7
  81. package/dist-lib/cli-constants.cjs +1 -1
  82. package/dist-lib/cloud-contract.cjs +1 -1
  83. package/dist-lib/cloud-contract.mjs +1 -1
  84. package/dist-lib/cloud-test-args.cjs +1 -1
  85. package/dist-lib/cloud-test-args.mjs +1 -1
  86. package/dist-lib/cloud.cjs +17 -11
  87. package/dist-lib/cloud.mjs +17 -11
  88. package/dist-lib/config.cjs +1 -1
  89. package/dist-lib/detox/index.cjs +321 -122
  90. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  91. package/dist-lib/home-paths.cjs +1 -1
  92. package/dist-lib/host/bridge-host.cjs +427 -212
  93. package/dist-lib/host/fetch-proxy-handler.cjs +5 -28
  94. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  95. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  96. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  97. package/dist-lib/host/websocket-proxy.cjs +1 -1
  98. package/dist-lib/index.cjs +315 -58
  99. package/dist-lib/jump-to-source-babel.cjs +1 -1
  100. package/dist-lib/jump-to-source-native.cjs +1 -1
  101. package/dist-lib/menu.cjs +67 -31
  102. package/dist-lib/menu.mjs +64 -28
  103. package/dist-lib/metro-fingerprint-registry.cjs +8 -4
  104. package/dist-lib/metro-fingerprint-registry.mjs +6 -4
  105. package/dist-lib/metro-production-bundle.cjs +2 -2
  106. package/dist-lib/metro-production-bundle.mjs +2 -2
  107. package/dist-lib/metro.cjs +8 -4
  108. package/dist-lib/profiles.cjs +1 -1
  109. package/dist-lib/public-brand.cjs +7 -4
  110. package/dist-lib/react-native-host-modules.cjs +1 -1
  111. package/dist-lib/react-native-host-modules.mjs +1 -1
  112. package/dist-lib/render-mode.cjs +3 -13
  113. package/dist-lib/scripts/dev-server-scanner.cjs +5 -17
  114. package/dist-lib/sdk.cjs +129 -10
  115. package/dist-lib/sdk.mjs +127 -10
  116. package/dist-lib/skills.cjs +1635 -1121
  117. package/dist-lib/swift-compile.cjs +277 -70
  118. package/dist-lib/swift-compile.mjs +275 -70
  119. package/dist-lib/swift-package.cjs +17 -5
  120. package/dist-lib/swift-package.mjs +15 -4
  121. package/dist-lib/swift-submit.cjs +32 -8
  122. package/dist-lib/swift-submit.mjs +29 -7
  123. package/dist-lib/swift.cjs +30912 -577
  124. package/dist-lib/vite-kotlin.cjs +356 -0
  125. package/dist-lib/vite-swift.cjs +1743 -0
  126. package/dist-lib/vite.cjs +208 -1039
  127. package/kotlin/README.md +18 -0
  128. package/kotlin/Sources/androidx/annotation/Keep.kt +4 -0
  129. package/kotlin/Sources/dev/onejs/one/OneNativeSourceDispatch.kt +7 -0
  130. package/kotlin/Sources/dev/onejs/one/source/OneModule.kt +4 -0
  131. package/kotlin/Sources/org/json/JSON.kt +244 -0
  132. package/kotlin/toolchain.json +19 -0
  133. package/package.json +14 -2
  134. package/scripts/dev-server-scanner.ts +0 -18
  135. package/scripts/optional-demo-registry.ts +0 -17
  136. package/skills/contrast/SKILL.md +2 -2
  137. package/skills/peach-setup/SKILL.md +16 -0
  138. package/src/agent-daemon-client.ts +36 -22
  139. package/src/agent-identity.ts +0 -15
  140. package/src/agent-prompt.ts +12 -0
  141. package/src/attached-projects.ts +0 -72
  142. package/src/backend-origin.ts +10 -0
  143. package/src/bridge-contract-input.ts +2 -5
  144. package/src/bridge-contract.ts +2 -0
  145. package/src/capture-contract.ts +53 -11
  146. package/src/cloud.ts +4 -9
  147. package/src/dev-server-open.ts +0 -99
  148. package/src/home-paths.ts +2 -3
  149. package/src/host/agent-host.ts +82 -36
  150. package/src/host/bridge-host.ts +5 -11
  151. package/src/host/fetch-proxy-handler.ts +19 -43
  152. package/src/is-record.ts +4 -0
  153. package/src/menu.ts +82 -37
  154. package/src/metro-fingerprint-registry.ts +1 -4
  155. package/src/metro-production-bundle.ts +1 -1
  156. package/src/plist.ts +1 -4
  157. package/src/public-brand.ts +5 -3
  158. package/src/render-mode.ts +0 -12
  159. package/src/runtime-assets.ts +9 -2
  160. package/src/screenshots/compose.ts +7 -1
  161. package/src/screenshots/frame-compose.ts +21 -10
  162. package/src/screenshots/schema.ts +1 -4
  163. package/src/sim-client.ts +2 -4
  164. package/src/skills/builtin/maestro-test.ts +1 -4
  165. package/src/sleep.ts +4 -0
  166. package/src/swift-compile.ts +209 -47
  167. package/src/swift-icu.ts +140 -0
  168. package/src/swift-package.ts +26 -2
  169. package/src/swift-submit.ts +84 -4
  170. package/src/swift.ts +2 -0
  171. package/src/vite-plugin-kotlin.ts +376 -0
  172. package/src/vite-plugin-one.ts +224 -36
  173. package/src/vite-plugin-swift.ts +382 -96
  174. package/src/vite-plugin.ts +121 -134
  175. package/src/vite.d.ts +10 -0
  176. package/cli/resolve-assets.ts +0 -63
  177. package/src/rolldown-plugin-swift.ts +0 -244
  178. package/src/team-machine-prompt.ts +0 -73
@@ -86,31 +86,6 @@ function applyFetchProxyCors(res: ServerResponse) {
86
86
  }
87
87
  }
88
88
 
89
- function formatFetchProxyError(targetUrl: string, err: unknown): string {
90
- const details: string[] = []
91
- const error = err as
92
- | {
93
- message?: string
94
- code?: string
95
- cause?: { message?: string; code?: string }
96
- }
97
- | undefined
98
-
99
- if (error?.code) details.push(error.code)
100
- if (error?.message) details.push(error.message)
101
- if (error?.cause?.code) details.push(error.cause.code)
102
- if (error?.cause?.message) details.push(error.cause.message)
103
-
104
- const uniqueDetails = [...new Set(details.filter(Boolean))]
105
- const message = uniqueDetails.join(' | ') || String(err)
106
-
107
- if (targetUrl.includes('stored-in-.env.local')) {
108
- return `${message} | upstream url still contains placeholder env values`
109
- }
110
-
111
- return message
112
- }
113
-
114
89
  export function buildFetchProxyHeaders(
115
90
  reqHeaders: Record<string, string | string[] | undefined>,
116
91
  targetUrl?: URL,
@@ -141,7 +116,7 @@ export function buildAppApiProxyHeaders(
141
116
  headers[key] = value
142
117
  }
143
118
  headers.host = targetUrl.host
144
- if (!Object.keys(headers).some((name) => name.toLowerCase() === 'expo-origin')) {
119
+ if (!Object.keys(headers).some((name) => name.toLowerCase() === 'mobile-origin')) {
145
120
  headers.origin = targetUrl.origin
146
121
  headers.referer = `${targetUrl.origin}/`
147
122
  }
@@ -307,12 +282,12 @@ export async function handleFetchProxyRequest(
307
282
  try {
308
283
  await proxy(upstreamUrl, originalMethod, body, 0)
309
284
  } catch (err) {
310
- if (res.headersSent) {
311
- res.destroy(err instanceof Error ? err : new Error(String(err)))
312
- return
313
- }
314
285
  const norm = normalizeHostname(upstreamUrl.hostname)
315
- if (isLoopbackHost(norm) && (err as { code?: string })?.code === 'ECONNREFUSED') {
286
+ if (
287
+ !res.headersSent &&
288
+ isLoopbackHost(norm) &&
289
+ (err as { code?: string })?.code === 'ECONNREFUSED'
290
+ ) {
316
291
  const altHost =
317
292
  norm === '::1' ? '127.0.0.1' : norm === '127.0.0.1' ? '::1' : '127.0.0.1'
318
293
  if (altHost !== norm) {
@@ -324,9 +299,13 @@ export async function handleFetchProxyRequest(
324
299
  } catch {}
325
300
  }
326
301
  }
327
- applyFetchProxyCors(res)
328
- res.writeHead(502, { 'Content-Type': 'text/plain' })
329
- res.end(`fetch proxy error: ${formatFetchProxyError(upstreamUrl.href, err)}`)
302
+ // an upstream that cannot be reached fails the page's request the way a
303
+ // direct fetch would: as a network error, never a response. a synthetic
304
+ // 5xx reads as a live server answering (the bundle loader then polls it as
305
+ // a cold build) and hands guest code a Response where React Native rejects.
306
+ // close the connection itself: bun's node:http answers a destroyed
307
+ // response that never sent headers with an empty 200.
308
+ req.socket.destroy()
330
309
  }
331
310
  }
332
311
 
@@ -364,11 +343,9 @@ export async function handleBundleProxyRequest(
364
343
  }
365
344
 
366
345
  export function handleAppApiRequest(req: IncomingMessage, res: ServerResponse): boolean {
367
- // returns true if it handled the request, false if the caller should fall
368
- // through. /__app-api requires `origin` query param for stateless mode;
369
- // without it we return false so the caller decides what to do (the vite
370
- // middleware uses its own out-of-band origin registry; the daemon has
371
- // no such registry so it returns 400).
346
+ // returns true if it handled the request, false if it is not an /__app-api
347
+ // request. the proxy is stateless: a request without an `origin` query
348
+ // param gets a 400.
372
349
 
373
350
  const reqUrl = req.url || ''
374
351
 
@@ -441,10 +418,9 @@ export function handleAppApiRequest(req: IncomingMessage, res: ServerResponse):
441
418
  proxyRes.pipe(res)
442
419
  },
443
420
  )
444
- proxyReq.on('error', (err) => {
445
- res.statusCode = 502
446
- res.end(`app proxy error: ${err.message}`)
447
- })
421
+ // an unreachable app server is a network error, never a 502 (see
422
+ // handleFetchProxyRequest)
423
+ proxyReq.on('error', () => req.socket.destroy())
448
424
  req.pipe(proxyReq)
449
425
  return true
450
426
  }
@@ -0,0 +1,4 @@
1
+ // a plain object: not null and not an array
2
+ export function isRecord(value: unknown): value is Record<string, unknown> {
3
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
4
+ }
package/src/menu.ts CHANGED
@@ -29,26 +29,23 @@ export const SOOTSIM_AMBIENT_GLOW_LABELS: Record<SootSimAmbientGlowLevel, string
29
29
  full: 'Full',
30
30
  }
31
31
 
32
- // which ios home screen the simulator portrays. the home screen is authored
33
- // twice (the flat ios 26 home and the ios 27 home measured off apple's art),
34
- // so the era is one value every host carries: the menus, the persisted
35
- // setting, the ?homeScreen= boot seed, the live set-home-screen push and
36
- // electron's shared config.
37
- export type SootSimHomeScreenVersion = 'ios26' | 'ios27'
38
- export const SOOTSIM_HOME_SCREEN_VERSIONS = ['ios26', 'ios27'] as const
39
-
40
- export function isSootSimHomeScreenVersion(
32
+ // render resolution: how many pixels peach renders per iphone point. one
33
+ // setting with three states, carried by every host exactly like the ambient
34
+ // glow level above: the menus, the persisted setting, the ?renderResolution=
35
+ // boot seed, the live set-render-resolution push, electron's shared config.
36
+ export type SootSimRenderResolution = 'high' | 'medium' | 'low'
37
+ export const SOOTSIM_RENDER_RESOLUTION_LEVELS = ['high', 'medium', 'low'] as const
38
+
39
+ export function isSootSimRenderResolution(
41
40
  value: unknown,
42
- ): value is SootSimHomeScreenVersion {
43
- return value === 'ios26' || value === 'ios27'
41
+ ): value is SootSimRenderResolution {
42
+ return value === 'high' || value === 'medium' || value === 'low'
44
43
  }
45
44
 
46
- export const SOOTSIM_HOME_SCREEN_VERSION_LABELS: Record<
47
- SootSimHomeScreenVersion,
48
- string
49
- > = {
50
- ios26: 'iOS 26',
51
- ios27: 'iOS 27',
45
+ export const SOOTSIM_RENDER_RESOLUTION_LABELS: Record<SootSimRenderResolution, string> = {
46
+ high: 'High',
47
+ medium: 'Medium',
48
+ low: 'Low',
52
49
  }
53
50
 
54
51
  export type SootSimMenuItemDef =
@@ -78,16 +75,16 @@ export type SootSimMenuDef = {
78
75
 
79
76
  export interface SootSimMenuState {
80
77
  currentDevice: string
81
- homeScreenVersion: SootSimHomeScreenVersion
82
78
  // whether this surface is a contributor development surface. unreleased
83
- // simulator surfaces (the iPhone Duo and its home-screen era switch) are
84
- // offered here and nowhere a user can reach.
79
+ // simulator surfaces (the iPhone Duo) are offered here and nowhere a user
80
+ // can reach.
85
81
  developmentMode: boolean
86
82
  showBezels: boolean
87
83
  showMenuBar: boolean
88
84
  showRail: boolean
89
85
  solidWindow: boolean
90
86
  ambientGlow: SootSimAmbientGlowLevel
87
+ renderResolution: SootSimRenderResolution
91
88
  // presentation: the real rendering, or the clean structural wireframe
92
89
  wireframe: boolean
93
90
  screenshotMode: boolean
@@ -99,6 +96,12 @@ export interface SootSimMenuState {
99
96
  sendToBoxCarryAuth: boolean
100
97
  stayOnTop: boolean
101
98
  scaleMode: SootSimScaleMode
99
+ // fold posture for devices with more than one physical state; null when
100
+ // the current device has none. drives the open/close menu item.
101
+ devicePosture: 'closed' | 'open' | null
102
+ // whether flipping orientation changes the current spec's geometry.
103
+ // computed engine-side; the menu never reads the device catalog.
104
+ canRotate: boolean
102
105
  }
103
106
 
104
107
  export interface SootSimMenuDeviceChoice {
@@ -129,6 +132,7 @@ export type MenuItemDef = SootSimMenuItemDef
129
132
  export type MenuDef = SootSimMenuDef
130
133
  export type MenuState = SootSimMenuState
131
134
  export type AmbientGlowLevel = SootSimAmbientGlowLevel
135
+ export type RenderResolution = SootSimRenderResolution
132
136
  export type ShortcutPlatform = SootSimMenuPlatform
133
137
  export type MenuShortcutEntry = SootSimMenuShortcutEntry
134
138
  export type ShortcutDocEntry = SootSimShortcutDocEntry
@@ -321,6 +325,47 @@ function deviceMenu(
321
325
  },
322
326
  { type: 'separator' },
323
327
  { label: 'Trigger Screenshot', action: 'screenshot' },
328
+ // pose controls for the current device: open/close on foldables,
329
+ // rotate left/right wherever a turn changes geometry, the way ios
330
+ // simulator holds them (rotate left is cmd+left, rotate right
331
+ // cmd+right). truthiness, not !== null: a partial state must hide
332
+ // these, never show duo items for the wrong device.
333
+ ...(state.devicePosture || state.canRotate
334
+ ? [
335
+ { type: 'separator' } as const,
336
+ ...(state.devicePosture
337
+ ? [
338
+ {
339
+ label:
340
+ state.devicePosture === 'open'
341
+ ? 'Close iPhone Duo'
342
+ : 'Open iPhone Duo',
343
+ action: 'toggle-duo-posture',
344
+ },
345
+ ]
346
+ : []),
347
+ ...(state.canRotate
348
+ ? [
349
+ {
350
+ label: 'Rotate Left',
351
+ accelerator: 'CmdOrCtrl+Left',
352
+ action: 'rotate-left',
353
+ browserAccelerator: true,
354
+ shortcutLabel: 'rotate left',
355
+ showInShortcutDocs: true,
356
+ },
357
+ {
358
+ label: 'Rotate Right',
359
+ accelerator: 'CmdOrCtrl+Right',
360
+ action: 'rotate-right',
361
+ browserAccelerator: true,
362
+ shortcutLabel: 'rotate right',
363
+ showInShortcutDocs: true,
364
+ },
365
+ ]
366
+ : []),
367
+ ]
368
+ : []),
324
369
  { type: 'separator' },
325
370
  ...buildSootSimDeviceChoiceDefs(state.currentDevice, deviceChoices, {
326
371
  action: 'switch-device',
@@ -386,6 +431,11 @@ function debugMenu(state: SootSimMenuState): SootSimMenuDef {
386
431
  checked: state.alwaysShowLiveWebViews,
387
432
  action: 'toggle-live-webviews',
388
433
  },
434
+ {
435
+ label: 'Share This Tab for WebViews',
436
+ action: 'capture-webviews',
437
+ hiddenIn: 'electron',
438
+ },
389
439
  {
390
440
  label: 'Send to Box Carries Sign-In',
391
441
  type: 'checkbox',
@@ -482,23 +532,18 @@ function windowMenu(state: SootSimMenuState): SootSimMenuDef {
482
532
  actionPayload: level,
483
533
  })),
484
534
  },
485
- // both eras stay selectable while the ios 27 home surfaces land one at a
486
- // time, and the device's own os picks the starting value. the switch is
487
- // the iPhone Duo's, so it is offered exactly where the Duo is.
488
- ...(state.developmentMode
489
- ? [
490
- {
491
- label: 'Home Screen',
492
- items: SOOTSIM_HOME_SCREEN_VERSIONS.map((version) => ({
493
- label: SOOTSIM_HOME_SCREEN_VERSION_LABELS[version],
494
- type: 'radio' as const,
495
- checked: state.homeScreenVersion === version,
496
- action: 'set-home-screen',
497
- actionPayload: version,
498
- })),
499
- },
500
- ]
501
- : []),
535
+ {
536
+ // every host: the level only changes backing-store sizes, so the
537
+ // device-shaped electron window shows it the same as the browser.
538
+ label: 'Render Resolution',
539
+ items: SOOTSIM_RENDER_RESOLUTION_LEVELS.map((level) => ({
540
+ label: SOOTSIM_RENDER_RESOLUTION_LABELS[level],
541
+ type: 'radio' as const,
542
+ checked: state.renderResolution === level,
543
+ action: 'set-render-resolution',
544
+ actionPayload: level,
545
+ })),
546
+ },
502
547
  {
503
548
  label: 'Wireframe',
504
549
  type: 'checkbox',
@@ -1,3 +1,4 @@
1
+ import { isRecord } from './is-record'
1
2
  export const RNX_METRO_FINGERPRINT_SCHEMA_VERSION = 2
2
3
  export const RNX_METRO_FINGERPRINT_POINTER_PATH = '/compile/metro-fingerprints.json'
3
4
  export const RNX_METRO_FINGERPRINT_MAX_POINTER_BYTES = 16 * 1024
@@ -55,10 +56,6 @@ export interface RNXMetroFingerprintPointer {
55
56
  payloadUrl: string
56
57
  }
57
58
 
58
- function isRecord(value: unknown): value is Record<string, unknown> {
59
- return typeof value === 'object' && value !== null && !Array.isArray(value)
60
- }
61
-
62
59
  function isSha256(value: unknown): value is string {
63
60
  return typeof value === 'string' && /^[a-f0-9]{64}$/.test(value)
64
61
  }
@@ -72,7 +72,7 @@ export interface RNXMetroDetachedSourceMapUrl {
72
72
  export function detachRNXMetroSourceMapUrl(
73
73
  bundleText: string,
74
74
  ): RNXMetroDetachedSourceMapUrl {
75
- const trimmedEnd = bundleText.replace(/\s+$/, '')
75
+ const trimmedEnd = bundleText.trimEnd()
76
76
  const commentStart = trimmedEnd.lastIndexOf(`\n${RNXSIM_METRO_SOURCE_MAP_COMMENT}`)
77
77
  if (commentStart < 0) return { source: bundleText, sourceMappingUrl: null }
78
78
  const url = trimmedEnd.slice(commentStart + 1 + RNXSIM_METRO_SOURCE_MAP_COMMENT.length)
package/src/plist.ts CHANGED
@@ -1,8 +1,5 @@
1
1
  import plistModule from '@expo/plist'
2
-
3
- function isRecord(value: unknown): value is Record<string, unknown> {
4
- return typeof value === 'object' && value !== null && !Array.isArray(value)
5
- }
2
+ import { isRecord } from './is-record'
6
3
 
7
4
  function resolvePlistParser(value: unknown): ((source: string) => unknown) | null {
8
5
  if (!isRecord(value)) return null
@@ -1,9 +1,11 @@
1
1
  const name = 'Peach'
2
2
 
3
- export const rnxPublicBrand = Object.freeze({
3
+ export const peachPublicBrand = Object.freeze({
4
4
  name,
5
5
  packageName: 'peachsim',
6
6
  commandName: 'peach',
7
- domain: 'rnxsim.com',
8
- origin: 'https://rnxsim.com',
7
+ domain: 'peachsim.com',
8
+ origin: 'https://peachsim.com',
9
9
  })
10
+
11
+ export const rnxPublicBrand = peachPublicBrand
@@ -1,10 +1,5 @@
1
1
  export type RenderMode = 'worker'
2
2
 
3
- // worker rendering is now the only supported mode.
4
- export function getRenderMode(_searchParams: URLSearchParams): RenderMode {
5
- return 'worker'
6
- }
7
-
8
3
  export function isRenderWorkerMode(_searchParams: URLSearchParams): boolean {
9
4
  return true
10
5
  }
@@ -18,10 +13,3 @@ export function hasRenderWorkerBootstrap(
18
13
  ): boolean {
19
14
  return typeof workerModuleBaseUrl === 'string' && workerModuleBaseUrl.length > 0
20
15
  }
21
-
22
- export function shouldEnableRenderWorker(
23
- _searchParams: URLSearchParams,
24
- workerModuleBaseUrl: string | null | undefined,
25
- ): boolean {
26
- return hasRenderWorkerBootstrap(workerModuleBaseUrl)
27
- }
@@ -25,7 +25,7 @@ export const SOOTSIM_RUNTIME_MISSING_MESSAGE =
25
25
  // until a manual publish — the silent trap that cost a full night of
26
26
  // already-fixed bugs re-reproducing.
27
27
  //
28
- // the isolated CI headless path (scripts/local-publish-runtime.sh) sets
28
+ // the isolated CI headless path (scripts/dev/local-publish-runtime.sh) sets
29
29
  // PEACH_HOME to a dedicated dir precisely to validate the published-runtime
30
30
  // code path; keying off that EXISTING isolation env keeps it (and packaged
31
31
  // electron with a custom home) on the installed runtime.
@@ -112,6 +112,9 @@ const ROOT_RUNTIME_PATHS = [
112
112
  '/test-wallpaper.jpg',
113
113
  '/preview-sw.js',
114
114
  '/sootsim.svg',
115
+ '/bedrock-sqlite.wasm',
116
+ '/bedrock-sqlite.js',
117
+ '/sootsim/',
115
118
  ]
116
119
 
117
120
  export function resolveActiveRuntimeRoot(): string | null {
@@ -198,7 +201,11 @@ export function resolveRuntimeFilePath(
198
201
  if (segment === '..') return null
199
202
  }
200
203
 
201
- const fullPath = path.resolve(runtimeRoot, pathname.replace(/^\/+/, ''))
204
+ const normalized = pathname.startsWith('/sootsim/')
205
+ ? pathname.slice('/sootsim'.length)
206
+ : pathname
207
+
208
+ const fullPath = path.resolve(runtimeRoot, normalized.replace(/^\/+/, ''))
202
209
  const rootWithSep = runtimeRoot.endsWith(path.sep)
203
210
  ? runtimeRoot
204
211
  : runtimeRoot + path.sep
@@ -352,7 +352,13 @@ export async function composeMarketingScreenshots(
352
352
  })
353
353
  })())
354
354
  try {
355
- const context = await browser.newContext({ viewport: { width: 1280, height: 720 } })
355
+ // each slide's canvas width/height is the authored output raster (the
356
+ // composed image is written at exactly that size), so density stays 1:1
357
+ // and the page is only the compositor host.
358
+ const context = await browser.newContext({
359
+ viewport: { width: 1280, height: 720 },
360
+ deviceScaleFactor: 1,
361
+ })
356
362
  try {
357
363
  for (const slideSource of slides) {
358
364
  const buffer = readFileSync(slideSource.imagePath)
@@ -27,7 +27,7 @@ const METALLIC_RING_SHADOW =
27
27
  const FRAME_OUTLINE_BLEED = 1
28
28
 
29
29
  type FrameButtonLayout = {
30
- side: 'left' | 'right'
30
+ side: 'left' | 'right' | 'top' | 'bottom'
31
31
  top: number
32
32
  height: number
33
33
  width: number
@@ -184,21 +184,32 @@ export function getFramedScreenshotLayout(model: DeviceModel): FramedScreenshotL
184
184
 
185
185
  function renderSideButtonHtml(button: FrameButtonLayout): string {
186
186
  const isRight = button.side === 'right'
187
- const clip = isRight ? 'inset(-3px -3px -3px 0)' : 'inset(-3px 0 -3px -3px)'
187
+ const isBottom = button.side === 'bottom'
188
+ const horizontal = button.side === 'top' || isBottom
189
+ const clip = horizontal
190
+ ? isBottom
191
+ ? 'inset(0 -3px -3px -3px)'
192
+ : 'inset(-3px -3px 0 -3px)'
193
+ : isRight
194
+ ? 'inset(-3px -3px -3px 0)'
195
+ : 'inset(-3px 0 -3px -3px)'
188
196
  return `
189
197
  <div
190
198
  aria-hidden="true"
191
199
  style="
192
200
  position:absolute;
193
- ${isRight ? `right:-${button.width}px;` : `left:-${button.width}px;`}
194
- top:${button.top}px;
195
- width:${button.width}px;
196
- height:${button.height}px;
201
+ ${
202
+ horizontal
203
+ ? `left:${button.top}px;${isBottom ? `bottom:-${button.width}px;` : `top:-${button.width}px;`}`
204
+ : `${isRight ? `right:-${button.width}px;` : `left:-${button.width}px;`}top:${button.top}px;`
205
+ }
206
+ width:${horizontal ? button.height : button.width}px;
207
+ height:${horizontal ? button.width : button.height}px;
197
208
  background-color:#212121;
198
- border-top-left-radius:${isRight ? 0 : 1.5}px;
199
- border-bottom-left-radius:${isRight ? 0 : 1.5}px;
200
- border-top-right-radius:${isRight ? 1.5 : 0}px;
201
- border-bottom-right-radius:${isRight ? 1.5 : 0}px;
209
+ border-top-left-radius:${isRight || isBottom ? 0 : 1.5}px;
210
+ border-bottom-left-radius:${isRight || button.side === 'top' ? 0 : 1.5}px;
211
+ border-top-right-radius:${button.side === 'left' || isBottom ? 0 : 1.5}px;
212
+ border-bottom-right-radius:${button.side === 'left' || button.side === 'top' ? 0 : 1.5}px;
202
213
  box-shadow:
203
214
  inset 0 0.5px 0 #616161,
204
215
  inset 0 -0.5px 0 #3F3F3F,
@@ -16,6 +16,7 @@ import {
16
16
  type PosePresetName,
17
17
  type TextPresetName,
18
18
  } from './registry'
19
+ import { isRecord } from '../is-record'
19
20
 
20
21
  export type CaptureMode = 'raw' | 'framed' | 'raw+framed'
21
22
  export type CapturePathMode = 'auto' | 'plan' | 'flow'
@@ -69,10 +70,6 @@ export interface NormalizedScreenshotsPlan {
69
70
  compose: NormalizedComposePlan
70
71
  }
71
72
 
72
- function isRecord(value: unknown): value is Record<string, unknown> {
73
- return !!value && typeof value === 'object' && !Array.isArray(value)
74
- }
75
-
76
73
  function expectRecord(value: unknown, label: string): Record<string, unknown> {
77
74
  if (!isRecord(value)) throw new Error(`${label} must be an object`)
78
75
  return value
package/src/sim-client.ts CHANGED
@@ -48,6 +48,7 @@ import {
48
48
  type WaitForSootsimIdleResult,
49
49
  } from '../cli/commands/inspect/settling'
50
50
  import { openAppUrl } from './app-url'
51
+ import { sleep } from './sleep'
51
52
  import type { BridgeClaimResult, BridgeSimInfo, WsBridge } from '../cli/ws-bridge'
52
53
  import type {
53
54
  PerformOptions,
@@ -159,10 +160,6 @@ export interface SimClientOptions {
159
160
  commandTimeoutMs?: number
160
161
  }
161
162
 
162
- function sleep(ms: number): Promise<void> {
163
- return new Promise((resolve) => setTimeout(resolve, ms))
164
- }
165
-
166
163
  function decodePngDataUrl(dataUrl: unknown): SimScreenshot {
167
164
  if (typeof dataUrl !== 'string' || !dataUrl.startsWith('data:image/png;base64,')) {
168
165
  throw new SimClientError(
@@ -192,6 +189,7 @@ const TARGET_SEARCH_PARAMS = [
192
189
  'inspectOpen',
193
190
  'appFonts',
194
191
  'appSplash',
192
+ 'appCatalog',
195
193
  ]
196
194
 
197
195
  export function buildSimOpenUrl(currentUrl: string, target: number | string): string {
@@ -1,11 +1,8 @@
1
1
  // built-in skill: run Maestro tests against the app
2
2
 
3
+ import { sleep } from '../../sleep'
3
4
  import type { RNXSkill } from '../types'
4
5
 
5
- function sleep(ms: number) {
6
- return new Promise((resolve) => setTimeout(resolve, ms))
7
- }
8
-
9
6
  export const skill: RNXSkill = {
10
7
  name: 'maestro-test',
11
8
  description: 'Run Maestro YAML tests against the app',
package/src/sleep.ts ADDED
@@ -0,0 +1,4 @@
1
+ // resolve after ms
2
+ export function sleep(ms: number): Promise<void> {
3
+ return new Promise((resolve) => setTimeout(resolve, ms))
4
+ }