synthos 0.8.0 → 0.10.0

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 (368) hide show
  1. package/README.md +1 -1
  2. package/default-pages/application/page.html +42 -0
  3. package/default-pages/application/page.json +10 -0
  4. package/default-pages/elevenlabs_effects_studio/page.html +1363 -0
  5. package/default-pages/elevenlabs_effects_studio/page.json +11 -0
  6. package/default-pages/elevenlabs_voice_studio/page.html +801 -0
  7. package/default-pages/elevenlabs_voice_studio/page.json +11 -0
  8. package/default-pages/{json_tools.html → json_tools/page.html} +13 -11
  9. package/default-pages/json_tools/page.json +10 -0
  10. package/default-pages/my_notes/notes/a1b2c3d4-e5f6-7890-abcd-ef1234567890.json +5 -0
  11. package/default-pages/my_notes/page.html +132 -0
  12. package/default-pages/{my_notes.json → my_notes/page.json} +2 -2
  13. package/default-pages/neon_asteroids/files/Ambient_Space.mp3 +0 -0
  14. package/default-pages/neon_asteroids/files/Ambient_Space2.mp3 +0 -0
  15. package/default-pages/neon_asteroids/files/Ambient_Space3.mp3 +0 -0
  16. package/default-pages/neon_asteroids/files/Asteroid_Explosion.mp3 +0 -0
  17. package/default-pages/neon_asteroids/files/Hyperspace_Jump.mp3 +0 -0
  18. package/default-pages/neon_asteroids/files/Laser_Fire.mp3 +0 -0
  19. package/default-pages/neon_asteroids/files/Menu_Navigate.mp3 +0 -0
  20. package/default-pages/neon_asteroids/files/Power_Up_Collect.mp3 +0 -0
  21. package/default-pages/neon_asteroids/files/Saucer_Alert.mp3 +0 -0
  22. package/default-pages/neon_asteroids/files/Ship_Thrust.mp3 +0 -0
  23. package/default-pages/neon_asteroids/files/effects.json +74 -0
  24. package/default-pages/neon_asteroids/page.html +1803 -0
  25. package/default-pages/{neon_asteroids.json → neon_asteroids/page.json} +3 -3
  26. package/default-pages/{oregon_trail.html → oregon_trail/page.html} +16 -30
  27. package/default-pages/{oregon_trail.json → oregon_trail/page.json} +2 -2
  28. package/default-pages/retro_game_starter/page.html +1308 -0
  29. package/default-pages/retro_game_starter/page.json +12 -0
  30. package/default-pages/{sidebar_page.html → sidebar_page/page.html} +12 -10
  31. package/default-pages/sidebar_page/page.json +10 -0
  32. package/default-pages/{solar_explorer.html → solar_explorer/page.html} +15 -12
  33. package/default-pages/{solar_explorer.json → solar_explorer/page.json} +2 -2
  34. package/default-pages/{solar_tutorial.html → solar_tutorial/page.html} +12 -10
  35. package/default-pages/solar_tutorial/page.json +10 -0
  36. package/default-pages/{two-panel_page.html → two-panel_page/page.html} +13 -11
  37. package/default-pages/two-panel_page/page.json +10 -0
  38. package/default-pages/{us_map.html → us_map/page.html} +193 -192
  39. package/default-pages/{us_map.json → us_map/page.json} +12 -12
  40. package/default-pages/{us_map_1850.html → us_map_1850/page.html} +326 -325
  41. package/default-pages/{us_map_1850.json → us_map_1850/page.json} +12 -12
  42. package/default-pages/{western_cities_1850.html → western_cities_1850/page.html} +527 -526
  43. package/default-pages/{western_cities_1850.json → western_cities_1850/page.json} +12 -12
  44. package/default-themes/aurora-dawn.json +19 -0
  45. package/default-themes/aurora-dawn.v3.css +198 -0
  46. package/default-themes/aurora-dusk.json +19 -0
  47. package/default-themes/aurora-dusk.v3.css +200 -0
  48. package/default-themes/cosmos-dawn.json +19 -0
  49. package/default-themes/cosmos-dawn.v3.css +198 -0
  50. package/default-themes/cosmos-dusk.json +19 -0
  51. package/default-themes/cosmos-dusk.v3.css +200 -0
  52. package/default-themes/high-contrast-dark.json +19 -0
  53. package/default-themes/high-contrast-dark.v3.css +200 -0
  54. package/default-themes/high-contrast-light.json +19 -0
  55. package/default-themes/high-contrast-light.v3.css +198 -0
  56. package/default-themes/nebula-dawn.v2.css +110 -0
  57. package/default-themes/nebula-dawn.v3.css +199 -0
  58. package/default-themes/nebula-dusk.v2.css +104 -0
  59. package/default-themes/nebula-dusk.v3.css +201 -0
  60. package/default-themes/solar-flare-dawn.json +19 -0
  61. package/default-themes/solar-flare-dawn.v3.css +198 -0
  62. package/default-themes/solar-flare-dusk.json +19 -0
  63. package/default-themes/solar-flare-dusk.v3.css +200 -0
  64. package/dist/agents/index.d.ts +1 -1
  65. package/dist/agents/index.d.ts.map +1 -1
  66. package/dist/agents/index.js +2 -1
  67. package/dist/agents/index.js.map +1 -1
  68. package/dist/agents/openclaw/gatewayManager.d.ts +4 -0
  69. package/dist/agents/openclaw/gatewayManager.d.ts.map +1 -1
  70. package/dist/agents/openclaw/gatewayManager.js +27 -11
  71. package/dist/agents/openclaw/gatewayManager.js.map +1 -1
  72. package/dist/agents/openclaw/openclawProvider.d.ts.map +1 -1
  73. package/dist/agents/openclaw/openclawProvider.js +2 -4
  74. package/dist/agents/openclaw/openclawProvider.js.map +1 -1
  75. package/dist/agents/openclaw/sshTunnelManager.d.ts +2 -0
  76. package/dist/agents/openclaw/sshTunnelManager.d.ts.map +1 -1
  77. package/dist/agents/openclaw/sshTunnelManager.js +31 -12
  78. package/dist/agents/openclaw/sshTunnelManager.js.map +1 -1
  79. package/dist/builders/anthropic.d.ts +31 -0
  80. package/dist/builders/anthropic.d.ts.map +1 -0
  81. package/dist/builders/anthropic.js +227 -0
  82. package/dist/builders/anthropic.js.map +1 -0
  83. package/dist/builders/fireworksai.d.ts +9 -0
  84. package/dist/builders/fireworksai.d.ts.map +1 -0
  85. package/dist/builders/fireworksai.js +57 -0
  86. package/dist/builders/fireworksai.js.map +1 -0
  87. package/dist/builders/index.d.ts +13 -0
  88. package/dist/builders/index.d.ts.map +1 -0
  89. package/dist/builders/index.js +31 -0
  90. package/dist/builders/index.js.map +1 -0
  91. package/dist/builders/openai.d.ts +8 -0
  92. package/dist/builders/openai.d.ts.map +1 -0
  93. package/dist/builders/openai.js +87 -0
  94. package/dist/builders/openai.js.map +1 -0
  95. package/dist/builders/types.d.ts +54 -0
  96. package/dist/builders/types.d.ts.map +1 -0
  97. package/dist/builders/types.js +211 -0
  98. package/dist/builders/types.js.map +1 -0
  99. package/dist/connectors/index.d.ts +1 -1
  100. package/dist/connectors/index.d.ts.map +1 -1
  101. package/dist/connectors/index.js +3 -2
  102. package/dist/connectors/index.js.map +1 -1
  103. package/dist/connectors/registry.d.ts +2 -1
  104. package/dist/connectors/registry.d.ts.map +1 -1
  105. package/dist/connectors/registry.js +31 -8
  106. package/dist/connectors/registry.js.map +1 -1
  107. package/dist/customizer/Customizer.d.ts +62 -0
  108. package/dist/customizer/Customizer.d.ts.map +1 -0
  109. package/dist/customizer/Customizer.js +134 -0
  110. package/dist/customizer/Customizer.js.map +1 -0
  111. package/dist/customizer/index.d.ts +4 -0
  112. package/dist/customizer/index.d.ts.map +1 -0
  113. package/dist/customizer/index.js +9 -0
  114. package/dist/customizer/index.js.map +1 -0
  115. package/dist/files.d.ts +16 -0
  116. package/dist/files.d.ts.map +1 -1
  117. package/dist/files.js +60 -1
  118. package/dist/files.js.map +1 -1
  119. package/dist/index.d.ts +2 -0
  120. package/dist/index.d.ts.map +1 -1
  121. package/dist/index.js +2 -0
  122. package/dist/index.js.map +1 -1
  123. package/dist/init.d.ts +12 -6
  124. package/dist/init.d.ts.map +1 -1
  125. package/dist/init.js +150 -133
  126. package/dist/init.js.map +1 -1
  127. package/dist/migrations.d.ts.map +1 -1
  128. package/dist/migrations.js +23 -10
  129. package/dist/migrations.js.map +1 -1
  130. package/dist/models/anthropic.d.ts +4 -2
  131. package/dist/models/anthropic.d.ts.map +1 -1
  132. package/dist/models/anthropic.js +33 -6
  133. package/dist/models/anthropic.js.map +1 -1
  134. package/dist/models/fireworksai.d.ts.map +1 -1
  135. package/dist/models/fireworksai.js +9 -1
  136. package/dist/models/fireworksai.js.map +1 -1
  137. package/dist/models/index.d.ts +1 -1
  138. package/dist/models/index.d.ts.map +1 -1
  139. package/dist/models/index.js +2 -1
  140. package/dist/models/index.js.map +1 -1
  141. package/dist/models/openai.d.ts +1 -1
  142. package/dist/models/openai.d.ts.map +1 -1
  143. package/dist/models/openai.js +24 -3
  144. package/dist/models/openai.js.map +1 -1
  145. package/dist/models/types.d.ts +20 -1
  146. package/dist/models/types.d.ts.map +1 -1
  147. package/dist/models/types.js +6 -1
  148. package/dist/models/types.js.map +1 -1
  149. package/dist/pages.d.ts +34 -10
  150. package/dist/pages.d.ts.map +1 -1
  151. package/dist/pages.js +229 -79
  152. package/dist/pages.js.map +1 -1
  153. package/dist/service/createCompletePrompt.d.ts +2 -1
  154. package/dist/service/createCompletePrompt.d.ts.map +1 -1
  155. package/dist/service/createCompletePrompt.js +2 -2
  156. package/dist/service/createCompletePrompt.js.map +1 -1
  157. package/dist/service/requiresSettings.d.ts +2 -1
  158. package/dist/service/requiresSettings.d.ts.map +1 -1
  159. package/dist/service/requiresSettings.js +3 -3
  160. package/dist/service/requiresSettings.js.map +1 -1
  161. package/dist/service/server.d.ts +2 -1
  162. package/dist/service/server.d.ts.map +1 -1
  163. package/dist/service/server.js +37 -8
  164. package/dist/service/server.js.map +1 -1
  165. package/dist/service/transformPage.d.ts +47 -20
  166. package/dist/service/transformPage.d.ts.map +1 -1
  167. package/dist/service/transformPage.js +514 -293
  168. package/dist/service/transformPage.js.map +1 -1
  169. package/dist/service/useAgentRoutes.d.ts +2 -1
  170. package/dist/service/useAgentRoutes.d.ts.map +1 -1
  171. package/dist/service/useAgentRoutes.js +17 -14
  172. package/dist/service/useAgentRoutes.js.map +1 -1
  173. package/dist/service/useApiRoutes.d.ts +2 -1
  174. package/dist/service/useApiRoutes.d.ts.map +1 -1
  175. package/dist/service/useApiRoutes.js +287 -172
  176. package/dist/service/useApiRoutes.js.map +1 -1
  177. package/dist/service/useConnectorRoutes.js +17 -17
  178. package/dist/service/useConnectorRoutes.js.map +1 -1
  179. package/dist/service/useDataRoutes.d.ts.map +1 -1
  180. package/dist/service/useDataRoutes.js +13 -10
  181. package/dist/service/useDataRoutes.js.map +1 -1
  182. package/dist/service/useFileRoutes.d.ts +4 -0
  183. package/dist/service/useFileRoutes.d.ts.map +1 -0
  184. package/dist/service/useFileRoutes.js +122 -0
  185. package/dist/service/useFileRoutes.js.map +1 -0
  186. package/dist/service/usePageRoutes.d.ts +2 -1
  187. package/dist/service/usePageRoutes.d.ts.map +1 -1
  188. package/dist/service/usePageRoutes.js +671 -74
  189. package/dist/service/usePageRoutes.js.map +1 -1
  190. package/dist/service/useSharedDataRoutes.d.ts +4 -0
  191. package/dist/service/useSharedDataRoutes.d.ts.map +1 -0
  192. package/dist/service/useSharedDataRoutes.js +107 -0
  193. package/dist/service/useSharedDataRoutes.js.map +1 -0
  194. package/dist/service/useSharedFileRoutes.d.ts +4 -0
  195. package/dist/service/useSharedFileRoutes.d.ts.map +1 -0
  196. package/dist/service/useSharedFileRoutes.js +121 -0
  197. package/dist/service/useSharedFileRoutes.js.map +1 -0
  198. package/dist/settings.d.ts +5 -3
  199. package/dist/settings.d.ts.map +1 -1
  200. package/dist/settings.js +12 -10
  201. package/dist/settings.js.map +1 -1
  202. package/dist/storage/FsStorageProvider.d.ts +25 -0
  203. package/dist/storage/FsStorageProvider.d.ts.map +1 -0
  204. package/dist/storage/FsStorageProvider.js +103 -0
  205. package/dist/storage/FsStorageProvider.js.map +1 -0
  206. package/dist/storage/StorageProvider.d.ts +31 -0
  207. package/dist/storage/StorageProvider.d.ts.map +1 -0
  208. package/dist/storage/StorageProvider.js +3 -0
  209. package/dist/storage/StorageProvider.js.map +1 -0
  210. package/dist/storage/index.d.ts +3 -0
  211. package/dist/storage/index.d.ts.map +1 -0
  212. package/dist/storage/index.js +6 -0
  213. package/dist/storage/index.js.map +1 -0
  214. package/dist/synthos-cli.d.ts.map +1 -1
  215. package/dist/synthos-cli.js +4 -3
  216. package/dist/synthos-cli.js.map +1 -1
  217. package/dist/themes.d.ts +1 -0
  218. package/dist/themes.d.ts.map +1 -1
  219. package/dist/themes.js +65 -28
  220. package/dist/themes.js.map +1 -1
  221. package/migration-rules/v1-to-v2.md +193 -0
  222. package/migration-rules/v2-to-v3.md +481 -0
  223. package/package.json +11 -10
  224. package/required-pages/builder/page.html +43 -0
  225. package/required-pages/builder/page.json +10 -0
  226. package/required-pages/{pages.html → pages/page.html} +238 -233
  227. package/required-pages/pages/page.json +10 -0
  228. package/required-pages/{settings.html → settings/page.html} +389 -275
  229. package/required-pages/settings/page.json +10 -0
  230. package/required-pages/synthos_apis/page.html +846 -0
  231. package/required-pages/synthos_apis/page.json +10 -0
  232. package/required-pages/{synthos_scripts.html → synthos_scripts/page.html} +13 -11
  233. package/required-pages/synthos_scripts/page.json +10 -0
  234. package/src/agents/index.ts +1 -1
  235. package/src/agents/openclaw/gatewayManager.ts +22 -11
  236. package/src/agents/openclaw/openclawProvider.ts +2 -4
  237. package/src/agents/openclaw/sshTunnelManager.ts +19 -11
  238. package/src/builders/anthropic.ts +283 -0
  239. package/src/builders/fireworksai.ts +59 -0
  240. package/src/builders/index.ts +33 -0
  241. package/src/builders/openai.ts +89 -0
  242. package/src/builders/types.ts +261 -0
  243. package/src/connectors/index.ts +1 -1
  244. package/src/connectors/registry.ts +28 -8
  245. package/src/customizer/Customizer.ts +163 -0
  246. package/src/customizer/index.ts +5 -0
  247. package/src/files.ts +57 -0
  248. package/src/index.ts +3 -1
  249. package/src/init.ts +195 -145
  250. package/src/migrations.ts +30 -10
  251. package/src/models/anthropic.ts +40 -10
  252. package/src/models/fireworksai.ts +9 -2
  253. package/src/models/index.ts +1 -1
  254. package/src/models/openai.ts +26 -6
  255. package/src/models/types.ts +31 -1
  256. package/src/pages.ts +230 -77
  257. package/src/service/createCompletePrompt.ts +3 -2
  258. package/src/service/requiresSettings.ts +4 -3
  259. package/src/service/server.ts +36 -9
  260. package/src/service/transformPage.ts +557 -326
  261. package/src/service/useAgentRoutes.ts +19 -14
  262. package/src/service/useApiRoutes.ts +208 -84
  263. package/src/service/useConnectorRoutes.ts +18 -18
  264. package/src/service/useDataRoutes.ts +13 -10
  265. package/src/service/useFileRoutes.ts +128 -0
  266. package/src/service/usePageRoutes.ts +730 -81
  267. package/src/service/useSharedDataRoutes.ts +109 -0
  268. package/src/service/useSharedFileRoutes.ts +127 -0
  269. package/src/settings.ts +14 -10
  270. package/src/storage/FsStorageProvider.ts +87 -0
  271. package/src/storage/StorageProvider.ts +34 -0
  272. package/src/storage/index.ts +2 -0
  273. package/src/synthos-cli.ts +4 -3
  274. package/src/themes.ts +64 -27
  275. package/static-files/favicon.svg +12 -0
  276. package/static-files/fluentlm-instructions.llmd +868 -0
  277. package/static-files/fluentlm-instructions.md +1595 -0
  278. package/static-files/fluentlm.css +4844 -0
  279. package/static-files/fluentlm.js +3602 -0
  280. package/static-files/fluentlm.min.css +1 -0
  281. package/static-files/fluentlm.min.js +1 -0
  282. package/{page-scripts/helpers-v2.js → static-files/helpers.v3.js} +82 -0
  283. package/static-files/page.v3.js +1290 -0
  284. package/static-files/recommended-frameworks.llmd +81 -0
  285. package/static-files/recommended-frameworks.md +137 -0
  286. package/static-files/retro-game.js +877 -0
  287. package/static-files/shell.css +797 -0
  288. package/static-files/theme-dark.css +169 -0
  289. package/static-files/theme-light.css +169 -0
  290. package/tests/builders.spec.ts +139 -0
  291. package/tests/pages.spec.ts +54 -84
  292. package/tests/transformPage.spec.ts +299 -360
  293. package/default-pages/application.html +0 -40
  294. package/default-pages/application.json +0 -1
  295. package/default-pages/json_tools.json +0 -1
  296. package/default-pages/my_notes.html +0 -33
  297. package/default-pages/neon_asteroids.html +0 -77
  298. package/default-pages/sidebar_page.json +0 -1
  299. package/default-pages/solar_tutorial.json +0 -1
  300. package/default-pages/two-panel_page.json +0 -1
  301. package/dist/service/useGatewayRoutes.d.ts +0 -4
  302. package/dist/service/useGatewayRoutes.d.ts.map +0 -1
  303. package/dist/service/useGatewayRoutes.js +0 -168
  304. package/dist/service/useGatewayRoutes.js.map +0 -1
  305. package/page-scripts/page-v2.js +0 -656
  306. package/required-pages/builder.html +0 -48
  307. package/required-pages/builder.json +0 -1
  308. package/required-pages/pages.json +0 -1
  309. package/required-pages/settings.json +0 -1
  310. package/required-pages/synthos_apis.html +0 -327
  311. package/required-pages/synthos_apis.json +0 -1
  312. package/required-pages/synthos_scripts.json +0 -1
  313. package/src/connectors/airtable/connector.json +0 -27
  314. package/src/connectors/alpha-vantage/connector.json +0 -26
  315. package/src/connectors/brave-search/connector.json +0 -26
  316. package/src/connectors/cloudinary/connector.json +0 -27
  317. package/src/connectors/deepl/connector.json +0 -28
  318. package/src/connectors/elevenlabs/connector.json +0 -30
  319. package/src/connectors/giphy/connector.json +0 -27
  320. package/src/connectors/github/connector.json +0 -29
  321. package/src/connectors/huggingface/connector.json +0 -27
  322. package/src/connectors/imgur/connector.json +0 -29
  323. package/src/connectors/instagram/connector.json +0 -43
  324. package/src/connectors/jira/connector.json +0 -28
  325. package/src/connectors/mapbox/connector.json +0 -26
  326. package/src/connectors/nasa/connector.json +0 -27
  327. package/src/connectors/newsapi/connector.json +0 -27
  328. package/src/connectors/notion/connector.json +0 -28
  329. package/src/connectors/open-exchange-rates/connector.json +0 -27
  330. package/src/connectors/openweathermap/connector.json +0 -26
  331. package/src/connectors/pexels/connector.json +0 -27
  332. package/src/connectors/resend/connector.json +0 -29
  333. package/src/connectors/rss2json/connector.json +0 -27
  334. package/src/connectors/sendgrid/connector.json +0 -27
  335. package/src/connectors/spoonacular/connector.json +0 -28
  336. package/src/connectors/stability-ai/connector.json +0 -27
  337. package/src/connectors/twilio/connector.json +0 -28
  338. package/src/connectors/unsplash/connector.json +0 -27
  339. package/src/connectors/wolfram-alpha/connector.json +0 -26
  340. package/src/connectors/youtube-data/connector.json +0 -30
  341. /package/{dist/connectors → service-connectors}/airtable/connector.json +0 -0
  342. /package/{dist/connectors → service-connectors}/alpha-vantage/connector.json +0 -0
  343. /package/{dist/connectors → service-connectors}/brave-search/connector.json +0 -0
  344. /package/{dist/connectors → service-connectors}/cloudinary/connector.json +0 -0
  345. /package/{dist/connectors → service-connectors}/deepl/connector.json +0 -0
  346. /package/{dist/connectors → service-connectors}/elevenlabs/connector.json +0 -0
  347. /package/{dist/connectors → service-connectors}/giphy/connector.json +0 -0
  348. /package/{dist/connectors → service-connectors}/github/connector.json +0 -0
  349. /package/{dist/connectors → service-connectors}/huggingface/connector.json +0 -0
  350. /package/{dist/connectors → service-connectors}/imgur/connector.json +0 -0
  351. /package/{dist/connectors → service-connectors}/instagram/connector.json +0 -0
  352. /package/{dist/connectors → service-connectors}/jira/connector.json +0 -0
  353. /package/{dist/connectors → service-connectors}/mapbox/connector.json +0 -0
  354. /package/{dist/connectors → service-connectors}/nasa/connector.json +0 -0
  355. /package/{dist/connectors → service-connectors}/newsapi/connector.json +0 -0
  356. /package/{dist/connectors → service-connectors}/notion/connector.json +0 -0
  357. /package/{dist/connectors → service-connectors}/open-exchange-rates/connector.json +0 -0
  358. /package/{dist/connectors → service-connectors}/openweathermap/connector.json +0 -0
  359. /package/{dist/connectors → service-connectors}/pexels/connector.json +0 -0
  360. /package/{dist/connectors → service-connectors}/resend/connector.json +0 -0
  361. /package/{dist/connectors → service-connectors}/rss2json/connector.json +0 -0
  362. /package/{dist/connectors → service-connectors}/sendgrid/connector.json +0 -0
  363. /package/{dist/connectors → service-connectors}/spoonacular/connector.json +0 -0
  364. /package/{dist/connectors → service-connectors}/stability-ai/connector.json +0 -0
  365. /package/{dist/connectors → service-connectors}/twilio/connector.json +0 -0
  366. /package/{dist/connectors → service-connectors}/unsplash/connector.json +0 -0
  367. /package/{dist/connectors → service-connectors}/wolfram-alpha/connector.json +0 -0
  368. /package/{dist/connectors → service-connectors}/youtube-data/connector.json +0 -0
package/src/files.ts CHANGED
@@ -72,3 +72,60 @@ export async function copyFolderRecursive(srcFolder: string, destFolder: string)
72
72
  export async function deleteFolder(dirPath: string): Promise<void> {
73
73
  await fs.rm(dirPath, { recursive: true });
74
74
  }
75
+
76
+ // --- Multi-folder helpers ---
77
+
78
+ /**
79
+ * Search folders in order, return the full path to the first existing match
80
+ * for the given filename.
81
+ */
82
+ export async function findFileInFolders(folders: string[], filename: string): Promise<string | undefined> {
83
+ for (const folder of folders) {
84
+ const candidate = path.join(folder, filename);
85
+ if (await checkIfExists(candidate)) {
86
+ return candidate;
87
+ }
88
+ }
89
+ return undefined;
90
+ }
91
+
92
+ /**
93
+ * Merge file listings from multiple folders. First folder takes priority on
94
+ * name collisions (earlier occurrence wins).
95
+ */
96
+ export async function listFilesFromFolders(folders: string[]): Promise<string[]> {
97
+ const seen = new Set<string>();
98
+ const result: string[] = [];
99
+ for (const folder of folders) {
100
+ if (!await checkIfExists(folder)) continue;
101
+ const files = await listFiles(folder);
102
+ for (const f of files) {
103
+ if (!seen.has(f)) {
104
+ seen.add(f);
105
+ result.push(f);
106
+ }
107
+ }
108
+ }
109
+ return result;
110
+ }
111
+
112
+ /**
113
+ * Copy files from multiple source folders into a single destination.
114
+ * First folder takes priority on duplicate filenames (copy is skipped
115
+ * if the file already exists in dest from an earlier folder).
116
+ */
117
+ export async function copyFilesFromFolders(folders: string[], destFolder: string): Promise<void> {
118
+ await ensureFolderExists(destFolder);
119
+ const copied = new Set<string>();
120
+ for (const folder of folders) {
121
+ if (!await checkIfExists(folder)) continue;
122
+ const files = await fs.readdir(folder);
123
+ for (const file of files) {
124
+ if (copied.has(file)) continue;
125
+ copied.add(file);
126
+ const srcPath = path.join(folder, file);
127
+ const destPath = path.join(destFolder, file);
128
+ await fs.copyFile(srcPath, destPath);
129
+ }
130
+ }
131
+ }
package/src/index.ts CHANGED
@@ -3,4 +3,6 @@ export * from './files';
3
3
  export * from './init';
4
4
  export * from './pages';
5
5
  export * from './scripts';
6
- export * from './settings';
6
+ export * from './settings';
7
+ export * from './customizer';
8
+ export * from './storage';
package/src/init.ts CHANGED
@@ -1,159 +1,165 @@
1
1
  import * as fs from 'fs/promises';
2
2
  import path from "path";
3
- import { checkIfExists, copyFile, copyFiles, deleteFile, ensureFolderExists, listFiles, saveFile } from "./files";
4
- import { PAGE_VERSION } from "./pages";
3
+ import { checkIfExists, findFileInFolders, listFolders } from "./files";
4
+ import { PAGE_VERSION, getRequiredPages } from "./pages";
5
5
  import { DefaultSettings } from "./settings";
6
- import { getOutdatedThemes, parseThemeFilename } from "./themes";
6
+ import { Customizer } from './customizer';
7
+ import { StorageProvider, FsStorageProvider } from './storage';
7
8
 
8
9
  export interface SynthOSConfig {
10
+ localFolder: string;
9
11
  pagesFolder: string;
10
- requiredPagesFolder: string;
11
- defaultPagesFolder: string;
12
- defaultScriptsFolder: string;
13
- defaultThemesFolder: string;
14
- pageScriptsFolder: string;
12
+ requiredPagesFolders: string[];
13
+ defaultPagesFolders: string[];
14
+ defaultScriptsFolders: string[];
15
+ defaultThemesFolders: string[];
16
+ staticFilesFolders: string[];
17
+ serviceConnectorsFolders: string[];
18
+ requiredPages: string[];
19
+ storageProvider: StorageProvider;
15
20
  debug: boolean;
16
21
  debugPageUpdates: boolean;
17
22
  }
18
23
 
19
- export function createConfig(pagesFolder = '.synthos', options?: { debug?: boolean; debugPageUpdates?: boolean }): SynthOSConfig {
24
+ /**
25
+ * Resolve a folder array from a Customizer getter: replace the `'default'`
26
+ * sentinel with the built-in SynthOS path.
27
+ */
28
+ function resolveFolders(folders: string[], builtInPath: string): string[] {
29
+ return folders.map(f => f === 'default' ? builtInPath : f);
30
+ }
31
+
32
+ export async function createConfig(
33
+ pagesFolder = '.synthos',
34
+ options?: { debug?: boolean; debugPageUpdates?: boolean },
35
+ customizer?: Customizer
36
+ ): Promise<SynthOSConfig> {
37
+ const requiredPagesFolders = resolveFolders(
38
+ customizer?.requiredPagesFolders ?? ['default'],
39
+ path.join(__dirname, '../required-pages')
40
+ );
41
+ const requiredPages = await getRequiredPages(requiredPagesFolders);
20
42
  return {
43
+ localFolder: pagesFolder,
21
44
  pagesFolder: path.join(process.cwd(), pagesFolder),
22
- requiredPagesFolder: path.join(__dirname, '../required-pages'),
23
- defaultPagesFolder: path.join(__dirname, '../default-pages'),
24
- defaultScriptsFolder: path.join(__dirname, '../default-scripts'),
25
- defaultThemesFolder: path.join(__dirname, '../default-themes'),
26
- pageScriptsFolder: path.join(__dirname, '../page-scripts'),
45
+ requiredPagesFolders,
46
+ defaultPagesFolders: resolveFolders(
47
+ customizer?.defaultPagesFolders ?? ['default'],
48
+ path.join(__dirname, '../default-pages')
49
+ ),
50
+ defaultScriptsFolders: resolveFolders(
51
+ customizer?.defaultScriptsFolders ?? ['default'],
52
+ path.join(__dirname, '../default-scripts')
53
+ ),
54
+ defaultThemesFolders: resolveFolders(
55
+ customizer?.defaultThemesFolders ?? ['default'],
56
+ path.join(__dirname, '../default-themes')
57
+ ),
58
+ staticFilesFolders: resolveFolders(
59
+ customizer?.staticFilesFolders ?? ['default'],
60
+ path.join(__dirname, '../static-files')
61
+ ),
62
+ serviceConnectorsFolders: resolveFolders(
63
+ customizer?.serviceConnectorsFolders ?? ['default'],
64
+ path.join(__dirname, '../service-connectors')
65
+ ),
66
+ requiredPages,
67
+ storageProvider: customizer?.storageProvider ?? new FsStorageProvider(),
27
68
  debug: options?.debug ?? false,
28
69
  debugPageUpdates: options?.debugPageUpdates ?? false
29
70
  };
30
71
  }
31
72
 
32
73
  export async function init(config: SynthOSConfig, includeDefaultPages: boolean = true): Promise<boolean> {
74
+ const sp = config.storageProvider;
75
+
33
76
  // Check for existing folder
34
- if (await checkIfExists(config.pagesFolder)) {
77
+ if (await sp.checkIfExists(config.pagesFolder)) {
35
78
  await repairMissingFolders(config);
36
79
  return false;
37
80
  }
38
81
 
39
- console.log(`Initializing .synthos folder...`);
82
+ console.log(`Initializing ${config.localFolder} folder...`);
40
83
 
41
84
  // Create pages folder
42
- await ensureFolderExists(config.pagesFolder);
85
+ await sp.ensureFolderExists(config.pagesFolder);
43
86
 
44
87
  // Create mandatory files
45
- await saveFile(path.join(config.pagesFolder, '.gitignore'), 'settings.json\n');
46
- await saveFile(path.join(config.pagesFolder, 'settings.json'), JSON.stringify(DefaultSettings, null, 4));
47
- await saveFile(path.join(config.pagesFolder, 'settings.json.example'), JSON.stringify(DefaultSettings, null, 4));
88
+ await sp.saveFile(path.join(config.pagesFolder, '.gitignore'), 'settings.json\n');
89
+ await sp.saveFile(path.join(config.pagesFolder, 'settings.json'), JSON.stringify(DefaultSettings, null, 4));
90
+ await sp.saveFile(path.join(config.pagesFolder, 'settings.json.example'), JSON.stringify(DefaultSettings, null, 4));
48
91
 
49
92
  // Setup default scripts
50
- console.log(`Copying default scripts to .synthos folder...`);
93
+ console.log(`Copying default scripts to ${config.localFolder} folder...`);
51
94
  const scriptsFolder = path.join(config.pagesFolder, 'scripts');
52
- await ensureFolderExists(scriptsFolder);
53
- switch (process.platform) {
54
- case 'win32':
55
- await copyFile(path.join(config.defaultScriptsFolder, 'windows-terminal.json'), scriptsFolder);
56
- break;
57
- case 'darwin':
58
- await copyFile(path.join(config.defaultScriptsFolder, 'mac-terminal.json'), scriptsFolder);
59
- break;
60
- case 'android':
61
- await copyFile(path.join(config.defaultScriptsFolder, 'android-terminal.json'), scriptsFolder);
62
- break;
63
- case 'linux':
64
- default:
65
- await copyFile(path.join(config.defaultScriptsFolder, 'linux-terminal.json'), scriptsFolder);
66
- break;
67
- }
68
-
69
- await saveFile(path.join(scriptsFolder, 'example.sh'), '#!/bin/bash\n\n# This is an example script\n\n# You can run this script using the following command:\n# sh .synthos/scripts/example.sh\n\n# This script will print "Hello, World!" to the console\n\necho "Hello, World!"\n');
70
-
71
- // Setup default themes
72
- console.log(`Copying default themes to .synthos folder...`);
73
- const themesFolder = path.join(config.pagesFolder, 'themes');
74
- await ensureFolderExists(themesFolder);
75
- await copyFiles(config.defaultThemesFolder, themesFolder);
95
+ await sp.ensureFolderExists(scriptsFolder);
96
+ const scriptFilename = ({
97
+ win32: 'windows-terminal.json',
98
+ darwin: 'mac-terminal.json',
99
+ android: 'android-terminal.json',
100
+ } as Record<string, string>)[process.platform] ?? 'linux-terminal.json';
101
+ const scriptSrc = await findFileInFolders(config.defaultScriptsFolders, scriptFilename);
102
+ if (scriptSrc) {
103
+ // Read from package (local fs), write to user storage (provider)
104
+ const data = await fs.readFile(scriptSrc);
105
+ await sp.saveBuffer(path.join(scriptsFolder, path.basename(scriptSrc)), data);
106
+ }
107
+
108
+ await sp.saveFile(path.join(scriptsFolder, 'example.sh'), `#!/bin/bash\n\n# This is an example script\n\n# You can run this script using the following command:\n# sh ${config.localFolder}/scripts/example.sh\n\n# This script will print "Hello, World!" to the console\n\necho "Hello, World!"\n`);
109
+
110
+ // Create empty themes folder — default themes are served directly from
111
+ // defaultThemesFolders; users can add custom themes here.
112
+ await sp.ensureFolderExists(path.join(config.pagesFolder, 'themes'));
76
113
 
77
114
  // Copy pages
78
115
  if (includeDefaultPages) {
79
- console.log(`Copying default pages to .synthos folder...`);
80
- await copyDefaultPages(config.defaultPagesFolder, config.pagesFolder);
116
+ console.log(`Copying default pages to ${config.localFolder} folder...`);
117
+ await copyDefaultPages(config, config.defaultPagesFolders);
81
118
  }
82
119
 
83
120
  return true;
84
121
  }
85
122
 
86
123
  async function repairMissingFolders(config: SynthOSConfig): Promise<void> {
124
+ const sp = config.storageProvider;
125
+
87
126
  // Rebuild scripts folder from defaults if missing
88
127
  const scriptsFolder = path.join(config.pagesFolder, 'scripts');
89
- if (!await checkIfExists(scriptsFolder)) {
90
- console.log(`Restoring default scripts to .synthos folder...`);
91
- await ensureFolderExists(scriptsFolder);
92
- switch (process.platform) {
93
- case 'win32':
94
- await copyFile(path.join(config.defaultScriptsFolder, 'windows-terminal.json'), scriptsFolder);
95
- break;
96
- case 'darwin':
97
- await copyFile(path.join(config.defaultScriptsFolder, 'mac-terminal.json'), scriptsFolder);
98
- break;
99
- case 'android':
100
- await copyFile(path.join(config.defaultScriptsFolder, 'android-terminal.json'), scriptsFolder);
101
- break;
102
- case 'linux':
103
- default:
104
- await copyFile(path.join(config.defaultScriptsFolder, 'linux-terminal.json'), scriptsFolder);
105
- break;
128
+ if (!await sp.checkIfExists(scriptsFolder)) {
129
+ console.log(`Restoring default scripts to ${config.localFolder} folder...`);
130
+ await sp.ensureFolderExists(scriptsFolder);
131
+ const scriptFilename = ({
132
+ win32: 'windows-terminal.json',
133
+ darwin: 'mac-terminal.json',
134
+ android: 'android-terminal.json',
135
+ } as Record<string, string>)[process.platform] ?? 'linux-terminal.json';
136
+ const scriptSrc = await findFileInFolders(config.defaultScriptsFolders, scriptFilename);
137
+ if (scriptSrc) {
138
+ const data = await fs.readFile(scriptSrc);
139
+ await sp.saveBuffer(path.join(scriptsFolder, path.basename(scriptSrc)), data);
106
140
  }
107
- await saveFile(path.join(scriptsFolder, 'example.sh'), '#!/bin/bash\n\n# This is an example script\n\n# You can run this script using the following command:\n# sh .synthos/scripts/example.sh\n\n# This script will print "Hello, World!" to the console\n\necho "Hello, World!"\n');
141
+ await sp.saveFile(path.join(scriptsFolder, 'example.sh'), `#!/bin/bash\n\n# This is an example script\n\n# You can run this script using the following command:\n# sh ${config.localFolder}/scripts/example.sh\n\n# This script will print "Hello, World!" to the console\n\necho "Hello, World!"\n`);
108
142
  }
109
143
 
110
- // Rebuild themes folder from defaults if missing
111
- const themesFolder = path.join(config.pagesFolder, 'themes');
112
- if (!await checkIfExists(themesFolder)) {
113
- console.log(`Restoring default themes to .synthos folder...`);
114
- await ensureFolderExists(themesFolder);
115
- await copyFiles(config.defaultThemesFolder, themesFolder);
116
- } else {
117
- // Upgrade outdated themes — copy newer versioned CSS from defaults
118
- const outdated = await getOutdatedThemes(config);
119
- if (outdated.length > 0) {
120
- console.log(`Upgrading ${outdated.length} theme(s): ${outdated.join(', ')}...`);
121
- const defaultFiles = await listFiles(config.defaultThemesFolder);
122
- for (const themeName of outdated) {
123
- // Remove old versioned CSS files for this theme
124
- const localFiles = await listFiles(themesFolder);
125
- for (const f of localFiles) {
126
- const parsed = parseThemeFilename(f);
127
- if (parsed && parsed.name === themeName) {
128
- await deleteFile(path.join(themesFolder, f));
129
- }
130
- }
131
- // Copy the new versioned CSS from defaults
132
- for (const f of defaultFiles) {
133
- const parsed = parseThemeFilename(f);
134
- if (parsed && parsed.name === themeName) {
135
- await copyFile(path.join(config.defaultThemesFolder, f), themesFolder);
136
- }
137
- }
138
- }
139
- }
140
- }
144
+ // Ensure themes folder exists default themes are served directly from
145
+ // defaultThemesFolders; this folder is for user-added custom themes only.
146
+ await sp.ensureFolderExists(path.join(config.pagesFolder, 'themes'));
141
147
 
142
148
  // Ensure pages/ subfolder exists
143
149
  const pagesSubdir = path.join(config.pagesFolder, 'pages');
144
- if (!await checkIfExists(pagesSubdir)) {
150
+ if (!await sp.checkIfExists(pagesSubdir)) {
145
151
  // No pages folder and no flat files — rebuild from defaults
146
- const htmlFiles = (await listFiles(config.pagesFolder)).filter(f => f.endsWith('.html'));
152
+ const htmlFiles = (await sp.listFiles(config.pagesFolder)).filter(f => f.endsWith('.html'));
147
153
  if (htmlFiles.length === 0) {
148
- console.log(`Restoring default pages to .synthos/pages/ folder...`);
149
- await copyDefaultPages(config.defaultPagesFolder, config.pagesFolder);
154
+ console.log(`Restoring default pages to ${config.localFolder}/pages/ folder...`);
155
+ await copyDefaultPages(config, config.defaultPagesFolders);
150
156
  } else {
151
- await ensureFolderExists(pagesSubdir);
157
+ await sp.ensureFolderExists(pagesSubdir);
152
158
  }
153
159
  }
154
160
 
155
161
  // Migrate any stray flat .html files from root into pages/<name>/
156
- await migrateFlatPages(config.pagesFolder);
162
+ await migrateFlatPages(config);
157
163
  }
158
164
 
159
165
  function toTitleCase(name: string): string {
@@ -164,13 +170,15 @@ function toTitleCase(name: string): string {
164
170
  .replace(/\b\w/g, c => c.toUpperCase());
165
171
  }
166
172
 
167
- async function migrateFlatPages(pagesFolder: string): Promise<void> {
173
+ async function migrateFlatPages(config: SynthOSConfig): Promise<void> {
174
+ const sp = config.storageProvider;
175
+ const pagesFolder = config.pagesFolder;
168
176
  const pagesSubdir = path.join(pagesFolder, 'pages');
169
- const htmlFiles = (await listFiles(pagesFolder)).filter(f => f.endsWith('.html'));
177
+ const htmlFiles = (await sp.listFiles(pagesFolder)).filter(f => f.endsWith('.html'));
170
178
  if (htmlFiles.length === 0) return;
171
179
 
172
- console.log(`Migrating ${htmlFiles.length} page(s) to .synthos/pages/ folder...`);
173
- await ensureFolderExists(pagesSubdir);
180
+ console.log(`Migrating ${htmlFiles.length} page(s) to ${config.localFolder}/pages/ folder...`);
181
+ await sp.ensureFolderExists(pagesSubdir);
174
182
  const now = new Date().toISOString();
175
183
 
176
184
  for (const file of htmlFiles) {
@@ -178,12 +186,11 @@ async function migrateFlatPages(pagesFolder: string): Promise<void> {
178
186
  const category = pageName.startsWith('[') ? 'Builder' : 'Pages';
179
187
  const title = toTitleCase(pageName);
180
188
  const pageFolder = path.join(pagesSubdir, pageName);
181
- await ensureFolderExists(pageFolder);
182
- await fs.copyFile(
183
- path.join(pagesFolder, file),
184
- path.join(pageFolder, 'page.html')
185
- );
186
- await saveFile(
189
+ await sp.ensureFolderExists(pageFolder);
190
+ // Both source and destination are user storage
191
+ const content = await sp.loadFile(path.join(pagesFolder, file));
192
+ await sp.saveFile(path.join(pageFolder, 'page.html'), content);
193
+ await sp.saveFile(
187
194
  path.join(pageFolder, 'page.json'),
188
195
  JSON.stringify({
189
196
  title,
@@ -196,44 +203,87 @@ async function migrateFlatPages(pagesFolder: string): Promise<void> {
196
203
  mode: 'unlocked',
197
204
  }, null, 4)
198
205
  );
199
- await deleteFile(path.join(pagesFolder, file));
206
+ await sp.deleteFile(path.join(pagesFolder, file));
200
207
  }
201
208
  }
202
209
 
203
- async function copyDefaultPages(srcFolder: string, destFolder: string): Promise<void> {
204
- const pagesDir = path.join(destFolder, 'pages');
205
- await ensureFolderExists(pagesDir);
206
- const files = await fs.readdir(srcFolder);
210
+ async function copyDefaultPages(config: SynthOSConfig, srcFolders: string[]): Promise<void> {
211
+ const sp = config.storageProvider;
212
+ const pagesDir = path.join(config.pagesFolder, 'pages');
213
+ await sp.ensureFolderExists(pagesDir);
207
214
  const now = new Date().toISOString();
208
- for (const file of files) {
209
- if (!file.endsWith('.html')) continue;
210
- const pageName = file.replace(/\.html$/, '');
211
- const pageFolder = path.join(pagesDir, pageName);
212
- await ensureFolderExists(pageFolder);
213
- await fs.copyFile(path.join(srcFolder, file), path.join(pageFolder, 'page.html'));
214
-
215
- // Read companion .json metadata from source folder, fall back to defaults
216
- let metadata: Record<string, unknown> = {};
217
- const jsonPath = path.join(srcFolder, `${pageName}.json`);
218
- if (await checkIfExists(jsonPath)) {
219
- try {
220
- const raw = await fs.readFile(jsonPath, 'utf-8');
221
- metadata = JSON.parse(raw);
222
- } catch {
223
- // use defaults
215
+ const seen = new Set<string>();
216
+
217
+ for (const srcFolder of srcFolders) {
218
+ if (!await checkIfExists(srcFolder)) continue; // source is always local fs
219
+ const dirs = await listFolders(srcFolder); // source is always local fs
220
+ for (const dir of dirs) {
221
+ const srcPageDir = path.join(srcFolder, dir);
222
+ if (!await checkIfExists(path.join(srcPageDir, 'page.html'))) continue;
223
+ if (seen.has(dir)) continue; // first folder wins
224
+ seen.add(dir);
225
+
226
+ const pageFolder = path.join(pagesDir, dir);
227
+ await sp.ensureFolderExists(pageFolder);
228
+
229
+ // Read from local fs, write via provider
230
+ const htmlData = await fs.readFile(path.join(srcPageDir, 'page.html'));
231
+ await sp.saveBuffer(path.join(pageFolder, 'page.html'), htmlData);
232
+
233
+ // Read companion page.json metadata from source folder, fall back to defaults
234
+ let metadata: Record<string, unknown> = {};
235
+ const jsonPath = path.join(srcPageDir, 'page.json');
236
+ if (await checkIfExists(jsonPath)) {
237
+ try {
238
+ const raw = await fs.readFile(jsonPath, 'utf-8');
239
+ metadata = JSON.parse(raw);
240
+ } catch {
241
+ // use defaults
242
+ }
243
+ }
244
+ const fullMetadata = {
245
+ title: typeof metadata.title === 'string' ? metadata.title : '',
246
+ categories: Array.isArray(metadata.categories) ? metadata.categories : [],
247
+ pinned: typeof metadata.pinned === 'boolean' ? metadata.pinned : false,
248
+ showInAll: typeof metadata.showInAll === 'boolean' ? metadata.showInAll : true,
249
+ createdDate: now,
250
+ lastModified: now,
251
+ pageVersion: typeof metadata.pageVersion === 'number' ? metadata.pageVersion
252
+ : typeof metadata.uxVersion === 'number' ? metadata.uxVersion : PAGE_VERSION,
253
+ mode: metadata.mode === 'locked' ? 'locked' : 'unlocked',
254
+ };
255
+ await sp.saveFile(path.join(pageFolder, 'page.json'), JSON.stringify(fullMetadata, null, 4));
256
+
257
+ // Copy data subfolders (anything that isn't page.html/page.json)
258
+ // Read from local fs, write via provider using cross-provider copy
259
+ const subEntries = await fs.readdir(srcPageDir, { withFileTypes: true });
260
+ for (const entry of subEntries) {
261
+ if (!entry.isDirectory()) continue;
262
+ await crossProviderCopyFolder(
263
+ path.join(srcPageDir, entry.name),
264
+ path.join(pageFolder, entry.name),
265
+ sp
266
+ );
224
267
  }
225
268
  }
226
- const fullMetadata = {
227
- title: typeof metadata.title === 'string' ? metadata.title : '',
228
- categories: Array.isArray(metadata.categories) ? metadata.categories : [],
229
- pinned: typeof metadata.pinned === 'boolean' ? metadata.pinned : false,
230
- showInAll: typeof metadata.showInAll === 'boolean' ? metadata.showInAll : true,
231
- createdDate: now,
232
- lastModified: now,
233
- pageVersion: typeof metadata.pageVersion === 'number' ? metadata.pageVersion
234
- : typeof metadata.uxVersion === 'number' ? metadata.uxVersion : PAGE_VERSION,
235
- mode: metadata.mode === 'locked' ? 'locked' : 'unlocked',
236
- };
237
- await saveFile(path.join(pageFolder, 'page.json'), JSON.stringify(fullMetadata, null, 4));
269
+ }
270
+ }
271
+
272
+ /**
273
+ * Copy a folder from local filesystem to the storage provider.
274
+ * Reads from local fs, writes via provider.
275
+ */
276
+ async function crossProviderCopyFolder(srcFolder: string, destFolder: string, sp: StorageProvider): Promise<void> {
277
+ await sp.ensureFolderExists(destFolder);
278
+ const entries = await fs.readdir(srcFolder, { withFileTypes: true });
279
+ for (const entry of entries) {
280
+ const srcPath = path.join(srcFolder, entry.name);
281
+ const destPath = path.join(destFolder, entry.name);
282
+ if (entry.isDirectory()) {
283
+ await crossProviderCopyFolder(srcPath, destPath, sp);
284
+ } else {
285
+ const data = await fs.readFile(srcPath);
286
+ await sp.saveBuffer(destPath, data);
287
+ }
238
288
  }
239
289
  }
package/src/migrations.ts CHANGED
@@ -10,6 +10,7 @@ import { deduplicateInlineScripts } from './service/transformPage';
10
10
  */
11
11
  const migrations: Record<number, (html: string, completePrompt: completePrompt) => Promise<string>> = {
12
12
  1: migrateV1toV2,
13
+ 2: migrateV2toV3,
13
14
  };
14
15
 
15
16
  /**
@@ -36,7 +37,6 @@ const SHARED_CSS_SELECTORS = [
36
37
  '.chat-panel', '.chat-header', '.chat-messages',
37
38
  '.chat-message', '.chat-message p', '.chat-message p strong', '.chat-message p code',
38
39
  '.chat-message strong', '.chat-message pre', '.chat-message code', '.chat-message a',
39
- '.link-group', '.link-group a', '.link-group a:hover',
40
40
  'form',
41
41
  '.chat-input', '.chat-input:focus', '.chat-input::placeholder', '.chat-input:disabled',
42
42
  '.chat-submit', '.chat-submit:hover', '.chat-submit:active', '.chat-submit:disabled',
@@ -80,14 +80,8 @@ const DEFAULT_CHAT_PANEL = `
80
80
  <div class="chat-messages" id="chatMessages">
81
81
  <div class="chat-message"><p>Welcome! How can I help you?</p></div>
82
82
  </div>
83
- <div class="link-group">
84
- <a href="#" id="saveLink">Save</a>
85
- <a href="/pages" id="pagesLink">Pages</a>
86
- <a href="#" id="resetLink">Reset</a>
87
- </div>
88
83
  <form action="/" method="POST" id="chatForm">
89
- <input type="text" class="chat-input" id="chatInput" name="message" placeholder="Type a message...">
90
- <button type="submit" class="chat-submit">Send</button>
84
+ <textarea class="chat-input" id="chatInput" name="message" rows="2" placeholder="Type a message..."></textarea>
91
85
  </form>
92
86
  </div>`;
93
87
 
@@ -125,6 +119,33 @@ async function migrateV1toV2(html: string, completePrompt: completePrompt): Prom
125
119
  return migrated;
126
120
  }
127
121
 
122
+ /**
123
+ * v2 -> v3: Cheerio-based migration (no LLM).
124
+ * - Removes .link-group div
125
+ * - Converts chat <input> to <textarea>
126
+ * - Removes .chat-submit button (v3 creates it dynamically)
127
+ */
128
+ async function migrateV2toV3(html: string, _completePrompt: completePrompt): Promise<string> {
129
+ const $ = cheerio.load(html, { decodeEntities: false });
130
+
131
+ // 1. Remove .link-group div and its children
132
+ $('.link-group').remove();
133
+
134
+ // 2. Replace <input type="text" id="chatInput" ...> with <textarea>
135
+ const chatInput = $('input#chatInput, input.chat-input');
136
+ if (chatInput.length > 0) {
137
+ chatInput.replaceWith(
138
+ '<textarea class="chat-input" id="chatInput" name="message" rows="2" placeholder="Type a message..."></textarea>'
139
+ );
140
+ }
141
+
142
+ // 3. Remove .chat-submit button inside #chatForm (or anywhere)
143
+ $('button.chat-submit').remove();
144
+
145
+ // Run through postProcessV2 to ensure structural integrity
146
+ return postProcessV2($.html());
147
+ }
148
+
128
149
  /**
129
150
  * Cheerio-based post-processing to verify the LLM output meets v2 requirements.
130
151
  * Uses the original HTML as a fallback source for critical elements.
@@ -146,8 +167,7 @@ export function postProcessV2(html: string, originalHtml?: string): string {
146
167
  // Append default form
147
168
  $('.chat-panel').append(`
148
169
  <form action="/" method="POST" id="chatForm">
149
- <input type="text" class="chat-input" id="chatInput" name="message" placeholder="Type a message...">
150
- <button type="submit" class="chat-submit">Send</button>
170
+ <textarea class="chat-input" id="chatInput" name="message" rows="2" placeholder="Type a message..."></textarea>
151
171
  </form>`);
152
172
  }
153
173
  } else {