sootsim 0.1.82 → 0.1.84

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 (209) hide show
  1. package/README.md +0 -1
  2. package/detox/colors.ts +54 -0
  3. package/detox/config-loader.ts +135 -0
  4. package/detox/expectations.ts +477 -0
  5. package/detox/gestures.ts +442 -0
  6. package/detox/index.ts +1436 -0
  7. package/detox/jest-environment.ts +86 -0
  8. package/detox/jest-preset.cjs +50 -0
  9. package/detox/matchers.ts +29 -0
  10. package/detox/navigation.ts +43 -0
  11. package/detox/run-test.ts +113 -0
  12. package/detox/screenshots/animated-color-test-rest-norngh.png +0 -0
  13. package/detox/screenshots/color-test-after-drag-norngh.png +0 -0
  14. package/detox/screenshots/color-test-rest-norngh.png +0 -0
  15. package/detox/screenshots/theme-blue-toggle.png +0 -0
  16. package/detox/screenshots/theme-blue.png +0 -0
  17. package/detox/screenshots/theme-red-toggle.png +0 -0
  18. package/detox/screenshots/theme-red.png +0 -0
  19. package/dist-cli/bin.js +3 -3
  20. package/dist-cli/chunks/{agent-3C6Z6YXA.js → agent-2CWD6W6P.js} +2 -2
  21. package/dist-cli/chunks/{agent-wrapper-7Z4UFACX.js → agent-wrapper-5W3LOX6S.js} +2 -2
  22. package/dist-cli/chunks/{assert-XYBIZRDK.js → assert-ZOMAMKRT.js} +2 -2
  23. package/dist-cli/chunks/auto-bootstrap-NYYSMTIM.js +2 -0
  24. package/dist-cli/chunks/beta-4K2SQACK.js +2 -0
  25. package/dist-cli/chunks/chunk-3HXQ7MJK.js +79 -0
  26. package/dist-cli/chunks/{chunk-EJGEDUOC.js → chunk-4K7BH2D4.js} +3 -3
  27. package/dist-cli/chunks/{chunk-2EFQQWEC.js → chunk-4OPRODFA.js} +2 -2
  28. package/dist-cli/chunks/{chunk-Z6G5SDG7.js → chunk-4OWVPRZV.js} +2 -2
  29. package/dist-cli/chunks/{chunk-DCFGNIJC.js → chunk-5XCXOLG2.js} +2 -2
  30. package/dist-cli/chunks/chunk-67ZZ2CM5.js +1 -0
  31. package/dist-cli/chunks/{chunk-M3OULYY3.js → chunk-73UZXB4B.js} +2 -2
  32. package/dist-cli/chunks/{chunk-QPDWMYCA.js → chunk-7NWNTUJF.js} +1 -1
  33. package/dist-cli/chunks/chunk-7YHDJLO2.js +119 -0
  34. package/dist-cli/chunks/{chunk-EX6IOT23.js → chunk-AJVTY6KY.js} +2 -2
  35. package/dist-cli/chunks/chunk-AWSQUOAS.js +67 -0
  36. package/dist-cli/chunks/{chunk-JVNGH5S7.js → chunk-BCBNVJVG.js} +1 -1
  37. package/dist-cli/chunks/{chunk-WZLKUS54.js → chunk-BKBL6K2G.js} +1 -1
  38. package/dist-cli/chunks/{chunk-DSYW2NOW.js → chunk-C3DPQZ4J.js} +2 -2
  39. package/dist-cli/chunks/chunk-D3ZSBIIY.js +2 -0
  40. package/dist-cli/chunks/{chunk-PYDAVGCZ.js → chunk-D4HUVLZR.js} +1 -1
  41. package/dist-cli/chunks/{chunk-H6NBDJIO.js → chunk-DUUSJDES.js} +1 -1
  42. package/dist-cli/chunks/{chunk-BVXP2GDN.js → chunk-ELJLF4SG.js} +3 -3
  43. package/dist-cli/chunks/{chunk-TR7NIFSL.js → chunk-EQ7TFQ2F.js} +1 -1
  44. package/dist-cli/chunks/{chunk-UOWBKSSI.js → chunk-EQCKGC4B.js} +1 -1
  45. package/dist-cli/chunks/chunk-FUCGLWNN.js +1 -0
  46. package/dist-cli/chunks/{chunk-BISEHRNE.js → chunk-HYPJW65U.js} +2 -2
  47. package/dist-cli/chunks/chunk-IILJQCZA.js +2 -0
  48. package/dist-cli/chunks/{chunk-2XULSYS6.js → chunk-KU6MSPAH.js} +2 -2
  49. package/dist-cli/chunks/{chunk-QTJJHBCI.js → chunk-OOOR7NT2.js} +1 -1
  50. package/dist-cli/chunks/{chunk-U3XCDQRH.js → chunk-P7WDNKOS.js} +3 -3
  51. package/dist-cli/chunks/{chunk-C7JOLDDQ.js → chunk-PPKKA5VW.js} +2 -2
  52. package/dist-cli/chunks/{chunk-JUCV3VHM.js → chunk-PS2G44GT.js} +2 -2
  53. package/dist-cli/chunks/{chunk-PO64TMRT.js → chunk-QMSJR5R2.js} +2 -2
  54. package/dist-cli/chunks/{chunk-4QUAOBUB.js → chunk-RF4R2U46.js} +2 -2
  55. package/dist-cli/chunks/{chunk-D3SM2JYB.js → chunk-RIXUH3NK.js} +2 -2
  56. package/dist-cli/chunks/{chunk-2JQIKL3B.js → chunk-SFGUPL2X.js} +2 -2
  57. package/dist-cli/chunks/{chunk-GI5MF6LP.js → chunk-SQX5CAYG.js} +1 -1
  58. package/dist-cli/chunks/{chunk-Q4JNA5VO.js → chunk-SQZAC7C4.js} +1 -1
  59. package/dist-cli/chunks/{chunk-M4ERVRM4.js → chunk-SV7FOGJ3.js} +2 -2
  60. package/dist-cli/chunks/{chunk-ZN2C7V5R.js → chunk-TK3OJSEO.js} +2 -2
  61. package/dist-cli/chunks/{chunk-7SCQEPXK.js → chunk-TL7SIZ7S.js} +1 -1
  62. package/dist-cli/chunks/{chunk-IZ2OO47Y.js → chunk-V2GQ4WXJ.js} +2 -2
  63. package/dist-cli/chunks/{chunk-JUDJXJSE.js → chunk-VH7F45CN.js} +1 -1
  64. package/dist-cli/chunks/chunk-WNVNU2OW.js +4 -0
  65. package/dist-cli/chunks/{chunk-O3AOQP3V.js → chunk-XQ2OBHBE.js} +2 -2
  66. package/dist-cli/chunks/{chunk-MQXYJTXM.js → chunk-YCIA4BHJ.js} +2 -2
  67. package/dist-cli/chunks/chunk-ZSMMJMPA.js +1 -0
  68. package/dist-cli/chunks/cli-version-QB4VH24H.js +2 -0
  69. package/dist-cli/chunks/{compat-2DVSCCR7.js → compat-FWSEEGEH.js} +3 -3
  70. package/dist-cli/chunks/{config-YDX4Q4XM.js → config-CYI2WAGP.js} +2 -2
  71. package/dist-cli/chunks/control-UXY7YQVX.js +2 -0
  72. package/dist-cli/chunks/{cpu-profile-GU62WVZZ.js → cpu-profile-IKAE3KTY.js} +2 -2
  73. package/dist-cli/chunks/{daemon-V5NLDTSB.js → daemon-ZUMF53YB.js} +2 -2
  74. package/dist-cli/chunks/{debug-HAOCONNB.js → debug-P6KULKKS.js} +3 -3
  75. package/dist-cli/chunks/{detox-YLC4DLXB.js → detox-SPWAZCYG.js} +2 -2
  76. package/dist-cli/chunks/{device-ZQ4DN4H6.js → device-JWEPK6I2.js} +2 -2
  77. package/dist-cli/chunks/{diagnose-HNUO3Z5F.js → diagnose-IZODTXV2.js} +2 -2
  78. package/dist-cli/chunks/drivers-MK6WJKBC.js +2 -0
  79. package/dist-cli/chunks/{electron-ZAASAHSW.js → electron-R5GP6RVB.js} +3 -3
  80. package/dist-cli/chunks/flow-6O4GEOPJ.js +2 -0
  81. package/dist-cli/chunks/{hints-BA3GE5W5.js → hints-DYDNYX7N.js} +2 -2
  82. package/dist-cli/chunks/{home-paths-MQXRHBTW.js → home-paths-GLMX5OKL.js} +2 -2
  83. package/dist-cli/chunks/{inspect-T4RMS5KX.js → inspect-FJOPCTY2.js} +3 -3
  84. package/dist-cli/chunks/install-A3TUGGHN.js +2 -0
  85. package/dist-cli/chunks/{install-desktop-MH26VONS.js → install-desktop-YPJZMZM5.js} +3 -3
  86. package/dist-cli/chunks/{keys-IELIDRGB.js → keys-GSYPHWNY.js} +2 -2
  87. package/dist-cli/chunks/{launch-VMT3OWOB.js → launch-4G2PKW5X.js} +3 -3
  88. package/dist-cli/chunks/{login-VZBANVLU.js → login-KJQGHA64.js} +4 -4
  89. package/dist-cli/chunks/{logout-GWXBTQ4H.js → logout-XM2SYH5C.js} +2 -2
  90. package/dist-cli/chunks/{maestro-JYHR4HFR.js → maestro-EOWGI7DG.js} +2 -2
  91. package/dist-cli/chunks/{preview-RPZ4UQ2B.js → preview-F73TKK37.js} +2 -2
  92. package/dist-cli/chunks/{profile-7FLDF2AP.js → profile-22FDKBUO.js} +2 -2
  93. package/dist-cli/chunks/{react-3RC4CNDZ.js → react-5L6VPFUP.js} +2 -2
  94. package/dist-cli/chunks/record-JZXCQ4IN.js +70 -0
  95. package/dist-cli/chunks/runtime-EEBX7CFV.js +2 -0
  96. package/dist-cli/chunks/{runtime-delivery-Z7I2KIRB.js → runtime-delivery-LXUM3R4A.js} +2 -2
  97. package/dist-cli/chunks/{screenshot-GRCZ6AM4.js → screenshot-HDRRG33Q.js} +2 -2
  98. package/dist-cli/chunks/{screenshot-mode-E4YHXHH5.js → screenshot-mode-WY63LZIX.js} +2 -2
  99. package/dist-cli/chunks/{screenshots-7SXMX2AY.js → screenshots-MPV2ENL5.js} +2 -2
  100. package/dist-cli/chunks/{server-GDJ2TCRV.js → server-5LBMCJ3G.js} +2 -2
  101. package/dist-cli/chunks/setup-repo-SZSYNKNI.js +2 -0
  102. package/dist-cli/chunks/{skills-62E7NDRC.js → skills-BQ73YOBF.js} +2 -2
  103. package/dist-cli/chunks/{start-Y7KR5ZQ3.js → start-2WU4W6ZU.js} +4 -4
  104. package/dist-cli/chunks/store-RE45SUBF.js +2 -0
  105. package/dist-cli/chunks/telemetry-DG6GJLCP.js +2 -0
  106. package/dist-cli/chunks/{test-IYMSUPVC.js → test-OVO4CQTG.js} +3 -3
  107. package/dist-cli/chunks/{three-mode-QKKXCCC2.js → three-mode-BKM3KFM7.js} +2 -2
  108. package/dist-cli/chunks/{timeline-PF6NQ7RT.js → timeline-MDXGEDQL.js} +2 -2
  109. package/dist-cli/chunks/{upgrade-CE2Y3TAN.js → upgrade-JGQABWVF.js} +2 -2
  110. package/dist-cli/chunks/upload-UJNUA4ZV.js +2 -0
  111. package/dist-cli/chunks/{web-XEO3ZCPF.js → web-WYFAYQ72.js} +2 -2
  112. package/dist-cli/chunks/{what-happened-372J7YF7.js → what-happened-PZW2KW6A.js} +2 -2
  113. package/dist-cli/chunks/{whoami-B4E7KCT5.js → whoami-7ATWJQS6.js} +2 -2
  114. package/dist-lib/agent-daemon-client.cjs +1 -1
  115. package/dist-lib/agent-events.cjs +1 -1
  116. package/dist-lib/agent-sessions.cjs +1 -1
  117. package/dist-lib/attached-projects.cjs +1 -1
  118. package/dist-lib/auth/shared-session.cjs +1 -1
  119. package/dist-lib/backend-origin.cjs +1 -1
  120. package/dist-lib/beta.cjs +44 -0
  121. package/dist-lib/bridge-constants.cjs +1 -1
  122. package/dist-lib/cli-constants.cjs +1 -1
  123. package/dist-lib/config.cjs +1 -1
  124. package/dist-lib/detox/index.cjs +1770 -0
  125. package/dist-lib/detox/jest-preset.cjs +50 -0
  126. package/dist-lib/dev-bundle-resolution.cjs +1 -1
  127. package/dist-lib/home-paths.cjs +1 -1
  128. package/dist-lib/host/bridge-host.cjs +1 -1
  129. package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
  130. package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
  131. package/dist-lib/index.cjs +1 -1
  132. package/dist-lib/metro.cjs +1 -1
  133. package/dist-lib/profiles.cjs +1 -1
  134. package/dist-lib/render-mode.cjs +1 -1
  135. package/dist-lib/scripts/demo-app-registry.cjs +809 -0
  136. package/dist-lib/scripts/dev-server-scanner.cjs +1269 -0
  137. package/dist-lib/skills.cjs +8322 -0
  138. package/dist-lib/vite-base.cjs +3 -3
  139. package/dist-lib/vite.cjs +1 -1
  140. package/package.json +39 -10
  141. package/scripts/demo-app-registry.ts +989 -0
  142. package/scripts/dev-server-scanner.ts +674 -0
  143. package/src/agent-daemon-client.ts +390 -0
  144. package/src/agent-events.ts +71 -0
  145. package/src/agent-prompt.ts +71 -0
  146. package/src/agent-sessions.ts +572 -0
  147. package/src/attached-projects.ts +536 -0
  148. package/src/auth/shared-session.ts +199 -0
  149. package/src/backend-origin.ts +49 -0
  150. package/src/beta.ts +21 -0
  151. package/src/bridge-constants.ts +10 -0
  152. package/src/cli-constants.ts +1 -0
  153. package/src/cli-version.ts +30 -0
  154. package/src/codex-client.ts +215 -0
  155. package/src/config.ts +110 -0
  156. package/src/dev-bundle-resolution.ts +180 -0
  157. package/src/home-paths.ts +382 -0
  158. package/src/host/agent-host.ts +576 -0
  159. package/src/host/bridge-host.ts +2293 -0
  160. package/src/host/fetch-proxy-handler.ts +288 -0
  161. package/src/host/fetch-proxy-overrides.ts +39 -0
  162. package/src/host/open-url.ts +234 -0
  163. package/src/index.ts +9 -0
  164. package/src/metro-plugin.ts +207 -0
  165. package/src/native-dev-bundle-url.ts +62 -0
  166. package/src/native-seam-manifest.ts +313 -0
  167. package/src/profiles.ts +179 -0
  168. package/src/render-mode.ts +27 -0
  169. package/src/runtime-delivery.ts +334 -0
  170. package/src/screenshots/compose.ts +422 -0
  171. package/src/screenshots/frame-compose.ts +438 -0
  172. package/src/screenshots/orchestrate.ts +244 -0
  173. package/src/screenshots/registry.ts +58 -0
  174. package/src/screenshots/schema.ts +364 -0
  175. package/src/skills/builtin/a11y-review.ts +126 -0
  176. package/src/skills/builtin/compat-check.ts +104 -0
  177. package/src/skills/builtin/perf-profile.ts +84 -0
  178. package/src/skills/builtin/screenshot-all.ts +46 -0
  179. package/src/skills/builtin/test-flow.ts +118 -0
  180. package/src/skills/builtin/visual-diff.ts +94 -0
  181. package/src/skills/registry.ts +107 -0
  182. package/src/skills/types.ts +41 -0
  183. package/src/vite-plugin-one.ts +187 -0
  184. package/src/vite-plugin.ts +1381 -0
  185. package/src/worklets-babel.ts +132 -0
  186. package/dist-cli/chunks/auto-bootstrap-D2EQVL7R.js +0 -2
  187. package/dist-cli/chunks/beta-VHPXECZY.js +0 -2
  188. package/dist-cli/chunks/chunk-27HBWBE6.js +0 -4
  189. package/dist-cli/chunks/chunk-2W5C5J4O.js +0 -64
  190. package/dist-cli/chunks/chunk-3OH4VCJA.js +0 -1
  191. package/dist-cli/chunks/chunk-45HLFQRI.js +0 -2
  192. package/dist-cli/chunks/chunk-7YLCK5HG.js +0 -5
  193. package/dist-cli/chunks/chunk-BRDUKIZI.js +0 -119
  194. package/dist-cli/chunks/chunk-GADW2Q5S.js +0 -1
  195. package/dist-cli/chunks/chunk-HST43CVE.js +0 -2
  196. package/dist-cli/chunks/chunk-QJBQOGTK.js +0 -73
  197. package/dist-cli/chunks/chunk-V26REV7G.js +0 -1
  198. package/dist-cli/chunks/cli-version-WF7T6IKI.js +0 -2
  199. package/dist-cli/chunks/control-EAK2OPGB.js +0 -2
  200. package/dist-cli/chunks/demo-app-registry-52A2MI72.js +0 -2
  201. package/dist-cli/chunks/drivers-B4QPIZ4B.js +0 -2
  202. package/dist-cli/chunks/flow-PFLHFNVM.js +0 -2
  203. package/dist-cli/chunks/install-ZCPEMK6U.js +0 -2
  204. package/dist-cli/chunks/record-CZ33G5FT.js +0 -70
  205. package/dist-cli/chunks/runtime-AZKHZHJ4.js +0 -2
  206. package/dist-cli/chunks/setup-repo-3Y2QAZRK.js +0 -2
  207. package/dist-cli/chunks/store-TDTFZMGA.js +0 -2
  208. package/dist-cli/chunks/telemetry-G3NIU5NP.js +0 -2
  209. package/dist-cli/chunks/upload-CLWFS7IL.js +0 -2
@@ -0,0 +1,199 @@
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'
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
+ teams?: SharedDesktopAuthTeamSummary[]
25
+ }
26
+
27
+ export type SharedDesktopAuthSession = {
28
+ version: 1
29
+ token: string
30
+ user: SharedDesktopAuthUser | null
31
+ origin: string
32
+ source: 'cli' | 'electron' | 'browser' | 'unknown'
33
+ updatedAt: string
34
+ validatedAt?: string
35
+ }
36
+
37
+ const SESSION_VERSION = 1 as const
38
+ const SESSION_FILE_ENV = 'SOOTSIM_SHARED_AUTH_FILE'
39
+ const DEFAULT_ORIGIN = 'https://sootbean.com'
40
+
41
+ function getBaseDir() {
42
+ const explicit = process.env[SESSION_FILE_ENV]
43
+ if (explicit?.trim()) {
44
+ return dirname(resolve(explicit))
45
+ }
46
+ return electronUserDataDir()
47
+ }
48
+
49
+ export function getSharedDesktopAuthFilePath() {
50
+ const explicit = process.env[SESSION_FILE_ENV]
51
+ if (explicit?.trim()) return resolve(explicit)
52
+ return join(getBaseDir(), 'desktop-auth.json')
53
+ }
54
+
55
+ function normalizeUser(input: unknown): SharedDesktopAuthUser | null {
56
+ if (!input || typeof input !== 'object') return null
57
+ const value = input as Record<string, unknown>
58
+ if (typeof value.id !== 'string' || !value.id.trim()) return null
59
+ return {
60
+ id: value.id.trim(),
61
+ name: typeof value.name === 'string' ? value.name : undefined,
62
+ email: typeof value.email === 'string' ? value.email : undefined,
63
+ image: typeof value.image === 'string' ? value.image : undefined,
64
+ teams: Array.isArray(value.teams)
65
+ ? value.teams
66
+ .map((team): SharedDesktopAuthTeamSummary | null => {
67
+ if (!team || typeof team !== 'object') return null
68
+ const t = team as Record<string, unknown>
69
+ if (typeof t.id !== 'string' || typeof t.name !== 'string') return null
70
+ return {
71
+ id: t.id,
72
+ name: t.name,
73
+ role: t.role === 'owner' ? 'owner' : 'member',
74
+ githubOrg: typeof t.githubOrg === 'string' ? t.githubOrg : null,
75
+ }
76
+ })
77
+ .filter((team): team is SharedDesktopAuthTeamSummary => !!team)
78
+ : undefined,
79
+ }
80
+ }
81
+
82
+ function normalizeSession(input: unknown): SharedDesktopAuthSession | null {
83
+ if (!input || typeof input !== 'object') return null
84
+ const value = input as Record<string, unknown>
85
+ if (value.version !== SESSION_VERSION) return null
86
+ if (typeof value.token !== 'string' || !value.token.trim()) return null
87
+ const origin =
88
+ typeof value.origin === 'string' && value.origin.trim()
89
+ ? value.origin.trim()
90
+ : DEFAULT_ORIGIN
91
+ const source =
92
+ value.source === 'cli' ||
93
+ value.source === 'electron' ||
94
+ value.source === 'browser' ||
95
+ value.source === 'unknown'
96
+ ? value.source
97
+ : 'unknown'
98
+ const updatedAt =
99
+ typeof value.updatedAt === 'string' && value.updatedAt
100
+ ? value.updatedAt
101
+ : new Date().toISOString()
102
+ const validatedAt =
103
+ typeof value.validatedAt === 'string' && value.validatedAt
104
+ ? value.validatedAt
105
+ : undefined
106
+
107
+ return {
108
+ version: SESSION_VERSION,
109
+ token: value.token.trim(),
110
+ user: normalizeUser(value.user),
111
+ origin,
112
+ source,
113
+ updatedAt,
114
+ validatedAt,
115
+ }
116
+ }
117
+
118
+ export function readSharedDesktopAuthSession(): SharedDesktopAuthSession | null {
119
+ const filepath = getSharedDesktopAuthFilePath()
120
+ if (!existsSync(filepath)) return null
121
+
122
+ try {
123
+ const parsed = JSON.parse(readFileSync(filepath, 'utf8')) as unknown
124
+ const normalized = normalizeSession(parsed)
125
+ if (!normalized) {
126
+ rmSync(filepath, { force: true })
127
+ return null
128
+ }
129
+ return normalized
130
+ } catch {
131
+ rmSync(filepath, { force: true })
132
+ return null
133
+ }
134
+ }
135
+
136
+ export function writeSharedDesktopAuthSession(
137
+ session: Omit<SharedDesktopAuthSession, 'version' | 'updatedAt'> & {
138
+ updatedAt?: string
139
+ },
140
+ ) {
141
+ const filepath = getSharedDesktopAuthFilePath()
142
+ mkdirSync(dirname(filepath), { recursive: true })
143
+ const normalized: SharedDesktopAuthSession = {
144
+ version: SESSION_VERSION,
145
+ token: session.token.trim(),
146
+ user: session.user ? normalizeUser(session.user) : null,
147
+ origin: session.origin?.trim() || DEFAULT_ORIGIN,
148
+ source: session.source,
149
+ updatedAt: session.updatedAt || new Date().toISOString(),
150
+ validatedAt: session.validatedAt,
151
+ }
152
+ writeFileSync(filepath, JSON.stringify(normalized, null, 2) + '\n')
153
+ try {
154
+ chmodSync(filepath, 0o600)
155
+ } catch {
156
+ // best-effort only
157
+ }
158
+ return normalized
159
+ }
160
+
161
+ export function clearSharedDesktopAuthSession() {
162
+ rmSync(getSharedDesktopAuthFilePath(), { force: true })
163
+ }
164
+
165
+ export async function refreshSharedDesktopAuthSession(
166
+ originOverride?: string,
167
+ ): Promise<SharedDesktopAuthSession | null> {
168
+ const current = readSharedDesktopAuthSession()
169
+ if (!current?.token) return null
170
+
171
+ const origin = originOverride || current.origin || DEFAULT_ORIGIN
172
+
173
+ try {
174
+ const res = await fetch(`${origin.replace(/\/$/, '')}/api/auth/me`, {
175
+ headers: { authorization: `Bearer ${current.token}` },
176
+ })
177
+ if (res.status === 401) {
178
+ clearSharedDesktopAuthSession()
179
+ return null
180
+ }
181
+ if (!res.ok) return current
182
+
183
+ const data = (await res.json()) as { user?: SharedDesktopAuthUser | null }
184
+ if (!data.user?.id) {
185
+ clearSharedDesktopAuthSession()
186
+ return null
187
+ }
188
+
189
+ return writeSharedDesktopAuthSession({
190
+ token: current.token,
191
+ user: data.user,
192
+ origin,
193
+ source: current.source,
194
+ validatedAt: new Date().toISOString(),
195
+ })
196
+ } catch {
197
+ return current
198
+ }
199
+ }
@@ -0,0 +1,49 @@
1
+ const DEFAULT_SOOT_BACKEND_ORIGIN = 'https://sootbean.com'
2
+
3
+ function normalizeHostname(hostname: string): string {
4
+ return hostname.replace(/^\[|\]$/g, '').toLowerCase()
5
+ }
6
+
7
+ export function isLoopbackHost(hostname: string): boolean {
8
+ const normalized = normalizeHostname(hostname)
9
+ return (
10
+ normalized === 'localhost' ||
11
+ normalized.endsWith('.localhost') ||
12
+ normalized === '0.0.0.0' ||
13
+ normalized === '::1' ||
14
+ /^127(?:\.\d{1,3}){3}$/.test(normalized)
15
+ )
16
+ }
17
+
18
+ export function isSootBackendHost(hostname: string): boolean {
19
+ const normalized = normalizeHostname(hostname)
20
+ return (
21
+ isLoopbackHost(normalized) ||
22
+ normalized === 'sootbean.com' ||
23
+ normalized.endsWith('.sootbean.com')
24
+ )
25
+ }
26
+
27
+ export function isSootBackendOrigin(origin: string): boolean {
28
+ try {
29
+ return isSootBackendHost(new URL(origin).hostname)
30
+ } catch {
31
+ return false
32
+ }
33
+ }
34
+
35
+ export function resolveSootBackendOrigin(currentOrigin?: string | null): string {
36
+ const origin = currentOrigin?.trim()
37
+ if (!origin) return DEFAULT_SOOT_BACKEND_ORIGIN
38
+
39
+ try {
40
+ const parsed = new URL(origin)
41
+ if (isLoopbackHost(parsed.hostname) || isSootBackendHost(parsed.hostname)) {
42
+ return parsed.origin
43
+ }
44
+ } catch {}
45
+
46
+ return DEFAULT_SOOT_BACKEND_ORIGIN
47
+ }
48
+
49
+ export { DEFAULT_SOOT_BACKEND_ORIGIN }
package/src/beta.ts ADDED
@@ -0,0 +1,21 @@
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
+ export const BETA_VERSION_TARGET = '1.0.0'
11
+
12
+ // short label — used on the rail pill, electron title, etc.
13
+ export const BETA_LABEL = 'public beta'
14
+
15
+ // one-line tagline — used in the CLI banner + privacy disclosures.
16
+ export const BETA_TAGLINE = `${BETA_LABEL} · free until ${BETA_VERSION_TARGET}`
17
+
18
+ // what we ask for in return — kept here so the onboarding flow, the
19
+ // privacy page, and any future surface render the same words.
20
+ export const BETA_ASK_HEADLINE =
21
+ 'free for everyone until 1.0 — we send anonymous error and dependency data while you use it'
@@ -0,0 +1,10 @@
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
+
6
+ // private websocket close code used when the daemon intentionally closes a
7
+ // sim. browser clients treat it as terminal and skip their normal reconnect
8
+ // loop; playwright-owned sims use it to close their hosting context.
9
+ export const SOOTSIM_BRIDGE_SIM_CLOSE_CODE = 4001
10
+ export const SOOTSIM_BRIDGE_SIM_CLOSE_REASON = 'sootsim close'
@@ -0,0 +1 @@
1
+ export const DEFAULT_SOOTSIM_SHELL_URL = 'http://localhost:5173/'
@@ -0,0 +1,30 @@
1
+ // sootsim CLI version — the `sootsim` npm package version. resolved once
2
+ // and cached. shared by `--version`, `--help`, the `upgrade` command, the
3
+ // startup flow, and the bridge-host (which injects it into the served
4
+ // runtime html as window.__sootsimCliVersion).
5
+
6
+ import { readFileSync } from 'node:fs'
7
+ import { fileURLToPath } from 'node:url'
8
+
9
+ let cached: string | null = null
10
+
11
+ export function getCliVersion(): string {
12
+ if (cached != null) return cached
13
+ // prefer package resolution (works for npm/global installs); fall back to
14
+ // the package.json next to this module's source for repo / bundled runs.
15
+ const candidates: Array<() => string> = [
16
+ () => fileURLToPath(import.meta.resolve('sootsim/package.json')),
17
+ () => fileURLToPath(new URL('../package.json', import.meta.url)),
18
+ ]
19
+ for (const resolve of candidates) {
20
+ try {
21
+ const version = JSON.parse(readFileSync(resolve(), 'utf8')).version
22
+ if (typeof version === 'string' && version) {
23
+ cached = version
24
+ return cached
25
+ }
26
+ } catch {}
27
+ }
28
+ cached = '0.0.0'
29
+ return cached
30
+ }
@@ -0,0 +1,215 @@
1
+ // minimal client for `codex app-server` — a long-lived child process that
2
+ // speaks line-delimited JSON-RPC 2.0 over stdio.
3
+ //
4
+ // scope: just the subset sootsim needs for the attached-projects agent
5
+ // wrapper. each connection owns exactly one spawned app-server child; caller
6
+ // is responsible for lifecycle. bidirectional: client can issue requests +
7
+ // receive server-initiated notifications. we treat every method we don't
8
+ // recognize as an observable event that we pass straight to the handler map.
9
+
10
+ import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process'
11
+ import readline from 'node:readline'
12
+
13
+ export interface CodexClientOptions {
14
+ /** resolved path to the codex binary (via `which codex` or user override). */
15
+ bin: string
16
+ /** working directory for the spawned process. defaults to process.cwd(). */
17
+ cwd?: string
18
+ /** extra environment overrides forwarded to the child. */
19
+ env?: Record<string, string | undefined>
20
+ }
21
+
22
+ export type CodexNotificationHandler = (params: unknown) => void
23
+
24
+ export interface CodexClient {
25
+ /** promise that resolves when the child exits. use to await shutdown. */
26
+ readonly exited: Promise<{ code: number | null; signal: NodeJS.Signals | null }>
27
+ /** register a handler for a server-sent notification method. returns
28
+ * a dispose fn. multiple handlers per method are allowed. */
29
+ on(method: string, handler: CodexNotificationHandler): () => void
30
+ /** send a JSON-RPC request and await its response. rejects on error
31
+ * response or child death. */
32
+ request<TResult = unknown>(method: string, params?: unknown): Promise<TResult>
33
+ /** send a JSON-RPC notification (no id, no response). */
34
+ notify(method: string, params?: unknown): void
35
+ /** close stdin (graceful shutdown), then SIGTERM after timeout. */
36
+ shutdown(timeoutMs?: number): Promise<void>
37
+ /** force-kill immediately. */
38
+ kill(signal?: NodeJS.Signals): void
39
+ }
40
+
41
+ interface JsonRpcResponse {
42
+ jsonrpc?: '2.0'
43
+ id?: number | string | null
44
+ result?: unknown
45
+ error?: { code: number; message: string; data?: unknown }
46
+ method?: string
47
+ params?: unknown
48
+ }
49
+
50
+ export class CodexRpcError extends Error {
51
+ code: number
52
+ data: unknown
53
+ constructor(message: string, code: number, data?: unknown) {
54
+ super(message)
55
+ this.name = 'CodexRpcError'
56
+ this.code = code
57
+ this.data = data
58
+ }
59
+ }
60
+
61
+ export function spawnCodexClient(options: CodexClientOptions): CodexClient {
62
+ const child: ChildProcessWithoutNullStreams = spawn(options.bin, ['app-server'], {
63
+ cwd: options.cwd,
64
+ env: { ...process.env, ...options.env },
65
+ stdio: ['pipe', 'pipe', 'pipe'],
66
+ })
67
+
68
+ const pending = new Map<
69
+ number,
70
+ {
71
+ resolve: (value: unknown) => void
72
+ reject: (err: unknown) => void
73
+ method: string
74
+ }
75
+ >()
76
+ const handlers = new Map<string, Set<CodexNotificationHandler>>()
77
+ let nextId = 1
78
+ let closed = false
79
+
80
+ const exited = new Promise<{
81
+ code: number | null
82
+ signal: NodeJS.Signals | null
83
+ }>((resolve) => {
84
+ child.on('exit', (code, signal) => {
85
+ closed = true
86
+ const err = new Error(
87
+ `codex app-server exited (code=${code}, signal=${signal ?? ''})`,
88
+ )
89
+ for (const { reject } of pending.values()) reject(err)
90
+ pending.clear()
91
+ resolve({ code, signal })
92
+ })
93
+ })
94
+
95
+ // stdout carries JSON-RPC messages, one per line.
96
+ const rl = readline.createInterface({ input: child.stdout, crlfDelay: Infinity })
97
+ rl.on('line', (line) => {
98
+ const trimmed = line.trim()
99
+ if (!trimmed) return
100
+ let msg: JsonRpcResponse
101
+ try {
102
+ msg = JSON.parse(trimmed) as JsonRpcResponse
103
+ } catch {
104
+ // app-server occasionally emits non-JSON warnings; ignore.
105
+ return
106
+ }
107
+ if (msg.id != null && (msg.result !== undefined || msg.error !== undefined)) {
108
+ const idNum = typeof msg.id === 'string' ? Number(msg.id) : msg.id
109
+ const entry = idNum != null ? pending.get(idNum) : undefined
110
+ if (!entry) return
111
+ pending.delete(idNum!)
112
+ if (msg.error) {
113
+ entry.reject(new CodexRpcError(msg.error.message, msg.error.code, msg.error.data))
114
+ } else {
115
+ entry.resolve(msg.result)
116
+ }
117
+ return
118
+ }
119
+ if (msg.method) {
120
+ const set = handlers.get(msg.method)
121
+ if (!set) return
122
+ for (const h of set) {
123
+ try {
124
+ h(msg.params)
125
+ } catch (err) {
126
+ // a buggy handler must not kill the bridge, but it MUST be visible —
127
+ // silent swallowing turned a notification-handler typo into a
128
+ // "nothing happens" bug once already.
129
+ console.error(
130
+ `[codex-client] handler for "${msg.method}" threw:`,
131
+ err instanceof Error ? (err.stack ?? err.message) : err,
132
+ )
133
+ }
134
+ }
135
+ }
136
+ })
137
+
138
+ // stderr is diagnostic only; surfaced via the "stderr" notification channel
139
+ // so callers can forward it into transcripts if they want.
140
+ child.stderr.setEncoding('utf8')
141
+ child.stderr.on('data', (chunk: string) => {
142
+ const set = handlers.get('__stderr__')
143
+ if (!set) return
144
+ for (const h of set) {
145
+ try {
146
+ h({ text: chunk })
147
+ } catch {}
148
+ }
149
+ })
150
+
151
+ function write(msg: unknown): void {
152
+ if (closed) return
153
+ try {
154
+ child.stdin.write(JSON.stringify(msg) + '\n')
155
+ } catch {
156
+ // if stdin has been closed, subsequent writes will throw; ignore.
157
+ }
158
+ }
159
+
160
+ return {
161
+ exited,
162
+ on(method, handler) {
163
+ let set = handlers.get(method)
164
+ if (!set) {
165
+ set = new Set()
166
+ handlers.set(method, set)
167
+ }
168
+ set.add(handler)
169
+ return () => {
170
+ set?.delete(handler)
171
+ }
172
+ },
173
+ request<TResult>(method: string, params?: unknown): Promise<TResult> {
174
+ if (closed) {
175
+ return Promise.reject(new Error(`codex app-server closed; cannot call ${method}`))
176
+ }
177
+ const id = nextId++
178
+ return new Promise<TResult>((resolve, reject) => {
179
+ pending.set(id, {
180
+ resolve: (v) => resolve(v as TResult),
181
+ reject,
182
+ method,
183
+ })
184
+ write({ jsonrpc: '2.0', id, method, params })
185
+ })
186
+ },
187
+ notify(method, params) {
188
+ write({ jsonrpc: '2.0', method, params })
189
+ },
190
+ async shutdown(timeoutMs = 1500) {
191
+ if (closed) return
192
+ try {
193
+ child.stdin.end()
194
+ } catch {}
195
+ const t = setTimeout(() => {
196
+ if (!closed) {
197
+ try {
198
+ child.kill('SIGTERM')
199
+ } catch {}
200
+ }
201
+ }, timeoutMs)
202
+ try {
203
+ await exited
204
+ } finally {
205
+ clearTimeout(t)
206
+ }
207
+ },
208
+ kill(signal = 'SIGTERM') {
209
+ if (closed) return
210
+ try {
211
+ child.kill(signal)
212
+ } catch {}
213
+ },
214
+ }
215
+ }
package/src/config.ts ADDED
@@ -0,0 +1,110 @@
1
+ // sootsim.config.ts — per-app module resolution, turbo modules, env, state, and settings.
2
+ //
3
+ // usage:
4
+ // import { defineConfig } from 'sootsim/config'
5
+ // export default defineConfig({ modules: { ... }, env: { ... } })
6
+
7
+ export type ModuleResolution =
8
+ | 'noop' // empty module
9
+ | false // disable builtin stub, use original
10
+ | { use: string } // redirect to existing compat stub
11
+ | { file: string } // resolve to a file relative to the config
12
+ | { inline: Record<string, any> } // inline stub object
13
+
14
+ export type NativeModuleResolution = ModuleResolution | Record<string, any> // direct native module object for non-url configs
15
+
16
+ export interface SootSimConfig<
17
+ Settings extends Record<string, any> = Record<string, any>,
18
+ > {
19
+ modules?: Record<string, ModuleResolution>
20
+ turboModules?: Record<string, NativeModuleResolution>
21
+ nativeModules?: Record<string, NativeModuleResolution>
22
+ env?: Record<string, string>
23
+ settings?: Partial<Settings>
24
+ initialState?: {
25
+ authenticated?: boolean
26
+ locale?: string
27
+ colorScheme?: 'light' | 'dark' | 'auto'
28
+ featureFlags?: Record<string, boolean | string | number>
29
+ [key: string]: any
30
+ }
31
+ }
32
+
33
+ export const SOOTSIM_CONFIG_QUERY_PARAM = 'sootsimConfig'
34
+
35
+ export function defineConfig<Settings extends Record<string, any> = Record<string, any>>(
36
+ config: SootSimConfig<Settings>,
37
+ ): SootSimConfig<Settings> {
38
+ return config
39
+ }
40
+
41
+ function hasOwnKeys(value: Record<string, unknown> | undefined): boolean {
42
+ return !!value && Object.keys(value).length > 0
43
+ }
44
+
45
+ export function hasSootSimConfig<
46
+ Settings extends Record<string, any> = Record<string, any>,
47
+ >(config?: SootSimConfig<Settings> | null): config is SootSimConfig<Settings> {
48
+ if (!config) return false
49
+ return (
50
+ hasOwnKeys(config.modules as Record<string, unknown> | undefined) ||
51
+ hasOwnKeys(config.turboModules as Record<string, unknown> | undefined) ||
52
+ hasOwnKeys(config.nativeModules as Record<string, unknown> | undefined) ||
53
+ hasOwnKeys(config.env as Record<string, unknown> | undefined) ||
54
+ hasOwnKeys(config.settings as Record<string, unknown> | undefined) ||
55
+ hasOwnKeys(config.initialState as Record<string, unknown> | undefined)
56
+ )
57
+ }
58
+
59
+ export function mergeSootSimConfig<
60
+ Settings extends Record<string, any> = Record<string, any>,
61
+ >(
62
+ base?: SootSimConfig<Settings> | null,
63
+ override?: SootSimConfig<Settings> | null,
64
+ ): SootSimConfig<Settings> | undefined {
65
+ if (!hasSootSimConfig(base) && !hasSootSimConfig(override)) return undefined
66
+ if (!hasSootSimConfig(base)) return override ? { ...override } : undefined
67
+ if (!hasSootSimConfig(override)) return base ? { ...base } : undefined
68
+
69
+ return {
70
+ modules: { ...(base.modules || {}), ...(override.modules || {}) },
71
+ turboModules: { ...(base.turboModules || {}), ...(override.turboModules || {}) },
72
+ nativeModules: {
73
+ ...(base.nativeModules || {}),
74
+ ...(override.nativeModules || {}),
75
+ },
76
+ env: { ...(base.env || {}), ...(override.env || {}) },
77
+ settings: {
78
+ ...(base.settings || {}),
79
+ ...(override.settings || {}),
80
+ } as Partial<Settings>,
81
+ initialState: { ...(base.initialState || {}), ...(override.initialState || {}) },
82
+ }
83
+ }
84
+
85
+ export function readSootSimConfigFromSearchParams<
86
+ Settings extends Record<string, any> = Record<string, any>,
87
+ >(searchParams: URLSearchParams): SootSimConfig<Settings> | undefined {
88
+ const raw = searchParams.get(SOOTSIM_CONFIG_QUERY_PARAM)
89
+ if (!raw) return undefined
90
+ try {
91
+ const parsed = JSON.parse(raw) as SootSimConfig<Settings>
92
+ return hasSootSimConfig(parsed) ? parsed : undefined
93
+ } catch (error) {
94
+ const message = error instanceof Error ? error.message : String(error)
95
+ console.warn(`[sootsim] invalid ${SOOTSIM_CONFIG_QUERY_PARAM}: ${message}`)
96
+ return undefined
97
+ }
98
+ }
99
+
100
+ export function applySootSimConfigToUrl<
101
+ Settings extends Record<string, any> = Record<string, any>,
102
+ >(url: string, config?: SootSimConfig<Settings> | null): string {
103
+ const parsed = new URL(url)
104
+ if (hasSootSimConfig(config)) {
105
+ parsed.searchParams.set(SOOTSIM_CONFIG_QUERY_PARAM, JSON.stringify(config))
106
+ } else {
107
+ parsed.searchParams.delete(SOOTSIM_CONFIG_QUERY_PARAM)
108
+ }
109
+ return parsed.toString()
110
+ }