sootsim 0.1.83 → 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 (207) 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-MQ7GLVIB.js → agent-2CWD6W6P.js} +2 -2
  21. package/dist-cli/chunks/{agent-wrapper-7KAFDQCN.js → agent-wrapper-5W3LOX6S.js} +2 -2
  22. package/dist-cli/chunks/{assert-TV46GUNU.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-4LS5MZAI.js → chunk-4K7BH2D4.js} +3 -3
  27. package/dist-cli/chunks/{chunk-FJYT7XL2.js → chunk-4OPRODFA.js} +2 -2
  28. package/dist-cli/chunks/{chunk-DP7O5MHK.js → chunk-4OWVPRZV.js} +2 -2
  29. package/dist-cli/chunks/{chunk-PM5NVKLP.js → chunk-5XCXOLG2.js} +2 -2
  30. package/dist-cli/chunks/chunk-67ZZ2CM5.js +1 -0
  31. package/dist-cli/chunks/{chunk-WN7M3QON.js → chunk-73UZXB4B.js} +2 -2
  32. package/dist-cli/chunks/{chunk-5DJXZIFZ.js → chunk-7NWNTUJF.js} +1 -1
  33. package/dist-cli/chunks/{chunk-Y2VJBRSP.js → chunk-7YHDJLO2.js} +1 -1
  34. package/dist-cli/chunks/{chunk-6NN2D4EJ.js → chunk-AJVTY6KY.js} +1 -1
  35. package/dist-cli/chunks/chunk-AWSQUOAS.js +67 -0
  36. package/dist-cli/chunks/{chunk-CJY3AVI7.js → chunk-BCBNVJVG.js} +1 -1
  37. package/dist-cli/chunks/{chunk-OYMFNU3M.js → chunk-BKBL6K2G.js} +1 -1
  38. package/dist-cli/chunks/{chunk-IBNRRAES.js → chunk-C3DPQZ4J.js} +2 -2
  39. package/dist-cli/chunks/chunk-D3ZSBIIY.js +2 -0
  40. package/dist-cli/chunks/{chunk-2AWQ7OB2.js → chunk-D4HUVLZR.js} +1 -1
  41. package/dist-cli/chunks/{chunk-F3HP444U.js → chunk-DUUSJDES.js} +1 -1
  42. package/dist-cli/chunks/{chunk-277XAALA.js → chunk-ELJLF4SG.js} +3 -3
  43. package/dist-cli/chunks/{chunk-RH4F2TF7.js → chunk-EQ7TFQ2F.js} +1 -1
  44. package/dist-cli/chunks/{chunk-HNWEELAE.js → chunk-EQCKGC4B.js} +1 -1
  45. package/dist-cli/chunks/chunk-FUCGLWNN.js +1 -0
  46. package/dist-cli/chunks/{chunk-FRM355UL.js → chunk-HYPJW65U.js} +2 -2
  47. package/dist-cli/chunks/chunk-IILJQCZA.js +2 -0
  48. package/dist-cli/chunks/{chunk-Y4BUVURT.js → chunk-KU6MSPAH.js} +2 -2
  49. package/dist-cli/chunks/{chunk-DM6WT7QM.js → chunk-OOOR7NT2.js} +1 -1
  50. package/dist-cli/chunks/{chunk-HAWOAQAG.js → chunk-P7WDNKOS.js} +3 -3
  51. package/dist-cli/chunks/{chunk-6TNANCQC.js → chunk-PPKKA5VW.js} +2 -2
  52. package/dist-cli/chunks/{chunk-JQ7ZXOXJ.js → chunk-PS2G44GT.js} +2 -2
  53. package/dist-cli/chunks/{chunk-ECJBV65H.js → chunk-QMSJR5R2.js} +2 -2
  54. package/dist-cli/chunks/{chunk-J2GYISVJ.js → chunk-RF4R2U46.js} +2 -2
  55. package/dist-cli/chunks/{chunk-VMXWC2JO.js → chunk-RIXUH3NK.js} +2 -2
  56. package/dist-cli/chunks/{chunk-2PY3UZVO.js → chunk-SFGUPL2X.js} +2 -2
  57. package/dist-cli/chunks/{chunk-572VSFNP.js → chunk-SQX5CAYG.js} +1 -1
  58. package/dist-cli/chunks/{chunk-NXATOWWF.js → chunk-SQZAC7C4.js} +1 -1
  59. package/dist-cli/chunks/{chunk-WTKTOL3C.js → chunk-SV7FOGJ3.js} +2 -2
  60. package/dist-cli/chunks/{chunk-JHJNODXN.js → chunk-TK3OJSEO.js} +2 -2
  61. package/dist-cli/chunks/{chunk-KASUZ5XV.js → chunk-TL7SIZ7S.js} +1 -1
  62. package/dist-cli/chunks/{chunk-6XZOEBTZ.js → chunk-V2GQ4WXJ.js} +2 -2
  63. package/dist-cli/chunks/{chunk-IP3QJLRH.js → chunk-VH7F45CN.js} +1 -1
  64. package/dist-cli/chunks/chunk-WNVNU2OW.js +4 -0
  65. package/dist-cli/chunks/{chunk-YUELRHGB.js → chunk-XQ2OBHBE.js} +2 -2
  66. package/dist-cli/chunks/{chunk-CYV6Y6YV.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-QLLWBTS3.js → compat-FWSEEGEH.js} +3 -3
  70. package/dist-cli/chunks/{config-2DSLDCXV.js → config-CYI2WAGP.js} +2 -2
  71. package/dist-cli/chunks/control-UXY7YQVX.js +2 -0
  72. package/dist-cli/chunks/{cpu-profile-GEIKHCPC.js → cpu-profile-IKAE3KTY.js} +2 -2
  73. package/dist-cli/chunks/{daemon-4EBUFN4D.js → daemon-ZUMF53YB.js} +2 -2
  74. package/dist-cli/chunks/{debug-WGD6XWOF.js → debug-P6KULKKS.js} +3 -3
  75. package/dist-cli/chunks/{detox-LNKGRZU6.js → detox-SPWAZCYG.js} +2 -2
  76. package/dist-cli/chunks/{device-AYKXKVIQ.js → device-JWEPK6I2.js} +2 -2
  77. package/dist-cli/chunks/{diagnose-TMXSDOOC.js → diagnose-IZODTXV2.js} +2 -2
  78. package/dist-cli/chunks/drivers-MK6WJKBC.js +2 -0
  79. package/dist-cli/chunks/{electron-QFPF7TBY.js → electron-R5GP6RVB.js} +3 -3
  80. package/dist-cli/chunks/flow-6O4GEOPJ.js +2 -0
  81. package/dist-cli/chunks/{hints-MXKRR4TG.js → hints-DYDNYX7N.js} +2 -2
  82. package/dist-cli/chunks/{home-paths-REMWQDAO.js → home-paths-GLMX5OKL.js} +2 -2
  83. package/dist-cli/chunks/{inspect-XGSQNFV7.js → inspect-FJOPCTY2.js} +3 -3
  84. package/dist-cli/chunks/install-A3TUGGHN.js +2 -0
  85. package/dist-cli/chunks/{install-desktop-NQG3RZSA.js → install-desktop-YPJZMZM5.js} +3 -3
  86. package/dist-cli/chunks/{keys-5QZWXL3F.js → keys-GSYPHWNY.js} +2 -2
  87. package/dist-cli/chunks/{launch-SBXOZWKO.js → launch-4G2PKW5X.js} +3 -3
  88. package/dist-cli/chunks/{login-EACQXE24.js → login-KJQGHA64.js} +4 -4
  89. package/dist-cli/chunks/{logout-IBQLMUML.js → logout-XM2SYH5C.js} +2 -2
  90. package/dist-cli/chunks/{maestro-LFYXUX7O.js → maestro-EOWGI7DG.js} +2 -2
  91. package/dist-cli/chunks/{preview-U4SBOEGQ.js → preview-F73TKK37.js} +2 -2
  92. package/dist-cli/chunks/{profile-GWS5ECMY.js → profile-22FDKBUO.js} +2 -2
  93. package/dist-cli/chunks/{react-QDHLMVYL.js → react-5L6VPFUP.js} +2 -2
  94. package/dist-cli/chunks/{record-BUEUWPDI.js → record-JZXCQ4IN.js} +2 -2
  95. package/dist-cli/chunks/runtime-EEBX7CFV.js +2 -0
  96. package/dist-cli/chunks/{runtime-delivery-G7L6RVZ7.js → runtime-delivery-LXUM3R4A.js} +2 -2
  97. package/dist-cli/chunks/{screenshot-T2HBA3VI.js → screenshot-HDRRG33Q.js} +2 -2
  98. package/dist-cli/chunks/{screenshot-mode-EG5HMIH3.js → screenshot-mode-WY63LZIX.js} +2 -2
  99. package/dist-cli/chunks/{screenshots-S52AFHTV.js → screenshots-MPV2ENL5.js} +2 -2
  100. package/dist-cli/chunks/{server-MFFVYUGG.js → server-5LBMCJ3G.js} +2 -2
  101. package/dist-cli/chunks/setup-repo-SZSYNKNI.js +2 -0
  102. package/dist-cli/chunks/{skills-HQGWBS2O.js → skills-BQ73YOBF.js} +2 -2
  103. package/dist-cli/chunks/{start-E3DRYY7W.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-ZY3EF62K.js → test-OVO4CQTG.js} +3 -3
  107. package/dist-cli/chunks/{three-mode-WSPKQCJ5.js → three-mode-BKM3KFM7.js} +2 -2
  108. package/dist-cli/chunks/{timeline-3XAB5EWZ.js → timeline-MDXGEDQL.js} +2 -2
  109. package/dist-cli/chunks/{upgrade-WNENPFM5.js → upgrade-JGQABWVF.js} +2 -2
  110. package/dist-cli/chunks/upload-UJNUA4ZV.js +2 -0
  111. package/dist-cli/chunks/{web-D2AOZY44.js → web-WYFAYQ72.js} +2 -2
  112. package/dist-cli/chunks/{what-happened-F43KNSG6.js → what-happened-PZW2KW6A.js} +2 -2
  113. package/dist-cli/chunks/{whoami-T22VBR7C.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-FQS4ZD2K.js +0 -2
  187. package/dist-cli/chunks/beta-VG7CDY2U.js +0 -2
  188. package/dist-cli/chunks/chunk-2OIBDYHW.js +0 -1
  189. package/dist-cli/chunks/chunk-6BNLVMXA.js +0 -1
  190. package/dist-cli/chunks/chunk-6XD6CBJM.js +0 -2
  191. package/dist-cli/chunks/chunk-CHQTO426.js +0 -1
  192. package/dist-cli/chunks/chunk-FAPYGVIU.js +0 -4
  193. package/dist-cli/chunks/chunk-PEHFE3LG.js +0 -64
  194. package/dist-cli/chunks/chunk-RXH2SLKF.js +0 -2
  195. package/dist-cli/chunks/chunk-UXQWC5ZR.js +0 -79
  196. package/dist-cli/chunks/chunk-XFQL74PF.js +0 -5
  197. package/dist-cli/chunks/cli-version-PWF6I6LY.js +0 -2
  198. package/dist-cli/chunks/control-UIOXGYXU.js +0 -2
  199. package/dist-cli/chunks/demo-app-registry-G3BDOFWC.js +0 -2
  200. package/dist-cli/chunks/drivers-IDQF34HP.js +0 -2
  201. package/dist-cli/chunks/flow-3JN3Y7RF.js +0 -2
  202. package/dist-cli/chunks/install-2N3YOOSN.js +0 -2
  203. package/dist-cli/chunks/runtime-PVB4VGUH.js +0 -2
  204. package/dist-cli/chunks/setup-repo-YOF7NV5D.js +0 -2
  205. package/dist-cli/chunks/store-MAI6D3UO.js +0 -2
  206. package/dist-cli/chunks/telemetry-RCQKCJTH.js +0 -2
  207. package/dist-cli/chunks/upload-YLJ4RA73.js +0 -2
@@ -0,0 +1,179 @@
1
+ import fs from 'fs'
2
+ import path from 'path'
3
+ import { electronUserDataDir, ensureSootsimHome, 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
+ ensureSootsimHome()
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
+ }
@@ -0,0 +1,27 @@
1
+ export type RenderMode = 'worker'
2
+
3
+ // worker rendering is now the only supported mode.
4
+ export function getRenderMode(_searchParams: URLSearchParams): RenderMode {
5
+ return 'worker'
6
+ }
7
+
8
+ export function isRenderWorkerMode(_searchParams: URLSearchParams): boolean {
9
+ return true
10
+ }
11
+
12
+ export function isRenderMainMode(_searchParams: URLSearchParams): boolean {
13
+ return false
14
+ }
15
+
16
+ export function hasRenderWorkerBootstrap(
17
+ workerModuleBaseUrl: string | null | undefined,
18
+ ): boolean {
19
+ return typeof workerModuleBaseUrl === 'string' && workerModuleBaseUrl.length > 0
20
+ }
21
+
22
+ export function shouldEnableRenderWorker(
23
+ _searchParams: URLSearchParams,
24
+ workerModuleBaseUrl: string | null | undefined,
25
+ ): boolean {
26
+ return hasRenderWorkerBootstrap(workerModuleBaseUrl)
27
+ }
@@ -0,0 +1,334 @@
1
+ import { spawn } from 'child_process'
2
+ import crypto from 'crypto'
3
+ import fs from 'fs'
4
+ import path from 'path'
5
+ import { Readable } from 'stream'
6
+ import { pipeline } from 'stream/promises'
7
+ import {
8
+ cacheDir,
9
+ compareSemver,
10
+ configFilePath,
11
+ ensureSootsimHome,
12
+ readActiveRuntime,
13
+ runtimeDir,
14
+ writeActiveRuntime,
15
+ } from './home-paths'
16
+
17
+ export const DEFAULT_RUNTIME_CDN_ORIGIN = 'https://sootbean.com'
18
+ export const RUNTIME_CDN_ORIGIN_ENV = 'SOOTSIM_CDN_ORIGIN'
19
+ export const RUNTIME_CHANNEL_ENV = 'SOOTSIM_RUNTIME_CHANNEL'
20
+
21
+ export interface RuntimeManifestChannel {
22
+ latest: string
23
+ }
24
+
25
+ export interface RuntimeManifestVersion {
26
+ tarball?: string
27
+ sha256: string
28
+ size?: number
29
+ published?: string
30
+ }
31
+
32
+ export interface RuntimeManifest {
33
+ channels: Record<string, RuntimeManifestChannel>
34
+ versions: Record<string, RuntimeManifestVersion>
35
+ }
36
+
37
+ export interface RuntimeInstallOptions {
38
+ version?: string | null
39
+ channel?: string
40
+ cdnOrigin?: string
41
+ force?: boolean
42
+ setActive?: boolean
43
+ }
44
+
45
+ export interface RuntimeInstallResult {
46
+ version: string
47
+ channel: string
48
+ cdnOrigin: string
49
+ runtimeDir: string
50
+ installed: boolean
51
+ activated: boolean
52
+ manifest: RuntimeManifest
53
+ }
54
+
55
+ export interface RuntimeUpdateResult {
56
+ checked: boolean
57
+ updated: boolean
58
+ reason?: string
59
+ activeVersion: string | null
60
+ latestVersion?: string
61
+ install?: RuntimeInstallResult
62
+ }
63
+
64
+ interface SootsimConfig {
65
+ runtimeChannel?: string
66
+ cdnOrigin?: string
67
+ }
68
+
69
+ function readConfig(): SootsimConfig {
70
+ try {
71
+ const parsed = JSON.parse(fs.readFileSync(configFilePath(), 'utf8')) as SootsimConfig
72
+ return parsed && typeof parsed === 'object' ? parsed : {}
73
+ } catch {
74
+ return {}
75
+ }
76
+ }
77
+
78
+ export function resolveRuntimeCdnOrigin(input?: string): string {
79
+ const config = readConfig()
80
+ const value =
81
+ input ||
82
+ process.env[RUNTIME_CDN_ORIGIN_ENV] ||
83
+ config.cdnOrigin ||
84
+ DEFAULT_RUNTIME_CDN_ORIGIN
85
+ return value.replace(/\/+$/, '')
86
+ }
87
+
88
+ export function resolveRuntimeChannel(input?: string): string {
89
+ const config = readConfig()
90
+ return input || process.env[RUNTIME_CHANNEL_ENV] || config.runtimeChannel || 'stable'
91
+ }
92
+
93
+ export function runtimeManifestUrl(cdnOrigin?: string): string {
94
+ const url = new URL(`${resolveRuntimeCdnOrigin(cdnOrigin)}/runtimes/manifest.json`)
95
+ // cdn edges can keep the no-query manifest around longer than its
96
+ // max-age. runtime checks need the latest manifest every time.
97
+ url.searchParams.set('t', String(Date.now()))
98
+ return url.toString()
99
+ }
100
+
101
+ export function runtimeTarballUrl(version: string, cdnOrigin?: string): string {
102
+ return `${resolveRuntimeCdnOrigin(cdnOrigin)}/runtimes/sootsim-runtime-${version}.tar.gz`
103
+ }
104
+
105
+ export async function fetchRuntimeManifest(cdnOrigin?: string): Promise<RuntimeManifest> {
106
+ const url = runtimeManifestUrl(cdnOrigin)
107
+ const res = await fetch(url, { headers: { Accept: 'application/json' } })
108
+ if (!res.ok) {
109
+ throw new Error(`manifest fetch failed: ${res.status} ${res.statusText} (${url})`)
110
+ }
111
+ return (await res.json()) as RuntimeManifest
112
+ }
113
+
114
+ export function resolveRuntimeVersion(
115
+ manifest: RuntimeManifest,
116
+ opts: Pick<RuntimeInstallOptions, 'version' | 'channel'> = {},
117
+ ): { version: string; channel: string; entry: RuntimeManifestVersion } {
118
+ const channel = resolveRuntimeChannel(opts.channel)
119
+ const version = opts.version || manifest.channels[channel]?.latest
120
+ if (!version) {
121
+ throw new Error(
122
+ `no version specified and channel '${channel}' has no latest entry in the manifest`,
123
+ )
124
+ }
125
+ const entry = manifest.versions[version]
126
+ if (!entry) {
127
+ throw new Error(
128
+ `version ${version} not found in manifest; available: ${
129
+ Object.keys(manifest.versions).slice(-10).join(', ') || '(none)'
130
+ }`,
131
+ )
132
+ }
133
+ return { version, channel, entry }
134
+ }
135
+
136
+ export async function installRuntime(
137
+ opts: RuntimeInstallOptions = {},
138
+ ): Promise<RuntimeInstallResult> {
139
+ ensureSootsimHome()
140
+
141
+ const cdnOrigin = resolveRuntimeCdnOrigin(opts.cdnOrigin)
142
+ const manifest = await fetchRuntimeManifest(cdnOrigin)
143
+ const { version, channel, entry } = resolveRuntimeVersion(manifest, opts)
144
+ const destDir = runtimeDir(version)
145
+ const setActive = opts.setActive !== false
146
+
147
+ if (!opts.force && fs.existsSync(path.join(destDir, 'index.html'))) {
148
+ if (setActive) writeActiveRuntime(version)
149
+ return {
150
+ version,
151
+ channel,
152
+ cdnOrigin,
153
+ runtimeDir: destDir,
154
+ installed: false,
155
+ activated: setActive,
156
+ manifest,
157
+ }
158
+ }
159
+
160
+ const tarUrl = entry.tarball || runtimeTarballUrl(version, cdnOrigin)
161
+ const tarCachePath = path.join(cacheDir(), `sootsim-runtime-${version}.tar.gz`)
162
+ process.stderr.write(`sootsim: downloading runtime ${version}…\n`)
163
+ await downloadToFile(tarUrl, tarCachePath)
164
+ process.stderr.write(`sootsim: extracting runtime ${version}…\n`)
165
+
166
+ const actualSha = await sha256File(tarCachePath)
167
+ if (actualSha !== entry.sha256) {
168
+ fs.rmSync(tarCachePath, { force: true })
169
+ throw new Error(
170
+ `sha256 mismatch for runtime ${version}: expected ${entry.sha256}, actual ${actualSha}`,
171
+ )
172
+ }
173
+
174
+ const tmpDir = path.join(path.dirname(destDir), `.installing-${version}-${process.pid}`)
175
+ fs.rmSync(tmpDir, { recursive: true, force: true })
176
+ fs.mkdirSync(tmpDir, { recursive: true })
177
+ try {
178
+ await extractTarball(tarCachePath, tmpDir)
179
+ if (!fs.existsSync(path.join(tmpDir, 'index.html'))) {
180
+ throw new Error(`extracted tarball for runtime ${version} is missing index.html`)
181
+ }
182
+ fs.rmSync(destDir, { recursive: true, force: true })
183
+ fs.renameSync(tmpDir, destDir)
184
+ } catch (err) {
185
+ fs.rmSync(tmpDir, { recursive: true, force: true })
186
+ throw err
187
+ }
188
+
189
+ if (setActive) writeActiveRuntime(version)
190
+ return {
191
+ version,
192
+ channel,
193
+ cdnOrigin,
194
+ runtimeDir: destDir,
195
+ installed: true,
196
+ activated: setActive,
197
+ manifest,
198
+ }
199
+ }
200
+
201
+ export async function updateRuntimeToLatest(
202
+ opts: Pick<RuntimeInstallOptions, 'channel' | 'cdnOrigin'> = {},
203
+ ): Promise<RuntimeUpdateResult> {
204
+ ensureSootsimHome()
205
+ const cdnOrigin = resolveRuntimeCdnOrigin(opts.cdnOrigin)
206
+ const channel = resolveRuntimeChannel(opts.channel)
207
+ const manifest = await fetchRuntimeManifest(cdnOrigin)
208
+ const latestVersion = manifest.channels[channel]?.latest
209
+ if (!latestVersion) {
210
+ return {
211
+ checked: true,
212
+ updated: false,
213
+ reason: `channel '${channel}' has no latest runtime`,
214
+ activeVersion: readActiveRuntime(),
215
+ }
216
+ }
217
+ const entry = manifest.versions[latestVersion]
218
+ if (!entry) {
219
+ return {
220
+ checked: true,
221
+ updated: false,
222
+ reason: `manifest is missing version ${latestVersion}`,
223
+ activeVersion: readActiveRuntime(),
224
+ latestVersion,
225
+ }
226
+ }
227
+
228
+ const activeVersion = readActiveRuntime()
229
+ const activeDir = activeVersion ? runtimeDir(activeVersion) : null
230
+ const activeInstalled = activeDir
231
+ ? fs.existsSync(path.join(activeDir, 'index.html'))
232
+ : false
233
+ const shouldInstall =
234
+ !activeVersion || !activeInstalled || compareSemver(latestVersion, activeVersion) > 0
235
+
236
+ if (!shouldInstall) {
237
+ return {
238
+ checked: true,
239
+ updated: false,
240
+ reason: 'active runtime is current',
241
+ activeVersion,
242
+ latestVersion,
243
+ }
244
+ }
245
+
246
+ // setActive=false: callers like the daemon flip the active runtime
247
+ // themselves so they can broadcast runtime:changed to connected
248
+ // browsers. CLI install uses installRuntime() directly with
249
+ // setActive=true and is unaffected.
250
+ const install = await installRuntime({
251
+ version: latestVersion,
252
+ channel,
253
+ cdnOrigin,
254
+ setActive: false,
255
+ })
256
+ return {
257
+ checked: true,
258
+ updated: true,
259
+ activeVersion: latestVersion,
260
+ latestVersion,
261
+ install,
262
+ }
263
+ }
264
+
265
+ export interface RuntimeUpToDateCheck {
266
+ active: string | null
267
+ latest: string | null
268
+ // true only when a latest version is known and it is newer than the
269
+ // active one (or nothing is active yet). network failures resolve to
270
+ // false so callers can treat this as a best-effort hint.
271
+ outdated: boolean
272
+ }
273
+
274
+ /** read-only check: is the active runtime the latest on its channel?
275
+ * unlike updateRuntimeToLatest this never downloads or activates — it's
276
+ * for surfacing a "newer runtime available" hint at startup / in help. */
277
+ export async function checkRuntimeUpToDate(
278
+ opts: Pick<RuntimeInstallOptions, 'channel' | 'cdnOrigin'> = {},
279
+ ): Promise<RuntimeUpToDateCheck> {
280
+ const active = readActiveRuntime()
281
+ try {
282
+ const manifest = await fetchRuntimeManifest(resolveRuntimeCdnOrigin(opts.cdnOrigin))
283
+ const channel = resolveRuntimeChannel(opts.channel)
284
+ const latest = manifest.channels[channel]?.latest ?? null
285
+ const outdated = !!(latest && (!active || compareSemver(latest, active) > 0))
286
+ return { active, latest, outdated }
287
+ } catch {
288
+ return { active, latest: null, outdated: false }
289
+ }
290
+ }
291
+
292
+ async function downloadToFile(url: string, destPath: string): Promise<void> {
293
+ const res = await fetch(url)
294
+ if (!res.ok || !res.body) {
295
+ throw new Error(`download failed: ${res.status} ${res.statusText} (${url})`)
296
+ }
297
+ fs.mkdirSync(path.dirname(destPath), { recursive: true })
298
+ const tmp = `${destPath}.partial`
299
+ try {
300
+ await pipeline(
301
+ Readable.fromWeb(res.body as unknown as Parameters<typeof Readable.fromWeb>[0]),
302
+ fs.createWriteStream(tmp),
303
+ )
304
+ fs.renameSync(tmp, destPath)
305
+ } catch (err) {
306
+ try {
307
+ fs.unlinkSync(tmp)
308
+ } catch {}
309
+ throw err
310
+ }
311
+ }
312
+
313
+ function sha256File(filePath: string): Promise<string> {
314
+ return new Promise((resolve, reject) => {
315
+ const hash = crypto.createHash('sha256')
316
+ const stream = fs.createReadStream(filePath)
317
+ stream.on('data', (chunk) => hash.update(chunk))
318
+ stream.on('error', reject)
319
+ stream.on('end', () => resolve(hash.digest('hex')))
320
+ })
321
+ }
322
+
323
+ function extractTarball(tarPath: string, destDir: string): Promise<void> {
324
+ return new Promise((resolve, reject) => {
325
+ const child = spawn('tar', ['-xzf', tarPath, '-C', destDir], {
326
+ stdio: ['ignore', 'inherit', 'inherit'],
327
+ })
328
+ child.on('error', reject)
329
+ child.on('exit', (code) => {
330
+ if (code === 0) resolve()
331
+ else reject(new Error(`tar exited with code ${code}`))
332
+ })
333
+ })
334
+ }