peachsim 0.1.1406 → 0.1.1463

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 (149) hide show
  1. package/LICENSE +8 -4
  2. package/README.md +22 -2
  3. package/cli/app-fonts.ts +50 -44
  4. package/cli/bin.ts +5 -4
  5. package/cli/bridge-diagnostics.ts +5 -0
  6. package/cli/bridge-flow-runner.ts +21 -13
  7. package/cli/cloud-client.ts +4 -3
  8. package/cli/commands/control.ts +155 -123
  9. package/cli/commands/daemon-mac-app.ts +3 -3
  10. package/cli/commands/daemon.ts +2 -2
  11. package/cli/commands/do-chain.ts +9 -2
  12. package/cli/commands/electron.ts +4 -1
  13. package/cli/commands/inspect/core.ts +1 -1
  14. package/cli/commands/inspect/settle.ts +19 -7
  15. package/cli/commands/inspect/settling.ts +92 -56
  16. package/cli/commands/inspect/wait-idle.ts +21 -9
  17. package/cli/commands/inspect/wait-ready.ts +22 -8
  18. package/cli/commands/inspect.ts +101 -134
  19. package/cli/commands/setup.ts +26 -16
  20. package/cli/commands/upgrade.ts +53 -9
  21. package/cli/desktop-companion.ts +1 -1
  22. package/cli/drivers/playwright-sim-host.ts +22 -13
  23. package/cli/drivers/playwright.ts +3 -3
  24. package/cli/main.ts +14 -5
  25. package/cli/outbound-endpoints.ts +1 -9
  26. package/detox/index.ts +6 -1
  27. package/dist-lib/agent-daemon-client.cjs +1 -1
  28. package/dist-lib/agent-events.cjs +1 -1
  29. package/dist-lib/attached-projects.cjs +1 -1
  30. package/dist-lib/auth/shared-session.cjs +1 -1
  31. package/dist-lib/backend-origin.cjs +1 -1
  32. package/dist-lib/beta.cjs +1 -1
  33. package/dist-lib/beta.mjs +1 -1
  34. package/dist-lib/bridge-constants.cjs +1 -1
  35. package/dist-lib/bridge-contract-input.cjs +1 -1
  36. package/dist-lib/bridge-contract-input.mjs +1 -1
  37. package/dist-lib/bridge-contract.cjs +3 -3
  38. package/dist-lib/bridge-contract.mjs +3 -3
  39. package/dist-lib/bundle-channel.cjs +322 -274
  40. package/dist-lib/capture-contract.cjs +1 -1
  41. package/dist-lib/capture-contract.mjs +1 -1
  42. package/dist-lib/cloud-contract.cjs +1 -1
  43. package/dist-lib/cloud-contract.mjs +1 -1
  44. package/dist-lib/cloud-test-args.cjs +1 -1
  45. package/dist-lib/cloud-test-args.mjs +1 -1
  46. package/dist-lib/cloud.cjs +1 -1
  47. package/dist-lib/cloud.mjs +1 -1
  48. package/dist-lib/config.cjs +1 -1
  49. package/dist-lib/config.mjs +1 -1
  50. package/dist-lib/detox/index.cjs +322 -45
  51. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  52. package/dist-lib/expo-swift-bundle.cjs +69 -33
  53. package/dist-lib/expo-swift-metadata.cjs +1 -1
  54. package/dist-lib/expo-swift-metadata.mjs +1 -1
  55. package/dist-lib/home-paths.cjs +1 -1
  56. package/dist-lib/host/bridge-host.cjs +3650 -3528
  57. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  58. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  59. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  60. package/dist-lib/host/replacement-module-handler.cjs +1 -1
  61. package/dist-lib/host/websocket-proxy.cjs +1 -1
  62. package/dist-lib/index.cjs +327 -123
  63. package/dist-lib/jump-to-source-babel.cjs +1 -1
  64. package/dist-lib/jump-to-source-native.cjs +1 -1
  65. package/dist-lib/kotlin-host.cjs +10 -5
  66. package/dist-lib/menu.cjs +1 -1
  67. package/dist-lib/menu.mjs +1 -1
  68. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  69. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  70. package/dist-lib/metro-production-bundle.cjs +1 -1
  71. package/dist-lib/metro-production-bundle.mjs +1 -1
  72. package/dist-lib/metro.cjs +49 -47
  73. package/dist-lib/native-app-identity-project.cjs +1 -1
  74. package/dist-lib/native-app-identity-project.mjs +1 -1
  75. package/dist-lib/native-app-identity.cjs +1 -1
  76. package/dist-lib/native-app-identity.mjs +1 -1
  77. package/dist-lib/native-firebase-metadata.cjs +1 -1
  78. package/dist-lib/native-firebase-metadata.mjs +1 -1
  79. package/dist-lib/plist.cjs +1 -1
  80. package/dist-lib/plist.mjs +1 -1
  81. package/dist-lib/profiles.cjs +1 -1
  82. package/dist-lib/public-brand.cjs +1 -1
  83. package/dist-lib/react-native-host-modules.cjs +1 -1
  84. package/dist-lib/react-native-host-modules.mjs +1 -1
  85. package/dist-lib/scripts/dev-server-scanner.cjs +23 -17
  86. package/dist-lib/sdk.cjs +99 -61
  87. package/dist-lib/sdk.mjs +97 -61
  88. package/dist-lib/skills.cjs +2959 -2780
  89. package/dist-lib/swift-compile.cjs +26 -12
  90. package/dist-lib/swift-compile.mjs +26 -12
  91. package/dist-lib/swift-package.cjs +1 -1
  92. package/dist-lib/swift-package.mjs +1 -1
  93. package/dist-lib/swift-submit.cjs +2 -2
  94. package/dist-lib/swift-submit.mjs +2 -2
  95. package/dist-lib/swift-toolchain.cjs +1 -1
  96. package/dist-lib/swift.cjs +1 -1
  97. package/dist-lib/vite-swift.cjs +66 -31
  98. package/dist-lib/vite.cjs +1 -1
  99. package/dist-lib/worklet-executable.cjs +1 -1
  100. package/dist-lib/worklet-executable.mjs +1 -1
  101. package/kotlin/AppSources/android/view/Window.kt +2 -0
  102. package/kotlin/AppSources/dev/peach/app/PeachApp.kt +1 -1
  103. package/kotlin/ComposeSources/PeachComposeHost.kt +35 -9
  104. package/kotlin/ExpoComposeSources/dev/peach/expo/PeachExpoCompose.kt +2 -2
  105. package/kotlin/ExpoSources/dev/peach/expo/PeachExpoRuntime.kt +9 -6
  106. package/kotlin/ExpoSources/dev/peach/expo/PeachExpoTypes.kt +4 -4
  107. package/kotlin/ExpoSources/expo/modules/kotlin/modules/Module.kt +2 -2
  108. package/kotlin/ExpoSources/expo/modules/kotlin/viewevent/ViewEvent.kt +22 -8
  109. package/kotlin/ExpoSources/expo/modules/kotlin/views/ExpoView.kt +0 -3
  110. package/kotlin/ExpoSources/expo/modules/kotlin/views/ViewDefinitionBuilder.kt +10 -9
  111. package/kotlin/ExpoSources/expo/modules/kotlin/views/ViewProps.kt +6 -5
  112. package/kotlin/fixtures/import-drawing/Drawing.d.kt.ts +7 -0
  113. package/kotlin/fixtures/import-drawing/Drawing.kt +101 -0
  114. package/kotlin/fixtures/import-drawing/ImportDrawing.native.tsx +18 -0
  115. package/kotlin/fixtures/import-drawing/README.md +34 -0
  116. package/kotlin/fixtures/import-drawing/app/_layout.tsx +4 -0
  117. package/kotlin/fixtures/import-drawing/app/index+spa.tsx +1 -0
  118. package/kotlin/fixtures/import-drawing/package.json +23 -0
  119. package/kotlin/fixtures/import-drawing/tsconfig.json +20 -0
  120. package/kotlin/fixtures/import-drawing/vite.config.ts +20 -0
  121. package/kotlin/fixtures/import-scroll/ImportNested.native.tsx +10 -0
  122. package/kotlin/fixtures/import-scroll/Nested.d.kt.ts +4 -0
  123. package/kotlin/fixtures/import-scroll/README.md +15 -0
  124. package/kotlin/fixtures/import-scroll/app/nested+spa.tsx +1 -0
  125. package/kotlin/fixtures/import-values/ImportValues.native.tsx +26 -0
  126. package/kotlin/fixtures/import-values/README.md +6 -1
  127. package/kotlin/fixtures/import-values/Values.d.kt.ts +4 -0
  128. package/kotlin/fixtures/import-values/Values.kt +18 -0
  129. package/kotlin/tools/expo-types.main.kts +17 -5
  130. package/package.json +2 -1
  131. package/scripts/dev-server-scanner.ts +33 -23
  132. package/src/assetCatalogContract.ts +7 -0
  133. package/src/bridge-contract.ts +21 -3
  134. package/src/connect.ts +18 -5
  135. package/src/desktop-host-delivery.ts +33 -3
  136. package/src/expo-swift-host.ts +6 -3
  137. package/src/host/bridge-host.ts +2 -0
  138. package/src/host/fetch-proxy-transport.ts +8 -0
  139. package/src/kotlin-host.ts +11 -5
  140. package/src/native-firebase-config.ts +95 -54
  141. package/src/process-output.ts +16 -0
  142. package/src/sdk.ts +6 -1
  143. package/src/sim-client.ts +6 -1
  144. package/src/swift-compile.ts +36 -15
  145. package/src/swift-distribution.lock.json +8 -8
  146. package/src/swift-standin-emit.ts +10 -1
  147. package/src/swift-standins.ts +21 -7
  148. package/src/swift-submit.ts +6 -1
  149. /package/kotlin/fixtures/{kotlin-app-variants → import-scroll}/Nested.kt +0 -0
@@ -84,7 +84,7 @@ import {
84
84
  } from './inspect/resolve-target'
85
85
  import { runScreensSubcommand } from './inspect/screens'
86
86
  import { runSettleSubcommand } from './inspect/settle'
87
- import { waitForSootsimIdle } from './inspect/settling'
87
+ import { IdleObservationError, waitForSootsimIdle } from './inspect/settling'
88
88
  import {
89
89
  callShellCommand,
90
90
  callShellCommandWhenReady,
@@ -1369,8 +1369,14 @@ export async function runInspect(args: string[], opts: InspectOptions) {
1369
1369
  ` ⚠ auto-wait timed out after ${elapsed ?? budgetMs}ms (${blockedBy}) — next command may see mid-animation state. use \`peach do settle\` for a longer wait.\n`,
1370
1370
  )
1371
1371
  }
1372
- } catch {
1373
- // best-effort — never fail the command because of a post-wait probe.
1372
+ } catch (error) {
1373
+ // the write already landed. an unread idle probe must not fail it or
1374
+ // claim the screen was watched until the budget.
1375
+ if (error instanceof IdleObservationError) {
1376
+ process.stderr.write(
1377
+ ` ⚠ ${error.message}. next command may see mid-animation state. use \`peach do settle\` to retry.\n`,
1378
+ )
1379
+ }
1374
1380
  }
1375
1381
  }
1376
1382
 
@@ -1721,7 +1727,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
1721
1727
  break
1722
1728
  }
1723
1729
  if (args.includes('--json')) {
1724
- console.log(JSON.stringify(nodes, null, 2))
1730
+ printJson(nodes)
1725
1731
  } else {
1726
1732
  console.log(` accessibility tree (${nodes.length} nodes):\n`)
1727
1733
  for (const n of nodes) {
@@ -1845,19 +1851,15 @@ export async function runInspect(args: string[], opts: InspectOptions) {
1845
1851
  }
1846
1852
  const absolute = Reflect.get(node, 'absolutePosition')
1847
1853
  const size = Reflect.get(node, 'layout')
1848
- console.log(
1849
- JSON.stringify(
1850
- absolute && size
1851
- ? {
1852
- x: Reflect.get(absolute, 'x'),
1853
- y: Reflect.get(absolute, 'y'),
1854
- width: Reflect.get(size, 'width'),
1855
- height: Reflect.get(size, 'height'),
1856
- }
1857
- : null,
1858
- null,
1859
- 2,
1860
- ),
1854
+ printJson(
1855
+ absolute && size
1856
+ ? {
1857
+ x: Reflect.get(absolute, 'x'),
1858
+ y: Reflect.get(absolute, 'y'),
1859
+ width: Reflect.get(size, 'width'),
1860
+ height: Reflect.get(size, 'height'),
1861
+ }
1862
+ : null,
1861
1863
  )
1862
1864
  break
1863
1865
  }
@@ -1899,7 +1901,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
1899
1901
  code: buildSampleColorEval(rect),
1900
1902
  })
1901
1903
  if (args.includes('--json')) {
1902
- console.log(JSON.stringify(result, null, 2))
1904
+ printJson(result)
1903
1905
  } else {
1904
1906
  const { r, g, b, a, hex, samples } = result as {
1905
1907
  r: number
@@ -1933,19 +1935,15 @@ export async function runInspect(args: string[], opts: InspectOptions) {
1933
1935
  ? null
1934
1936
  : await inspectFind(bridge, { text: matcher })
1935
1937
  const node = byTestId?.result ?? byText?.result ?? null
1936
- console.log(
1937
- JSON.stringify(
1938
- node
1939
- ? {
1940
- matcher,
1941
- found: true,
1942
- resolvedVia: byTestId?.result ? 'testID' : 'text',
1943
- node,
1944
- }
1945
- : { matcher, found: false },
1946
- null,
1947
- 2,
1948
- ),
1938
+ printJson(
1939
+ node
1940
+ ? {
1941
+ matcher,
1942
+ found: true,
1943
+ resolvedVia: byTestId?.result ? 'testID' : 'text',
1944
+ node,
1945
+ }
1946
+ : { matcher, found: false },
1949
1947
  )
1950
1948
  break
1951
1949
  }
@@ -1956,7 +1954,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
1956
1954
  type: 'evaluate',
1957
1955
  code: buildGetNodeEval(matcher),
1958
1956
  })
1959
- console.log(JSON.stringify(result, null, 2))
1957
+ printJson(result)
1960
1958
  break
1961
1959
  }
1962
1960
 
@@ -2013,16 +2011,10 @@ export async function runInspect(args: string[], opts: InspectOptions) {
2013
2011
  if (candidate) {
2014
2012
  await recordInspectAction('inspect tap', candidate.step, candidate.summary)
2015
2013
  }
2016
- console.log(
2017
- JSON.stringify(
2018
- {
2019
- ...(outcome.attempts > 1 ? { attempts: outcome.attempts } : {}),
2020
- ...outcome.result,
2021
- },
2022
- null,
2023
- 2,
2024
- ),
2025
- )
2014
+ printJson({
2015
+ ...(outcome.attempts > 1 ? { attempts: outcome.attempts } : {}),
2016
+ ...outcome.result,
2017
+ })
2026
2018
  break
2027
2019
  }
2028
2020
  if (!Number.isFinite(x) || !Number.isFinite(y)) {
@@ -2036,7 +2028,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
2036
2028
  if (candidate) {
2037
2029
  await recordInspectAction('inspect tap', candidate.step, candidate.summary)
2038
2030
  }
2039
- console.log(JSON.stringify(result, null, 2))
2031
+ printJson(result)
2040
2032
  break
2041
2033
  }
2042
2034
 
@@ -2087,7 +2079,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
2087
2079
  `${subcommand} ${fromX},${fromY} -> ${toX},${toY}`,
2088
2080
  )
2089
2081
  }
2090
- console.log(JSON.stringify(result, null, 2))
2082
+ printJson(result)
2091
2083
  break
2092
2084
  }
2093
2085
 
@@ -2149,7 +2141,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
2149
2141
  `pinch (${fromX1},${fromY1}) (${fromX2},${fromY2}) -> (${toX1},${toY1}) (${toX2},${toY2})`,
2150
2142
  )
2151
2143
  }
2152
- console.log(JSON.stringify(result, null, 2))
2144
+ printJson(result)
2153
2145
  break
2154
2146
  }
2155
2147
 
@@ -2302,33 +2294,27 @@ export async function runInspect(args: string[], opts: InspectOptions) {
2302
2294
  'text',
2303
2295
  )
2304
2296
  await recordInspectAction('inspect tap-text', candidate.step, candidate.summary)
2305
- console.log(
2306
- JSON.stringify(
2307
- {
2308
- matched: payload.match,
2309
- tapped: {
2310
- nodeId: payload.target?.nodeId ?? null,
2311
- id: payload.target?.id ?? null,
2312
- testID: payload.target?.testID ?? null,
2313
- type: payload.target?.type ?? null,
2314
- cx: payload.cx,
2315
- cy: payload.cy,
2316
- },
2317
- ...(payload.strategy && payload.strategy !== 'matched-node'
2318
- ? { strategy: payload.strategy }
2319
- : {}),
2320
- // expose the disambiguation outcome so agents can confirm which
2321
- // of the N matches was tapped, or notice that only one exists.
2322
- ...(payload.total > 1 || nthIndex !== null
2323
- ? { nth: { index: payload.idx, total: payload.total } }
2324
- : {}),
2325
- ...(outcome.attempts > 1 ? { attempts: outcome.attempts } : {}),
2326
- result: outcome.result,
2327
- },
2328
- null,
2329
- 2,
2330
- ),
2331
- )
2297
+ printJson({
2298
+ matched: payload.match,
2299
+ tapped: {
2300
+ nodeId: payload.target?.nodeId ?? null,
2301
+ id: payload.target?.id ?? null,
2302
+ testID: payload.target?.testID ?? null,
2303
+ type: payload.target?.type ?? null,
2304
+ cx: payload.cx,
2305
+ cy: payload.cy,
2306
+ },
2307
+ ...(payload.strategy && payload.strategy !== 'matched-node'
2308
+ ? { strategy: payload.strategy }
2309
+ : {}),
2310
+ // expose the disambiguation outcome so agents can confirm which
2311
+ // of the N matches was tapped, or notice that only one exists.
2312
+ ...(payload.total > 1 || nthIndex !== null
2313
+ ? { nth: { index: payload.idx, total: payload.total } }
2314
+ : {}),
2315
+ ...(outcome.attempts > 1 ? { attempts: outcome.attempts } : {}),
2316
+ result: outcome.result,
2317
+ })
2332
2318
  break
2333
2319
  }
2334
2320
 
@@ -2400,25 +2386,19 @@ export async function runInspect(args: string[], opts: InspectOptions) {
2400
2386
  payload.strategy === 'testid' ? 'id' : 'text',
2401
2387
  )
2402
2388
  await recordInspectAction('inspect tap-best', candidate.step, candidate.summary)
2403
- console.log(
2404
- JSON.stringify(
2405
- {
2406
- matched: {
2407
- strategy: payload.strategy,
2408
- nodeId: node.nodeId,
2409
- id: node.id,
2410
- testID: node.testID,
2411
- type: node.type,
2412
- text: node.text,
2413
- },
2414
- tapped: { cx: payload.cx, cy: payload.cy },
2415
- ...(outcome.attempts > 1 ? { attempts: outcome.attempts } : {}),
2416
- result: outcome.result,
2417
- },
2418
- null,
2419
- 2,
2420
- ),
2421
- )
2389
+ printJson({
2390
+ matched: {
2391
+ strategy: payload.strategy,
2392
+ nodeId: node.nodeId,
2393
+ id: node.id,
2394
+ testID: node.testID,
2395
+ type: node.type,
2396
+ text: node.text,
2397
+ },
2398
+ tapped: { cx: payload.cx, cy: payload.cy },
2399
+ ...(outcome.attempts > 1 ? { attempts: outcome.attempts } : {}),
2400
+ result: outcome.result,
2401
+ })
2422
2402
  break
2423
2403
  }
2424
2404
 
@@ -2577,20 +2557,14 @@ export async function runInspect(args: string[], opts: InspectOptions) {
2577
2557
  ? `type-into #${targetId} ${SECURE_TEXT_REDACTION}`
2578
2558
  : `type-into #${targetId} ${JSON.stringify(text)}`,
2579
2559
  )
2580
- console.log(
2581
- JSON.stringify(
2582
- {
2583
- target: targetId,
2584
- isTextInput: tiPayload.isTextInput,
2585
- secureTextEntry,
2586
- keyboardOpened: keyboardState.visible ?? tapResult?.keyboardOpened ?? false,
2587
- focusedInput,
2588
- typed: outputText,
2589
- },
2590
- null,
2591
- 2,
2592
- ),
2593
- )
2560
+ printJson({
2561
+ target: targetId,
2562
+ isTextInput: tiPayload.isTextInput,
2563
+ secureTextEntry,
2564
+ keyboardOpened: keyboardState.visible ?? tapResult?.keyboardOpened ?? false,
2565
+ focusedInput,
2566
+ typed: outputText,
2567
+ })
2594
2568
  break
2595
2569
  }
2596
2570
 
@@ -2770,7 +2744,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
2770
2744
  `double-tap @${x},${y}`,
2771
2745
  )
2772
2746
  }
2773
- console.log(JSON.stringify(result, null, 2))
2747
+ printJson(result)
2774
2748
  break
2775
2749
  }
2776
2750
 
@@ -2828,7 +2802,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
2828
2802
  `long-press @${x},${y}`,
2829
2803
  )
2830
2804
  }
2831
- console.log(JSON.stringify(result, null, 2))
2805
+ printJson(result)
2832
2806
  break
2833
2807
  }
2834
2808
 
@@ -2955,13 +2929,12 @@ export async function runInspect(args: string[], opts: InspectOptions) {
2955
2929
  `gesture ${preset}`,
2956
2930
  )
2957
2931
  }
2958
- console.log(
2959
- JSON.stringify(
2960
- { preset, from: { x: fromX, y: fromY }, to: { x: toX, y: toY }, result },
2961
- null,
2962
- 2,
2963
- ),
2964
- )
2932
+ printJson({
2933
+ preset,
2934
+ from: { x: fromX, y: fromY },
2935
+ to: { x: toX, y: toY },
2936
+ result,
2937
+ })
2965
2938
  break
2966
2939
  }
2967
2940
 
@@ -2996,16 +2969,10 @@ export async function runInspect(args: string[], opts: InspectOptions) {
2996
2969
  `scroll #${id} -> ${x},${y}`,
2997
2970
  )
2998
2971
  }
2999
- console.log(
3000
- JSON.stringify(
3001
- {
3002
- ...result,
3003
- ...(scrollNode ? { at: { x: scrollNode.cx, y: scrollNode.cy } } : {}),
3004
- },
3005
- null,
3006
- 2,
3007
- ),
3008
- )
2972
+ printJson({
2973
+ ...result,
2974
+ ...(scrollNode ? { at: { x: scrollNode.cx, y: scrollNode.cy } } : {}),
2975
+ })
3009
2976
  break
3010
2977
  }
3011
2978
 
@@ -3032,7 +2999,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
3032
2999
  if (shell) base.shell = shell
3033
3000
  } catch {}
3034
3001
  }
3035
- console.log(JSON.stringify(base, null, 2))
3002
+ printJson(base)
3036
3003
  break
3037
3004
  }
3038
3005
  if (!stateCmd || stateCmd === '--help' || stateCmd === '-h') {
@@ -3225,7 +3192,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
3225
3192
  console.error(` unknown state subcommand: ${stateCmd}`)
3226
3193
  peachExit(1)
3227
3194
  }
3228
- console.log(JSON.stringify(result, null, 2))
3195
+ printJson(result)
3229
3196
  break
3230
3197
  }
3231
3198
 
@@ -3410,7 +3377,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
3410
3377
  console.error(` unknown shell subcommand: ${shellCmd}`)
3411
3378
  peachExit(1)
3412
3379
  }
3413
- console.log(JSON.stringify({ ok, settled, state }, null, 2))
3380
+ printJson({ ok, settled, state })
3414
3381
  break
3415
3382
  }
3416
3383
 
@@ -3686,7 +3653,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
3686
3653
  type: 'evaluate',
3687
3654
  code: `(async () => SootSim.bridges.test.evalInTenant(${JSON.stringify(tenantCode)}))()`,
3688
3655
  })
3689
- console.log(JSON.stringify(result, null, 2))
3656
+ printJson(result)
3690
3657
  break
3691
3658
  }
3692
3659
  // wrap in async to support await in worker mode. no identifier
@@ -3701,7 +3668,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
3701
3668
  : `(async () => ${expandedCode})()`
3702
3669
  }
3703
3670
  const result = await bridge.send({ type: 'evaluate', code: expandedCode })
3704
- console.log(JSON.stringify(result, null, 2))
3671
+ printJson(result)
3705
3672
 
3706
3673
  // suggest CLI commands for common verbose eval patterns
3707
3674
  const lc = code.toLowerCase()
@@ -4124,7 +4091,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
4124
4091
  (await callTestBridge<Array<Record<string, any>>>(bridge, 'listAnimations')) ??
4125
4092
  []
4126
4093
  if (args.includes('--json')) {
4127
- console.log(JSON.stringify(anims, null, 2))
4094
+ printJson(anims)
4128
4095
  break
4129
4096
  }
4130
4097
  if (anims.length === 0) {
@@ -4171,7 +4138,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
4171
4138
  peachExit(1)
4172
4139
  }
4173
4140
  const result = await callTestBridge(bridge, 'getAnimation', id)
4174
- console.log(JSON.stringify(result, null, 2))
4141
+ printJson(result)
4175
4142
  break
4176
4143
  }
4177
4144
 
@@ -4302,7 +4269,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
4302
4269
  peachExit(1)
4303
4270
  }
4304
4271
  if (jsonOut) {
4305
- console.log(JSON.stringify(entry, null, 2))
4272
+ printJson(entry)
4306
4273
  } else {
4307
4274
  printNetworkDetail(entry)
4308
4275
  }
@@ -4365,7 +4332,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
4365
4332
  const all = await fetchEntries()
4366
4333
  const filtered = applyFilter(all).slice(-limit)
4367
4334
  if (jsonOut) {
4368
- console.log(JSON.stringify(filtered, null, 2))
4335
+ printJson(filtered)
4369
4336
  break
4370
4337
  }
4371
4338
  if (filtered.length === 0) {
@@ -4506,7 +4473,7 @@ export async function runInspect(args: string[], opts: InspectOptions) {
4506
4473
  const all = await fetchEntries()
4507
4474
  const filtered = applyFilter(all).slice(-limit)
4508
4475
  if (jsonOut) {
4509
- console.log(JSON.stringify(filtered, null, 2))
4476
+ printJson(filtered)
4510
4477
  break
4511
4478
  }
4512
4479
  if (filtered.length === 0) {
@@ -6,7 +6,7 @@ import os from 'node:os'
6
6
  import path from 'node:path'
7
7
  import { DEFAULT_SOOTSIM_BRIDGE_PORT } from '../../src/bridge-constants'
8
8
  import { getCliVersion } from '../../src/cli-version'
9
- import { desktopHostPlatform } from '../../src/desktop-host-delivery'
9
+ import { desktopHostDelivery, desktopHostPlatform } from '../../src/desktop-host-delivery'
10
10
  import {
11
11
  readActiveRuntime,
12
12
  readPrivacyPreferences,
@@ -129,6 +129,13 @@ export async function runSetup(
129
129
  ? await install()
130
130
  : await withSpinner(`Preparing runtime ${runtimeVersion}`, install)
131
131
  runtimeInstalled = result.installed
132
+ // headless runs on linux attach the standalone host; fetching it here keeps
133
+ // the download out of the first `peach open`
134
+ if (desktopHostPlatform() === 'linux-x64') {
135
+ const host = () => desktopHostDelivery().install({ quiet: !options.json })
136
+ if (options.json) await host()
137
+ else await withSpinner('Preparing the native host', host)
138
+ }
132
139
  }
133
140
 
134
141
  let serviceInstalled = serviceStatus.installed
@@ -150,6 +157,7 @@ export async function runSetup(
150
157
 
151
158
  if (
152
159
  simulatorDriver !== 'playwright' &&
160
+ process.platform !== 'darwin' &&
153
161
  !options.dryRun &&
154
162
  (simulatorDriver === 'desktop' || !findDesktopCompanion({ native: false }))
155
163
  ) {
@@ -315,27 +323,29 @@ async function decideService(
315
323
  async function decideSimulatorDriver(
316
324
  options: SetupOptions,
317
325
  ): Promise<SimulatorDriverPreference> {
326
+ if (process.platform === 'darwin' && !findDesktopCompanion()) {
327
+ if (options.simulatorDriver && options.simulatorDriver !== 'playwright') {
328
+ throw new Error('install the Mac app with `peach desktop install --native` first')
329
+ }
330
+ return 'playwright'
331
+ }
318
332
  if (options.simulatorDriver) return options.simulatorDriver
319
333
  const current = readSimulatorDriverPreference()
320
334
  if (current) return current
321
335
  if (options.ci || options.yes || options.dryRun) return 'playwright'
322
336
  requireInteractive(options, '--desktop or --browser')
323
337
  const desktopDescription =
324
- desktopHostPlatform() === 'mac-arm64'
325
- ? 'Desktop installs the signed native Peach.app.'
326
- : process.platform === 'darwin'
327
- ? 'Desktop downloads a DMG and installs peach.app.'
328
- : process.platform === 'win32'
329
- ? 'Desktop downloads and runs the peach installer.'
330
- : 'Desktop downloads the matching peach AppImage.'
338
+ process.platform === 'darwin'
339
+ ? 'Desktop opens your installed native app.'
340
+ : process.platform === 'win32'
341
+ ? 'Desktop downloads and runs the peach installer.'
342
+ : 'Desktop downloads the matching peach AppImage.'
331
343
  const desktopChoice =
332
- desktopHostPlatform() === 'mac-arm64'
333
- ? 'Desktop app Installs the signed native host'
334
- : process.platform === 'darwin'
335
- ? 'Desktop app Installs peach.app from a DMG'
336
- : process.platform === 'win32'
337
- ? 'Desktop app Runs the peach installer'
338
- : 'Desktop app Installs the matching AppImage'
344
+ process.platform === 'darwin'
345
+ ? 'Desktop app Opens your installed native host'
346
+ : process.platform === 'win32'
347
+ ? 'Desktop app Runs the peach installer'
348
+ : 'Desktop app Installs the matching AppImage'
339
349
  printPromptBlock('Simulator window', [
340
350
  'Browser opens immediately in an isolated Chrome window.',
341
351
  desktopDescription,
@@ -347,7 +357,7 @@ async function decideSimulatorDriver(
347
357
  ])
348
358
  return choice === 0
349
359
  ? 'playwright'
350
- : desktopHostPlatform() === 'mac-arm64'
360
+ : process.platform === 'darwin'
351
361
  ? 'desktop'
352
362
  : 'electron'
353
363
  }
@@ -1,6 +1,9 @@
1
- // peach upgrade updates the separately delivered runtime first, then atomically
2
- // replaces the standalone cli executable. runtime stays first because replacing
3
- // the running cli's files before later dynamic imports can strand the command.
1
+ // peach upgrade updates the separately delivered runtime (and on linux the native
2
+ // host headless runs attach) first, then atomically replaces the standalone cli
3
+ // executable. runtime stays first because replacing the running cli's files
4
+ // before later dynamic imports can strand the command.
5
+ // once both are in place, a running background daemon is restarted by the new
6
+ // executable, so nobody has to restart it by hand after upgrading.
4
7
 
5
8
  import { spawnSync } from 'node:child_process'
6
9
  import { createHash } from 'node:crypto'
@@ -8,7 +11,14 @@ import fs from 'node:fs'
8
11
  import path from 'node:path'
9
12
  import { compareSemver } from '@contrast/runtime-delivery'
10
13
  import { getCliVersion } from '../../src/cli-version'
11
- import { isSootsimDevCheckout, readActiveRuntime, runtimeDir } from '../../src/home-paths'
14
+ import { desktopHostDelivery, desktopHostPlatform } from '../../src/desktop-host-delivery'
15
+ import {
16
+ isDaemonLockfileFresh,
17
+ isSootsimDevCheckout,
18
+ readActiveRuntime,
19
+ readDaemonLockfile,
20
+ runtimeDir,
21
+ } from '../../src/home-paths'
12
22
  import { resolveRNXAppConfig } from '../app-config'
13
23
  import {
14
24
  DEFAULT_CLI_RELEASE_ORIGIN,
@@ -17,7 +27,9 @@ import {
17
27
  } from '../cli-update'
18
28
  import { peachExit } from '../run-rnx'
19
29
  import { fetchRuntimeReleaseNotes, formatRuntimeReleaseNotes } from '../runtime-notes'
30
+ import { rnxSelfInvocation } from '../self-invocation'
20
31
  import { IS_STANDALONE } from '../standalone'
32
+ import { getDaemonServiceStatus } from './daemon'
21
33
  import { runRuntime } from './runtime'
22
34
 
23
35
  const SHA256_PATTERN = /^[a-f0-9]{64}$/
@@ -88,20 +100,51 @@ export async function runUpgrade(
88
100
  ? await fetchRuntimeReleaseNotes(installedRuntime)
89
101
  : null
90
102
 
103
+ if (desktopHostPlatform() === 'linux-x64') {
104
+ // the first headless open would otherwise download the host it attaches
105
+ console.log('\nnative host:')
106
+ const host = await desktopHostDelivery().install({ quiet: true })
107
+ console.log(
108
+ ` result: v${host.version} ${host.installed ? 'installed' : 'already current'}`,
109
+ )
110
+ }
111
+
91
112
  console.log('\ncli:')
92
- await upgradeCli({ ...options, cliVersion })
113
+ const cliChanged = await upgradeCli({ ...options, cliVersion })
114
+ if (cliChanged || readActiveRuntime() !== beforeRuntime) restartRunningDaemon()
93
115
 
94
116
  if (installedRuntime && options.includeReleaseNotes !== false) {
95
117
  console.log(`\n${formatRuntimeReleaseNotes(installedRuntime, releaseNotes)}`)
96
118
  }
97
119
  }
98
120
 
99
- export async function upgradeCli(options: UpgradeOptions = {}): Promise<void> {
121
+ // the daemon process holds the cli code and runtime it started with. the
122
+ // service points at the executable path upgrade just replaced, so restarting
123
+ // it through the new executable is all a running daemon needs.
124
+ function restartRunningDaemon(): void {
125
+ if (!getDaemonServiceStatus().installed) return
126
+ if (!isDaemonLockfileFresh(readDaemonLockfile())) return
127
+ console.log('\ndaemon:')
128
+ const invocation = rnxSelfInvocation()
129
+ const result = spawnSync(
130
+ invocation.executable,
131
+ [...invocation.prefixArgs, 'daemon', 'restart'],
132
+ { stdio: 'inherit' },
133
+ )
134
+ if (result.status !== 0) {
135
+ throw new Error(
136
+ `the background daemon did not restart onto the upgraded peach (exit ${result.status}); check \`peach daemon logs\``,
137
+ )
138
+ }
139
+ }
140
+
141
+ /** returns whether the executable was replaced. */
142
+ export async function upgradeCli(options: UpgradeOptions = {}): Promise<boolean> {
100
143
  const standalone = options.standalone ?? IS_STANDALONE
101
144
  if (!standalone) {
102
145
  if (isSootsimDevCheckout()) {
103
146
  console.log(' running from the Contrast checkout; pull instead of upgrading')
104
- return
147
+ return false
105
148
  }
106
149
  throw new Error(
107
150
  'this is not the standalone peach executable; reinstall from https://peachsim.com/install.sh',
@@ -117,11 +160,11 @@ export async function upgradeCli(options: UpgradeOptions = {}): Promise<void> {
117
160
  options.cliVersion ?? (await refreshCliUpdateCache({ releaseOrigin })).latestVersion
118
161
  if (options.cliVersion === current) {
119
162
  console.log(` v${current} (requested)`)
120
- return
163
+ return false
121
164
  }
122
165
  if (!options.cliVersion && compareSemver(latest, current) <= 0) {
123
166
  console.log(` v${current} (latest)`)
124
- return
167
+ return false
125
168
  }
126
169
 
127
170
  const artifact = cliArtifactName()
@@ -157,6 +200,7 @@ export async function upgradeCli(options: UpgradeOptions = {}): Promise<void> {
157
200
  fs.rmSync(tempDir, { recursive: true, force: true })
158
201
  }
159
202
  console.log(` updated to v${latest}`)
203
+ return true
160
204
  }
161
205
 
162
206
  function cliArtifactName(): string {
@@ -143,7 +143,7 @@ function findWindowsInstall(): DesktopCompanionInstall | null {
143
143
 
144
144
  export interface DesktopCompanionOptions {
145
145
  // the native host: PEACH_DESKTOP_APP, else the active version
146
- // `peach desktop install --native` installed under ~/.peach/desktop.
146
+ // explicit `peach desktop install --native` places the mac app in /Applications.
147
147
  native?: boolean
148
148
  }
149
149