peachsim 0.1.638 → 0.1.640

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 (81) hide show
  1. package/cli/command-registry.ts +1 -0
  2. package/cli/commands/control.ts +75 -6
  3. package/cli/commands/swift.ts +326 -0
  4. package/cli/main.ts +11 -0
  5. package/cli/outbound-endpoints.ts +5 -1
  6. package/dist-lib/agent-daemon-client.cjs +1 -1
  7. package/dist-lib/agent-events.cjs +1 -1
  8. package/dist-lib/agent-identity.cjs +1 -1
  9. package/dist-lib/agent-sessions.cjs +1 -1
  10. package/dist-lib/attached-projects.cjs +1 -1
  11. package/dist-lib/auth/shared-session.cjs +1 -1
  12. package/dist-lib/backend-origin.cjs +1 -1
  13. package/dist-lib/beta.cjs +1 -1
  14. package/dist-lib/beta.mjs +1 -1
  15. package/dist-lib/bridge-constants.cjs +1 -1
  16. package/dist-lib/bridge-contract-input.cjs +1 -1
  17. package/dist-lib/bridge-contract-input.mjs +1 -1
  18. package/dist-lib/bridge-contract.cjs +1 -1
  19. package/dist-lib/bridge-contract.mjs +1 -1
  20. package/dist-lib/capture-contract.cjs +1 -1
  21. package/dist-lib/capture-contract.mjs +1 -1
  22. package/dist-lib/cli-constants.cjs +1 -1
  23. package/dist-lib/cloud-contract.cjs +1 -1
  24. package/dist-lib/cloud-contract.mjs +1 -1
  25. package/dist-lib/cloud-test-args.cjs +1 -1
  26. package/dist-lib/cloud-test-args.mjs +1 -1
  27. package/dist-lib/cloud.cjs +1 -1
  28. package/dist-lib/cloud.mjs +1 -1
  29. package/dist-lib/config.cjs +1 -1
  30. package/dist-lib/detox/index.cjs +1 -1
  31. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  32. package/dist-lib/home-paths.cjs +1 -1
  33. package/dist-lib/host/bridge-host.cjs +1 -1
  34. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  35. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  36. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  37. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  38. package/dist-lib/host/websocket-proxy.cjs +1 -1
  39. package/dist-lib/index.cjs +13 -4
  40. package/dist-lib/jump-to-source-babel.cjs +1 -1
  41. package/dist-lib/jump-to-source-native.cjs +1 -1
  42. package/dist-lib/kotlin-host.cjs +1 -1
  43. package/dist-lib/menu.cjs +1 -1
  44. package/dist-lib/menu.mjs +1 -1
  45. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  46. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  47. package/dist-lib/metro-production-bundle.cjs +1 -1
  48. package/dist-lib/metro-production-bundle.mjs +1 -1
  49. package/dist-lib/metro.cjs +1 -1
  50. package/dist-lib/native-app-identity.cjs +1 -1
  51. package/dist-lib/native-app-identity.mjs +1 -1
  52. package/dist-lib/profiles.cjs +1 -1
  53. package/dist-lib/public-brand.cjs +1 -1
  54. package/dist-lib/react-native-host-modules.cjs +1 -1
  55. package/dist-lib/react-native-host-modules.mjs +1 -1
  56. package/dist-lib/render-mode.cjs +1 -1
  57. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  58. package/dist-lib/sdk.cjs +1 -1
  59. package/dist-lib/sdk.mjs +1 -1
  60. package/dist-lib/skills.cjs +389 -232
  61. package/dist-lib/swift-compile.cjs +1 -1
  62. package/dist-lib/swift-compile.mjs +1 -1
  63. package/dist-lib/swift-package.cjs +1 -1
  64. package/dist-lib/swift-package.mjs +1 -1
  65. package/dist-lib/swift-submit.cjs +89 -8
  66. package/dist-lib/swift-submit.mjs +78 -8
  67. package/dist-lib/swift.cjs +16 -33308
  68. package/dist-lib/vite-swift.cjs +422 -247
  69. package/dist-lib/vite.cjs +13 -4
  70. package/package.json +1 -1
  71. package/skills/contrast/SKILL.md +1 -0
  72. package/src/assetCatalogContract.ts +16 -0
  73. package/src/dot-env.ts +148 -0
  74. package/src/metro-runtime-source.ts +15 -0
  75. package/src/swift-guest.ts +51 -0
  76. package/src/swift-host.ts +900 -0
  77. package/src/swift-submit.ts +97 -6
  78. package/src/swift.ts +36 -8
  79. package/src/swiftContract.ts +52 -0
  80. package/src/vite-plugin-one.ts +4 -3
  81. package/src/vite-plugin-swift.ts +43 -755
@@ -42,6 +42,7 @@ const NODE_COMMAND_NAMES = [
42
42
  'android',
43
43
  'remote',
44
44
  'preview',
45
+ 'swift',
45
46
  'box',
46
47
  'list',
47
48
  'use',
@@ -24,6 +24,7 @@ import {
24
24
  resolveConnectionInput,
25
25
  type ResolvedDevBundle,
26
26
  } from '../../src/dev-bundle-resolution'
27
+ import { readProjectDotEnv } from '../../src/dot-env'
27
28
  import { flowTimeoutScale } from '../../src/flow-timeout-scale'
28
29
  import {
29
30
  isDaemonLockfileFresh,
@@ -99,6 +100,14 @@ interface ControlCommandOptions {
99
100
  runtimeConfig?: RNXConfig
100
101
  }
101
102
 
103
+ export interface OpenCommandResources {
104
+ bridge?: ReturnType<typeof spawn>
105
+ bridgeReady?: boolean
106
+ port?: number
107
+ driver?: DriverLaunchResult & { driverId: DriverId }
108
+ simId?: string
109
+ }
110
+
102
111
  interface DriverConnectWaitOptions {
103
112
  attempts: number
104
113
  intervalMs: number
@@ -184,6 +193,7 @@ function normalizeKnownTarget(target: string): string {
184
193
  export async function ensureBridgeForOpen(
185
194
  preferredPort: number,
186
195
  explicitPort = false,
196
+ resources?: OpenCommandResources,
187
197
  ): Promise<number> {
188
198
  const lock = readDaemonLockfile()
189
199
  assertExplicitBridgeLock(preferredPort, explicitPort, lock)
@@ -218,6 +228,10 @@ export async function ensureBridgeForOpen(
218
228
 
219
229
  await ensureRuntimeForOpen()
220
230
  const child = spawnSootsim(['serve', '--quiet', '--port', String(preferredPort)])
231
+ if (resources) {
232
+ resources.bridge = child
233
+ resources.port = preferredPort
234
+ }
221
235
  let childExit: string | null = null
222
236
  child.once('exit', (code, signal) => {
223
237
  childExit = String(code ?? signal ?? 'unknown')
@@ -252,6 +266,10 @@ export async function ensureBridgeForOpen(
252
266
  console.error(
253
267
  ` Started a local bridge on port ${selectedPort}. Run \`peach daemon install\` to keep it ready between commands.`,
254
268
  )
269
+ if (resources) {
270
+ resources.bridgeReady = true
271
+ resources.port = selectedPort
272
+ }
255
273
  return selectedPort
256
274
  }
257
275
  if (childExit) {
@@ -869,6 +887,9 @@ export interface BuildShellUrlOptions {
869
887
  // launch-time environment variables, stamped as repeated ?env=KEY%3DVALUE.
870
888
  // see the --env flag above.
871
889
  env?: string[]
890
+ // the project's env file as react-native-config's iOS build compiles it,
891
+ // stamped as ?dotEnv=<json>. set by `peach open --load-env` only.
892
+ dotEnv?: Record<string, string>
872
893
  }
873
894
 
874
895
  function bundleUrlOf(urlStr: string): string | undefined {
@@ -904,10 +925,12 @@ export async function buildShellUrl(
904
925
  !opts.proof &&
905
926
  !opts.posture &&
906
927
  !opts.osVersion &&
907
- !opts.env?.length
928
+ !opts.env?.length &&
929
+ !opts.dotEnv
908
930
  )
909
931
  return stamped
910
932
  const url = new URL(stamped)
933
+ if (opts.dotEnv) url.searchParams.set('dotEnv', JSON.stringify(opts.dotEnv))
911
934
  if (opts.device) url.searchParams.set('device', opts.device)
912
935
  if (opts.posture) url.searchParams.set('posture', opts.posture)
913
936
  if (opts.osVersion) url.searchParams.set('osVersion', opts.osVersion)
@@ -1048,13 +1071,14 @@ export async function waitForSimMatchOrHostExit(
1048
1071
  commandTimeoutMs: number,
1049
1072
  predicate: (sim: BridgeSimInfo) => boolean,
1050
1073
  hostPid: number | undefined,
1051
- opts: { attempts?: number; intervalMs?: number } = {},
1074
+ opts: { attempts?: number; intervalMs?: number; signal?: AbortSignal } = {},
1052
1075
  ): Promise<WaitForSimMatchOrHostExitResult> {
1053
1076
  const attempts = opts.attempts ?? 30
1054
1077
  const intervalMs = opts.intervalMs ?? 500
1055
1078
  const watchHost = !!hostPid && Number.isInteger(hostPid) && hostPid > 1
1056
1079
  const hostExited = () => watchHost && hostPid !== undefined && !isProcessAlive(hostPid)
1057
1080
  for (let i = 0; i < attempts; i++) {
1081
+ opts.signal?.throwIfAborted()
1058
1082
  if (hostExited()) {
1059
1083
  return { kind: 'host-exit' }
1060
1084
  }
@@ -1430,7 +1454,11 @@ export async function runLinkCommand(args: string[], opts: ControlCommandOptions
1430
1454
 
1431
1455
  export async function runOpenCommand(
1432
1456
  args: string[],
1433
- opts: ControlCommandOptions = {},
1457
+ opts: ControlCommandOptions & {
1458
+ resources?: OpenCommandResources
1459
+ signal?: AbortSignal
1460
+ ownerPid?: number
1461
+ } = {},
1434
1462
  ): Promise<{ simId: string; launched: boolean } | undefined> {
1435
1463
  const parsed = parseBridgeCliArgs(args, {
1436
1464
  port: opts.port,
@@ -1445,6 +1473,7 @@ export async function runOpenCommand(
1445
1473
  '--no-describe',
1446
1474
  '--quiet',
1447
1475
  '--proof',
1476
+ '--load-env',
1448
1477
  ],
1449
1478
  stripValueFlags: [
1450
1479
  '--base-url',
@@ -1581,7 +1610,14 @@ export async function runOpenCommand(
1581
1610
  }
1582
1611
  }
1583
1612
 
1584
- parsed.wsPort = await ensureBridgeForOpen(parsed.wsPort, parsed.explicitPort)
1613
+ opts.signal?.throwIfAborted()
1614
+ parsed.wsPort = await ensureBridgeForOpen(
1615
+ parsed.wsPort,
1616
+ parsed.explicitPort,
1617
+ opts.resources,
1618
+ )
1619
+ if (opts.resources) opts.resources.port = parsed.wsPort
1620
+ opts.signal?.throwIfAborted()
1585
1621
 
1586
1622
  // playwright is the browser-sim launcher. it owns an isolated profile and
1587
1623
  // can reap exactly its own browser tree. electron remains available for the
@@ -1698,6 +1734,30 @@ export async function runOpenCommand(
1698
1734
  ? { width: Number(viewportMatch[1]), height: Number(viewportMatch[2]) }
1699
1735
  : undefined
1700
1736
 
1737
+ // --load-env reads the project's env file the way react-native-config's iOS
1738
+ // build phase compiles it into the binary (ENVFILE, then .env), so the app's
1739
+ // `Config` holds those values. without it `Config` is empty, as in a build
1740
+ // that found no env file, and the app's first read warns in the console.
1741
+ let dotEnv: Record<string, string> | undefined
1742
+ if (args.includes('--load-env')) {
1743
+ if (!project) {
1744
+ console.error(
1745
+ ' peach open: --load-env needs a local app project to read the env file from',
1746
+ )
1747
+ rnxExit(1)
1748
+ }
1749
+ try {
1750
+ const result = readProjectDotEnv(project.appDir)
1751
+ if (!quiet) for (const notice of result.notices) console.log(` ${notice}`)
1752
+ dotEnv = result.config
1753
+ } catch (error) {
1754
+ console.error(
1755
+ ` peach open: ${error instanceof Error ? error.message : String(error)}`,
1756
+ )
1757
+ rnxExit(1)
1758
+ }
1759
+ }
1760
+
1701
1761
  const buildShellUrlOpts: BuildShellUrlOptions = {
1702
1762
  hot: hotMode,
1703
1763
  device: deviceArg,
@@ -1707,6 +1767,7 @@ export async function runOpenCommand(
1707
1767
  project,
1708
1768
  proof: args.includes('--proof'),
1709
1769
  env: envArgs.length ? envArgs : undefined,
1770
+ dotEnv,
1710
1771
  }
1711
1772
 
1712
1773
  // resolve the shell base URL from the bridge port we settled on, so the page
@@ -1890,7 +1951,7 @@ export async function runOpenCommand(
1890
1951
 
1891
1952
  const configuredBaseUrl = applyRNXConfigToUrl(baseUrl, runtimeConfig)
1892
1953
  const openUrl = await buildOpenUrl(target, configuredBaseUrl, token, buildShellUrlOpts)
1893
- const ownerPid = getStableOwnerPid()
1954
+ const ownerPid = opts.ownerPid ?? getStableOwnerPid()
1894
1955
  if (!ownerPid) {
1895
1956
  console.error(
1896
1957
  ' could not resolve a stable owning process; refusing unsupervised sim host',
@@ -1903,6 +1964,7 @@ export async function runOpenCommand(
1903
1964
  rnxExit(1)
1904
1965
  }
1905
1966
  const connectWait = resolveDriverConnectWaitOptions()
1967
+ opts.signal?.throwIfAborted()
1906
1968
  const result = await driver.launch({
1907
1969
  url: openUrl,
1908
1970
  // the shell serves the sim page but its WS bridge may have drifted off
@@ -1925,6 +1987,8 @@ export async function runOpenCommand(
1925
1987
  : undefined),
1926
1988
  connectTimeoutMs: connectWait.hostTimeoutMs,
1927
1989
  })
1990
+ if (opts.resources) opts.resources.driver = { ...result, driverId }
1991
+ opts.signal?.throwIfAborted()
1928
1992
  if (!result.launched) {
1929
1993
  console.error(` ${driver.name} driver: ${result.message}`)
1930
1994
  rnxExit(1)
@@ -1934,7 +1998,11 @@ export async function runOpenCommand(
1934
1998
  parsed.commandTimeoutMs,
1935
1999
  simMatchesToken,
1936
2000
  result.pid,
1937
- { attempts: connectWait.attempts, intervalMs: connectWait.intervalMs },
2001
+ {
2002
+ attempts: connectWait.attempts,
2003
+ intervalMs: connectWait.intervalMs,
2004
+ signal: opts.signal,
2005
+ },
1938
2006
  )
1939
2007
  if (outcome.kind === 'host-exit') {
1940
2008
  printDriverDiagnosticLogTail(result)
@@ -1955,6 +2023,7 @@ export async function runOpenCommand(
1955
2023
  rnxExit(1)
1956
2024
  }
1957
2025
  const match = outcome.sim
2026
+ if (opts.resources) opts.resources.simId = match.id
1958
2027
  saveCurrentSimId(match.id)
1959
2028
  signalDriverLaunchConnected(result)
1960
2029
  printOpenedSim(openUrl, match, `${driver.name} driver`, quiet)
@@ -0,0 +1,326 @@
1
+ import fs from 'node:fs'
2
+ import http from 'node:http'
3
+ import path from 'node:path'
4
+ import { WebSocket, WebSocketServer } from 'ws'
5
+ import { isLoopbackHost, resolveContrastBackendOrigin } from '../../src/backend-origin'
6
+ import { swiftGuestBundle } from '../../src/swift-guest'
7
+ import { SWIFT_HMR_PATH, SWIFT_PACKAGE_PATH } from '../../src/swift-submit'
8
+ import { authHeaderValue, resolveCliAuth } from '../auth'
9
+ import { PeachExit } from '../run-rnx'
10
+ import { createBridge } from '../ws-bridge'
11
+ import { runCloseCommand, runOpenCommand, type OpenCommandResources } from './control'
12
+ import type { SwiftEntryModuleArgs } from '../../../sootsim-swift/src/entryModule'
13
+
14
+ export async function runSwift(
15
+ args: string[],
16
+ opts: { port?: number; device?: string; driver?: string; headless?: boolean } = {},
17
+ ): Promise<number> {
18
+ const input = [...args]
19
+ const openArgs: string[] = []
20
+ let hostPort = 0
21
+ let uploadSource = false
22
+ const valueFlags = new Set([
23
+ '--device',
24
+ '--driver',
25
+ '--profile',
26
+ '--cdp-port',
27
+ '--os-version',
28
+ '--base-url',
29
+ ])
30
+ const booleanFlags = new Set([
31
+ '--headless',
32
+ '--headed',
33
+ '--ephemeral',
34
+ '--quiet',
35
+ '--proof',
36
+ ])
37
+ let packagePath = ''
38
+ for (let index = 0; index < input.length; index++) {
39
+ const arg = input[index]
40
+ if (arg === '--upload-source') uploadSource = true
41
+ else if (arg === '--host-port' || valueFlags.has(arg)) {
42
+ const value = input[++index]
43
+ if (!value || value.startsWith('-')) throw new Error(`${arg} requires a value`)
44
+ if (arg === '--host-port') {
45
+ hostPort = Number(value)
46
+ if (!Number.isInteger(hostPort) || hostPort < 1 || hostPort > 65535) {
47
+ throw new Error('--host-port must be a port from 1 to 65535')
48
+ }
49
+ } else openArgs.push(arg, value)
50
+ } else if (booleanFlags.has(arg)) openArgs.push(arg)
51
+ else if (arg.startsWith('-')) throw new Error(`unknown peach swift flag: ${arg}`)
52
+ else if (packagePath) throw new Error('peach swift expects one SwiftPM directory')
53
+ else packagePath = path.resolve(arg)
54
+ }
55
+ if (!packagePath)
56
+ throw new Error('usage: peach swift <SwiftPM-directory> --upload-source')
57
+ if (!uploadSource) {
58
+ throw new Error(
59
+ 'peach swift uploads the selected package sources and resources to the authenticated Contrast compiler. Pass --upload-source to consent to this directory and its watched edits.',
60
+ )
61
+ }
62
+ const auth = resolveCliAuth()
63
+ if (!auth || auth.kind === 'github') {
64
+ throw new Error(
65
+ 'peach swift needs `peach login` or PEACH_API_KEY before reading project sources',
66
+ )
67
+ }
68
+ const compileOrigin = resolveContrastBackendOrigin(
69
+ auth.kind === 'session' ? auth.origin : null,
70
+ )
71
+ packagePath = fs.realpathSync(packagePath)
72
+ if (
73
+ !fs.statSync(packagePath).isDirectory() ||
74
+ !fs.existsSync(path.join(packagePath, 'Package.swift'))
75
+ ) {
76
+ throw new Error('peach swift requires a directory containing Package.swift')
77
+ }
78
+ const host = await import('../../src/swift-host')
79
+ // collection preflights every input path before any source bytes are read.
80
+ host.swiftPackageWatchFiles(packagePath, undefined)
81
+ const packageName = path.basename(packagePath)
82
+ const guest = swiftGuestBundle(packageName)
83
+ const controller = new AbortController()
84
+ const resources: OpenCommandResources = {}
85
+ let metadata: (SwiftEntryModuleArgs & { hmrPath: string }) | null = null
86
+ let compileError = 'Swift compilation is starting'
87
+ let origin = ''
88
+ let requested = 0
89
+ let completed = 0
90
+ let building: Promise<void> | undefined
91
+ const sockets = new WebSocketServer({ noServer: true })
92
+ const send = (message: object) => {
93
+ const text = JSON.stringify(message)
94
+ for (const client of sockets.clients)
95
+ if (client.readyState === WebSocket.OPEN) client.send(text)
96
+ }
97
+ const refresh = (): Promise<void> => {
98
+ requested++
99
+ if (building) return building
100
+ building = (async () => {
101
+ while (!controller.signal.aborted && completed !== requested) {
102
+ const revision = requested
103
+ try {
104
+ const next = await host.swiftPackageMetadata(
105
+ packagePath,
106
+ packageName,
107
+ undefined,
108
+ origin,
109
+ {
110
+ origin: compileOrigin,
111
+ authorization: authHeaderValue(auth),
112
+ signal: controller.signal,
113
+ },
114
+ )
115
+ if (controller.signal.aborted) return
116
+ if (revision === requested) {
117
+ metadata = { ...next, hmrPath: `${origin}${SWIFT_HMR_PATH}` }
118
+ compileError = ''
119
+ send({ type: 'package', packageName, metadata })
120
+ console.log(` Swift artifact: ${next.hash}`)
121
+ }
122
+ } catch (error) {
123
+ if (controller.signal.aborted) return
124
+ if (revision === requested) {
125
+ compileError = error instanceof Error ? error.message : String(error)
126
+ console.error(compileError)
127
+ send({ type: 'error', packageName, message: compileError })
128
+ }
129
+ }
130
+ completed = revision
131
+ }
132
+ })().finally(() => {
133
+ building = undefined
134
+ })
135
+ return building
136
+ }
137
+ const validRequest = (req: http.IncomingMessage): boolean => {
138
+ if (req.headers.host !== new URL(origin).host) return false
139
+ if (!req.headers.origin) return true
140
+ try {
141
+ const requestOrigin = new URL(req.headers.origin)
142
+ return (
143
+ (requestOrigin.protocol === 'http:' || requestOrigin.protocol === 'https:') &&
144
+ isLoopbackHost(requestOrigin.hostname)
145
+ )
146
+ } catch {
147
+ return false
148
+ }
149
+ }
150
+ const server = http.createServer((req, res) => {
151
+ if (!validRequest(req)) {
152
+ res.statusCode = 403
153
+ res.end('forbidden Swift host origin')
154
+ return
155
+ }
156
+ res.setHeader('access-control-allow-origin', req.headers.origin ?? '*')
157
+ res.setHeader('vary', 'origin')
158
+ res.setHeader('cache-control', 'no-store')
159
+ if (req.method !== 'GET') {
160
+ res.statusCode = 405
161
+ res.end('GET required')
162
+ return
163
+ }
164
+ const pathname = new URL(req.url ?? '/', origin).pathname
165
+ if (pathname === '/__peach/swift-guest.js') {
166
+ res.setHeader('content-type', 'application/javascript')
167
+ res.end(guest)
168
+ } else if (pathname === `${SWIFT_PACKAGE_PATH}${encodeURIComponent(packageName)}`) {
169
+ if (!metadata) {
170
+ res.statusCode = 503
171
+ res.end(compileError)
172
+ return
173
+ }
174
+ res.setHeader('content-type', 'application/json')
175
+ res.end(JSON.stringify(metadata))
176
+ } else if (!host.serveSwiftPackageAsset(pathname, res)) {
177
+ res.statusCode = 404
178
+ res.end('unknown Swift host asset')
179
+ }
180
+ })
181
+ server.on('upgrade', (req, socket, head) => {
182
+ if (
183
+ !validRequest(req) ||
184
+ new URL(req.url ?? '/', origin).pathname !== SWIFT_HMR_PATH
185
+ ) {
186
+ socket.destroy()
187
+ return
188
+ }
189
+ sockets.handleUpgrade(req, socket, head, (client) => {
190
+ sockets.emit('connection', client, req)
191
+ client.send(
192
+ JSON.stringify(
193
+ metadata
194
+ ? { type: 'package', packageName, metadata }
195
+ : { type: 'error', packageName, message: compileError },
196
+ ),
197
+ )
198
+ if (metadata && compileError)
199
+ client.send(JSON.stringify({ type: 'error', packageName, message: compileError }))
200
+ })
201
+ })
202
+ const stop = () => controller.abort()
203
+ process.once('SIGINT', stop)
204
+ process.once('SIGTERM', stop)
205
+ let watcher: fs.FSWatcher | undefined
206
+ let code = 0
207
+ try {
208
+ await new Promise<void>((resolve, reject) => {
209
+ server.once('error', reject)
210
+ server.listen(hostPort, '127.0.0.1', () => {
211
+ server.removeListener('error', reject)
212
+ const address = server.address()
213
+ if (!address || typeof address === 'string') {
214
+ reject(new Error('Swift host has no loopback port'))
215
+ return
216
+ }
217
+ origin = `http://127.0.0.1:${address.port}`
218
+ resolve()
219
+ })
220
+ })
221
+ server.on('error', (error) => {
222
+ console.error(error.message)
223
+ code = 1
224
+ stop()
225
+ })
226
+ watcher = fs.watch(packagePath, { recursive: true }, (_event, file) => {
227
+ if (
228
+ file &&
229
+ file
230
+ .split(/[\\/]/)
231
+ .some(
232
+ (segment) =>
233
+ (segment.startsWith('.') &&
234
+ segment !== '.one-native-source.json' &&
235
+ segment !== '.one-native-source.swift.txt') ||
236
+ ['node_modules', 'ios', 'android', 'build', 'dist', 'types'].includes(
237
+ segment,
238
+ ),
239
+ )
240
+ )
241
+ return
242
+ void refresh()
243
+ })
244
+ watcher.on('error', (error) => {
245
+ console.error(error.message)
246
+ code = 1
247
+ stop()
248
+ })
249
+ console.log(
250
+ ` Swift source upload: ${packagePath} → ${compileOrigin}/api/swift-compile`,
251
+ )
252
+ console.log(` Swift preview host: ${origin}`)
253
+ await refresh()
254
+ controller.signal.throwIfAborted()
255
+ if (opts.device) openArgs.push('--device', opts.device)
256
+ if (opts.driver) openArgs.push('--driver', opts.driver)
257
+ if (opts.headless) openArgs.push('--headless')
258
+ await runOpenCommand(
259
+ [`${origin}/__peach/swift-guest.js`, '--new', '--no-hmr', ...openArgs],
260
+ {
261
+ port: opts.port,
262
+ resources,
263
+ signal: controller.signal,
264
+ ownerPid: process.pid,
265
+ },
266
+ )
267
+ await new Promise<void>((resolve) => {
268
+ if (controller.signal.aborted) resolve()
269
+ else controller.signal.addEventListener('abort', () => resolve(), { once: true })
270
+ })
271
+ } catch (error) {
272
+ if (!controller.signal.aborted) {
273
+ console.error(
274
+ ` peach swift failed: ${error instanceof Error ? error.message : String(error)}`,
275
+ )
276
+ code = error instanceof PeachExit ? error.code : 1
277
+ }
278
+ } finally {
279
+ controller.abort()
280
+ process.removeListener('SIGINT', stop)
281
+ process.removeListener('SIGTERM', stop)
282
+ watcher?.close()
283
+ await building
284
+ for (const client of sockets.clients) client.terminate()
285
+ sockets.close()
286
+ server.closeAllConnections()
287
+ await new Promise<void>((resolve) => server.close(() => resolve()))
288
+ if (resources.simId && resources.port) {
289
+ try {
290
+ await runCloseCommand(['--sim', resources.simId], { port: resources.port })
291
+ } catch (error) {
292
+ console.error(` Swift preview cleanup failed: ${String(error)}`)
293
+ code = 1
294
+ }
295
+ }
296
+ if (
297
+ resources.driver?.driverId === 'playwright' &&
298
+ resources.driver.launched &&
299
+ resources.driver.pid
300
+ ) {
301
+ try {
302
+ process.kill(resources.driver.pid, 'SIGTERM')
303
+ } catch (error) {
304
+ if (!(error instanceof Error && 'code' in error && error.code === 'ESRCH')) {
305
+ console.error(String(error))
306
+ code = 1
307
+ }
308
+ }
309
+ }
310
+ if (resources.bridge && !resources.bridgeReady) resources.bridge.kill('SIGTERM')
311
+ if (resources.bridge && resources.bridgeReady && resources.port) {
312
+ const bridge = createBridge(resources.port)
313
+ try {
314
+ if ((await bridge.listSims()).length === 0) resources.bridge.kill('SIGTERM')
315
+ else console.log(' preserving the bridge adopted by another simulator')
316
+ } catch (error) {
317
+ console.error(` cannot verify owned bridge cleanup: ${String(error)}`)
318
+ code = 1
319
+ } finally {
320
+ bridge.close()
321
+ }
322
+ }
323
+ host.disposeSwiftProject(packagePath)
324
+ }
325
+ return code
326
+ }
package/cli/main.ts CHANGED
@@ -551,6 +551,17 @@ try {
551
551
  }
552
552
  }
553
553
 
554
+ case 'swift': {
555
+ if (globalSimTarget) {
556
+ console.error(' peach swift owns a new preview and cannot target --sim')
557
+ await exitWithFlush(1)
558
+ }
559
+ const { runSwift } = await import('./commands/swift')
560
+ await exitWithFlush(
561
+ await runSwift(parsed.commandArgs, { port, device, driver, headless }),
562
+ )
563
+ }
564
+
554
565
  case 'preview': {
555
566
  const { runPreview } = await import('./commands/preview')
556
567
  await exitWithFlush(await runPreview(parsed.commandArgs))
@@ -124,7 +124,7 @@ export const OUTBOUND_ENDPOINTS: Record<string, OutboundEndpointDeclaration> = {
124
124
  'the origin already serving the app, or the absolute Contrast origin a node caller passes',
125
125
  preConsent: false,
126
126
  payload:
127
- 'the project .swift sources, packed bundle resources and images, plus the signed preview token or ccdn_ api key that authorizes the compile',
127
+ 'the selected project .swift sources, packed bundle resources and images, authorized with a preview token, stored session or API key; peach swift requires --upload-source for that root and its watched edits',
128
128
  },
129
129
  peach_cloud_sim: {
130
130
  category: 'explicit-user-action',
@@ -221,6 +221,10 @@ export const DECLARED_OUTBOUND_CALLS: Record<string, OutboundCallDeclaration> =
221
221
  // compiling a project's .swift sources needs a swift toolchain, so the
222
222
  // bundler posts them to the app's own /api/swift-compile. it only fires with
223
223
  // a compile credential and only for a project that has .swift sources.
224
+ 'packages/peachsim/src/swift-host.ts :: fetch :: address.url': {
225
+ category: 'swift_compile',
226
+ count: 1,
227
+ },
224
228
  'packages/peachsim/src/swift-submit.ts :: fetch :: url': {
225
229
  category: 'swift_compile',
226
230
  count: 1,
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.638 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.640 | (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.638 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.640 | (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.638 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.640 | (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.638 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.640 | (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.638 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.640 | (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.638 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.640 | (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.638 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.640 | (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.638 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.640 | (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.638 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.640 | (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.638 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.640 | (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.638 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.640 | (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.638 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.640 | (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.638 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.640 | (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;