peachsim 0.1.527

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 (324) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +640 -0
  3. package/cli/app-config.ts +82 -0
  4. package/cli/app-fonts.ts +388 -0
  5. package/cli/app-project.ts +59 -0
  6. package/cli/app-splash.ts +171 -0
  7. package/cli/app-state-reset.ts +26 -0
  8. package/cli/auth.ts +184 -0
  9. package/cli/bin.ts +59 -0
  10. package/cli/bridge-diagnostics.ts +226 -0
  11. package/cli/bridge-flow-runner.ts +2806 -0
  12. package/cli/browser-backend.ts +44 -0
  13. package/cli/browser-evals.ts +96 -0
  14. package/cli/cli-update.ts +261 -0
  15. package/cli/cloud-client.ts +1530 -0
  16. package/cli/cloud-dispatch.ts +206 -0
  17. package/cli/cloud-session.ts +227 -0
  18. package/cli/command-contract.ts +221 -0
  19. package/cli/command-registry.ts +304 -0
  20. package/cli/commands/agent-wrapper.ts +1048 -0
  21. package/cli/commands/agent.ts +439 -0
  22. package/cli/commands/app-fonts.ts +99 -0
  23. package/cli/commands/assert.ts +542 -0
  24. package/cli/commands/auth.ts +71 -0
  25. package/cli/commands/box/bundle-plane.ts +71 -0
  26. package/cli/commands/box/checkout-plane.ts +256 -0
  27. package/cli/commands/box/plane-share.ts +208 -0
  28. package/cli/commands/box/rnx-command.ts +60 -0
  29. package/cli/commands/box/search-command.ts +157 -0
  30. package/cli/commands/box.ts +925 -0
  31. package/cli/commands/camera.ts +266 -0
  32. package/cli/commands/cleanup.ts +169 -0
  33. package/cli/commands/compat.ts +176 -0
  34. package/cli/commands/config.ts +34 -0
  35. package/cli/commands/control.ts +2154 -0
  36. package/cli/commands/cpu-profile.ts +941 -0
  37. package/cli/commands/daemon-mac-app.ts +165 -0
  38. package/cli/commands/daemon.ts +800 -0
  39. package/cli/commands/debug.ts +751 -0
  40. package/cli/commands/desktop.ts +39 -0
  41. package/cli/commands/detect.ts +201 -0
  42. package/cli/commands/detox.ts +481 -0
  43. package/cli/commands/device.ts +134 -0
  44. package/cli/commands/diagnose.ts +589 -0
  45. package/cli/commands/do-chain.ts +408 -0
  46. package/cli/commands/electron.ts +94 -0
  47. package/cli/commands/film.ts +379 -0
  48. package/cli/commands/flow.ts +1240 -0
  49. package/cli/commands/inspect/actions.ts +516 -0
  50. package/cli/commands/inspect/core.ts +2086 -0
  51. package/cli/commands/inspect/count.ts +17 -0
  52. package/cli/commands/inspect/describe.ts +119 -0
  53. package/cli/commands/inspect/env.ts +25 -0
  54. package/cli/commands/inspect/find.ts +169 -0
  55. package/cli/commands/inspect/get-layout.ts +53 -0
  56. package/cli/commands/inspect/keyboard.ts +54 -0
  57. package/cli/commands/inspect/list.ts +175 -0
  58. package/cli/commands/inspect/memory.ts +215 -0
  59. package/cli/commands/inspect/redaction.ts +39 -0
  60. package/cli/commands/inspect/resolve-target.ts +77 -0
  61. package/cli/commands/inspect/screens.ts +78 -0
  62. package/cli/commands/inspect/settle.ts +32 -0
  63. package/cli/commands/inspect/settling.ts +197 -0
  64. package/cli/commands/inspect/shared.ts +259 -0
  65. package/cli/commands/inspect/sleep.ts +15 -0
  66. package/cli/commands/inspect/tree.ts +23 -0
  67. package/cli/commands/inspect/url.ts +17 -0
  68. package/cli/commands/inspect/wait-event.ts +211 -0
  69. package/cli/commands/inspect/wait-idle.ts +25 -0
  70. package/cli/commands/inspect/wait-ready.ts +87 -0
  71. package/cli/commands/inspect/wait-selector.ts +59 -0
  72. package/cli/commands/inspect/wait.ts +32 -0
  73. package/cli/commands/inspect.ts +4738 -0
  74. package/cli/commands/install-cli.ts +275 -0
  75. package/cli/commands/install-desktop.ts +385 -0
  76. package/cli/commands/login.ts +357 -0
  77. package/cli/commands/logout.ts +24 -0
  78. package/cli/commands/maestro-generate.ts +375 -0
  79. package/cli/commands/maestro.ts +536 -0
  80. package/cli/commands/mode.ts +58 -0
  81. package/cli/commands/no-bridge-hint.ts +80 -0
  82. package/cli/commands/perf.ts +67 -0
  83. package/cli/commands/permissions.ts +203 -0
  84. package/cli/commands/platform.ts +410 -0
  85. package/cli/commands/preview.ts +103 -0
  86. package/cli/commands/profile.ts +106 -0
  87. package/cli/commands/react.ts +353 -0
  88. package/cli/commands/record.ts +1435 -0
  89. package/cli/commands/report-issue.ts +305 -0
  90. package/cli/commands/reset.ts +89 -0
  91. package/cli/commands/runtime.ts +348 -0
  92. package/cli/commands/screenshot-command.ts +106 -0
  93. package/cli/commands/screenshot-layers.ts +144 -0
  94. package/cli/commands/screenshot-mode.ts +38 -0
  95. package/cli/commands/screenshot.ts +492 -0
  96. package/cli/commands/screenshots-capture.ts +608 -0
  97. package/cli/commands/screenshots.ts +127 -0
  98. package/cli/commands/serve.ts +139 -0
  99. package/cli/commands/setup.ts +416 -0
  100. package/cli/commands/shell-boolean-mode.ts +82 -0
  101. package/cli/commands/skills.ts +468 -0
  102. package/cli/commands/slides.ts +357 -0
  103. package/cli/commands/state.ts +94 -0
  104. package/cli/commands/storage.ts +58 -0
  105. package/cli/commands/telemetry.ts +55 -0
  106. package/cli/commands/test.ts +535 -0
  107. package/cli/commands/three-mode.ts +771 -0
  108. package/cli/commands/timeline.ts +121 -0
  109. package/cli/commands/upgrade.ts +167 -0
  110. package/cli/commands/upload.ts +1203 -0
  111. package/cli/commands/version.ts +63 -0
  112. package/cli/commands/welcome.ts +17 -0
  113. package/cli/commands/what-happened.ts +328 -0
  114. package/cli/current-sim.ts +217 -0
  115. package/cli/desktop-companion.ts +315 -0
  116. package/cli/drivers/electron.ts +70 -0
  117. package/cli/drivers/index.ts +20 -0
  118. package/cli/drivers/playwright-provisioning.ts +184 -0
  119. package/cli/drivers/playwright-sim-host.ts +537 -0
  120. package/cli/drivers/playwright.ts +209 -0
  121. package/cli/drivers/registry.ts +65 -0
  122. package/cli/drivers/types.ts +102 -0
  123. package/cli/flow-export.ts +55 -0
  124. package/cli/flow-file.ts +142 -0
  125. package/cli/flow-live-status.ts +126 -0
  126. package/cli/flow-session.ts +140 -0
  127. package/cli/help-core.ts +143 -0
  128. package/cli/help.ts +44 -0
  129. package/cli/hidden-runtime-alias.ts +19 -0
  130. package/cli/hints.ts +214 -0
  131. package/cli/inspect-notice-state.ts +114 -0
  132. package/cli/internal-child.ts +176 -0
  133. package/cli/maestro-js.ts +323 -0
  134. package/cli/main.ts +704 -0
  135. package/cli/open-url.ts +8 -0
  136. package/cli/outbound-endpoints.ts +830 -0
  137. package/cli/parent-pid.ts +204 -0
  138. package/cli/parse-args.ts +189 -0
  139. package/cli/privacy.ts +53 -0
  140. package/cli/prompt.ts +56 -0
  141. package/cli/recording-access.ts +108 -0
  142. package/cli/registry.ts +2 -0
  143. package/cli/resolve-assets.ts +63 -0
  144. package/cli/rnx-in-process.ts +98 -0
  145. package/cli/run-registry.ts +226 -0
  146. package/cli/run-rnx-core.ts +148 -0
  147. package/cli/run-rnx.ts +137 -0
  148. package/cli/runtime-notes.ts +66 -0
  149. package/cli/runtime-summary.ts +25 -0
  150. package/cli/secure-file.ts +31 -0
  151. package/cli/self-invocation.ts +58 -0
  152. package/cli/send-to-box.ts +109 -0
  153. package/cli/setup-repository.ts +191 -0
  154. package/cli/shell-init.ts +98 -0
  155. package/cli/standalone.ts +4 -0
  156. package/cli/telemetry.ts +84 -0
  157. package/cli/ui.ts +68 -0
  158. package/cli/ws-bridge.ts +1020 -0
  159. package/detox/attempt-events.cjs +23 -0
  160. package/detox/colors.ts +54 -0
  161. package/detox/config-loader.ts +135 -0
  162. package/detox/element-types.ts +48 -0
  163. package/detox/expectations.ts +534 -0
  164. package/detox/gestures.ts +501 -0
  165. package/detox/index.ts +2694 -0
  166. package/detox/jest-preset.cjs +29 -0
  167. package/detox/jest-setup-after-env.cjs +22 -0
  168. package/detox/matchers.ts +30 -0
  169. package/detox/motion-change.cjs +109 -0
  170. package/detox/motion-change.d.cts +35 -0
  171. package/detox/navigation-lifecycle.ts +290 -0
  172. package/detox/navigation.ts +43 -0
  173. package/detox/proof-frame.cjs +114 -0
  174. package/detox/record-block-execution.cjs +78 -0
  175. package/detox/screenshots/animated-color-test-rest-norngh.png +0 -0
  176. package/detox/screenshots/color-test-after-drag-norngh.png +0 -0
  177. package/detox/screenshots/color-test-rest-norngh.png +0 -0
  178. package/detox/screenshots/theme-blue-toggle.png +0 -0
  179. package/detox/screenshots/theme-blue.png +0 -0
  180. package/detox/screenshots/theme-red-toggle.png +0 -0
  181. package/detox/screenshots/theme-red.png +0 -0
  182. package/dist-lib/agent-daemon-client.cjs +502 -0
  183. package/dist-lib/agent-events.cjs +48 -0
  184. package/dist-lib/agent-identity.cjs +53 -0
  185. package/dist-lib/agent-sessions.cjs +759 -0
  186. package/dist-lib/attached-projects.cjs +526 -0
  187. package/dist-lib/auth/shared-session.cjs +208 -0
  188. package/dist-lib/backend-origin.cjs +88 -0
  189. package/dist-lib/beta.cjs +41 -0
  190. package/dist-lib/beta.mjs +13 -0
  191. package/dist-lib/bridge-constants.cjs +161 -0
  192. package/dist-lib/bridge-contract-input.cjs +451 -0
  193. package/dist-lib/bridge-contract-input.mjs +414 -0
  194. package/dist-lib/bridge-contract.cjs +130 -0
  195. package/dist-lib/bridge-contract.mjs +94 -0
  196. package/dist-lib/capture-contract.cjs +220 -0
  197. package/dist-lib/capture-contract.mjs +190 -0
  198. package/dist-lib/cli-constants.cjs +32 -0
  199. package/dist-lib/cloud-contract.cjs +194 -0
  200. package/dist-lib/cloud-contract.mjs +145 -0
  201. package/dist-lib/cloud.cjs +670 -0
  202. package/dist-lib/cloud.mjs +639 -0
  203. package/dist-lib/config.cjs +96 -0
  204. package/dist-lib/detox/index.cjs +3789 -0
  205. package/dist-lib/detox/jest-preset.cjs +29 -0
  206. package/dist-lib/dev-bundle-resolution.cjs +290 -0
  207. package/dist-lib/home-paths.cjs +638 -0
  208. package/dist-lib/host/bridge-host.cjs +10903 -0
  209. package/dist-lib/host/fetch-proxy-handler.cjs +421 -0
  210. package/dist-lib/host/fetch-proxy-overrides.cjs +63 -0
  211. package/dist-lib/host/fetch-proxy-overrides.mjs +37 -0
  212. package/dist-lib/host/replacement-module-handler.cjs +469 -0
  213. package/dist-lib/host/websocket-proxy.cjs +298 -0
  214. package/dist-lib/index.cjs +4268 -0
  215. package/dist-lib/jump-to-source-babel.cjs +93 -0
  216. package/dist-lib/jump-to-source-native.cjs +112 -0
  217. package/dist-lib/menu.cjs +646 -0
  218. package/dist-lib/menu.mjs +604 -0
  219. package/dist-lib/metro-fingerprint-registry.cjs +186 -0
  220. package/dist-lib/metro-fingerprint-registry.mjs +156 -0
  221. package/dist-lib/metro-production-bundle.cjs +329 -0
  222. package/dist-lib/metro-production-bundle.mjs +295 -0
  223. package/dist-lib/metro.cjs +904 -0
  224. package/dist-lib/profiles.cjs +246 -0
  225. package/dist-lib/public-brand.cjs +39 -0
  226. package/dist-lib/react-native-host-modules.cjs +51 -0
  227. package/dist-lib/react-native-host-modules.mjs +25 -0
  228. package/dist-lib/render-mode.cjs +54 -0
  229. package/dist-lib/scripts/dev-server-scanner.cjs +1232 -0
  230. package/dist-lib/sdk.cjs +2433 -0
  231. package/dist-lib/sdk.mjs +2336 -0
  232. package/dist-lib/skills.cjs +39132 -0
  233. package/dist-lib/swift-submit.cjs +92 -0
  234. package/dist-lib/swift-submit.mjs +65 -0
  235. package/dist-lib/swift.cjs +1646 -0
  236. package/dist-lib/vite.cjs +1591 -0
  237. package/package.json +290 -0
  238. package/scripts/dev-server-scanner.ts +1291 -0
  239. package/scripts/optional-demo-registry.ts +113 -0
  240. package/skills/contrast/SKILL.md +165 -0
  241. package/skills/rnx-debug/SKILL.md +433 -0
  242. package/skills/rnx-perf/SKILL.md +399 -0
  243. package/skills/rnx-setup/SKILL.md +172 -0
  244. package/skills/rnx-test/SKILL.md +321 -0
  245. package/skills/rnx-visual/SKILL.md +126 -0
  246. package/src/agent-daemon-client.ts +410 -0
  247. package/src/agent-events.ts +71 -0
  248. package/src/agent-identity.ts +23 -0
  249. package/src/agent-prompt.ts +71 -0
  250. package/src/agent-sessions.ts +580 -0
  251. package/src/app-url.ts +39 -0
  252. package/src/attached-projects.ts +539 -0
  253. package/src/auth/shared-session.ts +235 -0
  254. package/src/backend-origin.ts +70 -0
  255. package/src/beta.ts +19 -0
  256. package/src/bridge-constants.ts +175 -0
  257. package/src/bridge-contract-input.ts +502 -0
  258. package/src/bridge-contract.ts +558 -0
  259. package/src/browser-cache.ts +16 -0
  260. package/src/capture-contract.ts +587 -0
  261. package/src/cli-constants.ts +1 -0
  262. package/src/cli-version.ts +40 -0
  263. package/src/cloud-contract.ts +274 -0
  264. package/src/cloud.ts +762 -0
  265. package/src/codex-client.ts +234 -0
  266. package/src/config.ts +132 -0
  267. package/src/connect.ts +101 -0
  268. package/src/dev-bundle-resolution.ts +346 -0
  269. package/src/dev-server-open.ts +198 -0
  270. package/src/disk-cleanup.ts +540 -0
  271. package/src/flow-timeout-scale.ts +15 -0
  272. package/src/home-paths.ts +806 -0
  273. package/src/host/agent-host.ts +666 -0
  274. package/src/host/bridge-host.ts +2788 -0
  275. package/src/host/fetch-proxy-handler.ts +449 -0
  276. package/src/host/fetch-proxy-overrides.ts +51 -0
  277. package/src/host/open-url.ts +89 -0
  278. package/src/host/plane-host.ts +203 -0
  279. package/src/host/replacement-module-handler.ts +158 -0
  280. package/src/host/websocket-proxy.ts +315 -0
  281. package/src/index.ts +71 -0
  282. package/src/jump-to-source-babel.ts +135 -0
  283. package/src/jump-to-source-native.ts +102 -0
  284. package/src/menu.ts +777 -0
  285. package/src/metro-fingerprint-registry.ts +302 -0
  286. package/src/metro-plugin.ts +341 -0
  287. package/src/metro-production-bundle.ts +471 -0
  288. package/src/native-dev-bundle-url.ts +118 -0
  289. package/src/native-seam-manifest.ts +341 -0
  290. package/src/playwright-host-log.ts +16 -0
  291. package/src/plist.ts +27 -0
  292. package/src/poll-until.ts +123 -0
  293. package/src/profiles.ts +179 -0
  294. package/src/public-brand.ts +9 -0
  295. package/src/react-native-host-modules.ts +26 -0
  296. package/src/render-mode.ts +27 -0
  297. package/src/rolldown-plugin-swift.ts +240 -0
  298. package/src/runtime-assets.ts +233 -0
  299. package/src/runtime-delivery.ts +54 -0
  300. package/src/screenshots/compose.ts +422 -0
  301. package/src/screenshots/frame-compose.ts +514 -0
  302. package/src/screenshots/orchestrate.ts +256 -0
  303. package/src/screenshots/org-deck.ts +91 -0
  304. package/src/screenshots/registry.ts +58 -0
  305. package/src/screenshots/schema.ts +364 -0
  306. package/src/sdk.ts +15 -0
  307. package/src/settings-bundle.ts +418 -0
  308. package/src/shell-bridge-identity.ts +15 -0
  309. package/src/sim-client.ts +697 -0
  310. package/src/skills/builtin/a11y-review.ts +132 -0
  311. package/src/skills/builtin/compat-check.ts +71 -0
  312. package/src/skills/builtin/maestro-test.ts +121 -0
  313. package/src/skills/builtin/perf-profile.ts +90 -0
  314. package/src/skills/builtin/screenshot-all.ts +52 -0
  315. package/src/skills/builtin/visual-diff.ts +100 -0
  316. package/src/skills/registry.ts +107 -0
  317. package/src/skills/types.ts +41 -0
  318. package/src/swift-submit.ts +185 -0
  319. package/src/swift.ts +7 -0
  320. package/src/team-machine-prompt.ts +73 -0
  321. package/src/vite-plugin-one.ts +264 -0
  322. package/src/vite-plugin-swift.ts +620 -0
  323. package/src/vite-plugin.ts +1946 -0
  324. package/src/worklets-babel.ts +250 -0
@@ -0,0 +1,341 @@
1
+ // native-seam manifest — the canonical registry of upstream packages whose
2
+ // native-module spec files we override.
3
+ //
4
+ // the rule: when a guest bundle imports from one of these packages, vite lets
5
+ // the package's JS resolve from node_modules normally. only the listed
6
+ // `seamBasenames` are redirected to our stub. this keeps the upstream JS layer
7
+ // (hooks, helpers, transforms, listeners) running unchanged.
8
+ //
9
+ // adding a new entry:
10
+ // 1. identify upstream's native-module file (look for files calling
11
+ // `requireNativeModule`, `TurboModuleRegistry.getEnforced`,
12
+ // `requireNativeComponent`, or in `src/specs/Native*Module.ts`)
13
+ // 2. write a thin stub at `packages/compat/src/stubs/native-seams/<pkg>.ts`
14
+ // that exports the same default + named exports upstream's seam file
15
+ // exports — except the implementation calls into sootsim's engine.
16
+ // 3. append an entry below. the plugin handles the rest.
17
+ //
18
+ // canonical example: react-native-reanimated. see
19
+ // - `packages/compat/src/stubs/react-native-reanimated.ts` (the seam stub)
20
+ // - `reanimatedNativeSeamRedirect` in vite-plugin.ts (still hard-coded
21
+ // because it has package-specific edge cases — fabricUtils,
22
+ // validate-worklets-version. new entries should use this manifest path
23
+ // for the simple basename-match case.)
24
+
25
+ export interface NativeSeamEntry {
26
+ /**
27
+ * upstream npm package name. all imports whose importer path includes
28
+ * `/<pkg>/` are candidates for redirect.
29
+ */
30
+ pkg: string
31
+ /**
32
+ * basenames of upstream files to redirect. each entry matches the file
33
+ * name (no extension, no platform suffix like `.ios` / `.android`) of any
34
+ * upstream module imported transitively from within `pkg`.
35
+ *
36
+ * example: if upstream has
37
+ * `node_modules/expo-notifications/src/NotificationsHandler.ts`, list
38
+ * `'NotificationsHandler'` here and we'll redirect any import resolving
39
+ * to that file to our seam stub.
40
+ */
41
+ seamBasenames: string[]
42
+ /**
43
+ * absolute path to the seam stub. typically
44
+ * `packages/compat/src/stubs/native-seams/<pkg>.ts` — but a single stub
45
+ * file may export every native module the package exposes, so the same
46
+ * `target` can appear in multiple entries if a package has multiple native
47
+ * modules that share an implementation file.
48
+ *
49
+ * authors set this via the `seamPath(<pkg>)` helper below.
50
+ */
51
+ target: string
52
+ /**
53
+ * notes — short reason this entry exists. shown by the debug logger when
54
+ * `SOOTSIM_NATIVE_SEAM_DEBUG=1` is set.
55
+ */
56
+ notes?: string
57
+ }
58
+
59
+ import fs from 'node:fs'
60
+ import path from 'node:path'
61
+ import { fileURLToPath } from 'node:url'
62
+
63
+ const HERE = path.dirname(fileURLToPath(import.meta.url))
64
+ const NATIVE_SEAMS_DIR = path.resolve(HERE, '../../compat/src/stubs/native-seams')
65
+
66
+ /** resolve a stub path under `packages/compat/src/stubs/native-seams/`.
67
+ * resolves to `.tsx` when only that exists (JSX-bearing seams like
68
+ * expo-image, react-native-webview). preferred order: `.ts`, `.tsx`. */
69
+ export function seamPath(basename: string): string {
70
+ const ts = path.resolve(NATIVE_SEAMS_DIR, `${basename}.ts`)
71
+ if (fs.existsSync(ts)) return ts
72
+ const tsx = path.resolve(NATIVE_SEAMS_DIR, `${basename}.tsx`)
73
+ if (fs.existsSync(tsx)) return tsx
74
+ // fall through to .ts so vite's "Could not load" error names the .ts
75
+ // path the author probably intended to create, instead of silently
76
+ // pointing at something that doesn't exist with no extension.
77
+ return ts
78
+ }
79
+
80
+ /**
81
+ * the manifest. agents append entries here during the native-seam migration
82
+ * (plans/sootsim/compat/compat-stubs-native-seam-audit.md). keep alphabetical by `pkg` to
83
+ * minimise merge conflicts.
84
+ */
85
+ export const NATIVE_SEAM_MANIFEST: NativeSeamEntry[] = [
86
+ // entries land here during the Phase 1 migration. example shape:
87
+ //
88
+ // {
89
+ // pkg: 'expo-notifications',
90
+ // seamBasenames: [
91
+ // 'NotificationsHandler',
92
+ // 'NotificationsEmitter',
93
+ // 'NotificationScheduler',
94
+ // 'NotificationCategoriesService',
95
+ // 'ServerRegistrationModule',
96
+ // ],
97
+ // target: seamPath('expo-notifications'),
98
+ // notes: 'route through engine NotificationBus',
99
+ // },
100
+ {
101
+ pkg: '@metamask/react-native-search-api',
102
+ // upstream is single-file `index.js`; no sub-imports inside the package
103
+ // to redirect at the basename layer. listed here for documentation parity
104
+ // — the bundle-loader's stub-registry routes the package-entry directly to
105
+ // `stubs/metamask-react-native-search-api.ts`, which pairs the upstream
106
+ // JS wrapper with the native seam below.
107
+ seamBasenames: ['index'],
108
+ target: seamPath('metamask-react-native-search-api'),
109
+ notes:
110
+ 'iOS CoreSpotlight + NSUserActivity native seam; pure-JS wrapper class lives at stubs/metamask-react-native-search-api.ts',
111
+ },
112
+ {
113
+ pkg: '@ua/react-native-airship',
114
+ seamBasenames: [
115
+ // src/NativeRNAirship.ts — the RNAirship TurboModule
116
+ 'NativeRNAirship',
117
+ // codegen host components (no native backing in sootsim)
118
+ 'RNAirshipEmbeddedViewNativeComponent',
119
+ 'RNAirshipMessageViewNativeComponent',
120
+ ],
121
+ target: seamPath('react-native-airship'),
122
+ notes:
123
+ 'push-notification SAAS — noop the SAAS layer, route banners through NotificationBus',
124
+ },
125
+ {
126
+ pkg: 'react-native-background-fetch',
127
+ // src/NativeBackgroundFetch.ts — TurboModuleRegistry.getEnforcing('RNBackgroundFetch')
128
+ seamBasenames: ['NativeBackgroundFetch'],
129
+ target: seamPath('react-native-background-fetch'),
130
+ notes:
131
+ 'iOS BGTaskScheduler / Android WorkManager TurboModule — no real scheduler in browser; acknowledge registrations, never fire periodic events',
132
+ },
133
+ {
134
+ pkg: 'expo-apple-authentication',
135
+ // src/ExpoAppleAuthenticationButton.ts is the package's native-view
136
+ // boundary. direct-compiled apps do not run the fetched-bundle lazy seam
137
+ // loader, so route that boundary to the same view manager implementation.
138
+ seamBasenames: ['ExpoAppleAuthenticationButton'],
139
+ target: seamPath('expo-apple-authentication-view'),
140
+ notes:
141
+ 'ASAuthorizationAppleIDButton native view — render the system button surface while upstream AppleAuthenticationButton.tsx remains unchanged',
142
+ },
143
+ {
144
+ pkg: 'expo-asset',
145
+ // src/ExpoAsset.ts — `requireNativeModule('ExpoAsset')`; only
146
+ // `downloadAsync` is on the native surface. all the rest (`Asset`,
147
+ // `AssetHooks`, `AssetSourceResolver`, ...) is pure JS that composes on
148
+ // top.
149
+ seamBasenames: ['ExpoAsset'],
150
+ target: seamPath('expo-asset'),
151
+ notes:
152
+ 'expo-modules-core requireNativeModule(ExpoAsset) — only downloadAsync; mirror upstream web build (return remote URL)',
153
+ },
154
+ {
155
+ pkg: '@bam.tech/react-native-image-resizer',
156
+ // upstream is not shipped to most guest bundles via node_modules — it
157
+ // arrives through the bundle-loader stub-registry which routes the
158
+ // package entry to `stubs/bam-react-native-image-resizer.ts`. listed here
159
+ // for documentation parity; vite-path redirect would key on
160
+ // `NativeImageResizer`.
161
+ seamBasenames: ['NativeImageResizer'],
162
+ target: seamPath('bam-react-native-image-resizer'),
163
+ notes:
164
+ 'ImageResizer TurboModule — browser-side canvas resize implementing the iOS contain/cover/stretch + onlyScaleDown semantics from ios/ImageResizer.mm',
165
+ },
166
+ {
167
+ pkg: 'react-native-adjust',
168
+ // upstream's `index.js` reads `NativeModules.Adjust` and
169
+ // `NativeModules.AdjustEventEmitter` directly — there is no NativeAdjust
170
+ // spec file. listed for documentation parity; the actual seam delivery is
171
+ // through the bundle-loader stub-registry routing the package entry to
172
+ // `stubs/react-native-adjust.ts`, which imports the native seam from this
173
+ // target.
174
+ seamBasenames: ['index'],
175
+ target: seamPath('react-native-adjust'),
176
+ notes:
177
+ 'Adjust attribution SAAS — noop native side (Adjust + AdjustEventEmitter NativeModules), pure-JS classes (AdjustConfig/AdjustEvent/...) live in stubs/react-native-adjust.ts',
178
+ },
179
+ {
180
+ pkg: 'expo-clipboard',
181
+ // src/ExpoClipboard.ts — `requireNativeModule<ExpoClipboardModule>('ExpoClipboard')`.
182
+ // upstream also ships `ExpoClipboard.web.ts` (which we mirror); the
183
+ // redirect catches both `.ts` and `.web.ts` since basenames match.
184
+ seamBasenames: ['ExpoClipboard'],
185
+ target: seamPath('expo-clipboard'),
186
+ notes:
187
+ 'expo-modules-core requireNativeModule(ExpoClipboard) — real navigator.clipboard text + ClipboardItem image; mirrors upstream ExpoClipboard.web.ts',
188
+ },
189
+ {
190
+ pkg: 'expo-font',
191
+ // src/ExpoFontLoader.ts — `requireNativeModule<ExpoFontLoaderModule>('ExpoFontLoader')`.
192
+ // we mirror upstream's web variant `ExpoFontLoader.web.ts` (FontFace API
193
+ // + managed <style id="expo-generated-fonts">).
194
+ seamBasenames: ['ExpoFontLoader'],
195
+ target: seamPath('expo-font'),
196
+ notes:
197
+ 'expo-modules-core requireNativeModule(ExpoFontLoader) — FontFace API registration; mirrors upstream ExpoFontLoader.web.ts',
198
+ },
199
+ {
200
+ pkg: '@react-native-async-storage/async-storage',
201
+ // src/NativeAsyncStorageModule.ts — TurboModule spec for `RNCAsyncStorage`.
202
+ // src/RCTAsyncStorage.ts — legacy `NativeModules['RNCAsyncStorage']` resolver.
203
+ // both basenames redirect to the same seam; upstream's AsyncStorage.ts
204
+ // imports default from either depending on bridgeless vs paper.
205
+ seamBasenames: ['NativeAsyncStorageModule', 'RCTAsyncStorage'],
206
+ target: seamPath('async-storage'),
207
+ notes:
208
+ 'RNCAsyncStorage TurboModule — localStorage-backed; preserves multiMerge JSON deep-merge semantics from upstream iOS module',
209
+ },
210
+ // expo-calendar and expo-contacts deliberately have NO entry here. expo
211
+ // renamed their native module ids across majors (`ExpoCalendar` →
212
+ // `CalendarNext`, `ExpoContacts` → `ExpoContactsNext`) and kept the old
213
+ // ids serving the `/legacy` subpath, so one package now asks for two
214
+ // different native modules depending on which entry point you import.
215
+ // basename matching cannot tell `src/ExpoCalendar.ts` from
216
+ // `src/legacy/ExpoCalendar.ts`, so a redirect here forces the legacy API
217
+ // onto the class-based seam, whose methods throw.
218
+ //
219
+ // registering by module NAME instead is what keeps old and new package
220
+ // versions both working: ../../compat/src/stubs/expo-{calendar,contacts}-register.ts
221
+ // installs every id either version asks for, and upstream's own
222
+ // `requireNativeModule` picks the right one per entry point. a file
223
+ // redirect can only match one layout, so it breaks the versions it was
224
+ // not written for.
225
+ {
226
+ pkg: 'expo-media-library',
227
+ // src/ExpoMediaLibrary.ts — `requireNativeModule('ExpoMediaLibrary')`.
228
+ // seam mirrors upstream's `ExpoMediaLibrary.web.ts` (refuse permissions
229
+ // + expose MediaType/SortBy constants).
230
+ seamBasenames: ['ExpoMediaLibrary'],
231
+ target: seamPath('expo-media-library'),
232
+ notes:
233
+ 'expo-modules-core requireNativeModule(ExpoMediaLibrary) — refuses permissions + exposes MediaType/SortBy enums per upstream ExpoMediaLibrary.web.ts',
234
+ },
235
+ {
236
+ pkg: 'expo-file-system',
237
+ // src/ExpoFileSystem.ts — `requireNativeModule('ExpoFileSystem')`. seam
238
+ // mirrors upstream's `ExpoFileSystem.web.ts` (warn-not-supported stubs).
239
+ seamBasenames: ['ExpoFileSystem'],
240
+ target: seamPath('expo-file-system'),
241
+ notes:
242
+ 'expo-modules-core requireNativeModule(ExpoFileSystem) — warn-not-supported stubs per upstream ExpoFileSystem.web.ts; no general browser fs access',
243
+ },
244
+ {
245
+ pkg: 'expo-image-picker',
246
+ // src/ExponentImagePicker.ts — `requireNativeModule('ExponentImagePicker')`.
247
+ // seam routes selection requests through sootsim's engine-owned image
248
+ // picker (`SootSim.bridges.imagePicker.pickFromLibrary`) so the same
249
+ // canvas-rendered iOS picker handles every image-source library.
250
+ seamBasenames: ['ExponentImagePicker'],
251
+ target: seamPath('expo-image-picker'),
252
+ notes:
253
+ "expo-modules-core requireNativeModule(ExponentImagePicker) — routes through engine SootSim.bridges.imagePicker (shell-worker ImagePickerView) instead of upstream web build's DOM <input type=file>",
254
+ },
255
+ {
256
+ pkg: 'expo-camera',
257
+ // upstream splits the native surface across two files:
258
+ // - src/ExpoCamera.ts → requireNativeViewManager('ExpoCamera')
259
+ // - src/ExpoCameraManager.ts → requireNativeModule('ExpoCamera')
260
+ // each basename redirects to a different seam — the view component vs.
261
+ // the imperative module. live preview + capture route through the same
262
+ // engine `camera-stream` module the built-in sootsim Camera app uses,
263
+ // so guest <CameraView> sees the same feed as the OS camera app.
264
+ seamBasenames: ['ExpoCamera'],
265
+ target: seamPath('expo-camera'),
266
+ notes:
267
+ 'expo-camera native view manager — host component backed by engine camera-stream (same getUserMedia pipeline as the built-in CameraApp); exposes CameraViewRef methods (takePicture/...) via useImperativeHandle',
268
+ },
269
+ {
270
+ pkg: 'expo-camera',
271
+ seamBasenames: ['ExpoCameraManager'],
272
+ target: seamPath('expo-camera-manager'),
273
+ notes:
274
+ 'expo-camera native module — permissions/feature-gates/codecs/Type/FlashMode constants; barcode scanner is noop (no engine scanner integration)',
275
+ },
276
+ {
277
+ pkg: 'expo-image',
278
+ // upstream splits the native surface across two files:
279
+ // - src/ExpoImage.tsx → requireNativeViewManager('ExpoImage')
280
+ // - src/ImageModule.ts → requireNativeModule('ExpoImage')
281
+ // the view-component seam translates expo-image's prop shape
282
+ // (contentFit, tintColor, source) onto sootsim's existing
283
+ // <sootsim-image> host. no new canvas component is needed — sootsim's
284
+ // image-loader + canvaskit-renderer already handle resizeMode,
285
+ // tintColor, and onLoad/onError lifecycle.
286
+ seamBasenames: ['ExpoImage'],
287
+ target: seamPath('expo-image'),
288
+ notes:
289
+ 'expo-image native view — thin prop translator onto sootsim-image host (contentFit→resizeMode, tintColor passthrough); upstream Image.tsx + ImageBackground + useImage resolve from node_modules unchanged',
290
+ },
291
+ {
292
+ pkg: 'expo-image',
293
+ seamBasenames: ['ImageModule'],
294
+ target: seamPath('expo-image-module'),
295
+ notes:
296
+ 'expo-image native module — prefetch/loadAsync route through sootsim image-loader; cache controls noop (browser owns HTTP cache); blurhash encode not yet implemented',
297
+ },
298
+ {
299
+ pkg: '@notifee/react-native',
300
+ // src/NotifeeNativeModule.ts — the base class NotifeeApiModule extends.
301
+ // upstream also ships .web.ts (returns `{}` from `native`); both
302
+ // basenames redirect to the same seam — a real implementation that
303
+ // routes `this.native.*` calls through the engine NotificationBus.
304
+ seamBasenames: ['NotifeeNativeModule'],
305
+ target: seamPath('notifee'),
306
+ notes:
307
+ 'Notifee NotifeeApiModule base class — routes display/cancel/channel/badge calls through engine NotificationBus; tap events fan out via the shared NotifeeJSEventEmitter so onForegroundEvent listeners fire',
308
+ },
309
+ {
310
+ pkg: 'expo-notifications',
311
+ // five native module spec files, each `requireNativeModule(...)`-shaped.
312
+ // every one redirects to the same `expo-notifications` seam — see
313
+ // `stubs/native-seams/expo-notifications.ts` for the union surface.
314
+ seamBasenames: [
315
+ // src/NotificationsHandlerModule.ts — presentation policy
316
+ 'NotificationsHandlerModule',
317
+ // src/NotificationsEmitterModule.ts — last-tap response + listener
318
+ 'NotificationsEmitterModule',
319
+ // src/NotificationScheduler.ts — schedule / cancel
320
+ 'NotificationScheduler',
321
+ // src/BadgeModule.ts — app badge count
322
+ 'BadgeModule',
323
+ // src/ServerRegistrationModule.ts — expo push registration
324
+ 'ServerRegistrationModule',
325
+ ],
326
+ target: seamPath('expo-notifications'),
327
+ notes:
328
+ 'expo-notifications native modules — routes scheduling/badge/installation-id through engine NotificationBus; tap callbacks fan out via the bus event emitters',
329
+ },
330
+ {
331
+ pkg: 'react-native-enriched-markdown',
332
+ seamBasenames: [
333
+ 'EnrichedMarkdownTextNativeComponent',
334
+ 'EnrichedMarkdownNativeComponent',
335
+ 'EnrichedMarkdownTextInputNativeComponent',
336
+ ],
337
+ target: seamPath('react-native-enriched-markdown'),
338
+ notes:
339
+ 'generated Fabric markdown host components; upstream Streamdown/enriched-markdown JS runs unchanged',
340
+ },
341
+ ]
@@ -0,0 +1,16 @@
1
+ // the playwright driver writes its browser-host stderr to a tmpdir log, and
2
+ // that log carries the only hardware-WebGL proof the hosted recorder accepts.
3
+ // the driver names the file and the recorder scans for it, so the prefix lives
4
+ // here rather than as a literal in both. it was already renamed once
5
+ // (sootsim- -> rnx-) without the recorder following, which silently failed
6
+ // every hosted job with "run completed without hardware WebGL proof" even when
7
+ // the preview itself uploaded fine.
8
+ export const PLAYWRIGHT_HOST_LOG_PREFIX = 'rnx-playwright-host-'
9
+
10
+ export function playwrightHostLogName(nowMs: number, pid: number): string {
11
+ return `${PLAYWRIGHT_HOST_LOG_PREFIX}${nowMs.toString(36)}-${pid}.log`
12
+ }
13
+
14
+ export function isPlaywrightHostLog(name: string): boolean {
15
+ return name.startsWith(PLAYWRIGHT_HOST_LOG_PREFIX) && name.endsWith('.log')
16
+ }
package/src/plist.ts ADDED
@@ -0,0 +1,27 @@
1
+ import plistModule from '@expo/plist'
2
+
3
+ function isRecord(value: unknown): value is Record<string, unknown> {
4
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
5
+ }
6
+
7
+ function resolvePlistParser(value: unknown): ((source: string) => unknown) | null {
8
+ if (!isRecord(value)) return null
9
+ const direct = value.parse
10
+ if (typeof direct === 'function') {
11
+ return (source) => Reflect.apply(direct, value, [source])
12
+ }
13
+ const nested = value.default
14
+ if (!isRecord(nested)) return null
15
+ const nestedParse = nested.parse
16
+ return typeof nestedParse === 'function'
17
+ ? (source) => Reflect.apply(nestedParse, nested, [source])
18
+ : null
19
+ }
20
+
21
+ // bun consumes @expo/plist's default export directly, while the compiled commonjs
22
+ // scanner receives the package namespace with that export nested under default.
23
+ const parsePlist = resolvePlistParser(plistModule)
24
+
25
+ export function parsePlistSource(source: string): unknown {
26
+ return parsePlist?.(source) ?? null
27
+ }
@@ -0,0 +1,123 @@
1
+ export interface PollUntilOptions {
2
+ timeoutMs: number
3
+ intervalMs?: number
4
+ label: string
5
+ signal?: AbortSignal
6
+ }
7
+
8
+ type PollReadOutcome<T> = { kind: 'value'; value: T } | { kind: 'error'; error: unknown }
9
+
10
+ export async function pollUntil<T>(
11
+ read: () => Promise<T>,
12
+ ready: (value: T) => boolean,
13
+ options: PollUntilOptions,
14
+ ): Promise<T> {
15
+ if (!Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0) {
16
+ throw new Error('poll timeout must be a positive finite number')
17
+ }
18
+ const intervalMs = options.intervalMs ?? 100
19
+ if (!Number.isFinite(intervalMs) || intervalMs < 0) {
20
+ throw new Error('poll interval must be a non-negative finite number')
21
+ }
22
+
23
+ const deadline = Date.now() + options.timeoutMs
24
+ let lastError: unknown
25
+ let lastRead: { value: T } | null = null
26
+ // a poll that expires because its predicate kept returning false is the
27
+ // ordinary case, and the error used to name nothing but the label: every one
28
+ // of those sent the reader back to re-run the whole flow just to learn what
29
+ // the state had been. reporting the last value the read produced makes the
30
+ // failure say it. bounded, because a polled value is often a whole evidence
31
+ // snapshot and a failure report is not a data dump.
32
+ const describeLastRead = (): string => {
33
+ if (lastRead === null) {
34
+ if (lastError === undefined) return 'no read settled'
35
+ const message = lastError instanceof Error ? lastError.message : String(lastError)
36
+ return `last read failed: ${message}`
37
+ }
38
+ let serialized: string
39
+ try {
40
+ const seen = new WeakSet()
41
+ serialized =
42
+ JSON.stringify(lastRead.value, (_key, entry: unknown) => {
43
+ if (typeof entry !== 'object' || entry === null) return entry
44
+ if (seen.has(entry)) return '[circular]'
45
+ seen.add(entry)
46
+ return entry
47
+ }) ?? String(lastRead.value)
48
+ } catch {
49
+ return 'last value (unserializable)'
50
+ }
51
+ if (serialized.length <= 300) return `last value ${serialized}`
52
+ return `last value ${serialized.slice(0, 300)}... (${serialized.length} chars)`
53
+ }
54
+ const timeoutError = () =>
55
+ new Error(
56
+ `${options.label} timed out after ${options.timeoutMs}ms; ${describeLastRead()}`,
57
+ { cause: lastError },
58
+ )
59
+
60
+ options.signal?.throwIfAborted()
61
+ let rejectBoundary: (reason: unknown) => void = () => {}
62
+ const boundary = new Promise<never>((_, reject) => {
63
+ rejectBoundary = reject
64
+ })
65
+ const onAbort = () =>
66
+ rejectBoundary(
67
+ options.signal?.reason ??
68
+ new DOMException('The operation was aborted', 'AbortError'),
69
+ )
70
+ options.signal?.addEventListener('abort', onAbort, { once: true })
71
+ const deadlineTimer = setTimeout(
72
+ () => rejectBoundary(timeoutError()),
73
+ options.timeoutMs,
74
+ )
75
+
76
+ try {
77
+ while (true) {
78
+ if (Date.now() >= deadline) throw timeoutError()
79
+
80
+ const outcome = await Promise.race([
81
+ Promise.resolve()
82
+ .then(read)
83
+ .then(
84
+ (value): PollReadOutcome<T> => ({ kind: 'value', value }),
85
+ (error: unknown): PollReadOutcome<T> => ({ kind: 'error', error }),
86
+ ),
87
+ boundary,
88
+ ])
89
+
90
+ options.signal?.throwIfAborted()
91
+ if (outcome.kind === 'error') {
92
+ lastError = outcome.error
93
+ } else {
94
+ lastRead = { value: outcome.value }
95
+ if (Date.now() >= deadline) throw timeoutError()
96
+ try {
97
+ if (ready(outcome.value)) return outcome.value
98
+ } catch (error) {
99
+ lastError = error
100
+ }
101
+ }
102
+
103
+ const remaining = deadline - Date.now()
104
+ if (remaining <= 0) throw timeoutError()
105
+ if (intervalMs > 0) {
106
+ let intervalTimer: ReturnType<typeof setTimeout> | undefined
107
+ try {
108
+ await Promise.race([
109
+ new Promise<void>((resolve) => {
110
+ intervalTimer = setTimeout(resolve, Math.min(intervalMs, remaining))
111
+ }),
112
+ boundary,
113
+ ])
114
+ } finally {
115
+ clearTimeout(intervalTimer)
116
+ }
117
+ }
118
+ }
119
+ } finally {
120
+ clearTimeout(deadlineTimer)
121
+ options.signal?.removeEventListener('abort', onAbort)
122
+ }
123
+ }
@@ -0,0 +1,179 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import { electronUserDataDir, ensureRnxHome, profilesDir } from './home-paths'
4
+
5
+ export const DEFAULT_PROFILE_ID = 'default'
6
+ const PROFILE_INDEX_FILE = 'profiles.json'
7
+ const PROFILE_INDEX_VERSION = 1 as const
8
+
9
+ export interface StorageProfile {
10
+ id: string
11
+ createdAt: string
12
+ updatedAt: string
13
+ }
14
+
15
+ interface ProfileIndex {
16
+ version: 1
17
+ profiles: StorageProfile[]
18
+ }
19
+
20
+ export function normalizeProfileId(input: string): string {
21
+ const id = input.trim()
22
+ if (!id) throw new Error('profile id is required')
23
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(id)) {
24
+ throw new Error(
25
+ 'profile ids must start with a letter or number and contain only letters, numbers, dot, dash, or underscore',
26
+ )
27
+ }
28
+ if (id === '.' || id === '..') throw new Error(`invalid profile id: ${id}`)
29
+ return id
30
+ }
31
+
32
+ export function profileIndexPath(): string {
33
+ return path.join(profilesDir(), PROFILE_INDEX_FILE)
34
+ }
35
+
36
+ export function electronProfilePartitionName(profileId: string): string {
37
+ return `sootsim-profile-${normalizeProfileId(profileId)}`
38
+ }
39
+
40
+ export function electronProfilePartition(profileId: string): string {
41
+ return `persist:${electronProfilePartitionName(profileId)}`
42
+ }
43
+
44
+ export function electronProfilePartitionDir(profileId: string): string {
45
+ return path.join(
46
+ electronUserDataDir(),
47
+ 'Partitions',
48
+ electronProfilePartitionName(profileId),
49
+ )
50
+ }
51
+
52
+ export function playwrightProfileUserDataDir(profileId: string): string {
53
+ return path.join(profilesDir(), 'playwright', normalizeProfileId(profileId))
54
+ }
55
+
56
+ function readIndexRaw(): ProfileIndex {
57
+ try {
58
+ const parsed = JSON.parse(
59
+ fs.readFileSync(profileIndexPath(), 'utf8'),
60
+ ) as Partial<ProfileIndex> | null
61
+ if (!parsed || parsed.version !== PROFILE_INDEX_VERSION) {
62
+ return { version: PROFILE_INDEX_VERSION, profiles: [] }
63
+ }
64
+ const profiles = Array.isArray(parsed.profiles)
65
+ ? parsed.profiles
66
+ .filter(
67
+ (profile): profile is StorageProfile =>
68
+ !!profile &&
69
+ typeof profile.id === 'string' &&
70
+ typeof profile.createdAt === 'string' &&
71
+ typeof profile.updatedAt === 'string',
72
+ )
73
+ .map((profile) => ({
74
+ id: normalizeProfileId(profile.id),
75
+ createdAt: profile.createdAt,
76
+ updatedAt: profile.updatedAt,
77
+ }))
78
+ : []
79
+ return { version: PROFILE_INDEX_VERSION, profiles }
80
+ } catch {
81
+ return { version: PROFILE_INDEX_VERSION, profiles: [] }
82
+ }
83
+ }
84
+
85
+ function sortProfiles(profiles: StorageProfile[]): StorageProfile[] {
86
+ return [...profiles].sort((a, b) => {
87
+ if (a.id === DEFAULT_PROFILE_ID) return -1
88
+ if (b.id === DEFAULT_PROFILE_ID) return 1
89
+ return a.id.localeCompare(b.id)
90
+ })
91
+ }
92
+
93
+ function withDefault(index: ProfileIndex): ProfileIndex {
94
+ if (index.profiles.some((profile) => profile.id === DEFAULT_PROFILE_ID)) {
95
+ return { ...index, profiles: sortProfiles(index.profiles) }
96
+ }
97
+ const now = new Date().toISOString()
98
+ return {
99
+ version: PROFILE_INDEX_VERSION,
100
+ profiles: sortProfiles([
101
+ { id: DEFAULT_PROFILE_ID, createdAt: now, updatedAt: now },
102
+ ...index.profiles,
103
+ ]),
104
+ }
105
+ }
106
+
107
+ function writeIndex(index: ProfileIndex): ProfileIndex {
108
+ ensureRnxHome()
109
+ const next = withDefault(index)
110
+ const tmp = `${profileIndexPath()}.tmp`
111
+ fs.writeFileSync(tmp, `${JSON.stringify(next, null, 2)}\n`, 'utf8')
112
+ fs.renameSync(tmp, profileIndexPath())
113
+ return next
114
+ }
115
+
116
+ export function listProfiles(): StorageProfile[] {
117
+ return writeIndex(readIndexRaw()).profiles
118
+ }
119
+
120
+ export function getProfile(id: string): StorageProfile | null {
121
+ const normalized = normalizeProfileId(id)
122
+ return listProfiles().find((profile) => profile.id === normalized) ?? null
123
+ }
124
+
125
+ export function ensureProfile(id = DEFAULT_PROFILE_ID): StorageProfile {
126
+ const normalized = normalizeProfileId(id)
127
+ const existing = getProfile(normalized)
128
+ if (existing) return existing
129
+ return createProfile(normalized)
130
+ }
131
+
132
+ export function createProfile(id: string): StorageProfile {
133
+ const normalized = normalizeProfileId(id)
134
+ const index = withDefault(readIndexRaw())
135
+ if (index.profiles.some((profile) => profile.id === normalized)) {
136
+ throw new Error(`profile already exists: ${normalized}`)
137
+ }
138
+ const now = new Date().toISOString()
139
+ const profile = { id: normalized, createdAt: now, updatedAt: now }
140
+ writeIndex({ version: PROFILE_INDEX_VERSION, profiles: [...index.profiles, profile] })
141
+ return profile
142
+ }
143
+
144
+ export function nextGeneratedProfileId(): string {
145
+ const existing = new Set(listProfiles().map((profile) => profile.id))
146
+ for (let i = 1; i < 10_000; i++) {
147
+ const id = `profile-${i}`
148
+ if (!existing.has(id)) return id
149
+ }
150
+ throw new Error('could not allocate a new profile id')
151
+ }
152
+
153
+ export function deleteProfile(id: string): StorageProfile {
154
+ const normalized = normalizeProfileId(id)
155
+ if (normalized === DEFAULT_PROFILE_ID) {
156
+ throw new Error('the default profile cannot be deleted; clear it instead')
157
+ }
158
+ const index = withDefault(readIndexRaw())
159
+ const profile = index.profiles.find((entry) => entry.id === normalized)
160
+ if (!profile) throw new Error(`profile not found: ${normalized}`)
161
+ writeIndex({
162
+ version: PROFILE_INDEX_VERSION,
163
+ profiles: index.profiles.filter((entry) => entry.id !== normalized),
164
+ })
165
+ clearProfileStorage(normalized)
166
+ return profile
167
+ }
168
+
169
+ export function clearProfileStorage(id: string): void {
170
+ const normalized = normalizeProfileId(id)
171
+ for (const dir of [
172
+ electronProfilePartitionDir(normalized),
173
+ playwrightProfileUserDataDir(normalized),
174
+ ]) {
175
+ try {
176
+ fs.rmSync(dir, { recursive: true, force: true })
177
+ } catch {}
178
+ }
179
+ }