@fluid-app/fluid-cli-theme-dev 0.1.59 → 0.1.61

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 (252) hide show
  1. package/README.md +109 -0
  2. package/dist/dist-cli-preview/.vite/manifest.json +1831 -0
  3. package/dist/dist-cli-preview/account-screens.js +1 -0
  4. package/dist/dist-cli-preview/capabilities.json +1 -0
  5. package/dist/dist-cli-preview/chunks/Breadcrumb-CZ-skiUy.js +91 -0
  6. package/dist/dist-cli-preview/chunks/Combobox-BGiCYa56.js +1455 -0
  7. package/dist/dist-cli-preview/chunks/ContactsScreen-vsNgy_gn.js +6930 -0
  8. package/dist/dist-cli-preview/chunks/MemberManagementProviders-DMwkh3JW.js +1509 -0
  9. package/dist/dist-cli-preview/chunks/MessagingScreen-DzJOwPGH.js +38475 -0
  10. package/dist/dist-cli-preview/chunks/MobileActionSheet-DqVPgFKI.js +2895 -0
  11. package/dist/dist-cli-preview/chunks/MySiteScreen-BSmWlCKA.js +4950 -0
  12. package/dist/dist-cli-preview/chunks/OrdersScreen-CVEl9FnN.js +1349 -0
  13. package/dist/dist-cli-preview/chunks/OrdersScreen-d4fJE_dm.js +9 -0
  14. package/dist/dist-cli-preview/chunks/ProfileScreen-BC5gyJDY.js +2581 -0
  15. package/dist/dist-cli-preview/chunks/SubscriptionsScreen-CFM4Pufg.js +38 -0
  16. package/dist/dist-cli-preview/chunks/account-screen-runtime-DxhJm6Qd.js +12407 -0
  17. package/dist/dist-cli-preview/chunks/ar-4OhalXtk.js +628 -0
  18. package/dist/dist-cli-preview/chunks/ar-8iaFSgop.js +189 -0
  19. package/dist/dist-cli-preview/chunks/ar-B7NQmkZ1.js +341 -0
  20. package/dist/dist-cli-preview/chunks/ar-DEUMqcX3.js +563 -0
  21. package/dist/dist-cli-preview/chunks/ar-DT9j-dOi.js +720 -0
  22. package/dist/dist-cli-preview/chunks/ar-D_B30x0o.js +176 -0
  23. package/dist/dist-cli-preview/chunks/arrow-left-UFKI-PQf.js +14 -0
  24. package/dist/dist-cli-preview/chunks/bg-CIgMfa9k.js +176 -0
  25. package/dist/dist-cli-preview/chunks/bg-CS2zt-UV.js +720 -0
  26. package/dist/dist-cli-preview/chunks/bg-CxfL8rZN.js +341 -0
  27. package/dist/dist-cli-preview/chunks/bg-DOdbUYGw.js +563 -0
  28. package/dist/dist-cli-preview/chunks/bg-D_C_HmWn.js +189 -0
  29. package/dist/dist-cli-preview/chunks/bg-DeBjzn9B.js +628 -0
  30. package/dist/dist-cli-preview/chunks/bn-BAGJspTJ.js +563 -0
  31. package/dist/dist-cli-preview/chunks/bn-BgnBXWHo.js +176 -0
  32. package/dist/dist-cli-preview/chunks/bn-CRBwi_3N.js +628 -0
  33. package/dist/dist-cli-preview/chunks/bn-CdhHCK--.js +341 -0
  34. package/dist/dist-cli-preview/chunks/bn-DiqHsF7K.js +720 -0
  35. package/dist/dist-cli-preview/chunks/bn-UEAt87N9.js +189 -0
  36. package/dist/dist-cli-preview/chunks/cli-preview-DunPfnjU.js +151 -0
  37. package/dist/dist-cli-preview/chunks/client.gen-CSTBgfxj.js +672 -0
  38. package/dist/dist-cli-preview/chunks/contacts-CpYlEgAq.js +4 -0
  39. package/dist/dist-cli-preview/chunks/countries-api-adapter-B3usO25w.js +108 -0
  40. package/dist/dist-cli-preview/chunks/create-card-entry-container-DHgsbSyr.js +23942 -0
  41. package/dist/dist-cli-preview/chunks/cs-B8rJsMOS.js +189 -0
  42. package/dist/dist-cli-preview/chunks/cs-ChKaPMtV.js +563 -0
  43. package/dist/dist-cli-preview/chunks/cs-CkdXQxaf.js +720 -0
  44. package/dist/dist-cli-preview/chunks/cs-DTe-xGeI.js +341 -0
  45. package/dist/dist-cli-preview/chunks/cs-DmLSumGB.js +176 -0
  46. package/dist/dist-cli-preview/chunks/cs-jXPm6Nhj.js +628 -0
  47. package/dist/dist-cli-preview/chunks/da-7uI_CROE.js +628 -0
  48. package/dist/dist-cli-preview/chunks/da-B8Cl_vkl.js +341 -0
  49. package/dist/dist-cli-preview/chunks/da-Bee-xQNq.js +563 -0
  50. package/dist/dist-cli-preview/chunks/da-DYlEkykt.js +720 -0
  51. package/dist/dist-cli-preview/chunks/da-EfkyseOR.js +176 -0
  52. package/dist/dist-cli-preview/chunks/da-J--1RxZp.js +189 -0
  53. package/dist/dist-cli-preview/chunks/de-BTNiErdB.js +189 -0
  54. package/dist/dist-cli-preview/chunks/de-CIhZnrdS.js +720 -0
  55. package/dist/dist-cli-preview/chunks/de-CKtChtTF.js +628 -0
  56. package/dist/dist-cli-preview/chunks/de-D8cYy8G-.js +341 -0
  57. package/dist/dist-cli-preview/chunks/de-EARi-xk5.js +176 -0
  58. package/dist/dist-cli-preview/chunks/de-zeOnL7Nl.js +563 -0
  59. package/dist/dist-cli-preview/chunks/el-0NNTwBnJ.js +563 -0
  60. package/dist/dist-cli-preview/chunks/el-BPz4rb5a.js +176 -0
  61. package/dist/dist-cli-preview/chunks/el-Bg6zRbVK.js +720 -0
  62. package/dist/dist-cli-preview/chunks/el-CUjODD23.js +628 -0
  63. package/dist/dist-cli-preview/chunks/el-Chjin2JW.js +341 -0
  64. package/dist/dist-cli-preview/chunks/el-DYay96x-.js +189 -0
  65. package/dist/dist-cli-preview/chunks/es-CFZtv6aF.js +176 -0
  66. package/dist/dist-cli-preview/chunks/es-CGgD1AV1.js +720 -0
  67. package/dist/dist-cli-preview/chunks/es-CXXB8ubn.js +628 -0
  68. package/dist/dist-cli-preview/chunks/es-Cf56ydFZ.js +189 -0
  69. package/dist/dist-cli-preview/chunks/es-DirUtV7k.js +563 -0
  70. package/dist/dist-cli-preview/chunks/es-mp8RwFN4.js +341 -0
  71. package/dist/dist-cli-preview/chunks/fi-91gcpxoy.js +341 -0
  72. package/dist/dist-cli-preview/chunks/fi-BfDoQ1rt.js +628 -0
  73. package/dist/dist-cli-preview/chunks/fi-Bn9_l4IH.js +176 -0
  74. package/dist/dist-cli-preview/chunks/fi-Bo5QdhP_.js +563 -0
  75. package/dist/dist-cli-preview/chunks/fi-CC2YqekN.js +189 -0
  76. package/dist/dist-cli-preview/chunks/fi-DUhIWph7.js +720 -0
  77. package/dist/dist-cli-preview/chunks/fr-BSouGEGN.js +176 -0
  78. package/dist/dist-cli-preview/chunks/fr-BnYsse99.js +563 -0
  79. package/dist/dist-cli-preview/chunks/fr-C32S8PvA.js +720 -0
  80. package/dist/dist-cli-preview/chunks/fr-DIQcYUq-.js +628 -0
  81. package/dist/dist-cli-preview/chunks/fr-Do6AIa1z.js +189 -0
  82. package/dist/dist-cli-preview/chunks/fr-ekvUCKTH.js +341 -0
  83. package/dist/dist-cli-preview/chunks/he-B7QiuquT.js +341 -0
  84. package/dist/dist-cli-preview/chunks/he-BYEZTUry.js +176 -0
  85. package/dist/dist-cli-preview/chunks/he-CO8gUz9X.js +628 -0
  86. package/dist/dist-cli-preview/chunks/he-DaWGyHjM.js +189 -0
  87. package/dist/dist-cli-preview/chunks/he-pFefco4W.js +563 -0
  88. package/dist/dist-cli-preview/chunks/he-xSGbvsti.js +720 -0
  89. package/dist/dist-cli-preview/chunks/hey-api-client-DSvKWGPq.js +153 -0
  90. package/dist/dist-cli-preview/chunks/hi-BLnEaAml.js +176 -0
  91. package/dist/dist-cli-preview/chunks/hi-BMFNssNU.js +720 -0
  92. package/dist/dist-cli-preview/chunks/hi-BXBdXuFm.js +189 -0
  93. package/dist/dist-cli-preview/chunks/hi-DNNhi7ud.js +341 -0
  94. package/dist/dist-cli-preview/chunks/hi-DVSXxEzh.js +628 -0
  95. package/dist/dist-cli-preview/chunks/hi-Uqd1FKtv.js +563 -0
  96. package/dist/dist-cli-preview/chunks/hr-B1T2UY_p.js +176 -0
  97. package/dist/dist-cli-preview/chunks/hr-B432GvmU.js +720 -0
  98. package/dist/dist-cli-preview/chunks/hr-BJR58Xci.js +563 -0
  99. package/dist/dist-cli-preview/chunks/hr-CAwxMKX_.js +341 -0
  100. package/dist/dist-cli-preview/chunks/hr-DBrPFNLx.js +189 -0
  101. package/dist/dist-cli-preview/chunks/hr-yH6ab4ea.js +628 -0
  102. package/dist/dist-cli-preview/chunks/hu-BcC628Ln.js +189 -0
  103. package/dist/dist-cli-preview/chunks/hu-Bv0_YQEn.js +628 -0
  104. package/dist/dist-cli-preview/chunks/hu-D_oQwH4Q.js +341 -0
  105. package/dist/dist-cli-preview/chunks/hu-Q0aE9GvJ.js +720 -0
  106. package/dist/dist-cli-preview/chunks/hu-TNzzMEzp.js +563 -0
  107. package/dist/dist-cli-preview/chunks/hu-cQw9hgLe.js +176 -0
  108. package/dist/dist-cli-preview/chunks/id-Becf5X73.js +720 -0
  109. package/dist/dist-cli-preview/chunks/id-CLvNjXjm.js +189 -0
  110. package/dist/dist-cli-preview/chunks/id-CzSVbo5E.js +628 -0
  111. package/dist/dist-cli-preview/chunks/id-Vudf_d_f.js +563 -0
  112. package/dist/dist-cli-preview/chunks/id-Zd_5csjz.js +176 -0
  113. package/dist/dist-cli-preview/chunks/id-oeAszBoo.js +341 -0
  114. package/dist/dist-cli-preview/chunks/isAfter-BUGgByEf.js +1414 -0
  115. package/dist/dist-cli-preview/chunks/it-BlwQOBaX.js +720 -0
  116. package/dist/dist-cli-preview/chunks/it-C0jghILa.js +563 -0
  117. package/dist/dist-cli-preview/chunks/it-CmxC_FIJ.js +176 -0
  118. package/dist/dist-cli-preview/chunks/it-D5BXSiSl.js +189 -0
  119. package/dist/dist-cli-preview/chunks/it-Dme6GXSG.js +341 -0
  120. package/dist/dist-cli-preview/chunks/it-xT70eFoJ.js +628 -0
  121. package/dist/dist-cli-preview/chunks/ja-BJ7phJBG.js +720 -0
  122. package/dist/dist-cli-preview/chunks/ja-Bc8dA1LN.js +189 -0
  123. package/dist/dist-cli-preview/chunks/ja-BcOunH2q.js +176 -0
  124. package/dist/dist-cli-preview/chunks/ja-CRnvQNCa.js +341 -0
  125. package/dist/dist-cli-preview/chunks/ja-CfYfDP_F.js +563 -0
  126. package/dist/dist-cli-preview/chunks/ja-Dh3z4uJk.js +628 -0
  127. package/dist/dist-cli-preview/chunks/ko-8mk8A3_I.js +341 -0
  128. package/dist/dist-cli-preview/chunks/ko-Bnp-5GAn.js +720 -0
  129. package/dist/dist-cli-preview/chunks/ko-CG_0ufw6.js +176 -0
  130. package/dist/dist-cli-preview/chunks/ko-CJ-8gCc9.js +189 -0
  131. package/dist/dist-cli-preview/chunks/ko-CnpxBQ2W.js +628 -0
  132. package/dist/dist-cli-preview/chunks/ko-CtkKNEZB.js +563 -0
  133. package/dist/dist-cli-preview/chunks/log-out-CBUq5Hd-.js +15 -0
  134. package/dist/dist-cli-preview/chunks/map-pin-DHfjSyoR.js +20 -0
  135. package/dist/dist-cli-preview/chunks/messaging-DkyH4CPP.js +4 -0
  136. package/dist/dist-cli-preview/chunks/mount-account-screen-pc1VISwF.js +11041 -0
  137. package/dist/dist-cli-preview/chunks/ms-BF5gF9D5.js +189 -0
  138. package/dist/dist-cli-preview/chunks/ms-Bs0GvdOw.js +341 -0
  139. package/dist/dist-cli-preview/chunks/ms-CyhqvfOb.js +563 -0
  140. package/dist/dist-cli-preview/chunks/ms-D6Cf3VUm.js +628 -0
  141. package/dist/dist-cli-preview/chunks/ms-DR0U3aeO.js +720 -0
  142. package/dist/dist-cli-preview/chunks/ms-DTN7PIDq.js +176 -0
  143. package/dist/dist-cli-preview/chunks/mysite-wvfITV7T.js +4 -0
  144. package/dist/dist-cli-preview/chunks/nl-C1v8tc97.js +720 -0
  145. package/dist/dist-cli-preview/chunks/nl-C5fWLZkK.js +189 -0
  146. package/dist/dist-cli-preview/chunks/nl-Cgt3zlZ8.js +628 -0
  147. package/dist/dist-cli-preview/chunks/nl-DWtWnOvH.js +563 -0
  148. package/dist/dist-cli-preview/chunks/nl-tsI6lLxl.js +341 -0
  149. package/dist/dist-cli-preview/chunks/nl-x2lc8Ct5.js +176 -0
  150. package/dist/dist-cli-preview/chunks/no-BeVycdYu.js +563 -0
  151. package/dist/dist-cli-preview/chunks/no-DMot6qX0.js +341 -0
  152. package/dist/dist-cli-preview/chunks/no-Dk9D1gjY.js +189 -0
  153. package/dist/dist-cli-preview/chunks/no-R8FScfrK.js +176 -0
  154. package/dist/dist-cli-preview/chunks/no-dhNRqvE4.js +720 -0
  155. package/dist/dist-cli-preview/chunks/no-xVFexdWN.js +628 -0
  156. package/dist/dist-cli-preview/chunks/orders-c13r499A.js +4 -0
  157. package/dist/dist-cli-preview/chunks/pl-BH1bEHfg.js +720 -0
  158. package/dist/dist-cli-preview/chunks/pl-Bl9G1yOR.js +176 -0
  159. package/dist/dist-cli-preview/chunks/pl-Cu0JzUUV.js +563 -0
  160. package/dist/dist-cli-preview/chunks/pl-D7AZQwtK.js +189 -0
  161. package/dist/dist-cli-preview/chunks/pl-Dl0M9JDp.js +628 -0
  162. package/dist/dist-cli-preview/chunks/pl-DmTxONNg.js +341 -0
  163. package/dist/dist-cli-preview/chunks/plus-C-lo0pFM.js +14 -0
  164. package/dist/dist-cli-preview/chunks/profile-CoWo9Sxe.js +4 -0
  165. package/dist/dist-cli-preview/chunks/pt-62Kd-NN1.js +189 -0
  166. package/dist/dist-cli-preview/chunks/pt-BAq7bseT.js +628 -0
  167. package/dist/dist-cli-preview/chunks/pt-DgmwnAV-.js +341 -0
  168. package/dist/dist-cli-preview/chunks/pt-K0EDcWuR.js +720 -0
  169. package/dist/dist-cli-preview/chunks/pt-nD60RpwN.js +176 -0
  170. package/dist/dist-cli-preview/chunks/pt-voHVxH2b.js +563 -0
  171. package/dist/dist-cli-preview/chunks/ro-B4M17oRc.js +341 -0
  172. package/dist/dist-cli-preview/chunks/ro-CFfjUJ1T.js +563 -0
  173. package/dist/dist-cli-preview/chunks/ro-CtL0oK2c.js +628 -0
  174. package/dist/dist-cli-preview/chunks/ro-D6ybG7vp.js +176 -0
  175. package/dist/dist-cli-preview/chunks/ro-DTauvv2V.js +189 -0
  176. package/dist/dist-cli-preview/chunks/ro-DUxhal7c.js +720 -0
  177. package/dist/dist-cli-preview/chunks/ru-45VizwWQ.js +628 -0
  178. package/dist/dist-cli-preview/chunks/ru-BS2gZnbs.js +341 -0
  179. package/dist/dist-cli-preview/chunks/ru-C0oMeuhb.js +176 -0
  180. package/dist/dist-cli-preview/chunks/ru-COxlz18L.js +563 -0
  181. package/dist/dist-cli-preview/chunks/ru-CjEXCUxb.js +720 -0
  182. package/dist/dist-cli-preview/chunks/ru-g5Cz5HSk.js +189 -0
  183. package/dist/dist-cli-preview/chunks/schemas-9_ZwC5nC.js +3977 -0
  184. package/dist/dist-cli-preview/chunks/screen-route-slug-utils-B254xI_J.js +14954 -0
  185. package/dist/dist-cli-preview/chunks/sheet-haptics-context-D2WUiXJm.js +116 -0
  186. package/dist/dist-cli-preview/chunks/sidebar-CMSWZCPS.js +848 -0
  187. package/dist/dist-cli-preview/chunks/sk-BGDxj2sS.js +563 -0
  188. package/dist/dist-cli-preview/chunks/sk-BdDjjHfn.js +341 -0
  189. package/dist/dist-cli-preview/chunks/sk-Bpdt_mn0.js +176 -0
  190. package/dist/dist-cli-preview/chunks/sk-DX2wOf1g.js +628 -0
  191. package/dist/dist-cli-preview/chunks/sk-QbdQWjZy.js +714 -0
  192. package/dist/dist-cli-preview/chunks/sk-YuAeAaeZ.js +189 -0
  193. package/dist/dist-cli-preview/chunks/store-api-context-BIdILNWL.js +16 -0
  194. package/dist/dist-cli-preview/chunks/subscriptions-BHALQrcU.js +4 -0
  195. package/dist/dist-cli-preview/chunks/sv-BlNLvbmj.js +628 -0
  196. package/dist/dist-cli-preview/chunks/sv-CEmTDhJy.js +176 -0
  197. package/dist/dist-cli-preview/chunks/sv-CW7FL5HA.js +341 -0
  198. package/dist/dist-cli-preview/chunks/sv-CiCeNX1x.js +720 -0
  199. package/dist/dist-cli-preview/chunks/sv-DefBhX0c.js +563 -0
  200. package/dist/dist-cli-preview/chunks/sv-iaAh80e4.js +189 -0
  201. package/dist/dist-cli-preview/chunks/th-BNEA6ESz.js +720 -0
  202. package/dist/dist-cli-preview/chunks/th-BYBMKmSh.js +341 -0
  203. package/dist/dist-cli-preview/chunks/th-DMPCj1Fg.js +189 -0
  204. package/dist/dist-cli-preview/chunks/th-DV3lk4-6.js +563 -0
  205. package/dist/dist-cli-preview/chunks/th-F8oLBw5n.js +176 -0
  206. package/dist/dist-cli-preview/chunks/th-fPXxGW_m.js +628 -0
  207. package/dist/dist-cli-preview/chunks/tl-BMLmbhme.js +628 -0
  208. package/dist/dist-cli-preview/chunks/tl-BXIS85ly.js +176 -0
  209. package/dist/dist-cli-preview/chunks/tl-CDigaLsK.js +341 -0
  210. package/dist/dist-cli-preview/chunks/tl-DVdSJCnc.js +563 -0
  211. package/dist/dist-cli-preview/chunks/tl-fdtlfqan.js +720 -0
  212. package/dist/dist-cli-preview/chunks/tl-w5CgYWXn.js +189 -0
  213. package/dist/dist-cli-preview/chunks/tr-BA1MDk8A.js +563 -0
  214. package/dist/dist-cli-preview/chunks/tr-CHri_kqV.js +189 -0
  215. package/dist/dist-cli-preview/chunks/tr-DD75kuPl.js +720 -0
  216. package/dist/dist-cli-preview/chunks/tr-Mq5Hfqt2.js +628 -0
  217. package/dist/dist-cli-preview/chunks/tr-PxiurNCr.js +341 -0
  218. package/dist/dist-cli-preview/chunks/tr-YXIIDIEV.js +176 -0
  219. package/dist/dist-cli-preview/chunks/trash-2-Bq4IqH0j.js +137 -0
  220. package/dist/dist-cli-preview/chunks/uk-B4aYnJ8t.js +341 -0
  221. package/dist/dist-cli-preview/chunks/uk-ByJtQJHi.js +189 -0
  222. package/dist/dist-cli-preview/chunks/uk-CCVTTskN.js +720 -0
  223. package/dist/dist-cli-preview/chunks/uk-CS0tJc6H.js +628 -0
  224. package/dist/dist-cli-preview/chunks/uk-CYYjeT-V.js +176 -0
  225. package/dist/dist-cli-preview/chunks/uk-DGb0m_JV.js +563 -0
  226. package/dist/dist-cli-preview/chunks/use-optional-store-Csor1Z3z.js +22 -0
  227. package/dist/dist-cli-preview/chunks/useInfiniteQuery-BpFtQ7DH.js +67 -0
  228. package/dist/dist-cli-preview/chunks/user-BGrQJ1GJ.js +31 -0
  229. package/dist/dist-cli-preview/chunks/users-DBL0isYy.js +27 -0
  230. package/dist/dist-cli-preview/chunks/vi-BoKSdzH3.js +341 -0
  231. package/dist/dist-cli-preview/chunks/vi-CHKjehVL.js +628 -0
  232. package/dist/dist-cli-preview/chunks/vi-DFicISRE.js +189 -0
  233. package/dist/dist-cli-preview/chunks/vi-DT4XS4HU.js +176 -0
  234. package/dist/dist-cli-preview/chunks/vi-RFwPNQ96.js +720 -0
  235. package/dist/dist-cli-preview/chunks/vi-SG6trKny.js +563 -0
  236. package/dist/dist-cli-preview/chunks/zh_CN-BJVGrpVl.js +714 -0
  237. package/dist/dist-cli-preview/chunks/zh_CN-BeGANQIJ.js +176 -0
  238. package/dist/dist-cli-preview/chunks/zh_CN-BwpNGMQN.js +189 -0
  239. package/dist/dist-cli-preview/chunks/zh_CN-CFu3GI2I.js +563 -0
  240. package/dist/dist-cli-preview/chunks/zh_CN-DrCrXsZC.js +341 -0
  241. package/dist/dist-cli-preview/chunks/zh_CN-x6mJyUQH.js +628 -0
  242. package/dist/dist-cli-preview/chunks/zh_TW-CorN6cwY.js +720 -0
  243. package/dist/dist-cli-preview/chunks/zh_TW-D9gGVyEb.js +176 -0
  244. package/dist/dist-cli-preview/chunks/zh_TW-DdD6Hf_A.js +628 -0
  245. package/dist/dist-cli-preview/chunks/zh_TW-JmyC1W5K.js +341 -0
  246. package/dist/dist-cli-preview/chunks/zh_TW-Oq4Chn8F.js +189 -0
  247. package/dist/dist-cli-preview/chunks/zh_TW-kUnD3TL4.js +563 -0
  248. package/dist/index.mjs +1220 -513
  249. package/dist/index.mjs.map +1 -1
  250. package/dist/skills/themes-review/SKILL.md +2 -2
  251. package/dist/skills/themes-review/references/navigation.md +3 -1
  252. package/package.json +60 -10
package/dist/index.mjs CHANGED
@@ -1,18 +1,20 @@
1
1
  import { Command } from "commander";
2
+ import prompts from "prompts";
2
3
  import { getAuthToken, gitSyncActorFromMe, gitSyncCommitSubject, readConfig, resolveGitSyncActor, summarizeChanges, updateConfig } from "@fluid-app/fluid-cli";
4
+ import { setTimeout as setTimeout$1 } from "node:timers/promises";
3
5
  import { cpSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
4
6
  import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
7
  import { createHash, randomBytes } from "node:crypto";
6
8
  import http from "node:http";
9
+ import { fileURLToPath } from "node:url";
10
+ import { parse, parseFragment, serialize } from "parse5";
7
11
  import https from "node:https";
8
12
  import chokidar from "chokidar";
9
13
  import net from "node:net";
10
14
  import { execFileSync, spawn } from "node:child_process";
11
15
  import { tmpdir } from "node:os";
12
16
  import chalk from "chalk";
13
- import prompts from "prompts";
14
17
  import ora from "ora";
15
- import { fileURLToPath } from "node:url";
16
18
  //#region ../../platform/api-client-core/src/api-error-shape.ts
17
19
  /**
18
20
  * Narrows a parsed JSON error payload to the envelope contract. Arrays and
@@ -292,10 +294,34 @@ async function backgroundToken(tokenOverride) {
292
294
  return tokenOverride ?? getAuthToken() ?? null;
293
295
  }
294
296
  function createApiClient(tokenOverride) {
295
- return createFetchClient({
297
+ const client = createFetchClient({
296
298
  baseUrl: getApiBase(),
297
299
  getAuthToken: () => process.env["FLUID_BACKGROUND_SYNC"] === "1" ? backgroundToken(tokenOverride) : tokenOverride ?? getAuthToken() ?? null
298
300
  });
301
+ const request = async (endpoint, options = {}) => {
302
+ for (let attempt = 0;; attempt++) {
303
+ options.signal?.throwIfAborted();
304
+ try {
305
+ return await client.request(endpoint, options);
306
+ } catch (error) {
307
+ if ((options.method ?? "GET") !== "GET" || !(error instanceof ApiError) || ![
308
+ 502,
309
+ 503,
310
+ 504
311
+ ].includes(error.status) || attempt >= 2 || options.signal?.aborted) throw error;
312
+ await setTimeout$1(500 * 2 ** attempt, void 0, { signal: options.signal });
313
+ }
314
+ }
315
+ };
316
+ return {
317
+ ...client,
318
+ request,
319
+ get: (endpoint, params, options) => request(endpoint, {
320
+ ...options,
321
+ method: "GET",
322
+ params
323
+ })
324
+ };
299
325
  }
300
326
  function requireToken() {
301
327
  const token = getAuthToken();
@@ -306,54 +332,6 @@ function requireToken() {
306
332
  return token;
307
333
  }
308
334
  //#endregion
309
- //#region src/theme-config.ts
310
- const CONFIG_FILE = ".fluid-theme.json";
311
- /**
312
- * `company` must be the bare subdomain slug — the dev server builds
313
- * `<company>.fluid.app` from it, so a full domain here produces an
314
- * unreachable `<slug>.fluid.app.fluid.app` host. Some external tooling
315
- * writes the full domain (or a URL); accept those and reduce them to
316
- * the subdomain.
317
- */
318
- function normalizeCompany(company) {
319
- return company.replace(/^[a-z][a-z0-9+.-]*:\/\//i, "").replace(/[/?#].*$/, "").replace(/\.fluid\.app$/i, "");
320
- }
321
- function configPath(themeRoot) {
322
- return join(themeRoot, CONFIG_FILE);
323
- }
324
- /** Read `.fluid-theme.json` from a theme directory, or null if it doesn't exist. */
325
- function readThemeConfig(themeRoot) {
326
- const path = configPath(themeRoot);
327
- if (!existsSync(path)) return null;
328
- try {
329
- const raw = readFileSync(path, "utf-8");
330
- const config = JSON.parse(raw);
331
- if (typeof config.company === "string") config.company = normalizeCompany(config.company);
332
- return config;
333
- } catch {
334
- return null;
335
- }
336
- }
337
- /**
338
- * Read a legacy config (pre-shadow-repo). Used only by the migration
339
- * path on first new-CLI pull to seed shadow HEAD from files whose
340
- * local content still matches their stored sha256 checksum.
341
- */
342
- function readLegacyThemeConfig(themeRoot) {
343
- const path = configPath(themeRoot);
344
- if (!existsSync(path)) return null;
345
- try {
346
- const raw = readFileSync(path, "utf-8");
347
- return JSON.parse(raw);
348
- } catch {
349
- return null;
350
- }
351
- }
352
- /** Write `.fluid-theme.json` to a theme directory. */
353
- function writeThemeConfig(themeRoot, config) {
354
- writeFileSync(configPath(themeRoot), JSON.stringify(config, null, 2) + "\n", "utf-8");
355
- }
356
- //#endregion
357
335
  //#region src/plugin-state.ts
358
336
  const PLUGIN_KEY = "theme-dev";
359
337
  function getState() {
@@ -473,13 +451,530 @@ function setLastDevThemeId(id) {
473
451
  });
474
452
  }
475
453
  /**
476
- * The dev theme to target by default in `navigate` — the most recently started
477
- * dev server. Falls back to the legacy global id for users who haven't yet run
478
- * the per-project `theme dev`.
454
+ * The dev theme to target by default in `navigate` — the most recently started
455
+ * dev server. Falls back to the legacy global id for users who haven't yet run
456
+ * the per-project `theme dev`.
457
+ */
458
+ function getLastDevThemeId() {
459
+ const state = getState();
460
+ return state.lastDevThemeId ?? state.devThemeId;
461
+ }
462
+ //#endregion
463
+ //#region src/theme/dev-server/member-preview-contract.ts
464
+ var MemberPreviewError = class extends Error {
465
+ constructor(message, status = 502) {
466
+ super(message);
467
+ this.status = status;
468
+ }
469
+ };
470
+ function isRecord$5(value) {
471
+ return typeof value === "object" && value !== null && !Array.isArray(value);
472
+ }
473
+ function isId(value) {
474
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
475
+ }
476
+ /** The metadata API still exposes storefront-era /accounts paths. Account hosts omit it. */
477
+ function accountPath(path) {
478
+ return path === "/accounts" ? "/" : path.startsWith("/accounts/") ? path.slice(9) : path;
479
+ }
480
+ function isProductionPath(value) {
481
+ if (typeof value !== "string" || !value.startsWith("/") || value.startsWith("//") || value.includes("\\")) return false;
482
+ return new URL(value, "http://preview.invalid").pathname === value && !value.startsWith("/_members");
483
+ }
484
+ function parseMemberMetadata(value) {
485
+ if (!isRecord$5(value) || !Array.isArray(value.members_sites) || !Array.isArray(value.system_screens)) throw new MemberPreviewError("This server does not provide compatible member preview metadata.");
486
+ const sites = value.members_sites.map((site) => {
487
+ if (!isRecord$5(site) || !isId(site.id) || typeof site.name !== "string" || !Array.isArray(site.pages)) throw new MemberPreviewError("Invalid member site metadata.");
488
+ const pages = site.pages.map((page) => {
489
+ if (!isRecord$5(page) || !isId(page.id) || typeof page.title !== "string" || !isProductionPath(page.path) || typeof page.published !== "boolean") throw new MemberPreviewError("Invalid member page metadata.");
490
+ return {
491
+ id: page.id,
492
+ title: page.title,
493
+ path: accountPath(page.path),
494
+ published: page.published
495
+ };
496
+ });
497
+ if (new Set(pages.map((p) => p.path)).size !== pages.length) throw new MemberPreviewError("Ambiguous member page paths.");
498
+ return {
499
+ id: site.id,
500
+ name: site.name,
501
+ pages
502
+ };
503
+ });
504
+ if (new Set(sites.map((s) => s.id)).size !== sites.length) throw new MemberPreviewError("Duplicate member site identifiers.");
505
+ return {
506
+ members_sites: sites,
507
+ system_screens: value.system_screens.map((screen) => {
508
+ if (!isRecord$5(screen) || typeof screen.key !== "string" || !/^[a-z][a-z0-9_-]*$/.test(screen.key)) throw new MemberPreviewError("Invalid system screen metadata.");
509
+ return { key: screen.key };
510
+ })
511
+ };
512
+ }
513
+ function parseSelection(params) {
514
+ const site = numericParam(params, "members_site_id");
515
+ if (!site) throw new MemberPreviewError("Select a member site at /_members first.", 400);
516
+ const memberType = params.get("member_type_id");
517
+ if (params.getAll("member_type_id").length > 1 || memberType !== null && !/^[\w-]{1,100}$/.test(memberType)) throw new MemberPreviewError("Invalid member type selection.", 400);
518
+ return {
519
+ members_site_id: site,
520
+ country_id: numericParam(params, "country_id"),
521
+ language_id: numericParam(params, "language_id"),
522
+ ...memberType ? { member_type_id: memberType } : {}
523
+ };
524
+ }
525
+ function numericParam(params, key) {
526
+ const value = params.get(key);
527
+ if (value === null) return void 0;
528
+ if (params.getAll(key).length !== 1 || !/^[1-9]\d*$/.test(value) || !isId(Number(value))) throw new MemberPreviewError(`Invalid ${key}.`, 400);
529
+ return Number(value);
530
+ }
531
+ function memberPreviewUrl(path, selection) {
532
+ const url = new URL(path, "http://preview.invalid");
533
+ if (url.origin !== "http://preview.invalid") throw new MemberPreviewError("Expected a local member path.", 400);
534
+ if (url.pathname !== "/_members" && !url.pathname.startsWith("/_members/")) url.pathname = `/_members${url.pathname}`;
535
+ for (const key of [
536
+ "members_site_id",
537
+ "country_id",
538
+ "language_id",
539
+ "member_type_id"
540
+ ]) {
541
+ const value = selection[key];
542
+ if (value === void 0) url.searchParams.delete(key);
543
+ else url.searchParams.set(key, String(value));
544
+ }
545
+ return url.pathname + url.search + url.hash;
546
+ }
547
+ function parseMemberRender(value, themeId, selection, pageId, screenKey = null) {
548
+ const preview = isRecord$5(value) ? value.preview : void 0;
549
+ if (!isRecord$5(preview) || preview.contract_version !== 1 || preview.mode !== "sample" || preview.theme_id !== themeId || preview.members_site_id !== selection.members_site_id || preview.members_page_id !== pageId || preview.system_screen_key !== screenKey || typeof preview.html_content !== "string" || !(preview.page_stylesheet === null || typeof preview.page_stylesheet === "string") || !(preview.template_stylesheet === null || typeof preview.template_stylesheet === "string")) throw new MemberPreviewError("Incompatible member preview response. Update the server and CLI together.");
550
+ return {
551
+ html: preview.html_content,
552
+ styles: preview.page_stylesheet ?? "",
553
+ templateStyles: preview.template_stylesheet ?? ""
554
+ };
555
+ }
556
+ //#endregion
557
+ //#region src/theme/navigation-members.ts
558
+ /** Read the running server's acknowledged route map, without sending credentials. */
559
+ async function readPreviewMetadata(address, themeId) {
560
+ let value;
561
+ try {
562
+ const response = await fetch(`${address}/_members/_metadata`, {
563
+ redirect: "error",
564
+ signal: AbortSignal.timeout(1e4)
565
+ });
566
+ if (!response.ok) throw new Error("Member metadata unavailable");
567
+ value = await response.json();
568
+ } catch {
569
+ console.warn("Member preview is unavailable. Start an updated theme dev server to list member pages.");
570
+ return;
571
+ }
572
+ if (!isRecord$5(value) || value.mode !== "sample") throw new Error("The running server does not provide compatible preview metadata.");
573
+ if (value.theme_id !== themeId) throw new Error(`The running server is not serving theme #${themeId}. Use its theme ID or restart theme dev.`);
574
+ if (!Array.isArray(value.supported_system_screens) || !value.supported_system_screens.every((key) => typeof key === "string")) throw new Error("The running server does not provide preview capabilities.");
575
+ return {
576
+ ...parseMemberMetadata(value),
577
+ supported_system_screens: value.supported_system_screens
578
+ };
579
+ }
580
+ function memberChoices(metadata) {
581
+ if (!metadata) return [];
582
+ return metadata.members_sites.flatMap((site) => {
583
+ const prefix = `Member · ${site.name} (#${site.id}) · `;
584
+ const selection = { members_site_id: site.id };
585
+ return [...site.pages.map((page) => ({
586
+ title: `${prefix}${page.title}${page.published ? "" : " (draft)"} — ${page.path}`,
587
+ value: memberPreviewUrl(page.path, selection)
588
+ })), ...metadata.system_screens.filter((screen) => metadata.supported_system_screens.includes(screen.key)).map((screen) => {
589
+ return {
590
+ title: `${prefix}${screen.key}`,
591
+ value: memberPreviewUrl(`/${screen.key}`, selection)
592
+ };
593
+ })];
594
+ });
595
+ }
596
+ /** A watcher update may remove a page while the prompt is open. */
597
+ async function validateMemberDestination(address, themeId, path) {
598
+ if (!memberChoices(await readPreviewMetadata(address, themeId)).some((choice) => choice.value === path)) throw new Error("This member page is no longer available. Run the navigator again to refresh the choices.");
599
+ }
600
+ //#endregion
601
+ //#region ../../api-clients/themes/src/namespaces/v0.ts
602
+ /**
603
+ * List application themes
604
+ * Get all application themes with optional filters
605
+ *
606
+ * @param client - Fetch client instance
607
+ * @param [params] - params
608
+ */
609
+ async function listApplicationThemes(client, params) {
610
+ return client.get(`/api/application_themes`, params);
611
+ }
612
+ /**
613
+ * Create an application theme
614
+ *
615
+ *
616
+ * @param client - Fetch client instance
617
+ * @param body - body
618
+ */
619
+ async function createApplicationTheme(client, body) {
620
+ return client.post(`/api/application_themes`, body);
621
+ }
622
+ /**
623
+ * Get an application theme
624
+ *
625
+ *
626
+ * @param client - Fetch client instance
627
+ * @param id - id
628
+ * @param [params] - params
629
+ */
630
+ async function getApplicationTheme(client, id, params) {
631
+ return client.get(`/api/application_themes/${id}`, params);
632
+ }
633
+ /**
634
+ * Returns available themeables for a given type scoped to the theme's company
635
+ *
636
+ *
637
+ * @param client - Fetch client instance
638
+ * @param id - id
639
+ * @param [params] - params
640
+ */
641
+ async function getApplicationThemeAvailableThemeables(client, id, params) {
642
+ return client.get(`/api/application_themes/${id}/available_themeables`, params);
643
+ }
644
+ /**
645
+ * Create a development reference clone of an application theme
646
+ * Creates an isolated development theme while preserving existing DAM and ImageKit references without transferring asset bytes.
647
+ *
648
+ * @param client - Fetch client instance
649
+ * @param id - id
650
+ * @param body - body
651
+ */
652
+ async function cloneApplicationThemeForDevelopment(client, id, body) {
653
+ return client.post(`/api/application_themes/${id}/clone_for_development`, body);
654
+ }
655
+ /**
656
+ * Publishes the theme
657
+ *
658
+ *
659
+ * @param client - Fetch client instance
660
+ * @param id - id
661
+ */
662
+ async function publishApplicationTheme(client, id) {
663
+ return client.post(`/api/application_themes/${id}/publish`);
664
+ }
665
+ /**
666
+ * Get theme assets
667
+ *
668
+ *
669
+ * @param client - Fetch client instance
670
+ * @param id - id
671
+ */
672
+ async function getThemeAssets(client, id) {
673
+ return client.get(`/api/application_themes/${id}/theme_assets`);
674
+ }
675
+ /**
676
+ * Lists all theme resources
677
+ * Lists resources for a theme owned by the current company, including the canonical members_sites.json snapshot. Themes without member sites receive an explicit empty version 2 document. Companyless root themes are not accessible through this endpoint. The content field remains JSON-encoded text.
678
+ *
679
+ * @param client - Fetch client instance
680
+ * @param application_theme_id - application_theme_id
681
+ */
682
+ async function listThemeResources(client, application_theme_id) {
683
+ return client.get(`/api/application_themes/${application_theme_id}/resources`);
684
+ }
685
+ /**
686
+ * Updates a theme resource
687
+ * Submit member template dependencies together with members_sites.json using member_files so navigation is captured before any file changes. The files and member graph apply atomically; a rejected dependency rolls back the bundle. Existing single-resource writes remain supported. Missing manifests preserve existing configuration; deleting the file is refused.
688
+ *
689
+ * @param client - Fetch client instance
690
+ * @param application_theme_id - application_theme_id
691
+ * @param body - body
692
+ */
693
+ async function updateThemeResource(client, application_theme_id, body) {
694
+ return client.put(`/api/application_themes/${application_theme_id}/resources`, body);
695
+ }
696
+ /**
697
+ * Deletes a theme resource
698
+ *
699
+ *
700
+ * @param client - Fetch client instance
701
+ * @param application_theme_id - application_theme_id
702
+ * @param body - body
703
+ */
704
+ async function deleteThemeResource(client, application_theme_id, body) {
705
+ return client.delete(`/api/application_themes/${application_theme_id}/resources`, { body });
706
+ }
707
+ /**
708
+ * Creates a file resource
709
+ *
710
+ *
711
+ * @param client - Fetch client instance
712
+ * @param body - body
713
+ */
714
+ async function createFileResource(client, body) {
715
+ return client.post(`/api/file_resources`, body);
716
+ }
717
+ /**
718
+ * Deletes a file resource
719
+ *
720
+ *
721
+ * @param client - Fetch client instance
722
+ * @param id - id
723
+ */
724
+ async function destroyFileResource(client, id) {
725
+ return client.delete(`/api/file_resources/${id}`);
726
+ }
727
+ //#endregion
728
+ //#region src/commands/navigate.ts
729
+ function localSuggest(input, choices) {
730
+ if (!input) return choices;
731
+ const lower = input.toLowerCase();
732
+ return choices.filter((c) => c.title.toLowerCase().includes(lower));
733
+ }
734
+ const THEMEABLE_TYPE_MAP = {
735
+ "/home": "home_page",
736
+ "/home/shop": "shop_page",
737
+ "/home/join": "join_page",
738
+ "/cart": "cart_page",
739
+ "/home/blog": "post_page",
740
+ "/home/categories": "category_page",
741
+ "/home/collections": "collection_page"
742
+ };
743
+ const STATIC_ROUTES = [
744
+ {
745
+ label: "Home",
746
+ path: "/home"
747
+ },
748
+ {
749
+ label: "Shop",
750
+ path: "/home/shop"
751
+ },
752
+ {
753
+ label: "Join / Sign Up",
754
+ path: "/home/join"
755
+ },
756
+ {
757
+ label: "Cart",
758
+ path: "/cart"
759
+ },
760
+ {
761
+ label: "Blog",
762
+ path: "/home/blog"
763
+ },
764
+ {
765
+ label: "Categories (all)",
766
+ path: "/home/categories"
767
+ },
768
+ {
769
+ label: "Collections (all)",
770
+ path: "/home/collections"
771
+ }
772
+ ];
773
+ const RESOURCE_ROUTES = [
774
+ {
775
+ label: "Category",
776
+ type: "category",
777
+ template: "/home/categories/%s",
778
+ fallback: "/home/categories"
779
+ },
780
+ {
781
+ label: "Collection",
782
+ type: "collection",
783
+ template: "/home/collections/%s",
784
+ fallback: "/home/collections"
785
+ },
786
+ {
787
+ label: "Product",
788
+ type: "product",
789
+ template: "/home/products/%s",
790
+ fallback: "/home/shop"
791
+ },
792
+ {
793
+ label: "Library",
794
+ type: "library",
795
+ template: "/home/libraries/%s",
796
+ fallback: "/home/libraries"
797
+ },
798
+ {
799
+ label: "Post",
800
+ type: "post",
801
+ template: "/home/posts/%s",
802
+ fallback: "/home/blog"
803
+ },
804
+ {
805
+ label: "Media",
806
+ type: "medium",
807
+ template: "/home/media/%s",
808
+ fallback: "/home/media"
809
+ },
810
+ {
811
+ label: "Enrollment Pack",
812
+ type: "enrollment_pack",
813
+ template: "/home/enrollments/%s",
814
+ fallback: "/home/join"
815
+ },
816
+ {
817
+ label: "Page",
818
+ type: "page",
819
+ template: "/home/pages/%s",
820
+ fallback: "/home/pages"
821
+ }
822
+ ];
823
+ async function fetchTemplatesForType(api, themeId, themeableType) {
824
+ const params = new URLSearchParams({
825
+ application_theme_id: String(themeId),
826
+ themeable_type: themeableType,
827
+ published: "true"
828
+ });
829
+ return (await api.get(`/api/application_theme_templates?${params}`)).templates ?? [];
830
+ }
831
+ async function selectTemplate(api, themeId, themeableType, onCancel) {
832
+ const templates = await fetchTemplatesForType(api, themeId, themeableType);
833
+ if (templates.length <= 1) return null;
834
+ const { templateId } = await prompts({
835
+ type: "autocomplete",
836
+ name: "templateId",
837
+ message: "Select a template",
838
+ choices: templates.map((t) => ({
839
+ title: `${t.name}${t.default ? " (default)" : ""}`,
840
+ value: t.id
841
+ })),
842
+ suggest: (input, choices) => Promise.resolve(localSuggest(input, choices))
843
+ }, { onCancel });
844
+ return templateId ?? null;
845
+ }
846
+ function createNavigateCommand() {
847
+ return new Command("navigate").description("Interactively navigate to a route in the dev server browser").option("--host <host>", "Dev server host", "127.0.0.1").option("--port <port>", "Dev server port", "9292").option("-t, --theme <id>", "Theme ID (defaults to active dev theme)").action(async (opts) => {
848
+ requireToken();
849
+ const themeId = opts.theme ? Number(opts.theme) : getLastDevThemeId();
850
+ if (!themeId) {
851
+ console.error("No active dev theme. Run `fluid theme dev` first, or pass --theme <id>.");
852
+ process.exit(1);
853
+ }
854
+ const address = `http://${opts.host}:${opts.port}`;
855
+ await navigateTheme(createApiClient(), themeId, address);
856
+ });
857
+ }
858
+ /** Shared by theme navigate and theme dev --navigate. */
859
+ async function navigateTheme(api, themeId, address) {
860
+ const choices = [...STATIC_ROUTES.map((r) => ({
861
+ title: r.label,
862
+ value: r.path
863
+ })), ...RESOURCE_ROUTES.map((r) => ({
864
+ title: `${r.label} (select specific)`,
865
+ value: {
866
+ resourceType: r.type,
867
+ template: r.template,
868
+ fallback: r.fallback,
869
+ label: r.label
870
+ }
871
+ }))];
872
+ let cancelled = false;
873
+ const onCancel = () => {
874
+ cancelled = true;
875
+ return false;
876
+ };
877
+ const metadata = await readPreviewMetadata(address, themeId);
878
+ choices.push(...memberChoices(metadata));
879
+ const unavailable = metadata?.system_screens.filter((screen) => !metadata.supported_system_screens.includes(screen.key));
880
+ if (unavailable?.length) console.log(`System-screen preview unavailable: ${unavailable.map((screen) => screen.key).join(", ")}.`);
881
+ const { dest } = await prompts({
882
+ type: "autocomplete",
883
+ name: "dest",
884
+ message: "Select a route",
885
+ choices,
886
+ suggest: (input, choices) => Promise.resolve(localSuggest(input, choices))
887
+ }, { onCancel });
888
+ if (!dest) return;
889
+ let path;
890
+ let themeableType;
891
+ if (typeof dest === "string") {
892
+ if (dest.startsWith("/_members/")) await validateMemberDestination(address, themeId, dest);
893
+ path = dest;
894
+ themeableType = THEMEABLE_TYPE_MAP[dest];
895
+ } else {
896
+ themeableType = dest.resourceType;
897
+ const resources = (await getApplicationThemeAvailableThemeables(api, themeId, {
898
+ themeable: dest.resourceType,
899
+ per_page: 50
900
+ })).available_themeables ?? [];
901
+ if (!resources.length) {
902
+ console.log(`No ${dest.label} resources found, using listing page.`);
903
+ path = dest.fallback;
904
+ } else {
905
+ const resourceChoices = resources.map((r) => ({
906
+ title: r.title ?? r.slug ?? "Untitled",
907
+ value: r.slug
908
+ }));
909
+ const { slug } = await prompts({
910
+ type: "autocomplete",
911
+ name: "slug",
912
+ message: `Select a ${dest.label.toLowerCase()}`,
913
+ choices: resourceChoices,
914
+ suggest: (input, choices) => Promise.resolve(localSuggest(input, choices))
915
+ }, { onCancel });
916
+ if (cancelled || !slug) return;
917
+ path = dest.template.replace("%s", encodeURIComponent(slug));
918
+ }
919
+ }
920
+ let templateParam = "";
921
+ if (themeableType) {
922
+ const templateId = await selectTemplate(api, themeId, themeableType, onCancel);
923
+ if (templateId) templateParam = `?theme_template_id=${templateId}`;
924
+ }
925
+ if (cancelled) return;
926
+ const url = `${address}${path}${templateParam}`;
927
+ console.log(`\nNavigating to: ${url}\n`);
928
+ const open = (await import("open")).default;
929
+ await open(url);
930
+ }
931
+ //#endregion
932
+ //#region src/theme-config.ts
933
+ const CONFIG_FILE = ".fluid-theme.json";
934
+ /**
935
+ * `company` must be the bare subdomain slug — the dev server builds
936
+ * `<company>.fluid.app` from it, so a full domain here produces an
937
+ * unreachable `<slug>.fluid.app.fluid.app` host. Some external tooling
938
+ * writes the full domain (or a URL); accept those and reduce them to
939
+ * the subdomain.
940
+ */
941
+ function normalizeCompany(company) {
942
+ return company.replace(/^[a-z][a-z0-9+.-]*:\/\//i, "").replace(/[/?#].*$/, "").replace(/\.fluid\.app$/i, "");
943
+ }
944
+ function configPath(themeRoot) {
945
+ return join(themeRoot, CONFIG_FILE);
946
+ }
947
+ /** Read `.fluid-theme.json` from a theme directory, or null if it doesn't exist. */
948
+ function readThemeConfig(themeRoot) {
949
+ const path = configPath(themeRoot);
950
+ if (!existsSync(path)) return null;
951
+ try {
952
+ const raw = readFileSync(path, "utf-8");
953
+ const config = JSON.parse(raw);
954
+ if (typeof config.company === "string") config.company = normalizeCompany(config.company);
955
+ return config;
956
+ } catch {
957
+ return null;
958
+ }
959
+ }
960
+ /**
961
+ * Read a legacy config (pre-shadow-repo). Used only by the migration
962
+ * path on first new-CLI pull to seed shadow HEAD from files whose
963
+ * local content still matches their stored sha256 checksum.
479
964
  */
480
- function getLastDevThemeId() {
481
- const state = getState();
482
- return state.lastDevThemeId ?? state.devThemeId;
965
+ function readLegacyThemeConfig(themeRoot) {
966
+ const path = configPath(themeRoot);
967
+ if (!existsSync(path)) return null;
968
+ try {
969
+ const raw = readFileSync(path, "utf-8");
970
+ return JSON.parse(raw);
971
+ } catch {
972
+ return null;
973
+ }
974
+ }
975
+ /** Write `.fluid-theme.json` to a theme directory. */
976
+ function writeThemeConfig(themeRoot, config) {
977
+ writeFileSync(configPath(themeRoot), JSON.stringify(config, null, 2) + "\n", "utf-8");
483
978
  }
484
979
  //#endregion
485
980
  //#region src/theme/mime-type.ts
@@ -533,7 +1028,8 @@ function mimeTypeFor(ext) {
533
1028
  const THEME_LEVEL_RESOURCE_KEYS = new Set([
534
1029
  "global_styles.css",
535
1030
  "styles.css",
536
- "variables.json"
1031
+ "variables.json",
1032
+ "members_sites.json"
537
1033
  ]);
538
1034
  const COMPOSITE_RESOURCE_FILE_NAMES = new Set([
539
1035
  "index.liquid",
@@ -1072,37 +1568,39 @@ var ThemeRoot = class {
1072
1568
  }
1073
1569
  };
1074
1570
  //#endregion
1075
- //#region src/theme/dev-server/sse.ts
1076
- var SSEStream = class {
1077
- responses = /* @__PURE__ */ new Set();
1078
- add(res) {
1079
- res.writeHead(200, {
1080
- "Content-Type": "text/event-stream",
1081
- "Cache-Control": "no-cache",
1082
- Connection: "keep-alive",
1083
- "Access-Control-Allow-Origin": "*"
1084
- });
1085
- res.write(":\n\n");
1086
- this.responses.add(res);
1087
- res.on("close", () => this.responses.delete(res));
1571
+ //#region src/theme/dev-server/member-screen-assets.ts
1572
+ /** The runtime is shipped with this CLI, not taken from a mutable theme/CDN URL. */
1573
+ function findMemberScreenAssets() {
1574
+ let directory = dirname(fileURLToPath(import.meta.url));
1575
+ while (true) {
1576
+ for (const candidate of [join(directory, "dist-cli-preview"), join(directory, "dist", "dist-cli-preview")]) if (existsSync(join(candidate, "capabilities.json"))) return candidate;
1577
+ const parent = dirname(directory);
1578
+ if (parent === directory) return void 0;
1579
+ directory = parent;
1580
+ }
1581
+ }
1582
+ var MemberScreenAssets = class {
1583
+ screens;
1584
+ constructor(directory = findMemberScreenAssets()) {
1585
+ this.directory = directory;
1586
+ this.screens = [];
1587
+ if (!directory) return;
1588
+ try {
1589
+ const value = JSON.parse(readFileSync(join(directory, "capabilities.json"), "utf8"));
1590
+ if (typeof value !== "object" || value === null || !("version" in value) || value.version !== 1 || !("screens" in value) || !Array.isArray(value.screens)) return;
1591
+ if (!value.screens.every((screen) => typeof screen === "string" && /^[a-z][a-z-]*$/.test(screen))) return;
1592
+ if (!existsSync(join(directory, "account-screens.js"))) return;
1593
+ this.screens = value.screens;
1594
+ } catch {}
1088
1595
  }
1089
- broadcast(data) {
1090
- const payload = `data: ${data}\n\n`;
1091
- for (const res of this.responses) try {
1092
- res.write(payload);
1596
+ read(path) {
1597
+ if (!this.directory || !/^(?:account-screens\.js|chunks\/[a-zA-Z0-9_.-]+\.js)$/.test(path)) return void 0;
1598
+ try {
1599
+ return readFileSync(join(this.directory, path));
1093
1600
  } catch {
1094
- this.responses.delete(res);
1601
+ return;
1095
1602
  }
1096
1603
  }
1097
- close() {
1098
- for (const res of this.responses) try {
1099
- res.end();
1100
- } catch {}
1101
- this.responses.clear();
1102
- }
1103
- get size() {
1104
- return this.responses.size;
1105
- }
1106
1604
  };
1107
1605
  //#endregion
1108
1606
  //#region src/theme/dev-server/hot-reload.ts
@@ -1166,6 +1664,287 @@ function injectHotReload(html, mode) {
1166
1664
  return html + script;
1167
1665
  }
1168
1666
  //#endregion
1667
+ //#region src/theme/dev-server/member-preview-html.ts
1668
+ function escapeHtml(value) {
1669
+ return value.replace(/[&<>"']/g, (char) => ({
1670
+ "&": "&amp;",
1671
+ "<": "&lt;",
1672
+ ">": "&gt;",
1673
+ "\"": "&quot;",
1674
+ "'": "&#39;"
1675
+ })[char] ?? char);
1676
+ }
1677
+ /** Use the configured Rails environment, never a browser-supplied upstream. */
1678
+ function memberOrigin(apiBase, company) {
1679
+ const url = new URL(apiBase);
1680
+ if (!/^[a-z0-9-]+$/i.test(company)) throw new Error("Invalid company subdomain.");
1681
+ if (url.hostname.startsWith("api.")) url.hostname = `${company}.account.${url.hostname.slice(4)}`;
1682
+ return url.origin;
1683
+ }
1684
+ function renderMemberHtml(render, opts) {
1685
+ const document = parse(render.html);
1686
+ const elements = [];
1687
+ function visit(parent) {
1688
+ for (const child of parent.childNodes) if ("tagName" in child) {
1689
+ elements.push(child);
1690
+ visit(child);
1691
+ }
1692
+ }
1693
+ visit(document);
1694
+ const head = elements.find((el) => el.tagName === "head");
1695
+ for (const el of elements) {
1696
+ if (el.tagName === "base" && el.parentNode) el.parentNode.childNodes.splice(el.parentNode.childNodes.indexOf(el), 1);
1697
+ if (el.tagName === "script") {
1698
+ const src = el.attrs.find((attr) => attr.name === "src")?.value ?? "";
1699
+ if (/\/(?:account-screens(?:\/|\.)|member-storefront-sdk\/)/.test(src) && el.parentNode) el.parentNode.childNodes.splice(el.parentNode.childNodes.indexOf(el), 1);
1700
+ }
1701
+ if (el.tagName === "fluid-account-screen" || el.tagName === "fluid-messaging") {
1702
+ el.attrs = el.attrs.filter((attr) => attr.name !== "preview" && attr.name !== "path");
1703
+ el.attrs.push({
1704
+ name: "preview",
1705
+ value: "true"
1706
+ });
1707
+ if (opts.screenKey) el.attrs.push({
1708
+ name: "path",
1709
+ value: new URL(opts.path, opts.origin).pathname.slice(1)
1710
+ });
1711
+ }
1712
+ if (el.tagName === "form") {
1713
+ el.attrs = el.attrs.filter((attr) => attr.name !== "action");
1714
+ el.attrs.push({
1715
+ name: "action",
1716
+ value: `${opts.localOrigin}/_members/_actions`
1717
+ });
1718
+ }
1719
+ const formAction = el.attrs.find((attr) => attr.name === "formaction");
1720
+ if (formAction) formAction.value = `${opts.localOrigin}/_members/_actions`;
1721
+ if (el.tagName === "a" || el.tagName === "area") {
1722
+ const href = el.attrs.find((a) => a.name === "href");
1723
+ if (href) href.value = previewLink(href.value, opts);
1724
+ }
1725
+ }
1726
+ const runtime = parseFragment(`<script type="module" src="${escapeHtml(opts.localOrigin)}/_members/_runtime/account-screens.js"><\/script>`);
1727
+ head.childNodes.unshift(...runtime.childNodes);
1728
+ const base = parseFragment(`<base href="${escapeHtml(new URL(opts.path, opts.origin).href)}">`);
1729
+ head.childNodes.unshift(...base.childNodes);
1730
+ const styles = parseFragment(render.styles);
1731
+ head.childNodes.push(...styles.childNodes);
1732
+ const style = parseFragment("<style></style>").childNodes[0];
1733
+ if (style && "childNodes" in style) {
1734
+ style.childNodes.push({
1735
+ nodeName: "#text",
1736
+ value: render.templateStyles,
1737
+ parentNode: style
1738
+ });
1739
+ head.childNodes.push(style);
1740
+ }
1741
+ const guard = parseFragment(`<script>document.addEventListener("submit",function(event){event.preventDefault();},true);<\/script>`);
1742
+ head.childNodes.unshift(...guard.childNodes);
1743
+ const navigation = parseFragment(`<script>
1744
+ document.addEventListener("fluid-account-navigate",function(event){
1745
+ event.preventDefault(); event.stopImmediatePropagation();
1746
+ var slug=event.detail && event.detail.slug;
1747
+ if(typeof slug!=="string" || !/^[a-z][a-z0-9_-]*(?:[/?#]|$)/.test(slug) || slug.includes("\\\\")) return;
1748
+ var next=new URL("/_members/"+slug,window.location.origin);
1749
+ if(next.origin!==window.location.origin || !next.pathname.startsWith("/_members/")) return;
1750
+ var context=${JSON.stringify(opts.selection).replace(/</g, "\\u003c")};
1751
+ ["members_site_id","country_id","language_id","member_type_id"].forEach(function(key){
1752
+ if(context[key]===undefined) next.searchParams.delete(key); else next.searchParams.set(key,context[key]);
1753
+ });
1754
+ window.location.assign(next.href);
1755
+ },true);
1756
+ <\/script>`);
1757
+ head.childNodes.unshift(...navigation.childNodes);
1758
+ return injectHotReload(serialize(document), opts.reloadMode);
1759
+ }
1760
+ function previewLink(href, opts) {
1761
+ if (href.startsWith("#")) return opts.localOrigin + memberPreviewUrl(opts.path, opts.selection) + href;
1762
+ let target;
1763
+ try {
1764
+ target = new URL(href, new URL(opts.path, opts.origin));
1765
+ } catch {
1766
+ return href;
1767
+ }
1768
+ if (!["http:", "https:"].includes(target.protocol)) return href;
1769
+ const absolute = /^[a-z][a-z\d+.-]*:/i.test(href) || href.startsWith("//");
1770
+ const accountLink = absolute && target.origin === opts.origin;
1771
+ if (absolute && !accountLink && target.origin !== opts.localOrigin) return href;
1772
+ target.pathname = accountPath(target.pathname);
1773
+ const isMember = accountLink || /^\/(auth(?:\/|$)|login$|logout$)/.test(target.pathname) || target.pathname.startsWith("/_members/") || opts.metadata.members_sites.some((site) => site.pages.some((p) => p.path === target.pathname)) || opts.metadata.system_screens.some((screen) => target.pathname === `/${screen.key}` || target.pathname.startsWith(`/${screen.key}/`));
1774
+ const path = target.pathname + target.search + target.hash;
1775
+ return opts.localOrigin + (isMember ? memberPreviewUrl(path, opts.selection) : path);
1776
+ }
1777
+ //#endregion
1778
+ //#region src/theme/dev-server/member-preview.ts
1779
+ /** Deny DNS rebinding and cross-origin access before using Node's CLI credential. */
1780
+ function isLocalPreviewRequest(req, host) {
1781
+ const port = req.socket.localPort;
1782
+ const hosts = new Set([
1783
+ host,
1784
+ "localhost",
1785
+ "127.0.0.1",
1786
+ "[::1]"
1787
+ ]);
1788
+ if ((host === "0.0.0.0" || host === "::") && req.socket.localAddress) {
1789
+ const address = req.socket.localAddress.replace(/^::ffff:/, "");
1790
+ hosts.add(address.includes(":") ? `[${address}]` : address);
1791
+ }
1792
+ let url;
1793
+ try {
1794
+ url = new URL(`http://${req.headers.host}`);
1795
+ } catch {
1796
+ return false;
1797
+ }
1798
+ if (!hosts.has(url.hostname) || Number(url.port || 80) !== port || url.username || url.password || url.pathname !== "/") return false;
1799
+ if (req.headers.origin && req.headers.origin !== url.origin) return false;
1800
+ return req.headers["sec-fetch-site"] !== "cross-site";
1801
+ }
1802
+ var MemberPreview = class {
1803
+ metadata;
1804
+ loading;
1805
+ origin;
1806
+ constructor(api, theme, reloadMode, screenAssets = new MemberScreenAssets()) {
1807
+ this.api = api;
1808
+ this.theme = theme;
1809
+ this.reloadMode = reloadMode;
1810
+ this.screenAssets = screenAssets;
1811
+ this.origin = memberOrigin(getApiBase(), theme.company);
1812
+ }
1813
+ /** Only call after every upload in a watcher batch has been acknowledged. */
1814
+ async refreshAfterSync() {
1815
+ if (!this.metadata && !this.loading) return;
1816
+ if (this.loading) await this.loading;
1817
+ this.metadata = void 0;
1818
+ this.metadata = await this.loadMetadata();
1819
+ }
1820
+ async loadMetadata() {
1821
+ return parseMemberMetadata(await this.api.get("/api/company/members_sites", { application_theme_id: this.theme.id }));
1822
+ }
1823
+ async getMetadata() {
1824
+ if (this.metadata) return this.metadata;
1825
+ this.loading ??= this.loadMetadata();
1826
+ try {
1827
+ this.metadata = await this.loading;
1828
+ return this.metadata;
1829
+ } finally {
1830
+ this.loading = void 0;
1831
+ }
1832
+ }
1833
+ async handle(req, res) {
1834
+ const url = new URL(req.url ?? "/", `http://${req.headers.host}`);
1835
+ if (url.pathname !== "/_members" && !url.pathname.startsWith("/_members/")) return false;
1836
+ res.setHeader("Cache-Control", "private, no-store");
1837
+ res.setHeader("Referrer-Policy", "no-referrer");
1838
+ res.setHeader("X-Content-Type-Options", "nosniff");
1839
+ try {
1840
+ if (req.method !== "GET" && req.method !== "HEAD") {
1841
+ res.setHeader("Allow", "GET, HEAD");
1842
+ throw new MemberPreviewError("Member preview is read-only.", 405);
1843
+ }
1844
+ if (url.pathname.startsWith("/_members/_runtime/")) {
1845
+ const asset = this.screenAssets.read(url.pathname.slice(19));
1846
+ if (!asset) throw new MemberPreviewError("Preview runtime asset unavailable. Rebuild or update the CLI.", 404);
1847
+ res.setHeader("Content-Type", "text/javascript; charset=utf-8");
1848
+ res.end(req.method === "HEAD" ? void 0 : asset);
1849
+ return true;
1850
+ }
1851
+ const metadata = await this.getMetadata();
1852
+ if (url.pathname === "/_members/_metadata") {
1853
+ res.setHeader("Content-Type", "application/json");
1854
+ res.end(req.method === "HEAD" ? void 0 : JSON.stringify({
1855
+ theme_id: this.theme.id,
1856
+ mode: "sample",
1857
+ ...metadata,
1858
+ supported_system_screens: metadata.system_screens.filter((screen) => this.screenAssets.screens.includes(screen.key)).map((screen) => screen.key)
1859
+ }));
1860
+ return true;
1861
+ }
1862
+ if (url.pathname === "/_members" || url.pathname === "/_members/") {
1863
+ const screens = metadata.system_screens.filter((screen) => this.screenAssets.screens.includes(screen.key));
1864
+ const links = metadata.members_sites.map((site) => {
1865
+ const destinations = [...site.pages.map((page) => ({
1866
+ path: page.path,
1867
+ title: page.title + (page.published ? "" : " (draft)")
1868
+ })), ...screens.map((screen) => ({
1869
+ path: `/${screen.key}`,
1870
+ title: screen.key + " (sample)"
1871
+ }))];
1872
+ return `<h2>${escapeHtml(site.name)}</h2><ul>${destinations.map((page) => `<li><a href="${escapeHtml(memberPreviewUrl(page.path, { members_site_id: site.id }))}">${escapeHtml(page.title)}</a></li>`).join("")}</ul>`;
1873
+ }).join("");
1874
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
1875
+ res.end(req.method === "HEAD" ? void 0 : `<!doctype html><meta charset="utf-8"><title>Member preview</title><h1>Member preview</h1><p>Sample data. Select a page to preview its site's layout.</p>${links || "<p>No member pages in this theme.</p>"}`);
1876
+ return true;
1877
+ }
1878
+ const selection = parseSelection(url.searchParams);
1879
+ const site = metadata.members_sites.find((s) => s.id === selection.members_site_id);
1880
+ if (!site) throw new MemberPreviewError("This site no longer belongs to the preview theme. Select a page at /_members.", 404);
1881
+ const path = url.pathname.slice(9);
1882
+ const page = site.pages.find((p) => p.path === path);
1883
+ const screen = page ? void 0 : metadata.system_screens.find((s) => path === `/${s.key}` || path.startsWith(`/${s.key}/`));
1884
+ if (!page && !screen) throw new MemberPreviewError("This member page is unavailable in the selected site. Select a page at /_members.", 404);
1885
+ if (screen && !this.screenAssets.screens.includes(screen.key)) throw new MemberPreviewError("This system screen has no packaged sample preview. Rebuild or update the CLI.", 501);
1886
+ const html = renderMemberHtml(parseMemberRender(await this.api.post(`/api/v202604/themes/${this.theme.id}/member-preview`, {
1887
+ ...selection,
1888
+ ...page ? { members_page_id: page.id } : { system_screen_key: screen.key }
1889
+ }), this.theme.id, selection, page?.id ?? null, screen?.key ?? null), {
1890
+ origin: this.origin,
1891
+ localOrigin: url.origin,
1892
+ path: path + url.search,
1893
+ selection,
1894
+ metadata,
1895
+ reloadMode: this.reloadMode,
1896
+ screenKey: screen?.key
1897
+ });
1898
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
1899
+ res.end(req.method === "HEAD" ? void 0 : html);
1900
+ } catch (error) {
1901
+ const status = error instanceof MemberPreviewError ? error.status : isRecord$5(error) && typeof error.status === "number" && [
1902
+ 401,
1903
+ 403,
1904
+ 404,
1905
+ 422
1906
+ ].includes(error.status) ? error.status : 502;
1907
+ const message = error instanceof MemberPreviewError ? error.message : status === 401 || status === 403 ? "Member preview authorization failed. Check your CLI login and theme permissions." : "Member preview could not be rendered. Check the selected page and server preview support.";
1908
+ res.writeHead(status, { "Content-Type": "text/plain; charset=utf-8" });
1909
+ res.end(req.method === "HEAD" ? void 0 : message);
1910
+ }
1911
+ return true;
1912
+ }
1913
+ };
1914
+ //#endregion
1915
+ //#region src/theme/dev-server/sse.ts
1916
+ var SSEStream = class {
1917
+ responses = /* @__PURE__ */ new Set();
1918
+ add(res) {
1919
+ res.writeHead(200, {
1920
+ "Content-Type": "text/event-stream",
1921
+ "Cache-Control": "no-cache",
1922
+ Connection: "keep-alive",
1923
+ "Access-Control-Allow-Origin": "*"
1924
+ });
1925
+ res.write(":\n\n");
1926
+ this.responses.add(res);
1927
+ res.on("close", () => this.responses.delete(res));
1928
+ }
1929
+ broadcast(data) {
1930
+ const payload = `data: ${data}\n\n`;
1931
+ for (const res of this.responses) try {
1932
+ res.write(payload);
1933
+ } catch {
1934
+ this.responses.delete(res);
1935
+ }
1936
+ }
1937
+ close() {
1938
+ for (const res of this.responses) try {
1939
+ res.end();
1940
+ } catch {}
1941
+ this.responses.clear();
1942
+ }
1943
+ get size() {
1944
+ return this.responses.size;
1945
+ }
1946
+ };
1947
+ //#endregion
1169
1948
  //#region src/theme/dev-server/proxy.ts
1170
1949
  const HOP_BY_HOP = new Set([
1171
1950
  "connection",
@@ -1240,143 +2019,16 @@ async function proxyRequest(req, res, opts) {
1240
2019
  reject(err);
1241
2020
  });
1242
2021
  if (body) proxyReq.write(body);
1243
- proxyReq.end();
1244
- });
1245
- }
1246
- function readBody(req) {
1247
- return new Promise((resolve, reject) => {
1248
- const chunks = [];
1249
- req.on("data", (chunk) => chunks.push(chunk));
1250
- req.on("end", () => resolve(Buffer.concat(chunks)));
1251
- req.on("error", reject);
1252
- });
1253
- }
1254
- //#endregion
1255
- //#region ../../api-clients/themes/src/namespaces/v0.ts
1256
- /**
1257
- * List application themes
1258
- * Get all application themes with optional filters
1259
- *
1260
- * @param client - Fetch client instance
1261
- * @param [params] - params
1262
- */
1263
- async function listApplicationThemes(client, params) {
1264
- return client.get(`/api/application_themes`, params);
1265
- }
1266
- /**
1267
- * Create an application theme
1268
- *
1269
- *
1270
- * @param client - Fetch client instance
1271
- * @param body - body
1272
- */
1273
- async function createApplicationTheme(client, body) {
1274
- return client.post(`/api/application_themes`, body);
1275
- }
1276
- /**
1277
- * Get an application theme
1278
- *
1279
- *
1280
- * @param client - Fetch client instance
1281
- * @param id - id
1282
- * @param [params] - params
1283
- */
1284
- async function getApplicationTheme(client, id, params) {
1285
- return client.get(`/api/application_themes/${id}`, params);
1286
- }
1287
- /**
1288
- * Returns available themeables for a given type scoped to the theme's company
1289
- *
1290
- *
1291
- * @param client - Fetch client instance
1292
- * @param id - id
1293
- * @param [params] - params
1294
- */
1295
- async function getApplicationThemeAvailableThemeables(client, id, params) {
1296
- return client.get(`/api/application_themes/${id}/available_themeables`, params);
1297
- }
1298
- /**
1299
- * Create a development reference clone of an application theme
1300
- * Creates an isolated development theme while preserving existing DAM and ImageKit references without transferring asset bytes.
1301
- *
1302
- * @param client - Fetch client instance
1303
- * @param id - id
1304
- * @param body - body
1305
- */
1306
- async function cloneApplicationThemeForDevelopment(client, id, body) {
1307
- return client.post(`/api/application_themes/${id}/clone_for_development`, body);
1308
- }
1309
- /**
1310
- * Publishes the theme
1311
- *
1312
- *
1313
- * @param client - Fetch client instance
1314
- * @param id - id
1315
- */
1316
- async function publishApplicationTheme(client, id) {
1317
- return client.post(`/api/application_themes/${id}/publish`);
1318
- }
1319
- /**
1320
- * Get theme assets
1321
- *
1322
- *
1323
- * @param client - Fetch client instance
1324
- * @param id - id
1325
- */
1326
- async function getThemeAssets(client, id) {
1327
- return client.get(`/api/application_themes/${id}/theme_assets`);
1328
- }
1329
- /**
1330
- * Lists all theme resources
1331
- *
1332
- *
1333
- * @param client - Fetch client instance
1334
- * @param application_theme_id - application_theme_id
1335
- */
1336
- async function listThemeResources(client, application_theme_id) {
1337
- return client.get(`/api/application_themes/${application_theme_id}/resources`);
1338
- }
1339
- /**
1340
- * Updates a theme resource
1341
- *
1342
- *
1343
- * @param client - Fetch client instance
1344
- * @param application_theme_id - application_theme_id
1345
- * @param body - body
1346
- */
1347
- async function updateThemeResource(client, application_theme_id, body) {
1348
- return client.put(`/api/application_themes/${application_theme_id}/resources`, body);
1349
- }
1350
- /**
1351
- * Deletes a theme resource
1352
- *
1353
- *
1354
- * @param client - Fetch client instance
1355
- * @param application_theme_id - application_theme_id
1356
- * @param body - body
1357
- */
1358
- async function deleteThemeResource(client, application_theme_id, body) {
1359
- return client.delete(`/api/application_themes/${application_theme_id}/resources`, { body });
1360
- }
1361
- /**
1362
- * Creates a file resource
1363
- *
1364
- *
1365
- * @param client - Fetch client instance
1366
- * @param body - body
1367
- */
1368
- async function createFileResource(client, body) {
1369
- return client.post(`/api/file_resources`, body);
2022
+ proxyReq.end();
2023
+ });
1370
2024
  }
1371
- /**
1372
- * Deletes a file resource
1373
- *
1374
- *
1375
- * @param client - Fetch client instance
1376
- * @param id - id
1377
- */
1378
- async function destroyFileResource(client, id) {
1379
- return client.delete(`/api/file_resources/${id}`);
2025
+ function readBody(req) {
2026
+ return new Promise((resolve, reject) => {
2027
+ const chunks = [];
2028
+ req.on("data", (chunk) => chunks.push(chunk));
2029
+ req.on("end", () => resolve(Buffer.concat(chunks)));
2030
+ req.on("error", reject);
2031
+ });
1380
2032
  }
1381
2033
  //#endregion
1382
2034
  //#region src/theme/format-error.ts
@@ -1638,9 +2290,9 @@ function readDocument(path) {
1638
2290
  }
1639
2291
  }
1640
2292
  function parseDocument$1(value) {
1641
- if (!isRecord$2(value) || value["version"] !== MANIFEST_VERSION) throw new Error(`expected version ${MANIFEST_VERSION}`);
2293
+ if (!isRecord$4(value) || value["version"] !== MANIFEST_VERSION) throw new Error(`expected version ${MANIFEST_VERSION}`);
1642
2294
  const rawAssets = value["assets"];
1643
- if (!isRecord$2(rawAssets)) throw new Error("expected an assets object");
2295
+ if (!isRecord$4(rawAssets)) throw new Error("expected an assets object");
1644
2296
  const assets = {};
1645
2297
  for (const [key, rawLink] of Object.entries(rawAssets)) {
1646
2298
  if (!isThemeAssetKey(key) || !isThemeAssetLink(rawLink)) throw new Error(`invalid asset entry for ${key}`);
@@ -1675,14 +2327,14 @@ function copyLink(link) {
1675
2327
  };
1676
2328
  }
1677
2329
  function isThemeAssetLink(value) {
1678
- return isRecord$2(value) && typeof value["sourceThemeId"] === "number" && Number.isInteger(value["sourceThemeId"]) && value["sourceThemeId"] > 0 && (value["checksum"] === void 0 || typeof value["checksum"] === "string" && value["checksum"].length > 0) && (value["url"] === void 0 || typeof value["url"] === "string" && value["url"].length > 0) && (value["contentType"] === void 0 || typeof value["contentType"] === "string" && value["contentType"].length > 0) && (value["contentSize"] === void 0 || typeof value["contentSize"] === "number" && Number.isInteger(value["contentSize"]) && value["contentSize"] > 0) && (value["previewImageUrl"] === void 0 || typeof value["previewImageUrl"] === "string" && value["previewImageUrl"].length > 0) && (value["altText"] === void 0 || typeof value["altText"] === "string") && (value["handle"] === void 0 || typeof value["handle"] === "string" && value["handle"].length > 0) && (value["pending"] === void 0 || typeof value["pending"] === "boolean") && (value["damAssetCode"] === void 0 || typeof value["damAssetCode"] === "string" && value["damAssetCode"].length > 0);
2330
+ return isRecord$4(value) && typeof value["sourceThemeId"] === "number" && Number.isInteger(value["sourceThemeId"]) && value["sourceThemeId"] > 0 && (value["checksum"] === void 0 || typeof value["checksum"] === "string" && value["checksum"].length > 0) && (value["url"] === void 0 || typeof value["url"] === "string" && value["url"].length > 0) && (value["contentType"] === void 0 || typeof value["contentType"] === "string" && value["contentType"].length > 0) && (value["contentSize"] === void 0 || typeof value["contentSize"] === "number" && Number.isInteger(value["contentSize"]) && value["contentSize"] > 0) && (value["previewImageUrl"] === void 0 || typeof value["previewImageUrl"] === "string" && value["previewImageUrl"].length > 0) && (value["altText"] === void 0 || typeof value["altText"] === "string") && (value["handle"] === void 0 || typeof value["handle"] === "string" && value["handle"].length > 0) && (value["pending"] === void 0 || typeof value["pending"] === "boolean") && (value["damAssetCode"] === void 0 || typeof value["damAssetCode"] === "string" && value["damAssetCode"].length > 0);
1679
2331
  }
1680
2332
  function isThemeAssetKey(key) {
1681
2333
  if (key.includes("\\") || key.includes("\0")) return false;
1682
2334
  const segments = key.split("/");
1683
2335
  return segments[0] === "assets" && segments.length === 2 && segments[1] !== void 0 && segments[1].length > 0 && segments[1] !== "." && segments[1] !== "..";
1684
2336
  }
1685
- function isRecord$2(value) {
2337
+ function isRecord$4(value) {
1686
2338
  return typeof value === "object" && value !== null && !Array.isArray(value);
1687
2339
  }
1688
2340
  //#endregion
@@ -1692,6 +2344,81 @@ function isStylesheetKey(key) {
1692
2344
  return STYLESHEET_KEY_PATTERN.test(key);
1693
2345
  }
1694
2346
  //#endregion
2347
+ //#region src/theme/member-dependencies.ts
2348
+ /** The same file graph is used for uploads and incoming omission protection.
2349
+ * Rails remains responsible for resolving company-specific roles and handles. */
2350
+ function memberDependencyPaths(document, availablePaths, deletedPaths = [], changedPaths = availablePaths) {
2351
+ const dependencies = /* @__PURE__ */ new Set();
2352
+ for (const entry of [...records(document.templates), ...records(document.menu_references)]) if (typeof entry.path === "string") dependencies.add(entry.path);
2353
+ for (const site of records(document.sites)) {
2354
+ for (const page of records(site.pages)) if (typeof page.template_path === "string") dependencies.add(page.template_path);
2355
+ const layout = site.layout_name;
2356
+ if (layout === "none") continue;
2357
+ if (typeof layout === "string" && layout && !["member", "theme"].includes(layout)) {
2358
+ dependencies.add(`layouts/${layout}.liquid`);
2359
+ continue;
2360
+ }
2361
+ const fallback = [
2362
+ layout,
2363
+ "member",
2364
+ "theme"
2365
+ ].filter((name) => typeof name === "string" && name.length > 0).map((name) => `layouts/${name}.liquid`).find((path) => availablePaths.has(path) || deletedPaths.includes(path));
2366
+ if (fallback) dependencies.add(fallback);
2367
+ }
2368
+ for (const path of dependencies) {
2369
+ if (!path.endsWith("/index.liquid")) continue;
2370
+ for (const suffix of ["variables.json", "styles.css"]) {
2371
+ const sibling = path.replace(/index\.liquid$/, suffix);
2372
+ if ((suffix === "styles.css" ? changedPaths : availablePaths).has(sibling) || deletedPaths.includes(sibling)) dependencies.add(sibling);
2373
+ }
2374
+ }
2375
+ return dependencies;
2376
+ }
2377
+ function records(entries) {
2378
+ return (Array.isArray(entries) ? entries : []).filter((entry) => typeof entry === "object" && entry !== null && !Array.isArray(entry));
2379
+ }
2380
+ //#endregion
2381
+ //#region src/theme/member-configuration.ts
2382
+ const MEMBER_CONFIGURATION_KEY = "members_sites.json";
2383
+ /** Apply member selectors and their document together, before losing old references. */
2384
+ function planThemeUploads(themeRoot, changedFiles, deletedPaths = []) {
2385
+ const localFiles = themeRoot.files();
2386
+ const manifest = localFiles.find((file) => file.relativePath === MEMBER_CONFIGURATION_KEY);
2387
+ if (!manifest || themeRoot.ignore.ignore("members_sites.json")) return individualUploads(changedFiles);
2388
+ const content = manifest.read();
2389
+ const document = JSON.parse(content);
2390
+ if (Array.isArray(document)) return individualUploads(changedFiles);
2391
+ if (!isRecord$3(document)) throw new Error("members_sites.json must contain a JSON object.");
2392
+ const dependencies = memberDependencyPaths(document, new Set(localFiles.map((file) => file.relativePath)), deletedPaths, new Set(changedFiles.map((file) => file.relativePath)));
2393
+ for (const path of deletedPaths) if (dependencies.has(path)) throw new Error(`Member dependency is still declared and cannot be deleted: ${path}`);
2394
+ if (!changedFiles.some((file) => file.relativePath === "members_sites.json" || dependencies.has(file.relativePath))) return changedFiles.map((file) => ({ file }));
2395
+ const availableFiles = new Map(localFiles.map((file) => [file.relativePath, file]));
2396
+ const memberFiles = [...dependencies].map((path) => {
2397
+ const file = availableFiles.get(path);
2398
+ if (!file || !file.isText || themeRoot.ignore.ignore(path)) throw new Error(`Member dependency must be an available text file: ${path}`);
2399
+ return file;
2400
+ });
2401
+ const uploads = changedFiles.filter((file) => file.relativePath !== "members_sites.json" && !dependencies.has(file.relativePath)).map((file) => ({ file }));
2402
+ uploads.push({
2403
+ file: manifest,
2404
+ memberConfiguration: {
2405
+ content,
2406
+ files: memberFiles
2407
+ }
2408
+ });
2409
+ return uploads;
2410
+ }
2411
+ function individualUploads(files) {
2412
+ return [...files].sort((left, right) => Number(left.relativePath === MEMBER_CONFIGURATION_KEY) - Number(right.relativePath === MEMBER_CONFIGURATION_KEY)).map((file) => ({ file }));
2413
+ }
2414
+ function isRecord$3(entry) {
2415
+ return typeof entry === "object" && entry !== null && !Array.isArray(entry);
2416
+ }
2417
+ /** Removing an older file is not an instruction to erase a member site. */
2418
+ function assertMemberConfigurationRetained(deletedPaths) {
2419
+ if (deletedPaths.includes("members_sites.json")) throw new Error("Keep members_sites.json. To remove member configuration, upload an explicit empty versioned document instead of deleting or renaming the file.");
2420
+ }
2421
+ //#endregion
1695
2422
  //#region src/theme/syncer.ts
1696
2423
  const ASSET_REFERENCE_CONCURRENCY = 6;
1697
2424
  /**
@@ -1712,6 +2439,7 @@ var PushConflictError = class extends Error {
1712
2439
  };
1713
2440
  var Syncer = class {
1714
2441
  checksumIndex = /* @__PURE__ */ new Map();
2442
+ memberAcknowledgments = /* @__PURE__ */ new Map();
1715
2443
  rawRemoteResources = /* @__PURE__ */ new Map();
1716
2444
  remoteResourceGroups = /* @__PURE__ */ new Map();
1717
2445
  remoteResourceIndex = /* @__PURE__ */ new Map();
@@ -1894,7 +2622,7 @@ var Syncer = class {
1894
2622
  }
1895
2623
  async fetchThemeAssetMetadata(sourceThemeId) {
1896
2624
  const body = await getThemeAssets(this.api, sourceThemeId);
1897
- if (!isRecord$1(body) || !Array.isArray(body["file_resources"])) throw new Error("Theme assets response did not include file_resources");
2625
+ if (!isRecord$2(body) || !Array.isArray(body["file_resources"])) throw new Error("Theme assets response did not include file_resources");
1898
2626
  const assets = /* @__PURE__ */ new Map();
1899
2627
  for (const value of body["file_resources"]) {
1900
2628
  const asset = parseThemeAssetMetadata(value);
@@ -2063,6 +2791,33 @@ var Syncer = class {
2063
2791
  if (isThemeAssetKey(file.relativePath)) this.externalizeBinaryFile(file, resource, opts.pendingAsset);
2064
2792
  return null;
2065
2793
  }
2794
+ /** Canonical member bytes accepted by the server during this transfer. */
2795
+ acknowledgedMemberContents() {
2796
+ return this.memberAcknowledgments;
2797
+ }
2798
+ /** Upload one member document and its changed dependencies as one transaction. */
2799
+ async uploadMemberConfiguration(upload, baseSha) {
2800
+ if (!upload.memberConfiguration) throw new Error("A member configuration upload is required.");
2801
+ const { content, files } = upload.memberConfiguration;
2802
+ const capturedFiles = files.map((file) => ({
2803
+ file,
2804
+ content: file.read()
2805
+ }));
2806
+ const resource = await this.putResource({
2807
+ key: MEMBER_CONFIGURATION_KEY,
2808
+ content,
2809
+ member_files: capturedFiles.map(({ file, content: capturedContent }) => ({
2810
+ key: file.relativePath,
2811
+ content: capturedContent
2812
+ }))
2813
+ }, baseSha, capturedFiles.map(({ file }) => file.relativePath));
2814
+ this.setRemoteResource(resource);
2815
+ if (resource.key && resource.content != null) this.memberAcknowledgments.set(resource.key, typeof resource.content === "string" ? resource.content : JSON.stringify(resource.content));
2816
+ return [{
2817
+ file: upload.file,
2818
+ content
2819
+ }, ...capturedFiles];
2820
+ }
2066
2821
  /**
2067
2822
  * Wraps the generated `updateThemeResource` client with the two
2068
2823
  * merge-aware Phase 003 additions: sending `base_sha` on the request
@@ -2085,12 +2840,21 @@ var Syncer = class {
2085
2840
  * "server rejected because of stale base" from generic upload
2086
2841
  * failures.
2087
2842
  */
2088
- async putResource(resource, baseSha) {
2843
+ async putResource(resource, baseSha, expectedMemberKeys) {
2089
2844
  const body = { application_theme_resource: resource };
2090
2845
  if (baseSha) body["base_sha"] = baseSha;
2091
2846
  try {
2092
2847
  const response = await updateThemeResource(this.api, this.themeId, body);
2848
+ if (expectedMemberKeys) {
2849
+ const acknowledged = new Map((response.member_resources ?? []).map((entry) => [entry.key, entry]));
2850
+ const manifest = response.application_theme_resource;
2851
+ if (manifest?.key !== "members_sites.json" || manifest.content == null || !manifest.checksum || expectedMemberKeys.some((key) => acknowledged.get(key)?.content == null || !acknowledged.get(key)?.checksum)) throw new Error("Fluid did not acknowledge every bundled member file. Update the server before retrying this push.");
2852
+ }
2093
2853
  if (response.content_version_sha) this.lastKnownRemoteSha = response.content_version_sha;
2854
+ for (const memberResource of response.member_resources ?? []) {
2855
+ this.setRemoteResource(memberResource);
2856
+ if (memberResource.key && memberResource.content != null) this.memberAcknowledgments.set(memberResource.key, typeof memberResource.content === "string" ? memberResource.content : JSON.stringify(memberResource.content));
2857
+ }
2094
2858
  return response.application_theme_resource ?? {
2095
2859
  key: typeof resource["key"] === "string" ? resource["key"] : "",
2096
2860
  checksum: null
@@ -2125,10 +2889,9 @@ var Syncer = class {
2125
2889
  * close together or splits one push across several commits. The client
2126
2890
  * knows; this says so.
2127
2891
  *
2128
- * Best-effort in that it never throws: the files are already on the server
2129
- * by the time this runs, and Fluid sweeps anything left unsynced, so a
2130
- * failure delays the commit rather than losing it and must not fail the
2131
- * push.
2892
+ * Returns false rather than throwing when the request fails. Callers must
2893
+ * retain their previous baseline until finalization succeeds; the server
2894
+ * can reject an incomplete member graph even after individual files land.
2132
2895
  *
2133
2896
  * Returns whether Fluid took the request, because a caller tracking edit
2134
2897
  * boundaries needs to know. A failed ask leaves the previous edit
@@ -2141,7 +2904,7 @@ var Syncer = class {
2141
2904
  if (response.content_version_sha) this.lastKnownRemoteSha = response.content_version_sha;
2142
2905
  return true;
2143
2906
  } catch (e) {
2144
- console.warn(` ⚠ couldn't ask Fluid to commit this push (${formatError(e)}). It will be picked up automatically.`);
2907
+ console.warn(` ⚠ couldn't ask Fluid to commit this push (${formatError(e)}). Retry the push to finalize it.`);
2145
2908
  return false;
2146
2909
  }
2147
2910
  }
@@ -2247,6 +3010,7 @@ var Syncer = class {
2247
3010
  }[category] ?? "files"}/${assetCode}`;
2248
3011
  }
2249
3012
  async deleteRemoteFile(relativePath, baseSha) {
3013
+ assertMemberConfigurationRetained([relativePath]);
2250
3014
  this.assetManifest.reload();
2251
3015
  if (this.assetManifest.has(relativePath)) return;
2252
3016
  const body = { application_theme_resource: { key: relativePath } };
@@ -2430,6 +3194,10 @@ var Syncer = class {
2430
3194
  else await this.fetchChecksums();
2431
3195
  if (!opts.skipPreflight) await this.preflightPush(opts.baseSha);
2432
3196
  let baseSha = opts.baseSha ?? null;
3197
+ const localPaths = new Set(localFiles.map((file) => file.relativePath));
3198
+ if (opts.delete) for (const key of this.assetManifest.keys()) localPaths.add(key);
3199
+ const toDelete = opts.delete ? this.remoteKeys().filter((key) => key !== "members_sites.json" && this.canDeleteRemoteResource(key) && !localPaths.has(key) && !this.themeRoot.ignore.ignore(key)) : [];
3200
+ const plannedUploads = planThemeUploads(this.themeRoot, localFiles.filter((f) => f.exists && this.hasChanged(f)), toDelete);
2433
3201
  if (opts.linkManagedAssets) {
2434
3202
  result.linked = await this.linkManagedAssets(opts.linkManagedAssets);
2435
3203
  baseSha = this.lastKnownRemoteSha ?? baseSha;
@@ -2437,31 +3205,34 @@ var Syncer = class {
2437
3205
  this.assetManifest.reload();
2438
3206
  this.ensureManagedAssetsAreResolved();
2439
3207
  }
2440
- const toUpload = localFiles.filter((f) => f.exists && this.hasChanged(f));
3208
+ const toUpload = plannedUploads.filter(({ file, memberConfiguration }) => memberConfiguration || file.exists && this.hasChanged(file));
3209
+ const uploadCount = toUpload.reduce((count, upload) => count + 1 + (upload.memberConfiguration?.files.length ?? 0), 0);
2441
3210
  let done = 0;
2442
- for (const file of toUpload) {
3211
+ for (const upload of toUpload) {
3212
+ const { file } = upload;
3213
+ if (file.relativePath === "members_sites.json" && result.errors.length > 0) {
3214
+ result.errors.push("Skipped members_sites.json because a dependency upload failed.");
3215
+ break;
3216
+ }
2443
3217
  try {
2444
- await this.uploadFile(file, baseSha, { pendingAsset: opts.pendingBinaryAssets });
3218
+ if (upload.memberConfiguration) await this.uploadMemberConfiguration(upload, baseSha);
3219
+ else await this.uploadFile(file, baseSha, { pendingAsset: opts.pendingBinaryAssets });
2445
3220
  baseSha = this.lastKnownRemoteSha;
2446
- result.uploaded++;
3221
+ result.uploaded += 1 + (upload.memberConfiguration?.files.length ?? 0);
2447
3222
  } catch (e) {
2448
3223
  if (e instanceof PushConflictError) throw e;
2449
3224
  result.errors.push(`Upload ${file.relativePath}: ${formatError(e)}`);
2450
3225
  }
2451
- opts.onProgress?.(++done, toUpload.length);
3226
+ done += 1 + (upload.memberConfiguration?.files.length ?? 0);
3227
+ opts.onProgress?.(done, uploadCount);
2452
3228
  }
2453
- if (opts.delete) {
2454
- const localPaths = new Set(localFiles.map((f) => f.relativePath));
2455
- for (const key of this.assetManifest.keys()) localPaths.add(key);
2456
- const toDelete = this.remoteKeys().filter((key) => this.canDeleteRemoteResource(key) && !localPaths.has(key) && !this.themeRoot.ignore.ignore(key));
2457
- for (const key of toDelete) try {
2458
- await this.deleteRemoteFile(key, baseSha);
2459
- baseSha = this.lastKnownRemoteSha;
2460
- result.deleted++;
2461
- } catch (e) {
2462
- if (e instanceof PushConflictError) throw e;
2463
- result.errors.push(`Delete ${key}: ${formatError(e)}`);
2464
- }
3229
+ if (opts.delete && result.errors.length === 0) for (const key of toDelete) try {
3230
+ await this.deleteRemoteFile(key, baseSha);
3231
+ baseSha = this.lastKnownRemoteSha;
3232
+ result.deleted++;
3233
+ } catch (e) {
3234
+ if (e instanceof PushConflictError) throw e;
3235
+ result.errors.push(`Delete ${key}: ${formatError(e)}`);
2465
3236
  }
2466
3237
  return result;
2467
3238
  }
@@ -2586,7 +3357,7 @@ function uploadedAssetMetadata(file, resource) {
2586
3357
  };
2587
3358
  }
2588
3359
  function parseThemeAssetMetadata(value) {
2589
- if (!isRecord$1(value)) return void 0;
3360
+ if (!isRecord$2(value)) return void 0;
2590
3361
  const filename = nonEmptyString(value["filename"]);
2591
3362
  const url = nonEmptyString(value["url"]);
2592
3363
  const contentType = nonEmptyString(value["content_type"]);
@@ -2608,7 +3379,7 @@ function parseThemeAssetMetadata(value) {
2608
3379
  };
2609
3380
  }
2610
3381
  function createdFileResourceId(value) {
2611
- if (!isRecord$1(value) || !isRecord$1(value["file_resource"])) return;
3382
+ if (!isRecord$2(value) || !isRecord$2(value["file_resource"])) return;
2612
3383
  return positiveInteger(value["file_resource"]["id"]);
2613
3384
  }
2614
3385
  function positiveInteger(value) {
@@ -2623,7 +3394,7 @@ function nonEmptyString(value) {
2623
3394
  function optionalString(value) {
2624
3395
  return typeof value === "string" ? value : void 0;
2625
3396
  }
2626
- function isRecord$1(value) {
3397
+ function isRecord$2(value) {
2627
3398
  return typeof value === "object" && value !== null && !Array.isArray(value);
2628
3399
  }
2629
3400
  //#endregion
@@ -2809,6 +3580,7 @@ function timestamp() {
2809
3580
  }
2810
3581
  async function startDevServer(api, theme, themeRoot, opts, onReady) {
2811
3582
  const sse = new SSEStream();
3583
+ const memberPreview = new MemberPreview(api, theme, opts.reloadMode);
2812
3584
  const syncer = new Syncer(api, theme.id, themeRoot);
2813
3585
  let remoteStateSafe = true;
2814
3586
  const invalidateRemoteState = () => {
@@ -2919,30 +3691,40 @@ async function startDevServer(api, theme, themeRoot, opts, onReady) {
2919
3691
  await syncInFlight;
2920
3692
  }
2921
3693
  if (remoteWritesBlocked) return;
3694
+ let uploads;
2922
3695
  try {
2923
3696
  assertNoCaseCollisions(themeRoot.files().map((file) => file.relativePath));
3697
+ assertMemberConfigurationRetained(removed.filter((file) => !themeRoot.ignore.ignore(file.relativePath)).map((file) => file.relativePath));
3698
+ uploads = planThemeUploads(themeRoot, [...modified, ...added], removed.filter((file) => !themeRoot.ignore.ignore(file.relativePath)).map((file) => file.relativePath));
2924
3699
  } catch (error) {
2925
3700
  console.error(`\n[Watcher] Sync blocked: ${String(error)}`);
2926
3701
  return;
2927
3702
  }
2928
- const changed = [...modified, ...added];
2929
3703
  let wroteRemote = false;
2930
- for (const file of changed) {
2931
- if (opts.validate && file.isLiquid) {
2932
- const diagnostics = file.validateSchema();
2933
- for (const d of diagnostics) {
2934
- const prefix = d.severity === "error" ? "Schema error" : "Schema warning";
2935
- console.warn(`\n[${prefix}] ${file.relativePath}: ${d.message}`);
3704
+ const memberTransfer = uploads.some((upload) => upload.memberConfiguration || upload.file.relativePath === "members_sites.json");
3705
+ for (const upload of uploads) {
3706
+ const { file } = upload;
3707
+ const uploadFiles = [file, ...upload.memberConfiguration?.files ?? []];
3708
+ if (opts.validate) for (const candidate of uploadFiles) {
3709
+ if (!candidate.isLiquid) continue;
3710
+ for (const diagnostic of candidate.validateSchema()) {
3711
+ const prefix = diagnostic.severity === "error" ? "Schema error" : "Schema warning";
3712
+ console.warn(`\n[${prefix}] ${candidate.relativePath}: ${diagnostic.message}`);
2936
3713
  }
2937
3714
  }
2938
- pendingUpdates.add(file.relativePath);
3715
+ for (const pendingFile of uploadFiles) pendingUpdates.add(pendingFile.relativePath);
2939
3716
  try {
2940
- const uploadedContent = await syncer.uploadFile(file, syncer.remoteSha(), { pendingAsset: true });
3717
+ const uploadedFiles = upload.memberConfiguration ? await syncer.uploadMemberConfiguration(upload, syncer.remoteSha()) : [{
3718
+ file,
3719
+ content: await syncer.uploadFile(file, syncer.remoteSha(), { pendingAsset: true })
3720
+ }];
2941
3721
  wroteRemote = true;
2942
- console.log(` ✓ synced ${file.relativePath} (${timestamp()})`);
2943
- if (isStylesheetKey(file.relativePath)) console.warn(` ⚠ ${file.relativePath}: the storefront inlines this CSS from a published asset, so it serves the previous bytes until the stylesheet asset sync finishes`);
2944
- if (file.isLiquid && uploadedContent !== null && hasUnbalancedLiquidDelimiters(uploadedContent)) console.warn(` ⚠ ${file.relativePath}: unbalanced liquid delimiters — the storefront may silently serve stale content for this section`);
2945
- if (file.isLiquid && uploadedContent !== null) for (const diagnostic of findLiquidBlockTagDiagnostics(uploadedContent)) console.warn(` ⚠ ${file.relativePath}: ${diagnostic.message}`);
3722
+ for (const { file: uploadedFile, content: uploadedContent } of uploadedFiles) {
3723
+ console.log(` ✓ synced ${uploadedFile.relativePath} (${timestamp()})`);
3724
+ if (isStylesheetKey(uploadedFile.relativePath)) console.warn(` ⚠ ${uploadedFile.relativePath}: the storefront inlines this CSS from a published asset, so it serves the previous bytes until the stylesheet asset sync finishes`);
3725
+ if (uploadedFile.isLiquid && uploadedContent !== null && hasUnbalancedLiquidDelimiters(uploadedContent)) console.warn(` ⚠ ${uploadedFile.relativePath}: unbalanced liquid delimiters — the storefront may silently serve stale content for this section`);
3726
+ if (uploadedFile.isLiquid && uploadedContent !== null) for (const diagnostic of findLiquidBlockTagDiagnostics(uploadedContent)) console.warn(` ⚠ ${uploadedFile.relativePath}: ${diagnostic.message}`);
3727
+ }
2946
3728
  } catch (e) {
2947
3729
  if (e instanceof PushConflictError) {
2948
3730
  blockRemoteWrites(e);
@@ -2950,8 +3732,11 @@ async function startDevServer(api, theme, themeRoot, opts, onReady) {
2950
3732
  }
2951
3733
  invalidateRemoteState();
2952
3734
  console.error(`\n[Watcher] Upload failed: ${file.relativePath}: ${e}`);
3735
+ remoteWritesBlocked = wroteRemote || memberTransfer;
3736
+ if (remoteWritesBlocked) console.error("[Watcher] Restart theme dev to retry the incomplete transfer.");
3737
+ return;
2953
3738
  } finally {
2954
- pendingUpdates.delete(file.relativePath);
3739
+ for (const pendingFile of uploadFiles) pendingUpdates.delete(pendingFile.relativePath);
2955
3740
  }
2956
3741
  }
2957
3742
  if (remoteWritesBlocked) return;
@@ -2967,15 +3752,32 @@ async function startDevServer(api, theme, themeRoot, opts, onReady) {
2967
3752
  break;
2968
3753
  }
2969
3754
  invalidateRemoteState();
3755
+ remoteWritesBlocked = wroteRemote || memberTransfer;
3756
+ console.error(`[Watcher] Delete failed: ${file.relativePath}: ${error}`);
3757
+ if (remoteWritesBlocked) console.error("[Watcher] Restart theme dev to retry the incomplete transfer.");
3758
+ return;
2970
3759
  }
2971
3760
  }
2972
3761
  if (remoteWritesBlocked) return;
2973
- if (wroteRemote) recordRemoteState();
3762
+ if (wroteRemote) {
3763
+ recordRemoteState();
3764
+ try {
3765
+ await memberPreview.refreshAfterSync();
3766
+ } catch {
3767
+ console.error("[Member preview] Could not refresh routes after sync. The next member request will retry.");
3768
+ }
3769
+ }
2974
3770
  if (removed.length > 0) sse.broadcast(JSON.stringify({ reload_page: true }));
2975
- else if (changed.length > 0) sse.broadcast(JSON.stringify({ modified: changed.map((f) => f.relativePath) }));
3771
+ else if (uploads.length > 0) sse.broadcast(JSON.stringify({ modified: uploads.flatMap(({ file, memberConfiguration }) => [file, ...memberConfiguration?.files ?? []].map((updatedFile) => updatedFile.relativePath)) }));
2976
3772
  scheduleSync();
2977
3773
  });
2978
3774
  const server = http.createServer(async (req, res) => {
3775
+ if (!isLocalPreviewRequest(req, opts.host)) {
3776
+ res.writeHead(403, { "Content-Type": "text/plain" });
3777
+ res.end("Local preview requests only.");
3778
+ return;
3779
+ }
3780
+ if (await memberPreview.handle(req, res)) return;
2979
3781
  if (req.url === "/hot-reload") {
2980
3782
  sse.add(res);
2981
3783
  return;
@@ -3092,7 +3894,7 @@ async function devRemoteStateFromSourceShadow(themeRoot, shadow, themeId, remote
3092
3894
  }
3093
3895
  }
3094
3896
  function parseDocument(document) {
3095
- if (!isRecord(document) || document["version"] !== BASELINE_VERSION) throw new Error(`expected version ${BASELINE_VERSION}`);
3897
+ if (!isRecord$1(document) || document["version"] !== BASELINE_VERSION) throw new Error(`expected version ${BASELINE_VERSION}`);
3096
3898
  const themeId = document["themeId"];
3097
3899
  const remoteSha = document["remoteSha"];
3098
3900
  const assetManifestSha = document["assetManifestSha"];
@@ -3111,7 +3913,7 @@ function parseDocument(document) {
3111
3913
  };
3112
3914
  }
3113
3915
  function parseResource(resource) {
3114
- if (!isRecord(resource)) throw new Error("invalid resource");
3916
+ if (!isRecord$1(resource)) throw new Error("invalid resource");
3115
3917
  const key = resource["key"];
3116
3918
  const checksum = resource["checksum"];
3117
3919
  const contentPresent = resource["contentPresent"];
@@ -3133,7 +3935,7 @@ function parseResource(resource) {
3133
3935
  url
3134
3936
  };
3135
3937
  }
3136
- function isRecord(candidate) {
3938
+ function isRecord$1(candidate) {
3137
3939
  return typeof candidate === "object" && candidate !== null;
3138
3940
  }
3139
3941
  function isNonEmptyString(candidate) {
@@ -3817,7 +4619,9 @@ function createDevCommand() {
3817
4619
  console.log(`\n Dev server: ${address}`);
3818
4620
  console.log(` Web editor: ${editorUrl}`);
3819
4621
  console.log("\n Watching for file changes…\n");
3820
- if (opts.navigate) import("open").then((m) => m.default(`${address}/home`));
4622
+ if (opts.navigate) navigateTheme(api, theme.id, address).catch((error) => {
4623
+ console.error(error instanceof Error ? error.message : "Could not open the preview navigator.");
4624
+ });
3821
4625
  });
3822
4626
  await new Promise(() => {});
3823
4627
  });
@@ -3911,17 +4715,25 @@ function containsConflictMarker(buf) {
3911
4715
  return buf.includes(CONFLICT_START) && buf.includes(CONFLICT_MID) && buf.includes(CONFLICT_END);
3912
4716
  }
3913
4717
  /**
3914
- * Commit the current working-tree state to shadow HEAD after a
3915
- * successful push. Ensures the next pull's merge base is the state
3916
- * we know the server just accepted.
4718
+ * Commit the pushed state to shadow HEAD. Server-acknowledged contents
4719
+ * override local bytes for normalized member files, including files edited
4720
+ * or deleted locally during upload. Local files remain untouched so those
4721
+ * later changes still differ from the next pull's merge base.
3917
4722
  */
3918
- async function commitPushedState(themeRoot, shadow, message) {
4723
+ async function commitPushedState(themeRoot, shadow, message, acknowledgedContents) {
3919
4724
  const entries = [];
3920
4725
  const assetManifest = new ThemeAssetManifest(themeRoot.root);
3921
- const localKeys = /* @__PURE__ */ new Set();
4726
+ const recordedKeys = /* @__PURE__ */ new Set();
4727
+ for (const [path, content] of new Map(acknowledgedContents)) {
4728
+ entries.push({
4729
+ path,
4730
+ sha: await shadow.writeBlob(content)
4731
+ });
4732
+ recordedKeys.add(path);
4733
+ }
3922
4734
  for (const file of themeRoot.files()) {
3923
- if (!file.exists) continue;
3924
- localKeys.add(file.relativePath);
4735
+ if (!file.exists || recordedKeys.has(file.relativePath)) continue;
4736
+ recordedKeys.add(file.relativePath);
3925
4737
  const buf = file.isText ? Buffer.from(file.read()) : file.readBinary();
3926
4738
  entries.push({
3927
4739
  path: file.relativePath,
@@ -3930,7 +4742,7 @@ async function commitPushedState(themeRoot, shadow, message) {
3930
4742
  }
3931
4743
  let managedAssetSentinelSha;
3932
4744
  for (const key of assetManifest.keys()) {
3933
- if (localKeys.has(key)) continue;
4745
+ if (recordedKeys.has(key)) continue;
3934
4746
  managedAssetSentinelSha ??= await shadow.writeBlob(MANAGED_ASSET_SHADOW_SENTINEL);
3935
4747
  entries.push({
3936
4748
  path: key,
@@ -4220,6 +5032,7 @@ function createPushCommand() {
4220
5032
  }
4221
5033
  }
4222
5034
  const { changed, deleted } = await diffAgainstShadow(themeRoot, shadow);
5035
+ if (!opts.nodelete) assertMemberConfigurationRetained(deleted);
4223
5036
  const managedAssetCount = new ThemeAssetManifest(themeRoot.root).keys().length;
4224
5037
  if (changed.length === 0 && deleted.length === 0 && managedAssetCount === 0) {
4225
5038
  console.log("Nothing to push — local matches the last synced state.");
@@ -4250,6 +5063,7 @@ function createPushCommand() {
4250
5063
  }
4251
5064
  throw e;
4252
5065
  }
5066
+ const uploads = planThemeUploads(themeRoot, changed, opts.nodelete ? [] : deleted);
4253
5067
  let linked = 0;
4254
5068
  try {
4255
5069
  linked = await syncer.linkManagedAssets({ replace: true });
@@ -4273,12 +5087,18 @@ function createPushCommand() {
4273
5087
  let deletedCount = 0;
4274
5088
  const errors = [];
4275
5089
  let progress = 0;
4276
- const total = changed.length + (opts.nodelete ? 0 : deleted.length);
4277
- for (const file of changed) {
5090
+ const total = uploads.reduce((count, upload) => count + 1 + (upload.memberConfiguration?.files.length ?? 0), 0) + (opts.nodelete ? 0 : deleted.length);
5091
+ for (const upload of uploads) {
5092
+ const { file } = upload;
5093
+ if (file.relativePath === "members_sites.json" && errors.length > 0) {
5094
+ errors.push("Skipped members_sites.json because a dependency upload failed.");
5095
+ break;
5096
+ }
4278
5097
  try {
4279
- await syncer.uploadFile(file, baseSha);
5098
+ if (upload.memberConfiguration) await syncer.uploadMemberConfiguration(upload, baseSha);
5099
+ else await syncer.uploadFile(file, baseSha);
4280
5100
  baseSha = syncer.remoteSha() ?? baseSha;
4281
- uploaded++;
5101
+ uploaded += 1 + (upload.memberConfiguration?.files.length ?? 0);
4282
5102
  } catch (e) {
4283
5103
  if (e instanceof PushConflictError) {
4284
5104
  spinner.stop();
@@ -4287,9 +5107,10 @@ function createPushCommand() {
4287
5107
  }
4288
5108
  errors.push(`Upload ${file.relativePath}: ${formatError(e)}`);
4289
5109
  }
4290
- spinner.text = `Pushing ${++progress}/${total} files…`;
5110
+ progress += 1 + (upload.memberConfiguration?.files.length ?? 0);
5111
+ spinner.text = `Pushing ${progress}/${total} files…`;
4291
5112
  }
4292
- if (!opts.nodelete) for (const key of deleted) {
5113
+ if (!opts.nodelete && errors.length === 0) for (const key of deleted) {
4293
5114
  try {
4294
5115
  await syncer.deleteRemoteFile(key, baseSha);
4295
5116
  baseSha = syncer.remoteSha() ?? baseSha;
@@ -4309,7 +5130,9 @@ function createPushCommand() {
4309
5130
  } catch (error) {
4310
5131
  errors.push(`Save asset provenance: ${formatError(error)}`);
4311
5132
  }
4312
- if (uploaded > 0 || deletedCount > 0 || linked > 0) await syncer.requestSync();
5133
+ if (errors.length === 0 && (uploaded > 0 || deletedCount > 0 || linked > 0)) {
5134
+ if (!await syncer.requestSync()) errors.push("Fluid did not accept sync finalization. Retry the push before publishing.");
5135
+ }
4313
5136
  if (errors.length) {
4314
5137
  spinner.warn(`Pushed with ${errors.length} error(s).`);
4315
5138
  for (const err of errors) console.error(` ${err}`);
@@ -4323,7 +5146,14 @@ function createPushCommand() {
4323
5146
  status: "deleted",
4324
5147
  path: key
4325
5148
  }))];
4326
- await commitPushedState(themeRoot, shadow, syncCommitSubject("Push", await getActor(), /* @__PURE__ */ new Date(), pushedChanges));
5149
+ try {
5150
+ await commitPushedState(themeRoot, shadow, syncCommitSubject("Push", await getActor(), /* @__PURE__ */ new Date(), pushedChanges), syncer.acknowledgedMemberContents());
5151
+ } catch (error) {
5152
+ console.error(`Theme content was uploaded to the server, but the local restore point and sync baseline could not be saved: ${error instanceof Error ? error.message : String(error)}. Do not repeat the push with --force. Review the remote theme and run a normal pull to recover the baseline.`);
5153
+ if (opts.publish) console.error("Publishing was not attempted.");
5154
+ process.exitCode = 1;
5155
+ return;
5156
+ }
4327
5157
  }
4328
5158
  await persistConfig();
4329
5159
  /**
@@ -4358,7 +5188,7 @@ function createPushCommand() {
4358
5188
  });
4359
5189
  } catch {}
4360
5190
  }
4361
- if (opts.publish) {
5191
+ if (opts.publish && errors.length === 0) {
4362
5192
  const pubSpinner = ora("Publishing theme…").start();
4363
5193
  try {
4364
5194
  await publishApplicationTheme(api, theme.id);
@@ -4410,6 +5240,50 @@ async function canPullThemeInBackground(root, themeId) {
4410
5240
  }
4411
5241
  }
4412
5242
  //#endregion
5243
+ //#region src/theme/member-transfer.ts
5244
+ /** Missing/ignored is not empty. Return paths to retain without acknowledging
5245
+ * their local bytes as remotely accepted. Used before pull writes or pruning. */
5246
+ function planOmittedMemberFiles(input) {
5247
+ if (input.incomingPaths.has("members_sites.json") && !input.ignored("members_sites.json")) return /* @__PURE__ */ new Set();
5248
+ const protectedPaths = /* @__PURE__ */ new Set();
5249
+ for (const content of input.previousDocuments) {
5250
+ if (content === null) continue;
5251
+ protectedPaths.add(MEMBER_CONFIGURATION_KEY);
5252
+ if (content.byteLength > 2 * 1024 * 1024) throw new Error("Cannot preserve member files: members_sites.json exceeds 2 MB.");
5253
+ const document = JSON.parse(content.toString("utf8"));
5254
+ if (!isRecord(document) || document.version !== 2) throw new Error("Cannot preserve member files: members_sites.json must be a version 2 document.");
5255
+ for (const field of [
5256
+ "sites",
5257
+ "menus",
5258
+ "menu_references"
5259
+ ]) if (!Array.isArray(document[field])) throw new Error(`Cannot preserve member files: members_sites.json ${field} must be an array.`);
5260
+ assertDependencyShape(document);
5261
+ if (document.sites.length > 0) {
5262
+ for (const path of input.availablePaths) if (/^(sections|blocks|components|snippets|navbar|footer)\//.test(path)) protectedPaths.add(path);
5263
+ }
5264
+ for (const path of memberDependencyPaths(document, input.availablePaths)) {
5265
+ if (!isThemeResourceKey(path) || path.includes("\\") || [...path].some((character) => character.charCodeAt(0) < 32) || path === "config/members_sites.json") throw new Error(`Cannot preserve member files: invalid dependency path ${path}.`);
5266
+ protectedPaths.add(path);
5267
+ }
5268
+ }
5269
+ return new Set([...protectedPaths].filter((path) => !input.incomingPaths.has(path) || input.ignored(path)));
5270
+ }
5271
+ function isRecord(value) {
5272
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5273
+ }
5274
+ /** Reject incomplete dependency declarations rather than silently pruning files. */
5275
+ function assertDependencyShape(document) {
5276
+ const entries = (value) => {
5277
+ if (!Array.isArray(value) || !value.every(isRecord)) throw new Error("Cannot preserve member files: malformed dependency declaration.");
5278
+ return value;
5279
+ };
5280
+ for (const entry of [...entries(document.templates ?? []), ...entries(document.menu_references)]) if (typeof entry.path !== "string" || !entry.path) throw new Error("Cannot preserve member files: dependency path is missing.");
5281
+ for (const site of entries(document.sites)) {
5282
+ if (site.layout_name !== null && typeof site.layout_name !== "string") throw new Error("Cannot preserve member files: invalid layout name.");
5283
+ for (const page of entries(site.pages)) if (page.template_path !== null && typeof page.template_path !== "string") throw new Error("Cannot preserve member files: invalid page template path.");
5284
+ }
5285
+ }
5286
+ //#endregion
4413
5287
  //#region src/theme/merge-pull.ts
4414
5288
  /** `write(..., RESOLVES_CONFLICT)`: this write is the chosen side of a conflict. */
4415
5289
  const RESOLVES_CONFLICT = true;
@@ -4499,6 +5373,21 @@ async function mergePull(input) {
4499
5373
  onProgress?.(++done, remote.length);
4500
5374
  }
4501
5375
  input.backgroundGuard?.assertUnchanged();
5376
+ let retainedMemberPaths;
5377
+ try {
5378
+ if (remoteKeys.has("members_sites.json") && !themeRoot.ignore.ignore("members_sites.json") && !remoteContent.has("members_sites.json")) throw new Error("members_sites.json was listed but could not be downloaded; previous member state retained.");
5379
+ retainedMemberPaths = planOmittedMemberFiles({
5380
+ previousDocuments: [readIfExists(themeRoot.file(MEMBER_CONFIGURATION_KEY).absolutePath), await shadow.blobAtHead(MEMBER_CONFIGURATION_KEY)],
5381
+ availablePaths: new Set([...themeRoot.files().map((file) => file.relativePath), ...await shadow.headPaths()]),
5382
+ incomingPaths: remoteKeys,
5383
+ ignored: (path) => themeRoot.ignore.ignore(path)
5384
+ });
5385
+ } catch (error) {
5386
+ result.errors.push(errMsg(error));
5387
+ result.baselinePreserved = true;
5388
+ return result;
5389
+ }
5390
+ for (const path of retainedMemberPaths) remoteContent.delete(path);
4502
5391
  for (const [key, remoteBuf] of remoteContent) {
4503
5392
  const file = themeRoot.file(key);
4504
5393
  if (!file.absolutePath.startsWith(themeRoot.root + sep)) {
@@ -4559,7 +5448,7 @@ async function mergePull(input) {
4559
5448
  for (const { key, file, content, record } of pendingWrites) if (tryWrite(key, file, content)) record();
4560
5449
  }
4561
5450
  if (doDelete && await shadow.hasHead()) for (const file of themeRoot.files()) {
4562
- if (remoteKeys.has(file.relativePath)) continue;
5451
+ if (remoteKeys.has(file.relativePath) || retainedMemberPaths.has(file.relativePath)) continue;
4563
5452
  if (isStylesheetKey(file.relativePath)) continue;
4564
5453
  const baseBuf = await shadow.blobAtHead(file.relativePath);
4565
5454
  if (!baseBuf) continue;
@@ -4574,37 +5463,43 @@ async function mergePull(input) {
4574
5463
  if (error instanceof BackgroundPullChangedError) throw error;
4575
5464
  }
4576
5465
  }
4577
- const commitEntries = [];
4578
- for (const [key, buf] of remoteContent) {
4579
- if (unwrittenKeys.has(key)) continue;
4580
- commitEntries.push({
4581
- path: key,
4582
- sha: await shadow.writeBlob(buf)
4583
- });
4584
- }
4585
- for (const key of remoteKeys) {
4586
- if (input.skipRemoteKeys?.has(key)) continue;
4587
- if (remoteContent.has(key) && !unwrittenKeys.has(key)) continue;
4588
- const prevBlob = await shadow.blobAtHead(key);
4589
- if (prevBlob == null) continue;
4590
- commitEntries.push({
4591
- path: key,
4592
- sha: await shadow.writeBlob(prevBlob)
4593
- });
4594
- }
4595
- const assetManifest = new ThemeAssetManifest(themeRoot.root);
4596
- let managedAssetSentinelSha;
4597
- for (const key of input.skipRemoteKeys ?? []) {
4598
- if (!remoteKeys.has(key) || !assetManifest.has(key)) continue;
4599
- managedAssetSentinelSha ??= await shadow.writeBlob(MANAGED_ASSET_SHADOW_SENTINEL);
4600
- commitEntries.push({
4601
- path: key,
4602
- sha: managedAssetSentinelSha
4603
- });
4604
- }
4605
- if (commitEntries.length > 0 || remoteKeys.size === 0 && await shadow.hasHead()) {
4606
- input.backgroundGuard?.assertUnchanged();
4607
- await shadow.commitState(commitEntries, syncCommitSubject("Pull", input.actor ?? null));
5466
+ try {
5467
+ const commitEntries = [];
5468
+ for (const [key, buf] of remoteContent) {
5469
+ if (unwrittenKeys.has(key)) continue;
5470
+ commitEntries.push({
5471
+ path: key,
5472
+ sha: await shadow.writeBlob(buf)
5473
+ });
5474
+ }
5475
+ for (const key of new Set([...remoteKeys, ...retainedMemberPaths])) {
5476
+ if (input.skipRemoteKeys?.has(key) && !retainedMemberPaths.has(key)) continue;
5477
+ if (remoteContent.has(key) && !unwrittenKeys.has(key)) continue;
5478
+ const prevBlob = await shadow.blobAtHead(key);
5479
+ if (prevBlob == null) continue;
5480
+ commitEntries.push({
5481
+ path: key,
5482
+ sha: await shadow.writeBlob(prevBlob)
5483
+ });
5484
+ }
5485
+ const assetManifest = new ThemeAssetManifest(themeRoot.root);
5486
+ let managedAssetSentinelSha;
5487
+ for (const key of input.skipRemoteKeys ?? []) {
5488
+ if (!remoteKeys.has(key) || !assetManifest.has(key)) continue;
5489
+ managedAssetSentinelSha ??= await shadow.writeBlob(MANAGED_ASSET_SHADOW_SENTINEL);
5490
+ commitEntries.push({
5491
+ path: key,
5492
+ sha: managedAssetSentinelSha
5493
+ });
5494
+ }
5495
+ if (commitEntries.length > 0 || remoteKeys.size === 0 && await shadow.hasHead()) {
5496
+ input.backgroundGuard?.assertUnchanged();
5497
+ await shadow.commitState(commitEntries, syncCommitSubject("Pull", input.actor ?? null));
5498
+ }
5499
+ } catch (error) {
5500
+ if (error instanceof BackgroundPullChangedError) throw error;
5501
+ result.baselinePreserved = true;
5502
+ result.errors.push(`Downloaded changes have already been applied locally, but the local restore point and sync baseline could not be saved: ${error instanceof Error ? error.message : String(error)}. Review the files and retry a normal pull before pushing; do not use --force to bypass the missing baseline.`);
4608
5503
  }
4609
5504
  return result;
4610
5505
  }
@@ -4726,7 +5621,8 @@ function createPullCommand() {
4726
5621
  spinner.warn(`Pulled with ${result.errors.length} error(s): ${parts.join(", ")}.`);
4727
5622
  for (const e of result.errors) console.error(` ${e}`);
4728
5623
  process.exitCode = 1;
4729
- } else if (result.conflicts.length > 0) {
5624
+ }
5625
+ if (result.conflicts.length > 0) {
4730
5626
  spinner.warn(`${result.conflicts.length} conflict(s) — resolve markers before pushing: ${parts.join(", ")}.`);
4731
5627
  console.log();
4732
5628
  for (const c of result.conflicts) console.log(` ${chalk.yellow("CONFLICT")} ${c}`);
@@ -4734,7 +5630,8 @@ function createPullCommand() {
4734
5630
  console.log(` Edit each file above to reconcile the ${chalk.cyan("<<<<<<<")} / ${chalk.cyan(">>>>>>>")} markers,`);
4735
5631
  console.log(` then run ${chalk.cyan("fluid theme push")} once your resolution is in place.`);
4736
5632
  console.log();
4737
- } else spinner.succeed(parts.join(", ") || "Already up to date.");
5633
+ } else if (result.errors.length === 0) spinner.succeed(parts.join(", ") || "Already up to date.");
5634
+ if (result.baselinePreserved) return;
4738
5635
  const remoteSha = syncer.remoteSha();
4739
5636
  backgroundGuard?.assertUnchanged();
4740
5637
  const updateConfig = () => writeThemeConfig(absoluteRoot, {
@@ -4881,196 +5778,6 @@ function createInitCommand() {
4881
5778
  });
4882
5779
  }
4883
5780
  //#endregion
4884
- //#region src/commands/navigate.ts
4885
- function localSuggest(input, choices) {
4886
- if (!input) return choices;
4887
- const lower = input.toLowerCase();
4888
- return choices.filter((c) => c.title.toLowerCase().includes(lower));
4889
- }
4890
- const THEMEABLE_TYPE_MAP = {
4891
- "/home": "home_page",
4892
- "/home/shop": "shop_page",
4893
- "/home/join": "join_page",
4894
- "/cart": "cart_page",
4895
- "/home/blog": "post_page",
4896
- "/home/categories": "category_page",
4897
- "/home/collections": "collection_page"
4898
- };
4899
- const STATIC_ROUTES = [
4900
- {
4901
- label: "Home",
4902
- path: "/home"
4903
- },
4904
- {
4905
- label: "Shop",
4906
- path: "/home/shop"
4907
- },
4908
- {
4909
- label: "Join / Sign Up",
4910
- path: "/home/join"
4911
- },
4912
- {
4913
- label: "Cart",
4914
- path: "/cart"
4915
- },
4916
- {
4917
- label: "Blog",
4918
- path: "/home/blog"
4919
- },
4920
- {
4921
- label: "Categories (all)",
4922
- path: "/home/categories"
4923
- },
4924
- {
4925
- label: "Collections (all)",
4926
- path: "/home/collections"
4927
- }
4928
- ];
4929
- const RESOURCE_ROUTES = [
4930
- {
4931
- label: "Category",
4932
- type: "category",
4933
- template: "/home/categories/%s",
4934
- fallback: "/home/categories"
4935
- },
4936
- {
4937
- label: "Collection",
4938
- type: "collection",
4939
- template: "/home/collections/%s",
4940
- fallback: "/home/collections"
4941
- },
4942
- {
4943
- label: "Product",
4944
- type: "product",
4945
- template: "/home/products/%s",
4946
- fallback: "/home/shop"
4947
- },
4948
- {
4949
- label: "Library",
4950
- type: "library",
4951
- template: "/home/libraries/%s",
4952
- fallback: "/home/libraries"
4953
- },
4954
- {
4955
- label: "Post",
4956
- type: "post",
4957
- template: "/home/posts/%s",
4958
- fallback: "/home/blog"
4959
- },
4960
- {
4961
- label: "Media",
4962
- type: "medium",
4963
- template: "/home/media/%s",
4964
- fallback: "/home/media"
4965
- },
4966
- {
4967
- label: "Enrollment Pack",
4968
- type: "enrollment_pack",
4969
- template: "/home/enrollments/%s",
4970
- fallback: "/home/join"
4971
- },
4972
- {
4973
- label: "Page",
4974
- type: "page",
4975
- template: "/home/pages/%s",
4976
- fallback: "/home/pages"
4977
- }
4978
- ];
4979
- async function fetchTemplatesForType(api, themeId, themeableType) {
4980
- const params = new URLSearchParams({
4981
- application_theme_id: String(themeId),
4982
- themeable_type: themeableType,
4983
- published: "true"
4984
- });
4985
- return (await api.get(`/api/application_theme_templates?${params}`)).templates ?? [];
4986
- }
4987
- async function selectTemplate(api, themeId, themeableType, onCancel) {
4988
- const templates = await fetchTemplatesForType(api, themeId, themeableType);
4989
- if (templates.length <= 1) return null;
4990
- const { templateId } = await prompts({
4991
- type: "autocomplete",
4992
- name: "templateId",
4993
- message: "Select a template",
4994
- choices: templates.map((t) => ({
4995
- title: `${t.name}${t.default ? " (default)" : ""}`,
4996
- value: t.id
4997
- })),
4998
- suggest: (input, choices) => Promise.resolve(localSuggest(input, choices))
4999
- }, { onCancel });
5000
- return templateId ?? null;
5001
- }
5002
- function createNavigateCommand() {
5003
- return new Command("navigate").description("Interactively navigate to a route in the dev server browser").option("--host <host>", "Dev server host", "127.0.0.1").option("--port <port>", "Dev server port", "9292").option("-t, --theme <id>", "Theme ID (defaults to active dev theme)").action(async (opts) => {
5004
- requireToken();
5005
- const themeId = opts.theme ? Number(opts.theme) : getLastDevThemeId();
5006
- if (!themeId) {
5007
- console.error("No active dev theme. Run `fluid theme dev` first, or pass --theme <id>.");
5008
- process.exit(1);
5009
- }
5010
- const address = `http://${opts.host}:${opts.port}`;
5011
- const choices = [...STATIC_ROUTES.map((r) => ({
5012
- title: r.label,
5013
- value: r.path
5014
- })), ...RESOURCE_ROUTES.map((r) => ({
5015
- title: `${r.label} (select specific)`,
5016
- value: {
5017
- resourceType: r.type,
5018
- template: r.template,
5019
- fallback: r.fallback,
5020
- label: r.label
5021
- }
5022
- }))];
5023
- const onCancel = () => process.exit(130);
5024
- const { dest } = await prompts({
5025
- type: "autocomplete",
5026
- name: "dest",
5027
- message: "Select a route",
5028
- choices,
5029
- suggest: (input, choices) => Promise.resolve(localSuggest(input, choices))
5030
- }, { onCancel });
5031
- if (!dest) return;
5032
- const api = createApiClient();
5033
- let path;
5034
- let themeableType;
5035
- if (typeof dest === "string") {
5036
- path = dest;
5037
- themeableType = THEMEABLE_TYPE_MAP[dest];
5038
- } else {
5039
- themeableType = dest.resourceType;
5040
- const resources = (await getApplicationThemeAvailableThemeables(api, themeId, {
5041
- themeable: dest.resourceType,
5042
- per_page: 50
5043
- })).available_themeables ?? [];
5044
- if (!resources.length) {
5045
- console.log(`No ${dest.label} resources found, using listing page.`);
5046
- path = dest.fallback;
5047
- } else {
5048
- const resourceChoices = resources.map((r) => ({
5049
- title: r.title ?? r.slug ?? "Untitled",
5050
- value: r.slug
5051
- }));
5052
- const { slug } = await prompts({
5053
- type: "autocomplete",
5054
- name: "slug",
5055
- message: `Select a ${dest.label.toLowerCase()}`,
5056
- choices: resourceChoices,
5057
- suggest: (input, choices) => Promise.resolve(localSuggest(input, choices))
5058
- }, { onCancel });
5059
- path = dest.template.replace("%s", slug);
5060
- }
5061
- }
5062
- let templateParam = "";
5063
- if (themeableType) {
5064
- const templateId = await selectTemplate(api, themeId, themeableType, onCancel);
5065
- if (templateId) templateParam = `?theme_template_id=${templateId}`;
5066
- }
5067
- const url = `${address}${path}${templateParam}`;
5068
- console.log(`\nNavigating to: ${url}\n`);
5069
- const open = (await import("open")).default;
5070
- await open(url);
5071
- });
5072
- }
5073
- //#endregion
5074
5781
  //#region src/skills/install.ts
5075
5782
  function listSkillNames(skillsDir) {
5076
5783
  if (!existsSync(skillsDir)) return [];