peachsim 0.1.657 → 0.1.659

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 (115) hide show
  1. package/README.md +21 -2
  2. package/cli/app-config.ts +25 -9
  3. package/cli/bridge-flow-runner.ts +10 -23
  4. package/cli/commands/compat.ts +234 -10
  5. package/cli/commands/do-chain.ts +5 -1
  6. package/cli/commands/inspect/actions.ts +33 -76
  7. package/cli/commands/inspect/resolve-target.ts +67 -2
  8. package/cli/commands/inspect.ts +16 -1
  9. package/cli/drivers/playwright-provisioning.ts +95 -0
  10. package/cli/ws-bridge.ts +7 -7
  11. package/dist-lib/agent-daemon-client.cjs +1 -1
  12. package/dist-lib/agent-events.cjs +1 -1
  13. package/dist-lib/agent-identity.cjs +1 -1
  14. package/dist-lib/agent-sessions.cjs +1 -1
  15. package/dist-lib/attached-projects.cjs +1 -1
  16. package/dist-lib/auth/shared-session.cjs +1 -1
  17. package/dist-lib/backend-origin.cjs +1 -1
  18. package/dist-lib/beta.cjs +1 -1
  19. package/dist-lib/beta.mjs +1 -1
  20. package/dist-lib/bridge-constants.cjs +1 -1
  21. package/dist-lib/bridge-contract-input.cjs +9 -1
  22. package/dist-lib/bridge-contract-input.mjs +9 -1
  23. package/dist-lib/bridge-contract.cjs +1 -1
  24. package/dist-lib/bridge-contract.mjs +1 -1
  25. package/dist-lib/capture-contract.cjs +1 -1
  26. package/dist-lib/capture-contract.mjs +1 -1
  27. package/dist-lib/cli-constants.cjs +1 -1
  28. package/dist-lib/cloud-contract.cjs +1 -1
  29. package/dist-lib/cloud-contract.mjs +1 -1
  30. package/dist-lib/cloud-test-args.cjs +1 -1
  31. package/dist-lib/cloud-test-args.mjs +1 -1
  32. package/dist-lib/cloud.cjs +1 -1
  33. package/dist-lib/cloud.mjs +1 -1
  34. package/dist-lib/config.cjs +3 -2
  35. package/dist-lib/detox/index.cjs +2 -2
  36. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  37. package/dist-lib/home-paths.cjs +1 -1
  38. package/dist-lib/host/bridge-host.cjs +78 -35
  39. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  40. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  41. package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
  42. package/dist-lib/host/replacement-module-handler.cjs +51 -8
  43. package/dist-lib/host/websocket-proxy.cjs +1 -1
  44. package/dist-lib/index.cjs +317 -70
  45. package/dist-lib/jump-to-source-babel.cjs +1 -1
  46. package/dist-lib/jump-to-source-native.cjs +1 -1
  47. package/dist-lib/kotlin-host.cjs +14 -4
  48. package/dist-lib/menu.cjs +1 -1
  49. package/dist-lib/menu.mjs +1 -1
  50. package/dist-lib/metro-fingerprint-registry.cjs +1 -1
  51. package/dist-lib/metro-fingerprint-registry.mjs +1 -1
  52. package/dist-lib/metro-production-bundle.cjs +1 -1
  53. package/dist-lib/metro-production-bundle.mjs +1 -1
  54. package/dist-lib/metro.cjs +300 -29
  55. package/dist-lib/native-app-identity.cjs +58 -25
  56. package/dist-lib/native-app-identity.mjs +57 -25
  57. package/dist-lib/plist.cjs +64 -0
  58. package/dist-lib/plist.mjs +29 -0
  59. package/dist-lib/profiles.cjs +1 -1
  60. package/dist-lib/public-brand.cjs +1 -1
  61. package/dist-lib/react-native-host-modules.cjs +1 -1
  62. package/dist-lib/react-native-host-modules.mjs +1 -1
  63. package/dist-lib/render-mode.cjs +1 -1
  64. package/dist-lib/scripts/dev-server-scanner.cjs +2 -2
  65. package/dist-lib/sdk.cjs +36 -49
  66. package/dist-lib/sdk.mjs +36 -49
  67. package/dist-lib/skills.cjs +257 -138
  68. package/dist-lib/swift-compile.cjs +1 -1
  69. package/dist-lib/swift-compile.mjs +1 -1
  70. package/dist-lib/swift-package.cjs +1 -1
  71. package/dist-lib/swift-package.mjs +1 -1
  72. package/dist-lib/swift-submit.cjs +1 -1
  73. package/dist-lib/swift-submit.mjs +1 -1
  74. package/dist-lib/swift.cjs +1 -1
  75. package/dist-lib/vite-swift.cjs +1 -1
  76. package/dist-lib/vite.cjs +51 -8
  77. package/kotlin/AppSources/android/app/Activity.kt +44 -0
  78. package/kotlin/AppSources/android/app/UiModeManager.kt +9 -0
  79. package/kotlin/AppSources/android/content/Context.kt +9 -2
  80. package/kotlin/AppSources/android/content/res/Configuration.kt +16 -0
  81. package/kotlin/AppSources/android/content/res/Resources.kt +7 -0
  82. package/kotlin/AppSources/android/graphics/Color.kt +13 -0
  83. package/kotlin/AppSources/android/os/Bundle.kt +9 -3
  84. package/kotlin/AppSources/android/view/View.kt +2 -0
  85. package/kotlin/AppSources/android/view/Window.kt +99 -8
  86. package/kotlin/AppSources/android/view/WindowInsetsController.kt +12 -0
  87. package/kotlin/AppSources/android/view/WindowManager.kt +16 -0
  88. package/kotlin/AppSources/androidx/activity/BackEventCompat.kt +17 -0
  89. package/kotlin/AppSources/androidx/activity/Cancellable.kt +5 -0
  90. package/kotlin/AppSources/androidx/activity/ComponentActivity.kt +22 -3
  91. package/kotlin/AppSources/androidx/activity/EdgeToEdge.kt +71 -0
  92. package/kotlin/AppSources/androidx/activity/OnBackPressedCallback.kt +28 -0
  93. package/kotlin/AppSources/androidx/activity/OnBackPressedDispatcher.kt +144 -0
  94. package/kotlin/AppSources/androidx/activity/OnBackPressedDispatcherOwner.kt +7 -0
  95. package/kotlin/AppSources/androidx/activity/compose/BackHandler.kt +55 -0
  96. package/kotlin/AppSources/androidx/core/view/WindowCompat.kt +32 -0
  97. package/kotlin/AppSources/dev/peach/app/PeachApp.kt +67 -12
  98. package/kotlin/AppSources/dev/peach/app/PeachWindowHost.kt +2 -1
  99. package/kotlin/AppSources/java/lang/Runnable.kt +6 -0
  100. package/kotlin/ComposeSources/PeachComposeHost.kt +11 -1
  101. package/package.json +8 -1
  102. package/skills/peach-setup/SKILL.md +27 -0
  103. package/src/android-theme.ts +167 -0
  104. package/src/android-xml.ts +70 -0
  105. package/src/bridge-contract-input.ts +10 -1
  106. package/src/bridge-contract.ts +7 -1
  107. package/src/config.ts +8 -0
  108. package/src/generated/android-framework-themes.json +6303 -0
  109. package/src/host/replacement-module-handler.ts +33 -0
  110. package/src/index.ts +1 -0
  111. package/src/kotlin-app.ts +86 -61
  112. package/src/kotlin-host.ts +13 -3
  113. package/src/metro-plugin.ts +46 -3
  114. package/src/native-app-identity.ts +75 -26
  115. package/src/sim-client.ts +2 -4
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.657 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.659 | (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;
@@ -946,9 +946,6 @@ function saveCurrentSimId(simId) {
946
946
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
947
947
  });
948
948
  }
949
- function clearCurrentSimId() {
950
- (0, import_fs2.rmSync)(getCurrentSimPath(), { force: true });
951
- }
952
949
 
953
950
  // cli/ws-bridge.ts
954
951
  var import_node_fs3 = __toESM(require("node:fs"), 1);
@@ -1295,9 +1292,8 @@ function createBridge(wsPort, opts = {}) {
1295
1292
  );
1296
1293
  return sendOnce(successor);
1297
1294
  }
1298
- clearCurrentSimId();
1299
1295
  throw new Error(
1300
- `saved sim ${sentSimId} is gone${message.slice(missingSimMessage.length)}; run \`peach list\` and \`peach use <sim>\`, or open a fresh sim`
1296
+ `saved sim ${sentSimId} is not connected${message.slice(missingSimMessage.length)}; commands resume when it reconnects, or run \`peach list\` and \`peach use <sim>\`, or open a fresh sim`
1301
1297
  );
1302
1298
  }
1303
1299
  throw error;
@@ -2121,10 +2117,20 @@ async function getShellState(bridge, readyTimeoutMs = 0) {
2121
2117
  }
2122
2118
 
2123
2119
  // cli/commands/inspect/resolve-target.ts
2120
+ function isScreenSize(value) {
2121
+ if (value === null || typeof value !== "object") return false;
2122
+ const width = Reflect.get(value, "width");
2123
+ const height = Reflect.get(value, "height");
2124
+ return typeof width === "number" && Number.isFinite(width) && width > 0 && typeof height === "number" && Number.isFinite(height) && height > 0;
2125
+ }
2126
+ function isOffscreen(x, y, screen) {
2127
+ return !(x >= 0 && y >= 0 && x <= screen.width && y <= screen.height);
2128
+ }
2129
+ var UNREPORTED_SCREEN_ERROR = "this runtime does not report the screen size a targeted gesture needs; update it with `peach runtime install`, or use coordinates";
2124
2130
  async function resolveTargetCoords(bridge, target) {
2125
2131
  const result = await bridge.send({
2126
2132
  type: "resolve",
2127
- selector: target.mode === "testid" ? { testID: target.value } : { text: target.value }
2133
+ selector: target.mode === "node" ? { nodeId: target.nodeId } : target.mode === "testid" ? { testID: target.value } : { text: target.value }
2128
2134
  });
2129
2135
  if (result === null) return null;
2130
2136
  if (typeof result !== "object") throw new Error("resolve returned an invalid result");
@@ -2132,6 +2138,9 @@ async function resolveTargetCoords(bridge, target) {
2132
2138
  const resolvedTarget = Reflect.get(result, "target");
2133
2139
  const semanticNodes = [match, resolvedTarget];
2134
2140
  const point = Reflect.get(result, "point");
2141
+ const viewport = Reflect.get(result, "viewport");
2142
+ const width = viewport !== null && typeof viewport === "object" ? Reflect.get(viewport, "width") : void 0;
2143
+ const height = viewport !== null && typeof viewport === "object" ? Reflect.get(viewport, "height") : void 0;
2135
2144
  const x = point !== null && typeof point === "object" ? Reflect.get(point, "x") : void 0;
2136
2145
  const y = point !== null && typeof point === "object" ? Reflect.get(point, "y") : void 0;
2137
2146
  if (!isSimSemanticTree(semanticNodes) || typeof x !== "number" || !Number.isFinite(x) || typeof y !== "number" || !Number.isFinite(y)) {
@@ -2145,7 +2154,8 @@ async function resolveTargetCoords(bridge, target) {
2145
2154
  nodeId: semanticMatch.nodeId,
2146
2155
  testID: semanticMatch.testID ?? null,
2147
2156
  text: semanticMatch.text ?? (target.mode === "text" ? target.value : semanticMatch.label),
2148
- type: semanticMatch.type
2157
+ type: semanticMatch.type,
2158
+ ...typeof width === "number" && typeof height === "number" ? { viewport: { width, height } } : {}
2149
2159
  };
2150
2160
  }
2151
2161
 
@@ -2340,30 +2350,6 @@ async function waitForSootsimIdle({
2340
2350
  }
2341
2351
 
2342
2352
  // cli/commands/inspect/actions.ts
2343
- function isScreenSize(value) {
2344
- if (value === null || typeof value !== "object") return false;
2345
- const width = Reflect.get(value, "width");
2346
- const height = Reflect.get(value, "height");
2347
- return typeof width === "number" && Number.isFinite(width) && width > 0 && typeof height === "number" && Number.isFinite(height) && height > 0;
2348
- }
2349
- function viewportFromCapture(value) {
2350
- if (value === null || typeof value !== "object") return void 0;
2351
- const tree = Reflect.get(value, "tree");
2352
- return tree !== null && typeof tree === "object" ? Reflect.get(tree, "viewport") : void 0;
2353
- }
2354
- async function readScreen(bridge) {
2355
- try {
2356
- const viewport = viewportFromCapture(
2357
- await bridge.send({ type: "capture", includeVisual: false })
2358
- );
2359
- return isScreenSize(viewport) ? { width: viewport.width, height: viewport.height } : null;
2360
- } catch {
2361
- return null;
2362
- }
2363
- }
2364
- function isOffscreen(cx, cy, screen) {
2365
- return !(cx >= 0 && cy >= 0 && cx <= screen.width && cy <= screen.height);
2366
- }
2367
2353
  var DEFAULT_AGENT_TIMING = {
2368
2354
  initialWaitMs: 3e3,
2369
2355
  deadlineMs: 5e3,
@@ -2425,17 +2411,24 @@ async function tapResolvedTarget(bridge, args) {
2425
2411
  }
2426
2412
  const deadline = Date.now() + timing.deadlineMs;
2427
2413
  let lastOffscreenAt = null;
2428
- const screen = bridge.plane === "cloud" ? null : await readScreen(bridge);
2429
2414
  while (Date.now() <= deadline || attempts === 0) {
2430
2415
  attempts++;
2431
- const resolved = await args.resolve();
2432
- const payload = resolved && typeof resolved.cx === "number" && typeof resolved.cy === "number" && screen && isOffscreen(resolved.cx, resolved.cy, screen) ? { ...resolved, offscreen: true, screen } : resolved;
2416
+ const { screen, ...resolved } = await args.resolve() ?? {};
2417
+ const checksScreen = bridge.plane !== "cloud" && typeof resolved.cx === "number";
2418
+ if (checksScreen && screen === void 0) {
2419
+ throw new Error(UNREPORTED_SCREEN_ERROR);
2420
+ }
2421
+ const unsized = checksScreen && !isScreenSize(screen);
2422
+ const payload = checksScreen && typeof resolved.cx === "number" && typeof resolved.cy === "number" && isScreenSize(screen) && isOffscreen(resolved.cx, resolved.cy, screen) ? { ...resolved, offscreen: true, screen } : resolved;
2433
2423
  lastPayload = payload;
2434
2424
  if (payload?.error === "bridge-not-ready" || payload?.ambiguous || payload?.nthOutOfRange) {
2435
2425
  return { payload, result: null, attempts, failure: "special" };
2436
2426
  }
2437
2427
  if (payload && typeof payload.cx === "number" && typeof payload.cy === "number") {
2438
- if (payload.offscreen) {
2428
+ if (unsized) {
2429
+ lastResult = { hit: false, reason: "screen-not-laid-out" };
2430
+ lastOffscreenAt = null;
2431
+ } else if (payload.offscreen) {
2439
2432
  lastResult = {
2440
2433
  hit: false,
2441
2434
  reason: "offscreen",
@@ -2508,6 +2501,7 @@ function payloadFromResolved(resolved, match, extra = {}) {
2508
2501
  return {
2509
2502
  cx: resolved.x,
2510
2503
  cy: resolved.y,
2504
+ screen: resolved.viewport,
2511
2505
  match: {
2512
2506
  nodeId: match.nodeId ?? null,
2513
2507
  id: match.testID ?? null,
@@ -2641,30 +2635,12 @@ async function resolveTextTapTarget(bridge, query, options = {}) {
2641
2635
  const picked = candidates[idx];
2642
2636
  if (!picked) return { matched: 0, total };
2643
2637
  const node = picked.node;
2644
- const tap = await bridge.send({
2645
- type: "call",
2646
- path: "SootSim.bridges.test.resolveTapTarget",
2647
- args: [node.nodeId]
2638
+ const resolved = await resolveTargetCoords(bridge, {
2639
+ mode: "node",
2640
+ nodeId: node.nodeId
2648
2641
  });
2649
- if (!isTapTargetInfo(tap)) return { matched: 0, total };
2650
- return payloadFromResolved(
2651
- {
2652
- x: tap.cx,
2653
- y: tap.cy,
2654
- nodeId: tap.target.nodeId,
2655
- testID: tap.target.testID,
2656
- text: tap.target.text ?? tap.target.accessibilityLabel,
2657
- type: tap.target.type
2658
- },
2659
- node,
2660
- { total, idx }
2661
- );
2662
- }
2663
- function isTapNode(value) {
2664
- return value !== null && typeof value === "object" && typeof Reflect.get(value, "nodeId") === "number";
2665
- }
2666
- function isTapTargetInfo(value) {
2667
- return value !== null && typeof value === "object" && typeof Reflect.get(value, "cx") === "number" && typeof Reflect.get(value, "cy") === "number" && isTapNode(Reflect.get(value, "target"));
2642
+ if (!resolved) return { matched: 0, total };
2643
+ return payloadFromResolved(resolved, node, { total, idx });
2668
2644
  }
2669
2645
  async function tapBest(bridge, query, opts = {}) {
2670
2646
  return tapResolvedTarget(bridge, {
@@ -3630,6 +3606,7 @@ function launchRnxDesktop(bundleUrl, appName) {
3630
3606
  }
3631
3607
 
3632
3608
  // src/host/replacement-module-handler.ts
3609
+ var import_node_fs5 = require("node:fs");
3633
3610
  var import_promises = __toESM(require("node:fs/promises"), 1);
3634
3611
  var import_node_path5 = __toESM(require("node:path"), 1);
3635
3612
 
@@ -3759,12 +3736,15 @@ function transformsFor(filePath) {
3759
3736
  );
3760
3737
  }
3761
3738
  }
3739
+ function isLocalSpecifier(specifier) {
3740
+ return specifier.startsWith("./") || specifier.startsWith("../");
3741
+ }
3762
3742
  function isContrastKitSpecifier(specifier) {
3763
3743
  return specifier === "contrast-kit" || specifier.startsWith("contrast-kit/");
3764
3744
  }
3765
3745
  function unsupportedSpecifier(specifier) {
3766
3746
  return new Error(
3767
- `replacement modules may only import react, react-native, contrast-kit, and the packages rnx stubs for this app; '${specifier}' is none of those. vendor the code into the replacement file, or add the package to peach.config so the simulator bridges it.`
3747
+ `replacement modules may only import react, react-native, contrast-kit, the packages rnx stubs for this app, and other project files by relative path; '${specifier}' is none of those. vendor the code into a project file, or add the package to peach.config so the simulator bridges it.`
3768
3748
  );
3769
3749
  }
3770
3750
  function bridgeDeclaration(name2, specifier) {
@@ -3789,11 +3769,9 @@ function compileReplacementModule(input) {
3789
3769
  const transformed = (0, import_dist.transform)(input.source, {
3790
3770
  transforms: transformsFor(input.filePath),
3791
3771
  filePath: input.filePath,
3792
- // classic runtime, matching what the app's own Metro build does for a file
3793
- // that has no automatic-runtime pragma: the file imports React itself.
3794
- jsxRuntime: "classic",
3795
- jsxPragma: "React.createElement",
3796
- jsxFragmentPragma: "React.Fragment",
3772
+ // the automatic runtime, as @react-native/babel-preset and babel-preset-expo
3773
+ // compile the app's own files: jsx needs no React import.
3774
+ jsxRuntime: "automatic",
3797
3775
  production: true,
3798
3776
  sourceMapOptions: { compiledFilename: input.filePath }
3799
3777
  });
@@ -3820,6 +3798,13 @@ function compileReplacementModule(input) {
3820
3798
  return name2;
3821
3799
  };
3822
3800
  const edits = [];
3801
+ const pointAtLocalModule = (specifierToken, specifier) => {
3802
+ edits.push({
3803
+ start: specifierToken.start,
3804
+ end: specifierToken.end,
3805
+ text: JSON.stringify(input.localModuleUrl(specifier))
3806
+ });
3807
+ };
3823
3808
  for (let index = 0; index < tokens.length; index++) {
3824
3809
  const token = tokens[index];
3825
3810
  if (token.type === EXPORT_KEYWORD) {
@@ -3839,6 +3824,10 @@ function compileReplacementModule(input) {
3839
3824
  }
3840
3825
  if (fromAt > 0 && tokens[fromAt] && read(tokens[fromAt]) === "from" && tokens[fromAt + 1]?.type === import_types.TokenType.string) {
3841
3826
  const specifier2 = readSpecifier(tokens[fromAt + 1]);
3827
+ if (isLocalSpecifier(specifier2)) {
3828
+ pointAtLocalModule(tokens[fromAt + 1], specifier2);
3829
+ continue;
3830
+ }
3842
3831
  throw new Error(
3843
3832
  `a replacement module cannot re-export from another module, and this one re-exports from '${specifier2}'. import what you need and export the binding.`
3844
3833
  );
@@ -3891,6 +3880,10 @@ function compileReplacementModule(input) {
3891
3880
  const specifierToken = tokens[cursor];
3892
3881
  if (!specifierToken) throw new Error("replacement module import is unterminated");
3893
3882
  const specifier = readSpecifier(specifierToken);
3883
+ if (isLocalSpecifier(specifier)) {
3884
+ pointAtLocalModule(specifierToken, specifier);
3885
+ continue;
3886
+ }
3894
3887
  if (specifier !== "react" && specifier !== "react-native" && !JSX_RUNTIME_SPECIFIERS.has(specifier) && !isContrastKitSpecifier(specifier) && !input.runtimePackages.has(specifier)) {
3895
3888
  throw unsupportedSpecifier(specifier);
3896
3889
  }
@@ -3938,6 +3931,25 @@ function sendJavaScript(req, res, source) {
3938
3931
  });
3939
3932
  res.end(req.method === "HEAD" ? void 0 : source);
3940
3933
  }
3934
+ function resolveLocalImport(fromFile, specifier) {
3935
+ const base = import_node_path5.default.resolve(import_node_path5.default.dirname(fromFile), specifier);
3936
+ const candidates = [
3937
+ ...REPLACEMENT_MODULE_EXTENSIONS.some(
3938
+ (ext) => ext === import_node_path5.default.extname(base).toLowerCase()
3939
+ ) ? [base] : [],
3940
+ ...REPLACEMENT_MODULE_EXTENSIONS.map((ext) => base + ext),
3941
+ ...REPLACEMENT_MODULE_EXTENSIONS.map((ext) => import_node_path5.default.join(base, `index${ext}`))
3942
+ ];
3943
+ const found = candidates.find(
3944
+ (candidate) => (0, import_node_fs5.statSync)(candidate, { throwIfNoEntry: false })?.isFile()
3945
+ );
3946
+ if (!found) {
3947
+ throw new Error(
3948
+ `'${specifier}' imported by ${fromFile} matches no ${REPLACEMENT_MODULE_EXTENSIONS.join(", ")} file`
3949
+ );
3950
+ }
3951
+ return found;
3952
+ }
3941
3953
  function isReplacementModuleRequestUrl(rawUrl) {
3942
3954
  if (!rawUrl) return false;
3943
3955
  try {
@@ -4028,7 +4040,14 @@ async function handleReplacementModuleRequest(req, res, options = {}) {
4028
4040
  const compiled = compileReplacementModule({
4029
4041
  filePath: resolvedTargetPath,
4030
4042
  source: await import_promises.default.readFile(resolvedTargetPath, "utf8"),
4031
- runtimePackages
4043
+ runtimePackages,
4044
+ // the imported file's url is this request's with only its path changed,
4045
+ // so the token, cache key and runtime packages carry over.
4046
+ localModuleUrl: (specifier) => {
4047
+ const local = new URL(url.href);
4048
+ local.searchParams.set("path", resolveLocalImport(targetPath, specifier));
4049
+ return `${local.pathname}${local.search}`;
4050
+ }
4032
4051
  });
4033
4052
  const map = Buffer.from(JSON.stringify(compiled.sourceMap), "utf8").toString("base64");
4034
4053
  sendJavaScript(
@@ -4562,8 +4581,205 @@ var import_module2 = require("module");
4562
4581
  var import_node_crypto5 = require("node:crypto");
4563
4582
  var import_path4 = __toESM(require("path"), 1);
4564
4583
 
4584
+ // src/native-app-identity.ts
4585
+ var NATIVE_APP_IDENTITY_PREFIX = "globalThis.__sootsimNativeAppIdentity=";
4586
+ var METRO_MODULE_PATHS_PREFIX = "globalThis.__sootsimModulePaths=";
4587
+ function isRecord2(value) {
4588
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4589
+ }
4590
+ function nonEmptyString(value) {
4591
+ return typeof value === "string" && value.length > 0 ? value : null;
4592
+ }
4593
+ function createNativeAppIdentityFooter(identity) {
4594
+ const payload = JSON.stringify(identity).replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029").replace(/<\//g, "<\\/");
4595
+ return `
4596
+ ${NATIVE_APP_IDENTITY_PREFIX}${payload};`;
4597
+ }
4598
+ function nativeAppIdentityFooterStart(source) {
4599
+ return source.lastIndexOf(`
4600
+ ${NATIVE_APP_IDENTITY_PREFIX}`);
4601
+ }
4602
+ function insertNativeAppIdentityFooter(source, identity) {
4603
+ const existing = nativeAppIdentityFooterStart(source);
4604
+ const existingEnd = existing < 0 ? -1 : source.includes("\n", existing + 1) ? source.indexOf("\n", existing + 1) : -1;
4605
+ const withoutExisting = existing < 0 ? source : source.slice(0, existing) + source.slice(existingEnd < 0 ? source.length : existingEnd);
4606
+ const moduleFooter = withoutExisting.lastIndexOf(`
4607
+ ${METRO_MODULE_PATHS_PREFIX}`);
4608
+ const footer = createNativeAppIdentityFooter(identity);
4609
+ if (moduleFooter < 0) return withoutExisting + footer;
4610
+ return withoutExisting.slice(0, moduleFooter) + footer + withoutExisting.slice(moduleFooter);
4611
+ }
4612
+ function plistString(plist, key) {
4613
+ const match = plist.match(new RegExp(`<key>${key}</key>\\s*<string>([^<]*)</string>`));
4614
+ return match ? match[1].trim() : null;
4615
+ }
4616
+ function resolveBuildSetting(value, settings, depth = 0) {
4617
+ if (depth > 8) return null;
4618
+ let unresolved = false;
4619
+ const resolved = value.replace(/\$\(([^()]+)\)/g, (_match, reference) => {
4620
+ const separator = reference.indexOf(":");
4621
+ if (separator >= 0) {
4622
+ unresolved = true;
4623
+ return "";
4624
+ }
4625
+ const replacement = settings[reference];
4626
+ if (replacement === void 0) {
4627
+ unresolved = true;
4628
+ return "";
4629
+ }
4630
+ return replacement;
4631
+ });
4632
+ if (unresolved) return null;
4633
+ if (resolved.includes("$(")) return resolveBuildSetting(resolved, settings, depth + 1);
4634
+ return resolved;
4635
+ }
4636
+ function resolvedIdentityField(value) {
4637
+ const trimmed = value?.trim() ?? "";
4638
+ if (!trimmed || trimmed.includes("$(")) return null;
4639
+ return trimmed;
4640
+ }
4641
+ function identityFieldFrom(settings, raw, fallbackSetting) {
4642
+ if (raw !== null) return resolvedIdentityField(resolveBuildSetting(raw, settings));
4643
+ return resolvedIdentityField(nonEmptyString(fallbackSetting ?? null));
4644
+ }
4645
+ function readBuildSettingsBlock(source, start) {
4646
+ let depth = 0;
4647
+ for (let index = start; index < source.length; index++) {
4648
+ const character = source[index];
4649
+ if (character === "{") depth++;
4650
+ else if (character === "}") {
4651
+ depth--;
4652
+ if (depth === 0) return source.slice(start, index + 1);
4653
+ }
4654
+ }
4655
+ return source.slice(start);
4656
+ }
4657
+ function parseBuildSettings(block) {
4658
+ const settings = {};
4659
+ const matchPattern = /([A-Za-z0-9_]+) = ("(?:[^"\\]|\\.)*"|[^;{}]*);/g;
4660
+ let match;
4661
+ while ((match = matchPattern.exec(block)) !== null) {
4662
+ const raw = match[2].trim();
4663
+ settings[match[1]] = raw.startsWith('"') ? raw.slice(1, -1) : raw.trim();
4664
+ }
4665
+ return settings;
4666
+ }
4667
+ function appTargetReleaseSettings(pbxproj, targetName, infoPlistRelativePath) {
4668
+ const configurationPattern = /\/\* (Release|Debug) \*\/ = \{/g;
4669
+ const candidates = [];
4670
+ let match;
4671
+ while ((match = configurationPattern.exec(pbxproj)) !== null) {
4672
+ const blockStart = pbxproj.indexOf("{", match.index);
4673
+ if (blockStart < 0) continue;
4674
+ const block = readBuildSettingsBlock(pbxproj, blockStart);
4675
+ const buildSettingsStart = block.indexOf("buildSettings = {");
4676
+ if (buildSettingsStart < 0) continue;
4677
+ const settings = parseBuildSettings(
4678
+ readBuildSettingsBlock(block, block.indexOf("{", buildSettingsStart))
4679
+ );
4680
+ candidates.push({ name: match[1], settings });
4681
+ }
4682
+ const plistMatch = (settings) => {
4683
+ const plist = settings.INFOPLIST_FILE;
4684
+ if (!plist) return false;
4685
+ const normalized = plist.replace(/^"|"$/g, "");
4686
+ return normalized === infoPlistRelativePath || infoPlistRelativePath.endsWith(`/${normalized}`) || normalized.endsWith(`/${infoPlistRelativePath}`);
4687
+ };
4688
+ const named = (settings) => resolveBuildSetting(settings.PRODUCT_NAME ?? "", settings) === targetName;
4689
+ return candidates.find(
4690
+ (candidate) => candidate.name === "Release" && plistMatch(candidate.settings)
4691
+ )?.settings ?? candidates.find(
4692
+ (candidate) => candidate.name === "Release" && named(candidate.settings)
4693
+ )?.settings ?? null;
4694
+ }
4695
+ function nativeAppIdentityFromIos(files, parseInfoPlist) {
4696
+ const pbxprojPath = Object.keys(files).find(
4697
+ (path9) => /(?:^|\/)ios\/[^/]+\.xcodeproj\/project\.pbxproj$/.test(path9)
4698
+ );
4699
+ if (!pbxprojPath) return null;
4700
+ const pbxproj = files[pbxprojPath];
4701
+ const applicationTargets = new Set(
4702
+ [
4703
+ ...pbxproj.matchAll(
4704
+ /isa = PBXNativeTarget;[^}]*?name = "?([^";]+)"?;[^}]*?productType = "com\.apple\.product-type\.application";/g
4705
+ )
4706
+ ].map((match) => match[1])
4707
+ );
4708
+ const appDelegatePattern = /(?:^|\/)ios\/([^/]+)\/AppDelegate\.(?:swift|mm|m)$/;
4709
+ const targetName = Object.keys(files).filter((path9) => !path9.includes("/Pods/")).map((path9) => path9.match(appDelegatePattern)?.[1]).find((name2) => name2 !== void 0 && applicationTargets.has(name2)) ?? null;
4710
+ if (!targetName) return null;
4711
+ const plist = files[`ios/${targetName}/Info.plist`];
4712
+ if (!plist) return null;
4713
+ const settings = appTargetReleaseSettings(
4714
+ pbxproj,
4715
+ targetName,
4716
+ `${targetName}/Info.plist`
4717
+ );
4718
+ if (!settings) return null;
4719
+ const bundleIdentifier = identityFieldFrom(
4720
+ settings,
4721
+ plistString(plist, "CFBundleIdentifier"),
4722
+ settings.PRODUCT_BUNDLE_IDENTIFIER
4723
+ );
4724
+ if (!bundleIdentifier) return null;
4725
+ const appVersion = identityFieldFrom(
4726
+ settings,
4727
+ plistString(plist, "CFBundleShortVersionString"),
4728
+ settings.MARKETING_VERSION
4729
+ );
4730
+ const buildVersion = identityFieldFrom(
4731
+ settings,
4732
+ plistString(plist, "CFBundleVersion"),
4733
+ settings.CURRENT_PROJECT_VERSION
4734
+ );
4735
+ if (!appVersion || !buildVersion) return null;
4736
+ const applicationName = identityFieldFrom(settings, plistString(plist, "CFBundleDisplayName"), void 0) ?? identityFieldFrom(
4737
+ settings,
4738
+ plistString(plist, "CFBundleName"),
4739
+ settings.PRODUCT_NAME
4740
+ ) ?? resolvedIdentityField(resolveBuildSetting(settings.PRODUCT_NAME ?? "", settings)) ?? resolvedIdentityField(targetName);
4741
+ if (!applicationName) return null;
4742
+ const dictionary = parseInfoPlist(plist);
4743
+ if (!isRecord2(dictionary)) throw new Error("native app Info.plist is not a dictionary");
4744
+ const permissionUsageDescriptions = {};
4745
+ for (const [key, value] of Object.entries(dictionary)) {
4746
+ if (!/^NS.*UsageDescription$/.test(key)) continue;
4747
+ if (typeof value !== "string") {
4748
+ permissionUsageDescriptions[key] = value;
4749
+ continue;
4750
+ }
4751
+ const resolved = resolveBuildSetting(value, settings);
4752
+ if (resolved === null) throw new Error(`native app Info.plist ${key} is unresolved`);
4753
+ permissionUsageDescriptions[key] = resolved;
4754
+ }
4755
+ if (settings.GENERATE_INFOPLIST_FILE === "YES") {
4756
+ for (const [setting, value] of Object.entries(settings)) {
4757
+ if (!/^INFOPLIST_KEY_NS.*UsageDescription$/.test(setting)) continue;
4758
+ const key = setting.slice("INFOPLIST_KEY_".length);
4759
+ const resolved = resolveBuildSetting(value, settings);
4760
+ if (resolved === null)
4761
+ throw new Error(`native app build setting ${setting} is unresolved`);
4762
+ permissionUsageDescriptions[key] = resolved;
4763
+ }
4764
+ }
4765
+ return {
4766
+ applicationName,
4767
+ appVersion,
4768
+ buildVersion,
4769
+ bundleIdentifier,
4770
+ platform: "ios",
4771
+ permissionUsageDescriptions
4772
+ };
4773
+ }
4774
+ function canExtractNativeAppIdentity(platform) {
4775
+ return platform === "ios";
4776
+ }
4777
+ function extractNativeAppIdentityFromFiles(files, platform, parseInfoPlist) {
4778
+ return canExtractNativeAppIdentity(platform) ? nativeAppIdentityFromIos(files, parseInfoPlist) : null;
4779
+ }
4780
+
4565
4781
  // src/native-firebase-config.ts
4566
- var import_node_fs5 = __toESM(require("node:fs"), 1);
4782
+ var import_node_fs6 = __toESM(require("node:fs"), 1);
4567
4783
  var import_node_module2 = require("node:module");
4568
4784
  var import_node_path6 = __toESM(require("node:path"), 1);
4569
4785
 
@@ -4594,7 +4810,7 @@ function readNativeFirebaseConfig(projectRoot) {
4594
4810
  "app.config.ts",
4595
4811
  "app.config.cjs",
4596
4812
  "app.config.mjs"
4597
- ].some((name2) => import_node_fs5.default.existsSync(import_node_path6.default.join(projectRoot, name2))))
4813
+ ].some((name2) => import_node_fs6.default.existsSync(import_node_path6.default.join(projectRoot, name2))))
4598
4814
  return result;
4599
4815
  const require2 = (0, import_node_module2.createRequire)(import_node_path6.default.join(projectRoot, "package.json"));
4600
4816
  const configModule = require2("@expo/config");
@@ -4610,7 +4826,7 @@ function readNativeFirebaseConfig(projectRoot) {
4610
4826
  const native = config[platform];
4611
4827
  if (!isRecord(native) || typeof native.googleServicesFile !== "string") continue;
4612
4828
  try {
4613
- const source = import_node_fs5.default.readFileSync(
4829
+ const source = import_node_fs6.default.readFileSync(
4614
4830
  import_node_path6.default.resolve(projectRoot, native.googleServicesFile),
4615
4831
  "utf8"
4616
4832
  );
@@ -4755,7 +4971,38 @@ function withRNX(config) {
4755
4971
  customSerializer: async (entryPoint, preModules, graph, serializerOptions) => {
4756
4972
  const serializer = existingSerializer || (defaultSerializer ||= createMetroDefaultSerializer(cfgProjectRoot));
4757
4973
  const result = await serializer(entryPoint, preModules, graph, serializerOptions);
4758
- return appendRNXModulePaths(result, graph, serializerOptions, cfgProjectRoot);
4974
+ let annotated = appendRNXModulePaths(
4975
+ result,
4976
+ graph,
4977
+ serializerOptions,
4978
+ cfgProjectRoot
4979
+ );
4980
+ const iosRoot = import_path4.default.join(cfgProjectRoot, "ios");
4981
+ const bundlePlatform = graph.transformOptions?.platform;
4982
+ if (canExtractNativeAppIdentity(bundlePlatform) && import_fs5.default.existsSync(iosRoot)) {
4983
+ const files = {};
4984
+ for (const entry of import_fs5.default.readdirSync(iosRoot, { withFileTypes: true })) {
4985
+ if (!entry.isDirectory() || entry.name === "Pods") continue;
4986
+ const names = entry.name.endsWith(".xcodeproj") ? ["project.pbxproj"] : ["Info.plist", "AppDelegate.swift", "AppDelegate.mm", "AppDelegate.m"];
4987
+ for (const name2 of names) {
4988
+ const relative2 = `ios/${entry.name}/${name2}`;
4989
+ const file = import_path4.default.join(cfgProjectRoot, relative2);
4990
+ if (import_fs5.default.existsSync(file)) files[relative2] = import_fs5.default.readFileSync(file, "utf8");
4991
+ }
4992
+ }
4993
+ const identity = extractNativeAppIdentityFromFiles(
4994
+ files,
4995
+ bundlePlatform,
4996
+ parsePlistSource
4997
+ );
4998
+ if (identity) {
4999
+ annotated = typeof annotated === "string" ? insertNativeAppIdentityFooter(annotated, identity) : {
5000
+ ...annotated,
5001
+ code: insertNativeAppIdentityFooter(annotated.code, identity)
5002
+ };
5003
+ }
5004
+ }
5005
+ return annotated;
4759
5006
  }
4760
5007
  }
4761
5008
  };
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.657 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.659 | (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.657 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.659 | (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.657 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.659 | (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;
@@ -294,17 +294,27 @@ async function kotlinAppArtifact(app, moduleRoot, origin) {
294
294
  "",
295
295
  "@JsExport",
296
296
  "fun peachAppMount(density: Float, fontScale: Float, locale: String, backend: String, stencilBits: Int,",
297
- " regular: String, medium: String, bold: String, statusBar: Int, navigationBar: Int,",
297
+ " regular: String, medium: String, bold: String, statusBar: Int, navigationBar: Int, night: Boolean,",
298
298
  " invalidate: (Boolean) -> Unit): String = dev.peach.app.peachStartApp(",
299
299
  " dev.peach.app.PeachAppSpec(",
300
300
  ` android.content.ComponentName(${kotlinString(app.packageName)}, ${kotlinString(app.launcher.name)}),`,
301
301
  ` android.content.ComponentName(${kotlinString(app.packageName)}, ${kotlinString(app.launcher.targetClass)}),`,
302
- ` ${app.targetSdk}, ${app.optOutEdgeToEdge},`,
302
+ " android.view.WindowTheme(",
303
+ ` ${app.window.edgeToEdgeEnforced}, 0x${app.window.statusBarColor.toString(16).padStart(8, "0")}.toInt(),`,
304
+ ` ${app.window.statusBarContrastEnforced}, ${app.window.navigationBarContrastEnforced},`,
305
+ ` ${app.window.lightStatusBar}, ${app.window.lightNavigationBar},`,
306
+ " ),",
303
307
  ` { ${app.applicationClass ?? "android.app.Application"}() },`,
304
308
  ` { ${app.launcher.targetClass}() },`,
305
309
  " ),",
306
- " density, fontScale, locale, backend, stencilBits, regular, medium, bold, statusBar, navigationBar, invalidate,",
310
+ " density, fontScale, locale, backend, stencilBits, regular, medium, bold, statusBar, navigationBar, night, invalidate,",
307
311
  ")",
312
+ "",
313
+ "@JsExport",
314
+ `fun peachAppBack(): Boolean = dev.peach.app.peachAppBack(${app.predictiveBack})`,
315
+ "",
316
+ "@JsExport",
317
+ "fun peachAppTask(foreground: Boolean) = dev.peach.app.peachAppTask(foreground)",
308
318
  ""
309
319
  ].join("\n")
310
320
  };
package/dist-lib/menu.cjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.657 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.659 | (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/menu.mjs CHANGED
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.657 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.659 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
 
3
3
  // src/public-brand.ts
4
4
  var name = "Peach";
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.657 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.659 | (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.657 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.659 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
 
3
3
  // src/is-record.ts
4
4
  function isRecord(value) {
@@ -1,4 +1,4 @@
1
- /*! rnx v0.1.657 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.659 | (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.657 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
1
+ /*! rnx v0.1.659 | (c) 2026 Tamagui LLC | Proprietary — see LICENSE */
2
2
 
3
3
  // src/metro-production-bundle.ts
4
4
  var RNXSIM_METRO_MODULE_PATHS_PREFIX = "globalThis.__sootsimModulePaths=";