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,235 @@
1
+ import {
2
+ chmodSync,
3
+ existsSync,
4
+ mkdirSync,
5
+ readFileSync,
6
+ rmSync,
7
+ writeFileSync,
8
+ } from 'node:fs'
9
+ import { dirname, join, resolve } from 'node:path'
10
+ import { electronUserDataDir } from '../home-paths.ts'
11
+
12
+ export type SharedDesktopAuthTeamSummary = {
13
+ id: string
14
+ name: string
15
+ role: 'owner' | 'member'
16
+ githubOrg: string | null
17
+ }
18
+
19
+ export type SharedDesktopAuthUser = {
20
+ id: string
21
+ name?: string
22
+ email?: string
23
+ image?: string
24
+ githubUsername?: string
25
+ /** the person's own account; every team they belong to is in `teams` */
26
+ personalAccountId?: string
27
+ teams?: SharedDesktopAuthTeamSummary[]
28
+ isAdmin?: boolean
29
+ }
30
+
31
+ export type SharedDesktopAuthSession = {
32
+ version: 1
33
+ token: string
34
+ user: SharedDesktopAuthUser | null
35
+ origin: string
36
+ source: 'cli' | 'electron' | 'browser' | 'unknown'
37
+ updatedAt: string
38
+ validatedAt?: string
39
+ /**
40
+ * the account this sign-in bills cloud work to. a session token names the
41
+ * person, not an account, so the CLI chooses one at `rnx login` and names it
42
+ * on every request that opens a paid session.
43
+ */
44
+ accountId?: string
45
+ }
46
+
47
+ /** the accounts a signed-in person can bill: their own, then their teams. */
48
+ export function sharedDesktopAuthAccounts(
49
+ user: SharedDesktopAuthUser | null | undefined,
50
+ ): { id: string; name: string }[] {
51
+ return [
52
+ ...(user?.personalAccountId
53
+ ? [{ id: user.personalAccountId, name: 'Personal' }]
54
+ : []),
55
+ ...(user?.teams ?? []).map((team) => ({ id: team.id, name: team.name })),
56
+ ]
57
+ }
58
+
59
+ const SESSION_VERSION = 1 as const
60
+ const SESSION_FILE_ENV = 'SOOTSIM_SHARED_AUTH_FILE'
61
+ const DEFAULT_ORIGIN = 'https://contrast.dev'
62
+
63
+ function getBaseDir() {
64
+ const explicit = process.env[SESSION_FILE_ENV]
65
+ if (explicit?.trim()) {
66
+ return dirname(resolve(explicit))
67
+ }
68
+ return electronUserDataDir()
69
+ }
70
+
71
+ export function getSharedDesktopAuthFilePath() {
72
+ const explicit = process.env[SESSION_FILE_ENV]
73
+ if (explicit?.trim()) return resolve(explicit)
74
+ return join(getBaseDir(), 'desktop-auth.json')
75
+ }
76
+
77
+ function normalizeUser(input: unknown): SharedDesktopAuthUser | null {
78
+ if (!input || typeof input !== 'object') return null
79
+ const value = input as Record<string, unknown>
80
+ if (typeof value.id !== 'string' || !value.id.trim()) return null
81
+ return {
82
+ id: value.id.trim(),
83
+ name: typeof value.name === 'string' ? value.name : undefined,
84
+ email: typeof value.email === 'string' ? value.email : undefined,
85
+ image: typeof value.image === 'string' ? value.image : undefined,
86
+ githubUsername:
87
+ typeof value.githubUsername === 'string' ? value.githubUsername : undefined,
88
+ personalAccountId:
89
+ typeof value.personalAccountId === 'string' && value.personalAccountId.trim()
90
+ ? value.personalAccountId.trim()
91
+ : undefined,
92
+ isAdmin: value.isAdmin === true,
93
+ teams: Array.isArray(value.teams)
94
+ ? value.teams
95
+ .map((team): SharedDesktopAuthTeamSummary | null => {
96
+ if (!team || typeof team !== 'object') return null
97
+ const t = team as Record<string, unknown>
98
+ if (typeof t.id !== 'string' || typeof t.name !== 'string') return null
99
+ return {
100
+ id: t.id,
101
+ name: t.name,
102
+ role: t.role === 'owner' ? 'owner' : 'member',
103
+ githubOrg: typeof t.githubOrg === 'string' ? t.githubOrg : null,
104
+ }
105
+ })
106
+ .filter((team): team is SharedDesktopAuthTeamSummary => !!team)
107
+ : undefined,
108
+ }
109
+ }
110
+
111
+ function normalizeSession(input: unknown): SharedDesktopAuthSession | null {
112
+ if (!input || typeof input !== 'object') return null
113
+ const value = input as Record<string, unknown>
114
+ if (value.version !== SESSION_VERSION) return null
115
+ if (typeof value.token !== 'string' || !value.token.trim()) return null
116
+ const origin =
117
+ typeof value.origin === 'string' && value.origin.trim()
118
+ ? value.origin.trim()
119
+ : DEFAULT_ORIGIN
120
+ const source =
121
+ value.source === 'cli' ||
122
+ value.source === 'electron' ||
123
+ value.source === 'browser' ||
124
+ value.source === 'unknown'
125
+ ? value.source
126
+ : 'unknown'
127
+ const updatedAt =
128
+ typeof value.updatedAt === 'string' && value.updatedAt
129
+ ? value.updatedAt
130
+ : new Date().toISOString()
131
+ const validatedAt =
132
+ typeof value.validatedAt === 'string' && value.validatedAt
133
+ ? value.validatedAt
134
+ : undefined
135
+ const accountId =
136
+ typeof value.accountId === 'string' && value.accountId.trim()
137
+ ? value.accountId.trim()
138
+ : undefined
139
+
140
+ return {
141
+ version: SESSION_VERSION,
142
+ token: value.token.trim(),
143
+ user: normalizeUser(value.user),
144
+ origin,
145
+ source,
146
+ updatedAt,
147
+ validatedAt,
148
+ accountId,
149
+ }
150
+ }
151
+
152
+ export function readSharedDesktopAuthSession(): SharedDesktopAuthSession | null {
153
+ const filepath = getSharedDesktopAuthFilePath()
154
+ if (!existsSync(filepath)) return null
155
+
156
+ try {
157
+ const parsed = JSON.parse(readFileSync(filepath, 'utf8')) as unknown
158
+ const normalized = normalizeSession(parsed)
159
+ if (!normalized) {
160
+ rmSync(filepath, { force: true })
161
+ return null
162
+ }
163
+ return normalized
164
+ } catch {
165
+ rmSync(filepath, { force: true })
166
+ return null
167
+ }
168
+ }
169
+
170
+ export function writeSharedDesktopAuthSession(
171
+ session: Omit<SharedDesktopAuthSession, 'version' | 'updatedAt'> & {
172
+ updatedAt?: string
173
+ },
174
+ ) {
175
+ const filepath = getSharedDesktopAuthFilePath()
176
+ mkdirSync(dirname(filepath), { recursive: true })
177
+ const normalized: SharedDesktopAuthSession = {
178
+ version: SESSION_VERSION,
179
+ token: session.token.trim(),
180
+ user: session.user ? normalizeUser(session.user) : null,
181
+ origin: session.origin?.trim() || DEFAULT_ORIGIN,
182
+ source: session.source,
183
+ updatedAt: session.updatedAt || new Date().toISOString(),
184
+ validatedAt: session.validatedAt,
185
+ accountId: session.accountId?.trim() || undefined,
186
+ }
187
+ writeFileSync(filepath, JSON.stringify(normalized, null, 2) + '\n')
188
+ try {
189
+ chmodSync(filepath, 0o600)
190
+ } catch {
191
+ // best-effort only
192
+ }
193
+ return normalized
194
+ }
195
+
196
+ export function clearSharedDesktopAuthSession() {
197
+ rmSync(getSharedDesktopAuthFilePath(), { force: true })
198
+ }
199
+
200
+ export async function refreshSharedDesktopAuthSession(
201
+ originOverride?: string,
202
+ ): Promise<SharedDesktopAuthSession | null> {
203
+ const current = readSharedDesktopAuthSession()
204
+ if (!current?.token) return null
205
+
206
+ const origin = originOverride || current.origin || DEFAULT_ORIGIN
207
+
208
+ try {
209
+ const res = await fetch(`${origin.replace(/\/$/, '')}/api/auth/me`, {
210
+ headers: { authorization: `Bearer ${current.token}` },
211
+ })
212
+ if (res.status === 401) {
213
+ clearSharedDesktopAuthSession()
214
+ return null
215
+ }
216
+ if (!res.ok) return current
217
+
218
+ const data = (await res.json()) as { user?: SharedDesktopAuthUser | null }
219
+ if (!data.user?.id) {
220
+ clearSharedDesktopAuthSession()
221
+ return null
222
+ }
223
+
224
+ return writeSharedDesktopAuthSession({
225
+ token: current.token,
226
+ user: data.user,
227
+ origin,
228
+ source: current.source,
229
+ validatedAt: new Date().toISOString(),
230
+ accountId: current.accountId,
231
+ })
232
+ } catch {
233
+ return current
234
+ }
235
+ }
@@ -0,0 +1,70 @@
1
+ const DEFAULT_CONTRAST_BACKEND_ORIGIN = 'https://contrast.dev'
2
+
3
+ export const LOOPBACK_PROBE_HOSTNAMES = ['127.0.0.1', '::1'] as const
4
+
5
+ export function normalizeHostname(hostname: string): string {
6
+ return hostname.replace(/^\[|\]$/g, '').toLowerCase()
7
+ }
8
+
9
+ export function buildLoopbackProbeBaseUrl(
10
+ protocol: 'http:' | 'ws:',
11
+ hostname: string,
12
+ port: number,
13
+ ): string {
14
+ const normalizedHostname = normalizeHostname(hostname)
15
+ const urlHost = normalizedHostname.includes(':')
16
+ ? `[${normalizedHostname}]`
17
+ : normalizedHostname
18
+ return `${protocol}//${urlHost}:${port}`
19
+ }
20
+
21
+ export function isLoopbackHost(hostname: string): boolean {
22
+ const normalized = normalizeHostname(hostname)
23
+ return (
24
+ normalized === 'localhost' ||
25
+ normalized.endsWith('.localhost') ||
26
+ normalized === '0.0.0.0' ||
27
+ normalized === '::1' ||
28
+ /^127(?:\.\d{1,3}){3}$/.test(normalized)
29
+ )
30
+ }
31
+
32
+ export function buildBrowserAssetProxyUrl(targetUrl: string): string {
33
+ const proxyPath = isLoopbackHost(new URL(targetUrl).hostname)
34
+ ? '/__bundle-proxy'
35
+ : '/__fetch-proxy'
36
+ return `${proxyPath}?url=${encodeURIComponent(targetUrl)}`
37
+ }
38
+
39
+ export function isContrastBackendHost(hostname: string): boolean {
40
+ const normalized = normalizeHostname(hostname)
41
+ return (
42
+ isLoopbackHost(normalized) ||
43
+ normalized === 'contrast.dev' ||
44
+ normalized.endsWith('.contrast.dev')
45
+ )
46
+ }
47
+
48
+ export function isContrastBackendOrigin(origin: string): boolean {
49
+ try {
50
+ return isContrastBackendHost(new URL(origin).hostname)
51
+ } catch {
52
+ return false
53
+ }
54
+ }
55
+
56
+ export function resolveContrastBackendOrigin(currentOrigin?: string | null): string {
57
+ const origin = currentOrigin?.trim()
58
+ if (!origin) return DEFAULT_CONTRAST_BACKEND_ORIGIN
59
+
60
+ try {
61
+ const parsed = new URL(origin)
62
+ if (isLoopbackHost(parsed.hostname) || isContrastBackendHost(parsed.hostname)) {
63
+ return parsed.origin
64
+ }
65
+ } catch {}
66
+
67
+ return DEFAULT_CONTRAST_BACKEND_ORIGIN
68
+ }
69
+
70
+ export { DEFAULT_CONTRAST_BACKEND_ORIGIN }
package/src/beta.ts ADDED
@@ -0,0 +1,19 @@
1
+ // public beta flag + copy. single source of truth for the badge that
2
+ // appears on the CLI banner, electron title, shell rail pill, and the
3
+ // download page. flip IS_BETA to false when 1.0 ships and the badge
4
+ // disappears everywhere automatically.
5
+ //
6
+ // see plans/sootsim-public-beta.md for the rollout plan.
7
+
8
+ export const IS_BETA = true
9
+
10
+ // short label — used on the rail pill, electron title, etc.
11
+ export const BETA_LABEL = 'public beta'
12
+
13
+ // one-line tagline — used in the CLI banner.
14
+ export const BETA_TAGLINE = BETA_LABEL
15
+
16
+ // what we ask for in return — kept here so the onboarding flow, the
17
+ // privacy page, and any future surface render the same words.
18
+ export const BETA_ASK_HEADLINE =
19
+ 'we send anonymous error and dependency data while you use it'
@@ -0,0 +1,175 @@
1
+ // canonical bridge port for sootsim's WebSocket connection between the CLI
2
+ // and the running engine. matches main — kept in a single module so CLI,
3
+ // dev plugin, and runtime agree on the same value.
4
+ export const DEFAULT_SOOTSIM_BRIDGE_PORT = 7668
5
+ export const DEFAULT_SOOTSIM_BRIDGE_URL = `http://localhost:${DEFAULT_SOOTSIM_BRIDGE_PORT}/`
6
+
7
+ function parsePort(value: unknown): number | null {
8
+ if (typeof value === 'number') {
9
+ return Number.isInteger(value) && value > 0 ? value : null
10
+ }
11
+ if (typeof value !== 'string') return null
12
+ const trimmed = value.trim()
13
+ if (!trimmed) return null
14
+ const parsed = Number(trimmed)
15
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : null
16
+ }
17
+
18
+ function parsePortOffset(value: unknown): number {
19
+ const parsed = parsePort(value)
20
+ return parsed === null ? 0 : parsed
21
+ }
22
+
23
+ export function resolveSootsimBridgePort(
24
+ input: {
25
+ explicitPort?: unknown
26
+ portOffset?: unknown
27
+ } = {},
28
+ ): number {
29
+ const explicitPort = parsePort(input.explicitPort)
30
+ if (explicitPort !== null) return explicitPort
31
+ return DEFAULT_SOOTSIM_BRIDGE_PORT + parsePortOffset(input.portOffset)
32
+ }
33
+
34
+ export function resolveSootsimBridgePortForHttpPort(input: {
35
+ explicitPort?: unknown
36
+ httpPort?: unknown
37
+ baseHttpPort: number
38
+ }): number {
39
+ const explicitPort = parsePort(input.explicitPort)
40
+ if (explicitPort !== null) return explicitPort
41
+ const httpPort = parsePort(input.httpPort)
42
+ const portOffset = httpPort === null ? 0 : Math.max(0, httpPort - input.baseHttpPort)
43
+ return resolveSootsimBridgePort({ portOffset })
44
+ }
45
+
46
+ const CONTRAST_DEV_HTTP_BASE_PORT = 3000
47
+ const SOOTSIM_SITE_DEV_HTTP_BASE_PORT = 3001
48
+ const SOOTSIM_SHELL_DEV_HTTP_BASE_PORT = 5173
49
+ const LOCAL_DEV_HTTP_BASE_PORTS = [
50
+ CONTRAST_DEV_HTTP_BASE_PORT,
51
+ SOOTSIM_SHELL_DEV_HTTP_BASE_PORT,
52
+ ]
53
+ const MAX_LOCAL_DEV_PORT_OFFSET = 2000
54
+
55
+ function parseHrefUrl(value: unknown): URL | null {
56
+ if (typeof value !== 'string' || !value.trim()) return null
57
+ try {
58
+ return new URL(value)
59
+ } catch {
60
+ return null
61
+ }
62
+ }
63
+
64
+ function closestLocalDevBaseHttpPort(httpPort: number): number | null {
65
+ let closest: { basePort: number; offset: number } | null = null
66
+ for (const basePort of LOCAL_DEV_HTTP_BASE_PORTS) {
67
+ const offset = httpPort - basePort
68
+ if (offset < 0 || offset > MAX_LOCAL_DEV_PORT_OFFSET) continue
69
+ if (!closest || offset < closest.offset) closest = { basePort, offset }
70
+ }
71
+ return closest?.basePort ?? null
72
+ }
73
+
74
+ /** the vite dev shell that shares a world with a bridge port. the two are one
75
+ * offset apart by construction (bridge 7668 + N, shell 5173 + N), so a caller
76
+ * that pinned an explicit bridge port can name its own shell directly. that
77
+ * keeps explicit ports independent from default-world selection and lets a
78
+ * page register on the exact bridge its caller will poll. null when the port
79
+ * is not a local dev bridge port. */
80
+ export function resolveSootsimShellBaseUrlForBridgePort(
81
+ bridgePort: number,
82
+ ): string | null {
83
+ const offset = bridgePort - DEFAULT_SOOTSIM_BRIDGE_PORT
84
+ if (offset < 0 || offset > MAX_LOCAL_DEV_PORT_OFFSET) return null
85
+ return `http://localhost:${SOOTSIM_SHELL_DEV_HTTP_BASE_PORT + offset}/`
86
+ }
87
+
88
+ export function resolveSootsimBridgePortForRuntimeLocation(
89
+ input: {
90
+ explicitPort?: unknown
91
+ href?: unknown
92
+ httpPort?: unknown
93
+ } = {},
94
+ ): number {
95
+ const explicitPort = parsePort(input.explicitPort)
96
+ if (explicitPort !== null) return explicitPort
97
+ const hrefUrl = parseHrefUrl(input.href)
98
+ const httpPort = parsePort(input.httpPort) ?? parsePort(hrefUrl?.port)
99
+ if (httpPort === null) return DEFAULT_SOOTSIM_BRIDGE_PORT
100
+ if (
101
+ hrefUrl?.pathname.startsWith('/sootsim') &&
102
+ (hrefUrl.searchParams.has('embed') || hrefUrl.searchParams.has('nativePreview'))
103
+ ) {
104
+ return resolveSootsimBridgePortForHttpPort({
105
+ httpPort,
106
+ baseHttpPort: CONTRAST_DEV_HTTP_BASE_PORT,
107
+ })
108
+ }
109
+ if (
110
+ hrefUrl?.pathname.startsWith('/sootsim') &&
111
+ httpPort >= SOOTSIM_SITE_DEV_HTTP_BASE_PORT &&
112
+ httpPort <= SOOTSIM_SITE_DEV_HTTP_BASE_PORT + MAX_LOCAL_DEV_PORT_OFFSET
113
+ ) {
114
+ return resolveSootsimBridgePortForHttpPort({
115
+ httpPort,
116
+ baseHttpPort: SOOTSIM_SITE_DEV_HTTP_BASE_PORT,
117
+ })
118
+ }
119
+ const baseHttpPort = closestLocalDevBaseHttpPort(httpPort)
120
+ if (baseHttpPort === null) return DEFAULT_SOOTSIM_BRIDGE_PORT
121
+ return resolveSootsimBridgePortForHttpPort({ httpPort, baseHttpPort })
122
+ }
123
+
124
+ export function resolveSootsimWebSocketProxyUrl(input: {
125
+ targetUrl: string
126
+ href: string
127
+ }): string {
128
+ const pageUrl = parseHrefUrl(input.href)
129
+ if (!pageUrl || (pageUrl.protocol !== 'http:' && pageUrl.protocol !== 'https:')) {
130
+ throw new Error(`invalid rnx websocket proxy page URL: ${input.href}`)
131
+ }
132
+ let targetUrl: URL
133
+ try {
134
+ targetUrl = new URL(input.targetUrl)
135
+ } catch {
136
+ throw new Error(`invalid rnx websocket proxy target URL: ${input.targetUrl}`)
137
+ }
138
+ if (targetUrl.protocol !== 'ws:' && targetUrl.protocol !== 'wss:') {
139
+ throw new Error(`invalid rnx websocket proxy target URL: ${input.targetUrl}`)
140
+ }
141
+ const proxyUrl = new URL('/__websocket-proxy', pageUrl)
142
+ proxyUrl.protocol = pageUrl.protocol === 'https:' ? 'wss:' : 'ws:'
143
+ proxyUrl.searchParams.set('url', input.targetUrl)
144
+ return proxyUrl.href
145
+ }
146
+
147
+ export function resolveSootsimBridgeWebSocketUrl(input: {
148
+ bridgePort: number
149
+ href: string
150
+ }): string {
151
+ // 127.0.0.1, not localhost. the daemon binds ipv4 only (bridge-host bindOnce)
152
+ // so that localhost's ::1/127.0.0.1 coinflip cannot hide it; the health probe
153
+ // already talks to 127.0.0.1. targeting localhost here undoes that: when node
154
+ // tries ::1 first and does not fall back, vite's proxy answers 502 and the
155
+ // browser logs a console error that fails e2e.
156
+ return resolveSootsimWebSocketProxyUrl({
157
+ targetUrl: `ws://127.0.0.1:${input.bridgePort}`,
158
+ href: input.href,
159
+ })
160
+ }
161
+
162
+ export function isSootsimBridgeHealth(value: unknown, expectedPort: number): boolean {
163
+ return (
164
+ typeof value === 'object' &&
165
+ value !== null &&
166
+ Reflect.get(value, 'ok') === true &&
167
+ Reflect.get(value, 'bridgePort') === expectedPort
168
+ )
169
+ }
170
+
171
+ // private websocket close code used when the daemon intentionally closes a
172
+ // sim. browser clients treat it as terminal and skip their normal reconnect
173
+ // loop; playwright-owned sims use it to close their hosting context.
174
+ export const SOOTSIM_BRIDGE_SIM_CLOSE_CODE = 4001
175
+ export const SOOTSIM_BRIDGE_SIM_CLOSE_REASON = 'sootsim close'