peachsim 0.1.659 → 0.1.661

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 +10 -15
  2. package/cli/cli-update.ts +53 -21
  3. package/cli/commands/compat.ts +6 -2
  4. package/cli/commands/control.ts +12 -3
  5. package/cli/commands/inspect.ts +16 -14
  6. package/cli/commands/upgrade.ts +4 -2
  7. package/cli/current-sim.ts +4 -0
  8. package/cli/drivers/linux-browser-libs.lock.json +1088 -0
  9. package/cli/drivers/linux-browser-libs.ts +182 -0
  10. package/cli/drivers/playwright-provisioning.ts +20 -98
  11. package/cli/drivers/playwright-sim-host.ts +9 -0
  12. package/cli/drivers/playwright.ts +2 -1
  13. package/dist-lib/agent-daemon-client.cjs +1 -1
  14. package/dist-lib/agent-events.cjs +1 -1
  15. package/dist-lib/agent-identity.cjs +1 -1
  16. package/dist-lib/agent-sessions.cjs +1 -1
  17. package/dist-lib/attached-projects.cjs +1 -1
  18. package/dist-lib/auth/shared-session.cjs +1 -1
  19. package/dist-lib/backend-origin.cjs +18 -1
  20. package/dist-lib/beta.cjs +1 -1
  21. package/dist-lib/beta.mjs +1 -1
  22. package/dist-lib/bridge-constants.cjs +1 -1
  23. package/dist-lib/bridge-contract-input.cjs +1 -1
  24. package/dist-lib/bridge-contract-input.mjs +1 -1
  25. package/dist-lib/bridge-contract.cjs +1 -1
  26. package/dist-lib/bridge-contract.mjs +1 -1
  27. package/dist-lib/capture-contract.cjs +1 -1
  28. package/dist-lib/capture-contract.mjs +1 -1
  29. package/dist-lib/cli-constants.cjs +1 -1
  30. package/dist-lib/cloud-contract.cjs +1 -1
  31. package/dist-lib/cloud-contract.mjs +1 -1
  32. package/dist-lib/cloud-test-args.cjs +1 -1
  33. package/dist-lib/cloud-test-args.mjs +1 -1
  34. package/dist-lib/cloud.cjs +1 -1
  35. package/dist-lib/cloud.mjs +1 -1
  36. package/dist-lib/config.cjs +1 -1
  37. package/dist-lib/detox/index.cjs +1 -1
  38. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  39. package/dist-lib/home-paths.cjs +1 -1
  40. package/dist-lib/host/bridge-host.cjs +45 -41
  41. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  42. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  43. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  44. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  45. package/dist-lib/host/websocket-proxy.cjs +1 -1
  46. package/dist-lib/index.cjs +5 -1
  47. package/dist-lib/jump-to-source-babel.cjs +1 -1
  48. package/dist-lib/jump-to-source-native.cjs +1 -1
  49. package/dist-lib/kotlin-host.cjs +8 -3
  50. package/dist-lib/menu.cjs +1 -1
  51. package/dist-lib/menu.mjs +1 -1
  52. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  53. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  54. package/dist-lib/metro-production-bundle.cjs +1 -1
  55. package/dist-lib/metro-production-bundle.mjs +1 -1
  56. package/dist-lib/metro.cjs +1 -1
  57. package/dist-lib/native-app-identity.cjs +1 -1
  58. package/dist-lib/native-app-identity.mjs +1 -1
  59. package/dist-lib/plist.cjs +1 -1
  60. package/dist-lib/plist.mjs +1 -1
  61. package/dist-lib/profiles.cjs +1 -1
  62. package/dist-lib/public-brand.cjs +1 -1
  63. package/dist-lib/react-native-host-modules.cjs +1 -1
  64. package/dist-lib/react-native-host-modules.mjs +1 -1
  65. package/dist-lib/render-mode.cjs +1 -1
  66. package/dist-lib/scripts/dev-server-scanner.cjs +1 -1
  67. package/dist-lib/sdk.cjs +1 -1
  68. package/dist-lib/sdk.mjs +1 -1
  69. package/dist-lib/skills.cjs +1339 -180
  70. package/dist-lib/swift-compile.cjs +1 -1
  71. package/dist-lib/swift-compile.mjs +1 -1
  72. package/dist-lib/swift-package.cjs +1 -1
  73. package/dist-lib/swift-package.mjs +1 -1
  74. package/dist-lib/swift-submit.cjs +1 -1
  75. package/dist-lib/swift-submit.mjs +1 -1
  76. package/dist-lib/swift.cjs +1 -1
  77. package/dist-lib/vite-swift.cjs +1 -1
  78. package/dist-lib/vite.cjs +1 -1
  79. package/kotlin/AppSources/android/app/Activity.kt +48 -3
  80. package/kotlin/AppSources/android/app/Application.kt +4 -0
  81. package/kotlin/AppSources/android/content/res/Configuration.kt +4 -0
  82. package/kotlin/AppSources/android/content/res/Resources.kt +3 -1
  83. package/kotlin/AppSources/android/view/Window.kt +15 -9
  84. package/kotlin/AppSources/androidx/activity/ComponentActivity.kt +36 -3
  85. package/kotlin/AppSources/dev/peach/app/PeachApp.kt +233 -86
  86. package/kotlin/ComposeSources/PeachComposeHost.kt +15 -3
  87. package/kotlin/fork/README.md +1 -0
  88. package/kotlin/fork/compose-worker-globals.patch +26 -0
  89. package/kotlin/fork/fork.json +1 -1
  90. package/kotlin/toolchain.json +3 -3
  91. package/package.json +2 -1
  92. package/src/android-theme.ts +14 -0
  93. package/src/backend-origin.ts +25 -0
  94. package/src/host/bridge-host.ts +24 -37
  95. package/src/kotlin-app.ts +29 -4
  96. package/src/kotlin-host.ts +7 -2
package/README.md CHANGED
@@ -244,21 +244,16 @@ software renderer without the desktop browser's printing and service discovery
244
244
  libraries (`libcups2`, `libavahi-client3`). Headed launches and every launch on
245
245
  a Mac use full Chromium. When the needed browser is not installed, the CLI runs
246
246
  the resolved Playwright core's absolute installer entry for just that browser.
247
- On Linux the CLI checks the browser's shared libraries before launching and,
248
- when any are missing, exits naming them and the exact `apt-get install` line.
249
- The headless shell needs these Debian 12 packages (Ubuntu 24.04 and Debian 13
250
- add a `t64` suffix to `libglib2.0-0`, `libatk1.0-0`, `libatk-bridge2.0-0`,
251
- `libatspi2.0-0` and `libasound2`):
252
-
253
- ```sh
254
- apt-get install --no-install-recommends libglib2.0-0 libnspr4 libnss3 \
255
- libatk1.0-0 libatk-bridge2.0-0 libatspi2.0-0 libdbus-1-3 libx11-6 libxcb1 \
256
- libxcomposite1 libxdamage1 libxext6 libxfixes3 libxrandr2 libgbm1 libexpat1 \
257
- libxkbcommon0 libudev1 libasound2
258
- ```
259
-
260
- Headed launches on Linux use full Chromium, which also needs `libcups2`,
261
- `libcairo2` and `libpango-1.0-0`. Later opens reuse that sim and
247
+ On Linux the CLI checks the browser's shared libraries before launching. When
248
+ the system lacks any, it downloads peach's own bundle of them once (Ubuntu 20.04
249
+ builds, so they load on glibc 2.31 and newer; about 15 MB) into
250
+ `$PEACH_HOME/linux-libs/` and launches the browser with that bundle on its
251
+ library path, so no host needs `apt-get` or root. The bundle is pinned in
252
+ `cli/drivers/linux-browser-libs.lock.json`; `bun scripts/linux-browser-libs.ts
253
+ resolve` re-pins it, and the CLI release publishes any bundle the bucket lacks.
254
+
255
+ Headed launches on Linux use full Chromium, whose extra libraries the bundle
256
+ also carries. Later opens reuse that sim and
262
257
  reload it at the requested target. Use `peach do reload` when the target has not
263
258
  changed.
264
259
  Reserve `--new` for a genuinely concurrent sim; repeated new browser trees
package/cli/cli-update.ts CHANGED
@@ -195,29 +195,61 @@ export function takeCliUpdateNotification(
195
195
  return update
196
196
  }
197
197
 
198
- export async function fetchCliReleaseFile(
199
- url: string,
200
- timeoutMs: number,
201
- ): Promise<Buffer> {
202
- let response: Response
198
+ // `stallMs` bounds silence, not the whole transfer: the executable is tens of
199
+ // megabytes, and on a slow link it keeps arriving well past any fixed total.
200
+ // only a server that does not answer, or a stream that stops, is a failure.
201
+ export async function fetchCliReleaseFile(url: string, stallMs: number): Promise<Buffer> {
202
+ const controller = new AbortController()
203
+ let timer = setTimeout(() => controller.abort(), stallMs)
204
+ const seconds = Math.round(stallMs / 1000)
203
205
  try {
204
- response = await fetch(url, {
205
- headers: { accept: 'application/octet-stream' },
206
- signal: AbortSignal.timeout(timeoutMs),
207
- })
208
- } catch (error) {
209
- throw new Error(
210
- `could not reach the peach update service: ${
211
- error instanceof Error ? error.message : String(error)
212
- }`,
213
- )
214
- }
215
- if (!response.ok) {
216
- throw new Error(
217
- `peach update download failed: ${response.status} ${response.statusText}`,
218
- )
206
+ let response: Response
207
+ try {
208
+ response = await fetch(url, {
209
+ headers: { accept: 'application/octet-stream' },
210
+ signal: controller.signal,
211
+ })
212
+ } catch (error) {
213
+ throw new Error(
214
+ controller.signal.aborted
215
+ ? `the peach update service did not answer ${url} within ${seconds}s`
216
+ : `could not reach the peach update service at ${url}: ${
217
+ error instanceof Error ? error.message : String(error)
218
+ }`,
219
+ )
220
+ }
221
+ // the answer arrived; the body gets its own full window.
222
+ clearTimeout(timer)
223
+ timer = setTimeout(() => controller.abort(), stallMs)
224
+ if (!response.ok) {
225
+ throw new Error(
226
+ `peach update download failed: ${response.status} ${response.statusText} (${url})`,
227
+ )
228
+ }
229
+ if (!response.body) throw new Error(`peach update service sent no body for ${url}`)
230
+ const reader = response.body.getReader()
231
+ const chunks: Uint8Array[] = []
232
+ try {
233
+ for (;;) {
234
+ const { done, value } = await reader.read()
235
+ if (done) break
236
+ chunks.push(value)
237
+ clearTimeout(timer)
238
+ timer = setTimeout(() => controller.abort(), stallMs)
239
+ }
240
+ } catch (error) {
241
+ throw new Error(
242
+ controller.signal.aborted
243
+ ? `peach update download of ${url} stalled for ${seconds}s`
244
+ : `peach update download of ${url} failed: ${
245
+ error instanceof Error ? error.message : String(error)
246
+ }`,
247
+ )
248
+ }
249
+ return Buffer.concat(chunks)
250
+ } finally {
251
+ clearTimeout(timer)
219
252
  }
220
- return Buffer.from(await response.arrayBuffer())
221
253
  }
222
254
 
223
255
  export async function refreshCliUpdateCache(
@@ -277,9 +277,13 @@ async function scanLocalNative(
277
277
  }
278
278
  }
279
279
  // a local expo module declares its name in its native definition, Name("X"),
280
- // whether or not the app's javascript spells it out.
280
+ // whether or not the app's javascript spells it out. swift first, so a
281
+ // module the report marks Swift points at its swift definition.
281
282
  for (const [, files] of nativeFilesByModule) {
282
- for (const file of files) {
283
+ const swiftFirst = [...files].sort(
284
+ (a, b) => Number(b.endsWith('.swift')) - Number(a.endsWith('.swift')),
285
+ )
286
+ for (const file of swiftFirst) {
283
287
  const text = readSmallFile(file)
284
288
  if (!text) continue
285
289
  for (const match of text.matchAll(/\bName\(\s*"(\w+)"\s*\)/g)) {
@@ -2069,7 +2069,13 @@ async function runUseLikeCommand(args: string[], opts: ControlCommandOptions = {
2069
2069
  // human is actively driving is the same footgun `claim` guards against,
2070
2070
  // so honor the lock here too (this command historically did not, which
2071
2071
  // let `peach use <id>` steal the user's live tab).
2072
- if (target.lockedBy && target.lockExpiresAt && target.lockExpiresAt > Date.now()) {
2072
+ // a lease this CLI identity holds is its own target already.
2073
+ if (
2074
+ target.lockedBy &&
2075
+ !target.lockedByMe &&
2076
+ target.lockExpiresAt &&
2077
+ target.lockExpiresAt > Date.now()
2078
+ ) {
2073
2079
  const secs = Math.max(0, Math.round((target.lockExpiresAt - Date.now()) / 1000))
2074
2080
  if (target.lockedByKind === 'user-active') {
2075
2081
  // the active human is interacting with this tab. never steal it,
@@ -2086,7 +2092,7 @@ async function runUseLikeCommand(args: string[], opts: ControlCommandOptions = {
2086
2092
  // a cli lease is short (60s, refreshes per command) so this is
2087
2093
  // usually a real conflict, not a stale lock.
2088
2094
  console.error(
2089
- ` refused: ${target.id} is leased by ${target.lockedBy} (${secs}s)`,
2095
+ ` refused: ${target.id} is leased by ${formatLockOwner(target.lockedBy)} (${secs}s)`,
2090
2096
  )
2091
2097
  console.error(
2092
2098
  ` \`peach use ${target.id} --force\` to take it, or \`peach open --new\` for a fresh sim`,
@@ -2130,7 +2136,10 @@ export async function runClaimCommand(args: string[], opts: ControlCommandOption
2130
2136
  const sims = await bridge.listSims()
2131
2137
  const target = resolveTargetSim(sims, parsed.positional[0] || parsed.simId)
2132
2138
  const priorHolder =
2133
- force && target.lockedBy && target.lockedByKind !== 'user-active'
2139
+ force &&
2140
+ target.lockedBy &&
2141
+ !target.lockedByMe &&
2142
+ target.lockedByKind !== 'user-active'
2134
2143
  ? target.lockedBy
2135
2144
  : null
2136
2145
  const result = await bridge.claim(target.id, { force })
@@ -274,6 +274,13 @@ function formatNetworkStatus(e: NetworkEntryPayload): string {
274
274
  return String(e.status)
275
275
  }
276
276
 
277
+ // how long a reload waits for the guest to boot, emit ready and settle. one
278
+ // budget for an in-place app reload and a full page reload alike: a real app
279
+ // that signs in and fetches over the network remounts past 10 s on a large
280
+ // screen, while a full engine boot runs 5-8 s unloaded and well past 10 s on
281
+ // a busy box.
282
+ const RELOAD_READY_TIMEOUT_MS = 30000
283
+
277
284
  function describeReloadReadyBlocker(result: ReloadReadyResult): string {
278
285
  if (result.externalError) return `guest app errored: ${result.externalError}`
279
286
  if (result.loadingText) return `still showing "${result.loadingText}"`
@@ -3499,10 +3506,9 @@ export async function runInspect(args: string[], opts: InspectOptions) {
3499
3506
  rnxExit(1)
3500
3507
  }
3501
3508
  // reload waits for the guest app's `sootsim:externalAppReady` signal
3502
- // (not just a live ws connection), with a 10s budget. fatal bundle
3503
- // failures surface as console errors that pile up fast — bail early
3504
- // in that case instead of waiting the full budget.
3505
- const READY_TIMEOUT_MS = 10000
3509
+ // (not just a live ws connection) within RELOAD_READY_TIMEOUT_MS. fatal
3510
+ // bundle failures surface as console errors that pile up fast — bail
3511
+ // early in that case instead of waiting the full budget.
3506
3512
  let issuedReload = false
3507
3513
  let inPlaceReload = false
3508
3514
  try {
@@ -3560,30 +3566,27 @@ export async function runInspect(args: string[], opts: InspectOptions) {
3560
3566
  let readyResult: ReloadReadyResult | null = null
3561
3567
  if (inPlaceReload) {
3562
3568
  readyResult = await pollForReloadReady(bridge, {
3563
- timeoutMs: READY_TIMEOUT_MS,
3569
+ timeoutMs: RELOAD_READY_TIMEOUT_MS,
3564
3570
  })
3565
3571
  } else {
3566
3572
  // full page reload — wait for a fresh bridge before polling for ready.
3567
3573
  // engine-drift escalation made page reloads the COMMON case during
3568
- // engine iteration, and a full engine boot runs 5-8s unloaded and
3569
- // well past 10s on a busy box — a 10s reconnect budget here reads
3570
- // as "bridge-reconnect fails" exactly when someone is rebuilding.
3571
- const PAGE_RELOAD_TIMEOUT_MS = 30000
3574
+ // engine iteration.
3572
3575
  if (issuedReload) await sleep(300)
3573
3576
  const reconnected = await waitForBridgeConnected(
3574
3577
  wsPort,
3575
3578
  commandTimeoutMs,
3576
3579
  simId,
3577
- { timeoutMs: PAGE_RELOAD_TIMEOUT_MS, simIdSource },
3580
+ { timeoutMs: RELOAD_READY_TIMEOUT_MS, simIdSource },
3578
3581
  )
3579
3582
  if (reconnected) {
3580
3583
  finalBridge = reconnected
3581
3584
  readyResult = await pollForReloadReady(reconnected, {
3582
- timeoutMs: PAGE_RELOAD_TIMEOUT_MS,
3585
+ timeoutMs: RELOAD_READY_TIMEOUT_MS,
3583
3586
  })
3584
3587
  } else {
3585
3588
  console.log(
3586
- ` ⚠ reload: bridge never reconnected within ${PAGE_RELOAD_TIMEOUT_MS}ms`,
3589
+ ` ⚠ reload: bridge never reconnected within ${RELOAD_READY_TIMEOUT_MS}ms`,
3587
3590
  )
3588
3591
  finalBridge = null
3589
3592
  }
@@ -3643,7 +3646,6 @@ export async function runInspect(args: string[], opts: InspectOptions) {
3643
3646
  }
3644
3647
 
3645
3648
  case 'storage-clear': {
3646
- const READY_TIMEOUT_MS = 10000
3647
3649
  await clearConsole(bridge)
3648
3650
  const cleared = await bridge.send({
3649
3651
  type: 'evaluate',
@@ -3665,7 +3667,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
3665
3667
  }
3666
3668
 
3667
3669
  const readyResult = await pollForReloadReady(bridge, {
3668
- timeoutMs: READY_TIMEOUT_MS,
3670
+ timeoutMs: RELOAD_READY_TIMEOUT_MS,
3669
3671
  })
3670
3672
 
3671
3673
  if (wantsJson(effectiveArgs)) {
@@ -120,7 +120,7 @@ export async function upgradeCli(options: UpgradeOptions = {}): Promise<void> {
120
120
 
121
121
  console.log(` v${current} → v${latest}`)
122
122
  console.log(` downloading ${artifact}...`)
123
- const executable = await fetchCliReleaseFile(artifactUrl, 120_000)
123
+ const executable = await fetchCliReleaseFile(artifactUrl, 30_000)
124
124
  const actualSha = createHash('sha256').update(executable).digest('hex')
125
125
  if (actualSha !== expectedSha) {
126
126
  throw new Error(
@@ -129,7 +129,9 @@ export async function upgradeCli(options: UpgradeOptions = {}): Promise<void> {
129
129
  }
130
130
 
131
131
  const executablePath = path.resolve(options.cliExecutablePath ?? process.execPath)
132
- const tempDir = fs.mkdtempSync(path.join(path.dirname(executablePath), '.peach-update-'))
132
+ const tempDir = fs.mkdtempSync(
133
+ path.join(path.dirname(executablePath), '.peach-update-'),
134
+ )
133
135
  const candidatePath = path.join(tempDir, path.basename(executablePath))
134
136
  try {
135
137
  fs.writeFileSync(candidatePath, executable, { mode: 0o755 })
@@ -21,6 +21,10 @@ const IDENTITY_ENV_VARS = [
21
21
  'PEACH_CLI_IDENTITY', // explicit override — fastest path, skips all pid lookups
22
22
  'CLAUDE_CODE_SESSION_ID', // claude code, if ever set
23
23
  'CODEX_THREAD_ID', // codex / openai agent thread, stable across tool invocations
24
+ // a Team Machine session, per agent run. its other runners set no session
25
+ // variable of their own, and the terminal variables below are inherited from
26
+ // whatever shell started the daemon, so every such agent would share one key.
27
+ 'TM_SESSION',
24
28
  'TERM_SESSION_ID', // macOS Terminal.app + iTerm2
25
29
  'ITERM_SESSION_ID', // iTerm2 (older variant)
26
30
  'TMUX_PANE', // tmux — stable per pane