create-nextblock 0.2.78 → 0.8.1

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 (413) hide show
  1. package/bin/create-nextblock.js +793 -472
  2. package/package.json +1 -2
  3. package/scripts/sync-template.js +18 -1
  4. package/templates/nextblock-template/.browserslistrc +11 -0
  5. package/templates/nextblock-template/.swcrc +30 -30
  6. package/templates/nextblock-template/README.md +23 -114
  7. package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +27 -28
  8. package/templates/nextblock-template/app/(auth-pages)/sign-in/page.tsx +50 -25
  9. package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +111 -56
  10. package/templates/nextblock-template/app/(auth-pages)/two-factor/actions.ts +91 -0
  11. package/templates/nextblock-template/app/(auth-pages)/two-factor/components/TwoFactorForm.tsx +118 -0
  12. package/templates/nextblock-template/app/(auth-pages)/two-factor/page.tsx +51 -0
  13. package/templates/nextblock-template/app/.well-known/ucp/route.ts +16 -0
  14. package/templates/nextblock-template/app/[slug]/PageClientContent.tsx +48 -28
  15. package/templates/nextblock-template/app/[slug]/page.tsx +63 -6
  16. package/templates/nextblock-template/app/[slug]/page.utils.ts +374 -157
  17. package/templates/nextblock-template/app/[slug]/pageClientActions.ts +7 -0
  18. package/templates/nextblock-template/app/actions/consent.ts +57 -0
  19. package/templates/nextblock-template/app/actions/formActions.ts +130 -11
  20. package/templates/nextblock-template/app/actions/languageActions.ts +31 -30
  21. package/templates/nextblock-template/app/actions/package-actions.ts +183 -0
  22. package/templates/nextblock-template/app/actions/postActions.ts +146 -48
  23. package/templates/nextblock-template/app/actions/twoFactorEmail.ts +21 -0
  24. package/templates/nextblock-template/app/actions/visualEditingActions.test.ts +179 -0
  25. package/templates/nextblock-template/app/actions/visualEditingActions.ts +345 -0
  26. package/templates/nextblock-template/app/actions.ts +67 -12
  27. package/templates/nextblock-template/app/api/ai/cortex/build-widget/route.ts +153 -0
  28. package/templates/nextblock-template/app/api/ai/generate-blocks/route.ts +96 -0
  29. package/templates/nextblock-template/app/api/ai/global-agent/route.ts +965 -0
  30. package/templates/nextblock-template/app/api/checkout/freemius/sync/route.ts +29 -0
  31. package/templates/nextblock-template/app/api/checkout/route.ts +146 -0
  32. package/templates/nextblock-template/app/api/cms/full-backup/export/route.ts +33 -0
  33. package/templates/nextblock-template/app/api/cms/full-backup/restore/route.ts +63 -0
  34. package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +3413 -17
  35. package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +7830 -0
  36. package/templates/nextblock-template/app/api/cron/sync-currencies/route.ts +35 -0
  37. package/templates/nextblock-template/app/api/custom-blocks/db-relations/route.ts +92 -0
  38. package/templates/nextblock-template/app/api/custom-blocks/editor-definitions/route.ts +43 -0
  39. package/templates/nextblock-template/app/api/draft/disable/route.ts +25 -0
  40. package/templates/nextblock-template/app/api/draft/route.ts +93 -0
  41. package/templates/nextblock-template/app/api/draft/start/route.ts +77 -0
  42. package/templates/nextblock-template/app/api/media/library/route.ts +65 -0
  43. package/templates/nextblock-template/app/api/media/r2-presigned/route.ts +53 -0
  44. package/templates/nextblock-template/app/api/media/record/route.ts +160 -0
  45. package/templates/nextblock-template/app/api/search/route.ts +43 -0
  46. package/templates/nextblock-template/app/api/visual-editing/block-draft/route.ts +47 -0
  47. package/templates/nextblock-template/app/api/visual-editing/product-draft/route.ts +47 -0
  48. package/templates/nextblock-template/app/api/webhooks/freemius/route.ts +34 -0
  49. package/templates/nextblock-template/app/api/webhooks/stripe/route.ts +27 -0
  50. package/templates/nextblock-template/app/article/[slug]/PostClientContent.tsx +392 -128
  51. package/templates/nextblock-template/app/article/[slug]/page.tsx +179 -127
  52. package/templates/nextblock-template/app/article/[slug]/page.utils.ts +262 -77
  53. package/templates/nextblock-template/app/auth/callback/route.ts +31 -58
  54. package/templates/nextblock-template/app/cart/page.tsx +7 -0
  55. package/templates/nextblock-template/app/checkout/UcpCartHydrator.tsx +20 -0
  56. package/templates/nextblock-template/app/checkout/page.tsx +52 -0
  57. package/templates/nextblock-template/app/checkout/success/actions.ts +136 -0
  58. package/templates/nextblock-template/app/checkout/success/page.tsx +186 -0
  59. package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +163 -33
  60. package/templates/nextblock-template/app/cms/blocks/actions.ts +424 -235
  61. package/templates/nextblock-template/app/cms/blocks/components/BackgroundSelector.tsx +212 -151
  62. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +41 -20
  63. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +152 -19
  64. package/templates/nextblock-template/app/cms/blocks/components/BlockTypeCard.tsx +25 -17
  65. package/templates/nextblock-template/app/cms/blocks/components/BlockTypeSelector.tsx +200 -18
  66. package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +33 -16
  67. package/templates/nextblock-template/app/cms/blocks/components/CustomBlockEditorPreview.tsx +160 -0
  68. package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +37 -18
  69. package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +149 -67
  70. package/templates/nextblock-template/app/cms/blocks/components/SectionConfigPanel.tsx +108 -31
  71. package/templates/nextblock-template/app/cms/blocks/editors/DynamicCustomBlockEditor.tsx +167 -0
  72. package/templates/nextblock-template/app/cms/blocks/editors/FeaturedProductBlockEditor.tsx +31 -0
  73. package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +2 -2
  74. package/templates/nextblock-template/app/cms/blocks/editors/HeadingBlockEditor.tsx +1 -1
  75. package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +29 -29
  76. package/templates/nextblock-template/app/cms/blocks/editors/PostsGridBlockEditor.tsx +14 -18
  77. package/templates/nextblock-template/app/cms/blocks/editors/ProductGridBlockEditor.tsx +41 -0
  78. package/templates/nextblock-template/app/cms/blocks/editors/SectionBlockEditor.tsx +318 -118
  79. package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +98 -21
  80. package/templates/nextblock-template/app/cms/blocks/editors/VideoEmbedBlockEditor.tsx +1 -1
  81. package/templates/nextblock-template/app/cms/components/ContentLanguageSwitcher.tsx +27 -9
  82. package/templates/nextblock-template/app/cms/components/CopyContentFromLanguage.tsx +1 -1
  83. package/templates/nextblock-template/app/cms/components/CortexAiActiveContext.tsx +23 -0
  84. package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -0
  85. package/templates/nextblock-template/app/cms/components/CortexGlobalAgentChat.tsx +1507 -0
  86. package/templates/nextblock-template/app/cms/components/DraftStatusActions.tsx +145 -0
  87. package/templates/nextblock-template/app/cms/components/FeatureImageField.tsx +244 -0
  88. package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +38 -24
  89. package/templates/nextblock-template/app/cms/coupons/[id]/edit/page.tsx +16 -0
  90. package/templates/nextblock-template/app/cms/coupons/page.tsx +16 -0
  91. package/templates/nextblock-template/app/cms/custom-blocks/[id]/edit/page.tsx +66 -0
  92. package/templates/nextblock-template/app/cms/custom-blocks/actions.ts +519 -0
  93. package/templates/nextblock-template/app/cms/custom-blocks/components/BlockComposer.tsx +1522 -0
  94. package/templates/nextblock-template/app/cms/custom-blocks/components/BlocksLibraryTransferControls.tsx +256 -0
  95. package/templates/nextblock-template/app/cms/custom-blocks/components/DBRelationSelect.tsx +384 -0
  96. package/templates/nextblock-template/app/cms/custom-blocks/components/ImageR2Picker.tsx +221 -0
  97. package/templates/nextblock-template/app/cms/custom-blocks/new/page.tsx +12 -0
  98. package/templates/nextblock-template/app/cms/custom-blocks/page.tsx +438 -0
  99. package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -98
  100. package/templates/nextblock-template/app/cms/dashboard/components/DashboardComponents.tsx +200 -0
  101. package/templates/nextblock-template/app/cms/dashboard/page.tsx +182 -154
  102. package/templates/nextblock-template/app/cms/import-export/ContentTransferControls.tsx +391 -0
  103. package/templates/nextblock-template/app/cms/import-export/actions.ts +226 -0
  104. package/templates/nextblock-template/app/cms/layout.tsx +29 -10
  105. package/templates/nextblock-template/app/cms/media/UploadFolderContext.tsx +22 -22
  106. package/templates/nextblock-template/app/cms/media/actions.ts +45 -124
  107. package/templates/nextblock-template/app/cms/media/components/DeleteMediaButtonClient.tsx +1 -1
  108. package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +26 -26
  109. package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +69 -64
  110. package/templates/nextblock-template/app/cms/media/components/MediaPickerDialog.tsx +227 -158
  111. package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +101 -89
  112. package/templates/nextblock-template/app/cms/media/page.tsx +1 -1
  113. package/templates/nextblock-template/app/cms/navigation/components/NavigationItemForm.tsx +2 -2
  114. package/templates/nextblock-template/app/cms/orders/[id]/MarkPaidButton.tsx +44 -0
  115. package/templates/nextblock-template/app/cms/orders/[id]/page.tsx +16 -0
  116. package/templates/nextblock-template/app/cms/orders/actions.ts +201 -0
  117. package/templates/nextblock-template/app/cms/orders/page.tsx +20 -0
  118. package/templates/nextblock-template/app/cms/orders/types.ts +20 -0
  119. package/templates/nextblock-template/app/cms/pages/[id]/edit/EditPageClient.tsx +156 -121
  120. package/templates/nextblock-template/app/cms/pages/[id]/edit/page.tsx +79 -26
  121. package/templates/nextblock-template/app/cms/pages/actions.ts +54 -38
  122. package/templates/nextblock-template/app/cms/pages/components/DeletePageButtonClient.tsx +1 -1
  123. package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +267 -116
  124. package/templates/nextblock-template/app/cms/pages/page.tsx +25 -18
  125. package/templates/nextblock-template/app/cms/payments/page.tsx +16 -0
  126. package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +132 -90
  127. package/templates/nextblock-template/app/cms/posts/actions.ts +71 -72
  128. package/templates/nextblock-template/app/cms/posts/components/DeletePostButtonClient.tsx +1 -1
  129. package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +256 -245
  130. package/templates/nextblock-template/app/cms/posts/new/page.tsx +1 -1
  131. package/templates/nextblock-template/app/cms/posts/page.tsx +20 -13
  132. package/templates/nextblock-template/app/cms/products/ClientNotionEditor.tsx +16 -0
  133. package/templates/nextblock-template/app/cms/products/ProductFormClientShell.tsx +56 -0
  134. package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +292 -0
  135. package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -0
  136. package/templates/nextblock-template/app/cms/products/categories/page.tsx +12 -0
  137. package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -0
  138. package/templates/nextblock-template/app/cms/products/new/page.tsx +143 -0
  139. package/templates/nextblock-template/app/cms/products/page.tsx +42 -0
  140. package/templates/nextblock-template/app/cms/products/productFormData.ts +133 -0
  141. package/templates/nextblock-template/app/cms/products/settings/page.tsx +5 -0
  142. package/templates/nextblock-template/app/cms/promotions/PromotionsWorkspace.tsx +456 -0
  143. package/templates/nextblock-template/app/cms/promotions/actions.ts +115 -0
  144. package/templates/nextblock-template/app/cms/promotions/page.tsx +31 -0
  145. package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +2 -2
  146. package/templates/nextblock-template/app/cms/revisions/actions.ts +285 -285
  147. package/templates/nextblock-template/app/cms/revisions/service.ts +19 -16
  148. package/templates/nextblock-template/app/cms/revisions/utils.ts +8 -3
  149. package/templates/nextblock-template/app/cms/settings/backup-restore/BackupRestoreWorkspace.tsx +1004 -0
  150. package/templates/nextblock-template/app/cms/settings/backup-restore/page.tsx +29 -0
  151. package/templates/nextblock-template/app/cms/settings/bot-protection/actions.ts +93 -0
  152. package/templates/nextblock-template/app/cms/settings/bot-protection/components/BotProtectionForm.tsx +129 -0
  153. package/templates/nextblock-template/app/cms/settings/bot-protection/page.tsx +24 -0
  154. package/templates/nextblock-template/app/cms/settings/copyright/actions.ts +1 -1
  155. package/templates/nextblock-template/app/cms/settings/copyright/components/CopyrightForm.tsx +2 -2
  156. package/templates/nextblock-template/app/cms/settings/copyright/page.tsx +1 -1
  157. package/templates/nextblock-template/app/cms/settings/cortex-ai/SandboxCortexAiSettingsClient.tsx +496 -0
  158. package/templates/nextblock-template/app/cms/settings/cortex-ai/StoredCortexAiSettingsClient.tsx +410 -0
  159. package/templates/nextblock-template/app/cms/settings/cortex-ai/actions.ts +248 -0
  160. package/templates/nextblock-template/app/cms/settings/cortex-ai/page.tsx +80 -0
  161. package/templates/nextblock-template/app/cms/settings/currencies/actions.ts +331 -0
  162. package/templates/nextblock-template/app/cms/settings/currencies/page.tsx +494 -0
  163. package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -0
  164. package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +203 -44
  165. package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -242
  166. package/templates/nextblock-template/app/cms/settings/global-css/actions.ts +65 -0
  167. package/templates/nextblock-template/app/cms/settings/global-css/components/GlobalCssForm.tsx +46 -0
  168. package/templates/nextblock-template/app/cms/settings/global-css/page.tsx +24 -0
  169. package/templates/nextblock-template/app/cms/settings/languages/components/DeleteLanguageButton.tsx +1 -1
  170. package/templates/nextblock-template/app/cms/settings/languages/components/LanguageForm.tsx +2 -2
  171. package/templates/nextblock-template/app/cms/settings/languages/page.tsx +1 -1
  172. package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +7 -7
  173. package/templates/nextblock-template/app/cms/settings/logos/actions.ts +82 -6
  174. package/templates/nextblock-template/app/cms/settings/logos/components/BrandingSettingsForm.tsx +339 -0
  175. package/templates/nextblock-template/app/cms/settings/logos/components/DeleteLogoButton.tsx +21 -18
  176. package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +20 -16
  177. package/templates/nextblock-template/app/cms/settings/logos/components/SiteSeoSettingsForm.tsx +133 -0
  178. package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
  179. package/templates/nextblock-template/app/cms/settings/logos/page.tsx +120 -82
  180. package/templates/nextblock-template/app/cms/settings/logos/types.ts +8 -8
  181. package/templates/nextblock-template/app/cms/settings/packages/activation-form.tsx +84 -0
  182. package/templates/nextblock-template/app/cms/settings/packages/package-card.tsx +122 -0
  183. package/templates/nextblock-template/app/cms/settings/packages/page.tsx +49 -0
  184. package/templates/nextblock-template/app/cms/settings/privacy/actions.ts +53 -0
  185. package/templates/nextblock-template/app/cms/settings/privacy/components/PrivacyForm.tsx +196 -0
  186. package/templates/nextblock-template/app/cms/settings/privacy/page.tsx +26 -0
  187. package/templates/nextblock-template/app/cms/settings/security/actions.ts +251 -0
  188. package/templates/nextblock-template/app/cms/settings/security/components/SecurityPanel.tsx +453 -0
  189. package/templates/nextblock-template/app/cms/settings/security/page.tsx +13 -0
  190. package/templates/nextblock-template/app/cms/settings/taxes/page.tsx +21 -0
  191. package/templates/nextblock-template/app/cms/shipping/page.tsx +20 -0
  192. package/templates/nextblock-template/app/cms/users/[id]/edit/page.tsx +28 -23
  193. package/templates/nextblock-template/app/cms/users/actions.ts +105 -40
  194. package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +1 -1
  195. package/templates/nextblock-template/app/cms/users/components/UserForm.tsx +65 -152
  196. package/templates/nextblock-template/app/cms/users/page.tsx +15 -10
  197. package/templates/nextblock-template/app/globals.css +9 -0
  198. package/templates/nextblock-template/app/layout.tsx +372 -120
  199. package/templates/nextblock-template/app/lib/seo.test.ts +52 -0
  200. package/templates/nextblock-template/app/lib/seo.ts +279 -0
  201. package/templates/nextblock-template/app/lib/site-settings.ts +87 -0
  202. package/templates/nextblock-template/app/lib/sitemap-utils.ts +224 -39
  203. package/templates/nextblock-template/app/lib/ucp/protocol.ts +190 -0
  204. package/templates/nextblock-template/app/lib/ucp/server.test.ts +56 -0
  205. package/templates/nextblock-template/app/lib/ucp/server.ts +1914 -0
  206. package/templates/nextblock-template/app/page.tsx +165 -73
  207. package/templates/nextblock-template/app/product/[slug]/page.tsx +433 -0
  208. package/templates/nextblock-template/app/profile/ProfileAccountSidebar.tsx +73 -0
  209. package/templates/nextblock-template/app/profile/ProfilePageHeader.tsx +16 -0
  210. package/templates/nextblock-template/app/profile/ProfilePageMissingState.tsx +9 -0
  211. package/templates/nextblock-template/app/profile/account-data.ts +37 -0
  212. package/templates/nextblock-template/app/profile/account-links.ts +22 -0
  213. package/templates/nextblock-template/app/profile/account-types.ts +11 -0
  214. package/templates/nextblock-template/app/profile/orders/CustomerOrdersPageClient.tsx +124 -0
  215. package/templates/nextblock-template/app/profile/orders/[id]/CustomerOrderDetailPageClient.tsx +79 -0
  216. package/templates/nextblock-template/app/profile/orders/[id]/page.tsx +32 -0
  217. package/templates/nextblock-template/app/profile/orders/page.tsx +19 -0
  218. package/templates/nextblock-template/app/profile/page.tsx +51 -0
  219. package/templates/nextblock-template/app/profile/password/PasswordSettingsPageClient.tsx +128 -0
  220. package/templates/nextblock-template/app/profile/password/actions.ts +59 -0
  221. package/templates/nextblock-template/app/profile/password/page.tsx +27 -0
  222. package/templates/nextblock-template/app/providers.tsx +55 -17
  223. package/templates/nextblock-template/app/robots.txt/route.ts +11 -1
  224. package/templates/nextblock-template/app/sitemap.ts +128 -0
  225. package/templates/nextblock-template/app/ucp/v1/carts/[id]/cancel/route.ts +38 -0
  226. package/templates/nextblock-template/app/ucp/v1/carts/[id]/route.ts +68 -0
  227. package/templates/nextblock-template/app/ucp/v1/carts/route.ts +35 -0
  228. package/templates/nextblock-template/app/ucp/v1/catalog/lookup/route.ts +35 -0
  229. package/templates/nextblock-template/app/ucp/v1/catalog/product/route.ts +35 -0
  230. package/templates/nextblock-template/app/ucp/v1/catalog/search/route.ts +34 -0
  231. package/templates/nextblock-template/components/AppShell.tsx +154 -0
  232. package/templates/nextblock-template/components/BlockRenderer.tsx +210 -64
  233. package/templates/nextblock-template/components/CartDrawerLoader.tsx +7 -0
  234. package/templates/nextblock-template/components/CartTranslator.tsx +210 -0
  235. package/templates/nextblock-template/components/CurrentContentSetter.tsx +25 -0
  236. package/templates/nextblock-template/components/DeferredCartDrawer.tsx +23 -0
  237. package/templates/nextblock-template/components/DeferredCartTranslator.tsx +51 -0
  238. package/templates/nextblock-template/components/DeferredGlobalSearch.tsx +68 -0
  239. package/templates/nextblock-template/components/DeferredGoogleTagManager.tsx +70 -0
  240. package/templates/nextblock-template/components/DeferredSpeedInsights.tsx +69 -0
  241. package/templates/nextblock-template/components/FeatureImageHero.tsx +47 -0
  242. package/templates/nextblock-template/components/GitHubLoginButton.tsx +36 -0
  243. package/templates/nextblock-template/components/GlobalSearch.tsx +557 -0
  244. package/templates/nextblock-template/components/Header.tsx +49 -41
  245. package/templates/nextblock-template/components/LanguageSwitcher.tsx +55 -32
  246. package/templates/nextblock-template/components/ResponsiveNav.tsx +138 -43
  247. package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +12 -8
  248. package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -55
  249. package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +42 -37
  250. package/templates/nextblock-template/components/blocks/TestimonialBlock.tsx +6 -2
  251. package/templates/nextblock-template/components/blocks/ecommerceRendererLoaders.ts +23 -0
  252. package/templates/nextblock-template/components/blocks/publicRendererLoaders.ts +25 -0
  253. package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -84
  254. package/templates/nextblock-template/components/blocks/renderers/CartBlockRenderer.tsx +17 -0
  255. package/templates/nextblock-template/components/blocks/renderers/CheckoutBlockRenderer.tsx +19 -0
  256. package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +262 -8
  257. package/templates/nextblock-template/components/blocks/renderers/FeaturedProductBlockRenderer.tsx +22 -0
  258. package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +320 -37
  259. package/templates/nextblock-template/components/blocks/renderers/HeadingBlockRenderer.tsx +11 -8
  260. package/templates/nextblock-template/components/blocks/renderers/ImageBlockRenderer.tsx +12 -3
  261. package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +18 -13
  262. package/templates/nextblock-template/components/blocks/renderers/ProductDetailsBlockRenderer.tsx +90 -0
  263. package/templates/nextblock-template/components/blocks/renderers/ProductGridBlockRenderer.tsx +31 -0
  264. package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +424 -55
  265. package/templates/nextblock-template/components/blocks/renderers/SectionSlider.tsx +137 -0
  266. package/templates/nextblock-template/components/blocks/renderers/TestimonialBlockRenderer.tsx +57 -0
  267. package/templates/nextblock-template/components/blocks/renderers/TextBlockRenderer.tsx +37 -22
  268. package/templates/nextblock-template/components/blocks/renderers/VideoEmbedBlockRenderer.tsx +23 -15
  269. package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +1 -3
  270. package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +1 -3
  271. package/templates/nextblock-template/components/blocks/types.ts +7 -6
  272. package/templates/nextblock-template/components/env-var-warning.tsx +3 -3
  273. package/templates/nextblock-template/components/form-message.tsx +32 -26
  274. package/templates/nextblock-template/components/header-auth.tsx +69 -17
  275. package/templates/nextblock-template/components/privacy/ConsentBanner.tsx +127 -0
  276. package/templates/nextblock-template/components/privacy/ConsentGatedAnalytics.tsx +59 -0
  277. package/templates/nextblock-template/components/renderers/CachedDynamicLayoutEngine.tsx +28 -0
  278. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.test.tsx +166 -0
  279. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.tsx +464 -0
  280. package/templates/nextblock-template/components/theme-switcher.tsx +8 -8
  281. package/templates/nextblock-template/components/visual-editing/DeferredVisualEditing.tsx +21 -0
  282. package/templates/nextblock-template/components/visual-editing/NextblockVisualEditing.tsx +1172 -0
  283. package/templates/nextblock-template/context/AuthContext.tsx +23 -90
  284. package/templates/nextblock-template/context/CurrentContentContext.tsx +10 -4
  285. package/templates/nextblock-template/context/LanguageContext.tsx +16 -16
  286. package/templates/nextblock-template/context/language-rest-client.ts +31 -0
  287. package/templates/nextblock-template/docs/01-PROJECT-OVERVIEW.md +94 -0
  288. package/templates/nextblock-template/docs/02-ECOMMERCE-CAPABILITIES.md +364 -0
  289. package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +202 -0
  290. package/templates/nextblock-template/docs/04-DATABASE-AND-AUTH.md +252 -0
  291. package/templates/nextblock-template/docs/05-DEVELOPER-GUIDE.md +238 -0
  292. package/templates/nextblock-template/docs/06-CLI-AND-SCAFFOLDING.md +125 -0
  293. package/templates/nextblock-template/docs/07-BLOCK-SDK-AND-EXTENSIBILITY.md +146 -0
  294. package/templates/nextblock-template/docs/08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md +1319 -0
  295. package/templates/nextblock-template/docs/09-LIVE-DRAFT-MODE.md +104 -0
  296. package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +222 -0
  297. package/templates/nextblock-template/docs/README.md +34 -0
  298. package/templates/nextblock-template/docs/TECHNICAL_SPECIFICATION.md +12507 -0
  299. package/templates/nextblock-template/hooks/use-hotkeys.ts +21 -14
  300. package/templates/nextblock-template/hooks/useGlobalSearch.ts +101 -0
  301. package/templates/nextblock-template/index.d.ts +2 -0
  302. package/templates/nextblock-template/lib/ai-block-generation.ts +339 -0
  303. package/templates/nextblock-template/lib/ai-client.ts +247 -0
  304. package/templates/nextblock-template/lib/ai-config.ts +81 -0
  305. package/templates/nextblock-template/lib/ai-cortex-widget-builder.ts +125 -0
  306. package/templates/nextblock-template/lib/ai-global-agent-custom-block-tools.ts +363 -0
  307. package/templates/nextblock-template/lib/ai-global-agent-db-tools.test.ts +405 -0
  308. package/templates/nextblock-template/lib/ai-global-agent-db-tools.ts +1228 -0
  309. package/templates/nextblock-template/lib/ai-global-agent-ecommerce.ts +5 -0
  310. package/templates/nextblock-template/lib/ai-global-agent-tools-stats.test.ts +223 -0
  311. package/templates/nextblock-template/lib/ai-global-agent-tools.test.ts +2183 -0
  312. package/templates/nextblock-template/lib/ai-global-agent-tools.ts +4807 -0
  313. package/templates/nextblock-template/lib/ai-key-crypto.test.ts +70 -0
  314. package/templates/nextblock-template/lib/ai-key-crypto.ts +132 -0
  315. package/templates/nextblock-template/lib/ai-model-catalog.test.ts +49 -0
  316. package/templates/nextblock-template/lib/ai-model-catalog.ts +41 -0
  317. package/templates/nextblock-template/lib/ai-model-registry.test.ts +231 -0
  318. package/templates/nextblock-template/lib/ai-model-registry.ts +522 -0
  319. package/templates/nextblock-template/lib/auth/cookies.ts +47 -0
  320. package/templates/nextblock-template/lib/auth/crypto.ts +42 -0
  321. package/templates/nextblock-template/lib/auth/trustedDevices.ts +92 -0
  322. package/templates/nextblock-template/lib/auth/twoFactor.ts +167 -0
  323. package/templates/nextblock-template/lib/auth-redirects.ts +46 -0
  324. package/templates/nextblock-template/lib/blocks/FeaturedProductBlock.tsx +94 -0
  325. package/templates/nextblock-template/lib/blocks/ProductGridBlock.tsx +137 -0
  326. package/templates/nextblock-template/lib/blocks/README.md +13 -670
  327. package/templates/nextblock-template/lib/blocks/blockRegistry.ts +138 -56
  328. package/templates/nextblock-template/lib/blocks/blockTypes.ts +18 -0
  329. package/templates/nextblock-template/lib/blocks/ecommerce-block-schemas.ts +31 -0
  330. package/templates/nextblock-template/lib/cms-transfer/csv.test.ts +77 -0
  331. package/templates/nextblock-template/lib/cms-transfer/csv.ts +399 -0
  332. package/templates/nextblock-template/lib/cms-transfer/server.ts +2243 -0
  333. package/templates/nextblock-template/lib/cms-transfer/types.ts +145 -0
  334. package/templates/nextblock-template/lib/cortex-widget-registry.test.ts +199 -0
  335. package/templates/nextblock-template/lib/cortex-widget-registry.ts +88 -0
  336. package/templates/nextblock-template/lib/cortex-widget-schema.test.tsx +237 -0
  337. package/templates/nextblock-template/lib/cortex-widget-schema.ts +393 -0
  338. package/templates/nextblock-template/lib/custom-block-definitions.ts +87 -0
  339. package/templates/nextblock-template/lib/custom-block-r2-upload-shared.ts +178 -0
  340. package/templates/nextblock-template/lib/custom-block-r2-upload.test.ts +140 -0
  341. package/templates/nextblock-template/lib/custom-block-r2-upload.ts +68 -0
  342. package/templates/nextblock-template/lib/custom-block-relation-registry.ts +256 -0
  343. package/templates/nextblock-template/lib/custom-block-relations.test.ts +227 -0
  344. package/templates/nextblock-template/lib/custom-block-relations.ts +279 -0
  345. package/templates/nextblock-template/lib/custom-block-safelist.ts +14 -0
  346. package/templates/nextblock-template/lib/editor/dynamic-extension-core.test.ts +172 -0
  347. package/templates/nextblock-template/lib/editor/dynamic-extension-core.ts +213 -0
  348. package/templates/nextblock-template/lib/editor/dynamic-extension-loader.ts +22 -0
  349. package/templates/nextblock-template/lib/editor/dynamic-extensions.tsx +193 -0
  350. package/templates/nextblock-template/lib/full-backup/manifest.test.ts +121 -0
  351. package/templates/nextblock-template/lib/full-backup/manifest.ts +206 -0
  352. package/templates/nextblock-template/lib/full-backup/server.ts +743 -0
  353. package/templates/nextblock-template/lib/media/resolveMediaUrl.ts +45 -0
  354. package/templates/nextblock-template/lib/posts/readTime.ts +60 -0
  355. package/templates/nextblock-template/lib/privacy/consent-client.ts +57 -0
  356. package/templates/nextblock-template/lib/privacy/settings.ts +103 -0
  357. package/templates/nextblock-template/lib/privacy/types.ts +67 -0
  358. package/templates/nextblock-template/lib/promotions/server.test.ts +74 -0
  359. package/templates/nextblock-template/lib/promotions/server.ts +741 -0
  360. package/templates/nextblock-template/lib/resolve-block-relations.test.ts +142 -0
  361. package/templates/nextblock-template/lib/resolve-block-relations.ts +255 -0
  362. package/templates/nextblock-template/lib/search/server.ts +585 -0
  363. package/templates/nextblock-template/lib/search/types.ts +27 -0
  364. package/templates/nextblock-template/lib/visual-editing/draft-content.test.ts +105 -0
  365. package/templates/nextblock-template/lib/visual-editing/draft-content.ts +380 -0
  366. package/templates/nextblock-template/lib/visual-editing/draft-route.test.ts +42 -0
  367. package/templates/nextblock-template/lib/visual-editing/draft-route.ts +82 -0
  368. package/templates/nextblock-template/lib/visual-editing/edit-info.test.ts +143 -0
  369. package/templates/nextblock-template/lib/visual-editing/edit-info.ts +94 -0
  370. package/templates/nextblock-template/lib/visual-editing/mutations.ts +190 -0
  371. package/templates/nextblock-template/lib/visual-editing/product-drafts.test.ts +81 -0
  372. package/templates/nextblock-template/lib/visual-editing/product-drafts.ts +511 -0
  373. package/templates/nextblock-template/lib/visual-editing/types.ts +122 -0
  374. package/templates/nextblock-template/lib/zod-config.ts +5 -0
  375. package/templates/nextblock-template/next.config.js +190 -66
  376. package/templates/nextblock-template/package.json +34 -30
  377. package/templates/nextblock-template/proxy.ts +435 -253
  378. package/templates/nextblock-template/public/images/NBcover.webp +0 -0
  379. package/templates/nextblock-template/public/images/cap.webp +0 -0
  380. package/templates/nextblock-template/public/images/commerce-plan.webp +0 -0
  381. package/templates/nextblock-template/public/images/commerce-square.webp +0 -0
  382. package/templates/nextblock-template/public/images/commerce-wide.webp +0 -0
  383. package/templates/nextblock-template/public/images/cortex-ai-square.webp +0 -0
  384. package/templates/nextblock-template/public/images/cortex-ai.webp +0 -0
  385. package/templates/nextblock-template/public/images/extensibility.webp +0 -0
  386. package/templates/nextblock-template/public/images/goals.webp +0 -0
  387. package/templates/nextblock-template/public/images/included.webp +0 -0
  388. package/templates/nextblock-template/public/images/nx-graph.webp +0 -0
  389. package/templates/nextblock-template/public/images/pants.webp +0 -0
  390. package/templates/nextblock-template/public/images/t-shirt.webp +0 -0
  391. package/templates/nextblock-template/scripts/validate-editor-block-schema.ts +112 -0
  392. package/templates/nextblock-template/scripts/verify-cortex-ai-build-widget.tsx +100 -0
  393. package/templates/nextblock-template/scripts/verify-cortex-ai-generate-blocks.ts +62 -0
  394. package/templates/nextblock-template/scripts/verify-cortex-ai-global-tools.ts +537 -0
  395. package/templates/nextblock-template/scripts/verify-cortex-ai-routing.ts +58 -0
  396. package/templates/nextblock-template/scripts/verify-custom-block-definitions.ts +188 -0
  397. package/templates/nextblock-template/scripts/verify-dynamic-custom-block-extensions.ts +123 -0
  398. package/templates/nextblock-template/scripts/verify-dynamic-layout-engine.tsx +133 -0
  399. package/templates/nextblock-template/scripts/verify-milestone-2-custom-blocks.ts +65 -0
  400. package/templates/nextblock-template/tailwind.config.js +1 -0
  401. package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -0
  402. package/templates/nextblock-template/tools/deploy-supabase.js +69 -71
  403. package/templates/nextblock-template/tsconfig.json +52 -66
  404. package/templates/nextblock-template/tsconfig.tsbuildinfo +1 -1
  405. package/templates/nextblock-template/types/jsdom.d.ts +6 -0
  406. package/templates/nextblock-template/app/force-styles.tsx +0 -31
  407. package/templates/nextblock-template/app/sitemap.xml/route.ts +0 -63
  408. package/templates/nextblock-template/components/blocks/renderers/HeroBlockRenderer.tsx +0 -273
  409. package/templates/nextblock-template/docs/How to Create a Custom Block.md +0 -149
  410. package/templates/nextblock-template/docs/cms-application-overview.md +0 -56
  411. package/templates/nextblock-template/docs/cms-architecture-overview.md +0 -73
  412. package/templates/nextblock-template/docs/files-structure.md +0 -426
  413. package/templates/nextblock-template/docs/tiptap-bundle-optimization-summary.md +0 -174
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // eslint-disable-next-line @nx/enforce-module-boundaries
3
4
  import * as clack from '@clack/prompts';
4
5
  import { spawn } from 'node:child_process';
5
6
  import crypto from 'node:crypto';
@@ -10,7 +11,6 @@ import { execa } from 'execa';
10
11
  import { program } from 'commander';
11
12
  import chalk from 'chalk';
12
13
  import fs from 'fs-extra';
13
- import open from 'open';
14
14
 
15
15
  const DEFAULT_PROJECT_NAME = 'nextblock-cms';
16
16
  const __filename = fileURLToPath(import.meta.url);
@@ -19,6 +19,7 @@ const TEMPLATE_DIR = resolve(__dirname, '../templates/nextblock-template');
19
19
  const REPO_ROOT = resolve(__dirname, '../../..');
20
20
  const EDITOR_UTILS_SOURCE_DIR = resolve(REPO_ROOT, 'libs/editor/src/lib/utils');
21
21
  const IS_WINDOWS = process.platform === 'win32';
22
+ const CLI_VERSION = createRequire(import.meta.url)('../package.json').version;
22
23
 
23
24
  const UI_PROXY_MODULES = [
24
25
  'avatar',
@@ -54,15 +55,21 @@ const PACKAGE_VERSION_SOURCES = {
54
55
 
55
56
  program
56
57
  .name('create-nextblock')
57
- .description('Bootstrap a NextBlock CMS project')
58
- .argument(
59
- '[project-directory]',
60
- 'The name of the project directory to create',
61
- )
58
+ .description('NextBlock CMS CLI')
59
+ .version(CLI_VERSION, '-v, --version');
60
+
61
+ program
62
+ .command('create [project-directory]', { isDefault: true })
63
+ .description('Bootstrap a NextBlock™ CMS project')
62
64
  .option('--skip-install', 'Skip installing dependencies')
63
65
  .option('-y, --yes', 'Skip all interactive prompts and use defaults')
64
66
  .action(handleCommand);
65
67
 
68
+ program
69
+ .command('activate [module]')
70
+ .description('Activate a premium NextBlock™ CMS module')
71
+ .action(handleActivateCommand);
72
+
66
73
  await program.parseAsync(process.argv).catch((error) => {
67
74
  console.error(
68
75
  chalk.red(error instanceof Error ? error.message : String(error)),
@@ -74,6 +81,46 @@ async function handleCommand(projectDirectory, options) {
74
81
  const { skipInstall, yes } = options;
75
82
 
76
83
  try {
84
+ console.log(chalk.bold.cyan(`\n🧱 create-nextblock v${CLI_VERSION}\n`));
85
+
86
+ // Prerequisites gate (interactive only) — shown BEFORE we ask for a name, scaffold, or
87
+ // install, so anyone who isn't ready can cancel without creating anything.
88
+ if (!yes) {
89
+ clack.note(
90
+ [
91
+ '1. A Supabase project https://supabase.com/dashboard',
92
+ ' • Reference ID — Project Settings > General > "Reference ID"',
93
+ ' • Connection string — Connect (top bar) > Direct connection > URI',
94
+ ' • anon + service_role keys — Project Settings > API Keys',
95
+ ' • Personal Access Token — Account > Access Tokens > Generate new token',
96
+ '',
97
+ '2. A Cloudflare R2 bucket https://dash.cloudflare.com > R2',
98
+ ' • Create a bucket, then enable its Public Development URL (Bucket > Settings > General)',
99
+ ' • Create an R2 API token (Object Read & Write); copy the Access Key ID + Secret (shown once)',
100
+ '',
101
+ '3. SMTP credentials SMTP2GO works very well: https://www.smtp2go.com',
102
+ ' • Required so Supabase can email the confirmation link your first admin needs to sign in',
103
+ ].join('\n'),
104
+ 'Before you continue, have all of the following ready',
105
+ );
106
+
107
+ const ready = await clack.confirm({
108
+ message:
109
+ 'Do you have your Supabase, Cloudflare R2, and SMTP details ready?',
110
+ initialValue: true,
111
+ });
112
+ if (clack.isCancel(ready)) {
113
+ handleWizardCancel('Setup cancelled.');
114
+ }
115
+ if (!ready) {
116
+ clack.note(
117
+ 'No problem — nothing was created. Gather the items above, then run\n`npm create nextblock` again. Full guide: docs/05-DEVELOPER-GUIDE.md',
118
+ 'Come back when ready',
119
+ );
120
+ return;
121
+ }
122
+ }
123
+
77
124
  let projectName = projectDirectory;
78
125
 
79
126
  if (!projectName) {
@@ -159,32 +206,35 @@ async function handleCommand(projectDirectory, options) {
159
206
  await ensurePublicNpmrc(projectDir);
160
207
  console.log(chalk.green('Enforced public registry for initial install.'));
161
208
 
209
+ await initializeGit(projectDir);
210
+
162
211
  if (!skipInstall) {
163
212
  await installDependencies(projectDir);
164
213
  } else {
165
214
  console.log(chalk.yellow('Skipping dependency installation.'));
166
215
  }
167
216
 
168
- // Run setup wizard after dependencies are installed so package assets are available
217
+ // Run the setup wizard after dependencies are installed so package assets are available.
218
+ // When it runs, its own "next steps" outro (cd + npm run dev) is the final message, so we
219
+ // don't print a second closing block here — the whole flow completes in this one command.
169
220
  if (!yes) {
170
221
  await runSetupWizard(projectDir, projectName);
171
222
  } else {
223
+ // Non-interactive path: nothing was configured, so point the user at their env file.
172
224
  console.log(
173
- chalk.yellow(
174
- 'Skipping interactive setup wizard because --yes was provided.',
225
+ chalk.green(
226
+ `\nSuccess! Your NextBlock™ CMS project "${projectName}" is scaffolded.\n`,
175
227
  ),
176
228
  );
229
+ console.log(chalk.cyan('Next steps:'));
230
+ console.log(chalk.cyan(` 1. cd ${projectName}`));
231
+ console.log(
232
+ chalk.gray(
233
+ ' 2. Add your Supabase / R2 / SMTP values to .env.local (template in .env.example)',
234
+ ),
235
+ );
236
+ console.log(chalk.cyan(' 3. npm run dev'));
177
237
  }
178
-
179
- await initializeGit(projectDir);
180
-
181
- console.log(
182
- chalk.green(
183
- `\nSuccess! Your NextBlock CMS project "${projectName}" is ready.\n`,
184
- ),
185
- );
186
- console.log(chalk.cyan('Next step:'));
187
- console.log(chalk.cyan(` cd ${projectName} && npm run dev`));
188
238
  } catch (error) {
189
239
  console.error(
190
240
  chalk.red(
@@ -195,482 +245,745 @@ async function handleCommand(projectDirectory, options) {
195
245
  }
196
246
  }
197
247
 
198
- async function runSetupWizard(projectDir, projectName) {
199
- const projectPath = resolve(projectDir);
200
- process.chdir(projectPath);
248
+ async function handleActivateCommand(moduleName) {
249
+ if (!moduleName || moduleName !== 'ecommerce') {
250
+ console.error(
251
+ chalk.red('Invalid module name. Supported modules: ecommerce'),
252
+ );
253
+ process.exit(1);
254
+ }
201
255
 
202
- clack.intro('🚀 Welcome to the NextBlock setup wizard!');
256
+ clack.intro(`🚀 Activating NextBlock module: ${moduleName}`);
203
257
 
204
- const supabaseDir = resolve(projectPath, 'supabase');
205
- await fs.ensureDir(supabaseDir);
206
- await resetSupabaseProjectRef(projectPath);
258
+ const projectPath = process.cwd();
207
259
 
208
- clack.note(
209
- 'Before proceeding, ensure you have a Supabase project ready.\n\n' +
210
- '1. Supabase Cloud: Create a project at https://supabase.com/dashboard\n' +
211
- '2. Vercel Storage: If created via Vercel > Storage, check your .env.local snippet on Vercel for keys.',
212
- 'Supabase Prerequisites',
260
+ // 1. Install NPM package
261
+ clack.note(`Installing @nextblock-cms/${moduleName}...`);
262
+
263
+ await execa(
264
+ 'npm',
265
+ ['install', `@nextblock-cms/ecommerce@npm:@nextblock-cms/ecom@latest`],
266
+ { cwd: projectPath, stdio: 'inherit' },
213
267
  );
268
+ clack.note('NPM package installed!');
269
+
270
+ // 2. Inject Route Wrappers
271
+ clack.note('Injecting route wrappers...');
272
+
273
+ const routesToInject = {
274
+ 'app/cms/orders/page.tsx': `import { OrdersPage as OrdersPageUI } from '@nextblock-cms/ecommerce';
275
+ import { verifyPackageOnline } from '@nextblock-cms/db/server';
276
+ import { redirect } from 'next/navigation';
277
+
278
+ export default async function OrdersPage() {
279
+ const isOnline = await verifyPackageOnline('ecommerce');
280
+ if (!isOnline) {
281
+ redirect('/cms/settings/packages');
282
+ }
283
+
284
+ return <OrdersPageUI />;
285
+ }`,
286
+ 'app/cms/orders/[id]/page.tsx': `import { OrderDetailPage as OrderDetailPageUI } from '@nextblock-cms/ecommerce';
287
+ import { verifyPackageOnline } from '@nextblock-cms/db/server';
288
+ import { redirect } from 'next/navigation';
289
+
290
+ export default async function OrderDetailPage({
291
+ params,
292
+ }: {
293
+ params: Promise<{ id: string }>;
294
+ }) {
295
+ const isOnline = await verifyPackageOnline('ecommerce');
296
+ if (!isOnline) {
297
+ redirect('/cms/settings/packages');
298
+ }
299
+ const resolvedParams = await params;
300
+ return <OrderDetailPageUI params={resolvedParams} />;
301
+ }`,
302
+ 'app/cms/products/page.tsx': `import { ProductsPage as ProductsPageUI } from '@nextblock-cms/ecommerce';
303
+ import { verifyPackageOnline } from '@nextblock-cms/db/server';
304
+ import { redirect } from 'next/navigation';
305
+
306
+ export default async function ProductsPage() {
307
+ const isOnline = await verifyPackageOnline('ecommerce');
308
+ if (!isOnline) {
309
+ redirect('/cms/settings/packages');
310
+ }
311
+
312
+ return <ProductsPageUI />;
313
+ }`,
314
+ 'app/cms/products/new/page.tsx': `import { NewProductPage as NewProductPageUI } from '@nextblock-cms/ecommerce';
315
+ import { verifyPackageOnline } from '@nextblock-cms/db/server';
316
+ import { redirect } from 'next/navigation';
214
317
 
215
- clack.note('Connecting to Supabase...');
216
- clack.note('I will now open your browser to log into Supabase.');
217
- await runSupabaseCli(['login'], { cwd: projectPath });
318
+ export default async function NewProductPage() {
319
+ const isOnline = await verifyPackageOnline('ecommerce');
320
+ if (!isOnline) {
321
+ redirect('/cms/settings/packages');
322
+ }
218
323
 
219
- clack.note('Now, please select your NextBlock project when prompted.');
220
- await runSupabaseCli(['link'], { cwd: projectPath });
221
- if (process.stdin.isTTY) {
222
- try {
223
- process.stdin.setRawMode(false);
224
- } catch {
225
- // ignore
226
- }
227
- process.stdin.setEncoding('utf8');
228
- process.stdin.resume();
324
+ return <NewProductPageUI />;
325
+ }`,
326
+ 'app/cms/products/[id]/edit/page.tsx': `import { EditProductPage as EditProductPageUI } from '@nextblock-cms/ecommerce';
327
+ import { verifyPackageOnline } from '@nextblock-cms/db/server';
328
+ import { redirect } from 'next/navigation';
329
+
330
+ export default async function EditProductPage({ params }: { params: Promise<{ id: string }> }) {
331
+ const isOnline = await verifyPackageOnline('ecommerce');
332
+ if (!isOnline) {
333
+ redirect('/cms/settings/packages');
229
334
  }
230
335
 
231
- let projectId = await readSupabaseProjectRef(projectPath);
336
+ const resolvedParams = await params;
337
+ return <EditProductPageUI params={resolvedParams} />;
338
+ }`,
339
+ 'app/cms/payments/page.tsx': `import { PaymentsPage as PaymentsPageUI } from '@nextblock-cms/ecommerce';
340
+ import { verifyPackageOnline } from '@nextblock-cms/db/server';
341
+ import { redirect } from 'next/navigation';
232
342
 
233
- if (!projectId) {
234
- clack.note('I could not detect your Supabase project ref automatically.');
235
- const manual = await clack.text({
236
- message:
237
- 'Enter your Supabase project ref (from the Supabase dashboard URL or the link output, e.g., abcdefghijklmnopqrstu):',
238
- validate: (val) => (!val ? 'Project ref is required' : undefined),
239
- });
240
- if (clack.isCancel(manual)) {
241
- handleWizardCancel('Setup cancelled.');
343
+ export default async function PaymentsPage() {
344
+ const isOnline = await verifyPackageOnline('ecommerce');
345
+ if (!isOnline) {
346
+ redirect('/cms/settings/packages');
347
+ }
348
+
349
+ return <PaymentsPageUI />;
350
+ }`,
351
+ 'app/cms/coupons/page.tsx': `import { CouponsPage as CouponsPageUI } from '@nextblock-cms/ecommerce/server';
352
+ import { verifyPackageOnline } from '@nextblock-cms/db/server';
353
+ import { redirect } from 'next/navigation';
354
+
355
+ export default async function CouponsPage({
356
+ searchParams,
357
+ }: {
358
+ searchParams: Promise<{ status?: string; q?: string }>;
359
+ }) {
360
+ const isOnline = await verifyPackageOnline('ecommerce');
361
+ if (!isOnline) {
362
+ redirect('/cms/settings/packages');
363
+ }
364
+
365
+ return <CouponsPageUI searchParams={await searchParams} />;
366
+ }`,
367
+ 'app/cms/coupons/[id]/edit/page.tsx': `import { EditCouponPage as EditCouponPageUI } from '@nextblock-cms/ecommerce/server';
368
+ import { verifyPackageOnline } from '@nextblock-cms/db/server';
369
+ import { redirect } from 'next/navigation';
370
+
371
+ export default async function EditCouponPage({
372
+ params,
373
+ }: {
374
+ params: Promise<{ id: string }>;
375
+ }) {
376
+ const isOnline = await verifyPackageOnline('ecommerce');
377
+ if (!isOnline) {
378
+ redirect('/cms/settings/packages');
379
+ }
380
+
381
+ return <EditCouponPageUI params={params} />;
382
+ }`,
383
+ 'app/checkout/success/page.tsx': `import { CheckoutSuccessPage as CheckoutSuccessPageUI } from '@nextblock-cms/ecommerce';
384
+ import { verifyPackageOnline } from '@nextblock-cms/db/server';
385
+ import { notFound } from 'next/navigation';
386
+
387
+ export default async function CheckoutSuccessPage() {
388
+ const isOnline = await verifyPackageOnline('ecommerce');
389
+ if (!isOnline) {
390
+ notFound();
391
+ }
392
+
393
+ return <CheckoutSuccessPageUI />;
394
+ }`,
395
+ 'app/api/checkout/route.ts': `import { NextResponse } from 'next/server';
396
+ import { getPaymentProvider } from '@nextblock-cms/ecommerce/server';
397
+ import { createClient, verifyPackageOnline } from '@nextblock-cms/db/server';
398
+ import { normalizeCustomerAddress } from '@nextblock-cms/ecommerce';
399
+
400
+ function resolveProviderFromItem(item) {
401
+ if (item?.provider === 'stripe' || item?.provider === 'freemius') {
402
+ return item.provider;
403
+ }
404
+
405
+ if (item?.payment_provider === 'stripe' || item?.payment_provider === 'freemius') {
406
+ return item.payment_provider;
407
+ }
408
+
409
+ if (item?.product_type === 'digital') {
410
+ return 'freemius';
411
+ }
412
+
413
+ if (item?.product_type === 'physical') {
414
+ return 'stripe';
415
+ }
416
+
417
+ if (item?.freemius_product_id) {
418
+ return 'freemius';
419
+ }
420
+
421
+ return null;
422
+ }
423
+
424
+ export async function POST(req: Request) {
425
+ try {
426
+ const isOnline = await verifyPackageOnline('ecommerce');
427
+ if (!isOnline) {
428
+ return NextResponse.json({ error: 'Ecommerce module license is inactive' }, { status: 403 });
242
429
  }
243
- projectId = manual.trim();
430
+
431
+ const {
432
+ items,
433
+ customerEmail,
434
+ customerPhone,
435
+ billingAddress,
436
+ shippingAddress,
437
+ shippingMethodId,
438
+ currencyCode,
439
+ locale,
440
+ couponCode,
441
+ couponContextItems,
442
+ } = await req.json();
443
+
444
+ if (!items || !Array.isArray(items)) {
445
+ return NextResponse.json({ error: 'Invalid items data' }, { status: 400 });
446
+ }
447
+
448
+ const providerNames = Array.from(
449
+ new Set(items.map((item) => resolveProviderFromItem(item)).filter(Boolean))
450
+ );
451
+
452
+ if (providerNames.length === 0) {
453
+ return NextResponse.json(
454
+ { error: 'Each checkout request must include provider-aware cart items.' },
455
+ { status: 400 }
456
+ );
457
+ }
458
+
459
+ if (providerNames.length > 1) {
460
+ return NextResponse.json(
461
+ { error: 'Mixed-provider carts must be checked out in separate steps.' },
462
+ { status: 400 }
463
+ );
464
+ }
465
+
466
+ const providerName = providerNames[0];
467
+
468
+ if (providerName === 'freemius' && items.length !== 1) {
469
+ return NextResponse.json(
470
+ { error: 'Freemius items must be checked out one at a time.' },
471
+ { status: 400 }
472
+ );
473
+ }
474
+
475
+ if (!billingAddress) {
476
+ return NextResponse.json({ error: 'Billing address is required' }, { status: 400 });
477
+ }
478
+
479
+ const supabase = createClient();
480
+ const provider = getPaymentProvider(providerName);
481
+
482
+ const { data: { user } } = await supabase.auth.getUser();
483
+ const userId = user?.id;
484
+ const resolvedCustomerEmail = user?.email || customerEmail || null;
485
+
486
+ const { url, error, errorKey, errorParams, errorStatus, customProps } =
487
+ await provider.createCheckoutSession({
488
+ items,
489
+ customerEmail: resolvedCustomerEmail,
490
+ customerPhone,
491
+ userId,
492
+ billingAddress: normalizeCustomerAddress(billingAddress) ?? billingAddress,
493
+ shippingAddress:
494
+ providerName === 'stripe'
495
+ ? normalizeCustomerAddress(shippingAddress)
496
+ : null,
497
+ shippingMethodId: providerName === 'stripe' ? shippingMethodId : null,
498
+ currencyCode: typeof currencyCode === 'string' ? currencyCode : null,
499
+ locale: typeof locale === 'string' ? locale : null,
500
+ couponCode: typeof couponCode === 'string' ? couponCode : null,
501
+ couponContextItems: Array.isArray(couponContextItems) ? couponContextItems : items,
502
+ });
503
+
504
+ if (error) {
505
+ console.error('Checkout Error:', error);
506
+ return NextResponse.json(
507
+ { error, errorKey, errorParams },
508
+ { status: errorStatus ?? 500 }
509
+ );
510
+ }
511
+
512
+ return NextResponse.json({ url, customProps });
513
+ } catch (err: any) {
514
+ console.error('Checkout API Error:', err);
515
+ return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
244
516
  }
245
- await ensureSupabaseAssets(projectPath, { required: true });
517
+ }`,
518
+ };
246
519
 
247
- const siteUrlPrompt = await clack.text({
248
- message: 'What is the public URL of your site? (NEXT_PUBLIC_URL)',
249
- initialValue: 'http://localhost:3000',
250
- validate: (val) => (!val ? 'URL is required' : undefined),
251
- });
252
- if (clack.isCancel(siteUrlPrompt)) {
253
- handleWizardCancel('Setup cancelled.');
520
+ for (const [routePath, content] of Object.entries(routesToInject)) {
521
+ const fullPath = resolve(projectPath, routePath);
522
+ await fs.ensureDir(dirname(fullPath));
523
+ await fs.writeFile(fullPath, content);
254
524
  }
255
- const siteUrl = siteUrlPrompt.trim();
256
525
 
257
- clack.note(
258
- 'Please go to your Supabase project dashboard to get the following secrets.\n' +
259
- 'Note: For "Access Token", go to Account > Access Tokens > Generate New Token.',
526
+ clack.outro(
527
+ ' Ecommerce module activated successfully! You can now use the storefront features.',
260
528
  );
261
- const supabaseKeys = await clack.group(
529
+ }
530
+
531
+ // clack validator that rejects empty/whitespace-only input with a labelled message.
532
+ function requiredValue(label) {
533
+ return (value) =>
534
+ value && String(value).trim() ? undefined : `${label} is required`;
535
+ }
536
+
537
+ // Read the current value of a `KEY=` line from an .env file body (handles quotes),
538
+ // so re-runs can reuse already-generated secrets instead of regenerating them.
539
+ function readEnvValue(envContent, key) {
540
+ for (const line of envContent.split(/\r?\n/)) {
541
+ if (line.startsWith(key)) {
542
+ return line.slice(key.length).trim().replace(/^"(.*)"$/, '$1');
543
+ }
544
+ }
545
+ return '';
546
+ }
547
+
548
+ function generateSecret() {
549
+ return crypto.randomBytes(32).toString('hex');
550
+ }
551
+
552
+ async function runSetupWizard(projectDir, projectName) {
553
+ const projectPath = resolve(projectDir);
554
+ process.chdir(projectPath);
555
+
556
+ // Prerequisites + readiness were already confirmed up front in handleCommand (before any
557
+ // scaffolding), so the wizard goes straight to collecting configuration.
558
+ clack.intro('🚀 NextBlock™ CMS setup');
559
+
560
+ await fs.ensureDir(resolve(projectPath, 'supabase'));
561
+
562
+ // 1. Supabase — same questions/order as setup.mjs. Nothing is masked: you are pasting
563
+ // keys you just copied, and seeing them makes paste mistakes easy to spot.
564
+ clack.note('Get these from https://supabase.com/dashboard', 'Supabase project');
565
+ const supabase = await clack.group(
262
566
  {
567
+ projectId: () =>
568
+ clack.text({
569
+ message: 'Project ID (Project Settings > General > "Reference ID"):',
570
+ validate: requiredValue('Project Reference ID'),
571
+ }),
263
572
  postgresUrl: () =>
264
573
  clack.text({
265
574
  message:
266
- 'What is your Connection String? (Supabase: Project Dashboard > Connect (Top Left) > Connection String > URI | Vercel: POSTGRES_URL)',
575
+ 'Connection String (Connect > Direct connection > URI replace [YOUR-PASSWORD] with your DB password):',
267
576
  placeholder: 'postgresql://...',
268
- validate: (val) =>
269
- !val ? 'Connection string is required' : undefined,
577
+ validate: requiredValue('Connection string'),
270
578
  }),
271
579
  anonKey: () =>
272
- clack.password({
273
- message:
274
- 'What is your Project API Key (anon key)? (Supabase: Project Settings > API Keys > Project Legacy API Keys | Vercel: SUPABASE_ANON_KEY)',
275
- validate: (val) => (!val ? 'Anon Key is required' : undefined),
580
+ clack.text({
581
+ message: 'Project API Key — anon / public (Project Settings > API Keys):',
582
+ validate: requiredValue('Anon key'),
276
583
  }),
277
584
  serviceKey: () =>
278
- clack.password({
279
- message:
280
- 'What is your Service Role Key (service_role key)? (Supabase: Project Settings > API Keys > Project Legacy API Keys | Vercel: SUPABASE_SERVICE_ROLE_KEY)',
281
- validate: (val) =>
282
- !val ? 'Service Role Key is required' : undefined,
585
+ clack.text({
586
+ message: 'Service Role Key — service_role (Project Settings > API Keys):',
587
+ validate: requiredValue('Service role key'),
283
588
  }),
284
589
  accessToken: () =>
285
- clack.password({
590
+ clack.text({
286
591
  message:
287
- 'What is your Personal Access Token? (Supabase: Account > Access Tokens). Required for deployment.',
288
- validate: (val) =>
289
- !val ? 'Access Token is required for deployment' : undefined,
592
+ 'Personal Access Token (Account > Access Tokens > Generate new token):',
593
+ validate: requiredValue('Access token'),
594
+ }),
595
+ siteUrl: () =>
596
+ clack.text({
597
+ // Standalone `npm run dev` is plain `next dev` on :3000 (NOT `nx serve` on :4200),
598
+ // so the local default differs from the monorepo setup wizard on purpose.
599
+ message: 'Public site URL [NEXT_PUBLIC_URL]:',
600
+ initialValue: 'http://localhost:3000',
601
+ validate: requiredValue('Site URL'),
290
602
  }),
291
603
  },
292
604
  { onCancel: () => handleWizardCancel('Setup cancelled.') },
293
605
  );
294
606
 
295
- clack.note('Generating local secrets...');
296
- const revalidationToken = crypto.randomBytes(32).toString('hex');
607
+ const projectId = supabase.projectId.trim();
608
+ const postgresUrl = supabase.postgresUrl.trim();
609
+ const siteUrl = supabase.siteUrl.trim().replace(/\/+$/, '');
297
610
  const supabaseUrl = `https://${projectId}.supabase.co`;
298
611
 
299
- const postgresUrl = supabaseKeys.postgresUrl;
612
+ // Extract the database password from the connection string; prompt if it is missing
613
+ // or still the [YOUR-PASSWORD] placeholder.
300
614
  let dbPassword = '';
301
615
  try {
302
- const parsedUrl = new URL(postgresUrl);
303
- dbPassword = parsedUrl.password;
616
+ dbPassword = decodeURIComponent(new URL(postgresUrl).password);
304
617
  } catch {
305
- // Fallback if URL parsing fails, though validation above checks for non-empty
618
+ // Fall through to the manual prompt below.
306
619
  }
307
-
308
- if (!dbPassword) {
309
- const passwordPrompt = await clack.password({
620
+ if (!dbPassword || /YOUR-PASSWORD/i.test(dbPassword)) {
621
+ const passwordPrompt = await clack.text({
310
622
  message:
311
- 'Could not extract password from URL. What is your Database Password?',
312
- validate: (val) => (!val ? 'Password is required' : undefined),
623
+ 'Could not read the DB password from the URI. Enter your Postgres database password:',
624
+ validate: requiredValue('Database password'),
313
625
  });
314
626
  if (clack.isCancel(passwordPrompt)) {
315
627
  handleWizardCancel('Setup cancelled.');
316
628
  }
317
- dbPassword = passwordPrompt;
629
+ dbPassword = passwordPrompt.trim();
318
630
  }
319
631
 
320
- const envPath = resolve(projectPath, '.env');
321
- const appendEnvBlock = async (label, lines) => {
322
- const normalized = lines.join('\n');
323
- const blockContent = normalized.endsWith('\n')
324
- ? normalized
325
- : `${normalized}\n`;
326
- if (canWriteEnv) {
327
- await fs.appendFile(envPath, blockContent);
328
- } else {
329
- clack.note(
330
- `Add the following ${label} values to your existing .env:\n${blockContent}`,
331
- );
332
- }
632
+ // 2. Cloudflare R2 — required. Powers media uploads, image processing, and backups.
633
+ clack.note('https://dash.cloudflare.com > R2', 'Cloudflare R2 storage');
634
+ const r2 = await clack.group(
635
+ {
636
+ accountId: () =>
637
+ clack.text({
638
+ message: 'R2 Account ID (R2 overview > Account details):',
639
+ validate: requiredValue('R2 Account ID'),
640
+ }),
641
+ bucketName: () =>
642
+ clack.text({
643
+ message: 'R2 Bucket Name:',
644
+ validate: requiredValue('R2 Bucket Name'),
645
+ }),
646
+ publicBaseUrl: () =>
647
+ clack.text({
648
+ message:
649
+ 'R2 Public Development URL (Bucket > Settings > Public Development URL, e.g. https://pub-xxxx.r2.dev):',
650
+ validate: requiredValue('R2 Public Development URL'),
651
+ }),
652
+ accessKey: () =>
653
+ clack.text({
654
+ message: 'R2 Access Key ID (R2 > Manage API Tokens):',
655
+ validate: requiredValue('R2 Access Key ID'),
656
+ }),
657
+ secretKey: () =>
658
+ clack.text({
659
+ message:
660
+ 'R2 Secret Access Key (shown only once when the token is created):',
661
+ validate: requiredValue('R2 Secret Access Key'),
662
+ }),
663
+ },
664
+ { onCancel: () => handleWizardCancel('Setup cancelled.') },
665
+ );
666
+
667
+ // 3. SMTP — required. Sends the sign-up confirmation email your first admin needs.
668
+ clack.note('SMTP2GO works very well: https://www.smtp2go.com', 'SMTP email');
669
+ const smtp = await clack.group(
670
+ {
671
+ host: () =>
672
+ clack.text({
673
+ message: 'SMTP Host (e.g. mail.smtp2go.com):',
674
+ validate: requiredValue('SMTP Host'),
675
+ }),
676
+ port: () =>
677
+ clack.text({
678
+ message: 'SMTP Port (465 = SSL, 587 = STARTTLS):',
679
+ initialValue: '465',
680
+ validate: requiredValue('SMTP Port'),
681
+ }),
682
+ user: () =>
683
+ clack.text({
684
+ message: 'SMTP User:',
685
+ validate: requiredValue('SMTP User'),
686
+ }),
687
+ pass: () =>
688
+ clack.text({
689
+ message: 'SMTP Password:',
690
+ validate: requiredValue('SMTP Password'),
691
+ }),
692
+ fromEmail: () =>
693
+ clack.text({
694
+ message: 'From Email (the address confirmation emails are sent from):',
695
+ validate: requiredValue('From Email'),
696
+ }),
697
+ fromName: () =>
698
+ clack.text({
699
+ message: 'From Name (e.g. NextBlock):',
700
+ validate: requiredValue('From Name'),
701
+ }),
702
+ },
703
+ { onCancel: () => handleWizardCancel('Setup cancelled.') },
704
+ );
705
+
706
+ const smtpValues = {
707
+ host: smtp.host,
708
+ port: smtp.port,
709
+ user: smtp.user,
710
+ pass: smtp.pass,
711
+ fromEmail: smtp.fromEmail,
712
+ fromName: smtp.fromName,
333
713
  };
334
- const envLines = [
335
- `NEXT_PUBLIC_URL=${siteUrl}`,
336
- '# Vercel / Supabase',
337
- `SUPABASE_PROJECT_ID=${projectId}`,
338
- `NEXT_PUBLIC_SUPABASE_URL=${supabaseUrl}`,
339
- `NEXT_PUBLIC_SUPABASE_ANON_KEY=${supabaseKeys.anonKey}`,
340
- `SUPABASE_SERVICE_ROLE_KEY=${supabaseKeys.serviceKey}`,
341
- `SUPABASE_ACCESS_TOKEN=${supabaseKeys.accessToken}`,
342
- `POSTGRES_URL=${postgresUrl}`,
343
- '',
344
- '# Revalidation',
345
- `REVALIDATE_SECRET_TOKEN=${revalidationToken}`,
346
- '',
347
- ];
348
714
 
349
- let canWriteEnv = true;
715
+ // 4. Write .env.local with everything we collected. Mirror setup.mjs: seed from the
716
+ // template .env.example when present, replace keys line-by-line, append any missing,
717
+ // and reuse already-generated secrets so re-runs are idempotent. .env.local is what
718
+ // `next dev` loads first and is covered by the generated .gitignore.
719
+ clack.note('Writing .env.local...');
720
+ const envPath = resolve(projectPath, '.env.local');
721
+ const envExamplePath = resolve(projectPath, '.env.example');
722
+ let envContent = '';
350
723
  if (await fs.pathExists(envPath)) {
351
- const overwrite = await clack.confirm({
352
- message: '.env already exists. Overwrite with generated values?',
353
- initialValue: false,
354
- });
355
- if (clack.isCancel(overwrite)) {
356
- handleWizardCancel('Setup cancelled.');
357
- }
358
- if (!overwrite) {
359
- canWriteEnv = false;
360
- clack.note(
361
- 'Keeping existing .env. Add/merge the generated values manually.',
362
- );
724
+ envContent = await fs.readFile(envPath, 'utf8');
725
+ } else if (await fs.pathExists(envExamplePath)) {
726
+ envContent = await fs.readFile(envExamplePath, 'utf8');
727
+ }
728
+
729
+ const cronSecret = readEnvValue(envContent, 'CRON_SECRET=') || generateSecret();
730
+ const draftSecret =
731
+ readEnvValue(envContent, 'DRAFT_MODE_SECRET=') || generateSecret();
732
+ const revalidateSecret =
733
+ readEnvValue(envContent, 'REVALIDATE_SECRET_TOKEN=') || generateSecret();
734
+
735
+ const replacements = {
736
+ 'SUPABASE_PROJECT_ID=': `SUPABASE_PROJECT_ID=${projectId}`,
737
+ 'POSTGRES_URL=': `POSTGRES_URL=${postgresUrl}`,
738
+ 'POSTGRES_PASSWORD=': `POSTGRES_PASSWORD="${dbPassword}"`,
739
+ 'NEXT_PUBLIC_SUPABASE_URL=': `NEXT_PUBLIC_SUPABASE_URL=${supabaseUrl}`,
740
+ 'NEXT_PUBLIC_SUPABASE_ANON_KEY=': `NEXT_PUBLIC_SUPABASE_ANON_KEY=${supabase.anonKey}`,
741
+ 'SUPABASE_SERVICE_ROLE_KEY=': `SUPABASE_SERVICE_ROLE_KEY=${supabase.serviceKey}`,
742
+ 'SUPABASE_ACCESS_TOKEN=': `SUPABASE_ACCESS_TOKEN=${supabase.accessToken}`,
743
+ 'NEXT_PUBLIC_URL=': `NEXT_PUBLIC_URL=${siteUrl}`,
744
+ 'CRON_SECRET=': `CRON_SECRET=${cronSecret}`,
745
+ 'DRAFT_MODE_SECRET=': `DRAFT_MODE_SECRET=${draftSecret}`,
746
+ 'REVALIDATE_SECRET_TOKEN=': `REVALIDATE_SECRET_TOKEN=${revalidateSecret}`,
747
+ // The R2 public URL is consumed under two names (next/image remotePatterns + CSP, and
748
+ // media URL resolution) — write the same value to both, matching setup.mjs.
749
+ 'NEXT_PUBLIC_R2_PUBLIC_URL=': `NEXT_PUBLIC_R2_PUBLIC_URL=${r2.publicBaseUrl}`,
750
+ 'NEXT_PUBLIC_R2_BASE_URL=': `NEXT_PUBLIC_R2_BASE_URL=${r2.publicBaseUrl}`,
751
+ 'R2_ACCOUNT_ID=': `R2_ACCOUNT_ID=${r2.accountId}`,
752
+ 'R2_BUCKET_NAME=': `R2_BUCKET_NAME=${r2.bucketName}`,
753
+ 'R2_ACCESS_KEY_ID=': `R2_ACCESS_KEY_ID=${r2.accessKey}`,
754
+ 'R2_SECRET_ACCESS_KEY=': `R2_SECRET_ACCESS_KEY=${r2.secretKey}`,
755
+ 'SMTP_HOST=': `SMTP_HOST=${smtpValues.host}`,
756
+ 'SMTP_PORT=': `SMTP_PORT=${smtpValues.port}`,
757
+ 'SMTP_USER=': `SMTP_USER=${smtpValues.user}`,
758
+ 'SMTP_PASS=': `SMTP_PASS=${smtpValues.pass}`,
759
+ 'SMTP_FROM_EMAIL=': `SMTP_FROM_EMAIL=${smtpValues.fromEmail}`,
760
+ 'SMTP_FROM_NAME=': `SMTP_FROM_NAME=${smtpValues.fromName}`,
761
+ 'SUPABASE_AUTH_RATE_LIMIT_EMAIL_SENT=':
762
+ 'SUPABASE_AUTH_RATE_LIMIT_EMAIL_SENT=30',
763
+ };
764
+
765
+ const appliedKeys = new Set();
766
+ const updatedLines = envContent.split(/\r?\n/).map((line) => {
767
+ for (const [key, value] of Object.entries(replacements)) {
768
+ if (line.startsWith(key)) {
769
+ appliedKeys.add(key);
770
+ return value;
771
+ }
363
772
  }
364
- }
773
+ return line;
774
+ });
365
775
 
366
- if (canWriteEnv) {
367
- await fs.writeFile(envPath, envLines.join('\n'));
368
- clack.note('Supabase configuration saved to .env');
776
+ // Append any keys missing from the seed so nothing is silently dropped.
777
+ for (const [key, value] of Object.entries(replacements)) {
778
+ if (!appliedKeys.has(key)) {
779
+ updatedLines.push(value);
780
+ }
369
781
  }
370
782
 
371
- clack.note('Setting up your database...');
372
-
373
- const dbPushSpinner = clack.spinner();
374
- dbPushSpinner.start('Pushing database schema...');
375
- try {
376
- process.env.POSTGRES_URL = postgresUrl;
377
- const migrationsDir = resolve(projectPath, 'supabase', 'migrations');
378
- const hasMigrations = async () =>
379
- (await fs.pathExists(migrationsDir)) &&
380
- (await fs.readdir(migrationsDir)).some((name) => name.endsWith('.sql'));
381
-
382
- if (!(await hasMigrations())) {
383
- await ensureSupabaseAssets(projectPath);
384
- }
783
+ await fs.writeFile(envPath, updatedLines.join('\n'), 'utf8');
784
+ clack.note(
785
+ 'Supabase, R2, SMTP, site URL, and generated secrets saved to .env.local',
786
+ );
385
787
 
386
- if (!(await hasMigrations())) {
387
- dbPushSpinner.stop(
388
- `No migrations found in ${migrationsDir}; skipping db push. Ensure @nextblock-cms/db includes supabase/migrations.`,
389
- );
390
- } else {
391
- const supabaseBin = await getSupabaseBinary(projectPath);
392
- const command = supabaseBin === 'npx' ? 'npx' : supabaseBin;
788
+ // 5. Materialize Supabase assets (migrations, config.toml, branded auth email
789
+ // templates) out of the installed @nextblock-cms/db package.
790
+ await ensureSupabaseAssets(projectPath, { required: true });
393
791
 
394
- // 1. Link the project explicitly
395
- dbPushSpinner.message('Linking to Supabase project...');
396
- const linkArgs = supabaseBin === 'npx' ? ['supabase', 'link'] : ['link'];
397
- linkArgs.push('--project-ref', projectId);
398
- linkArgs.push('--password', dbPassword);
792
+ // 6. Link the project and apply the schema. These are the standalone equivalents of the
793
+ // monorepo `npm run db:link` + `npm run db:migrate:fresh` (which do not exist in a
794
+ // generated project): we drive the Supabase CLI directly, authenticating with the
795
+ // access token so no browser login is required.
796
+ const supabaseBin = await getSupabaseBinary(projectPath);
797
+ const command = supabaseBin === 'npx' ? 'npx' : supabaseBin;
798
+ const sbArgs = (args) => (supabaseBin === 'npx' ? ['supabase', ...args] : args);
799
+ const supabaseEnv = {
800
+ ...process.env,
801
+ SUPABASE_ACCESS_TOKEN: supabase.accessToken,
802
+ SUPABASE_DB_PASSWORD: dbPassword,
803
+ POSTGRES_URL: postgresUrl,
804
+ // Available for env() substitution in supabase config.toml during `config push`.
805
+ NEXT_PUBLIC_URL: siteUrl,
806
+ };
399
807
 
400
- await execa(command, linkArgs, {
401
- stdio: 'inherit',
402
- cwd: projectPath,
403
- });
808
+ const applySchema = await clack.confirm({
809
+ message:
810
+ 'Apply the database schema to the linked project now? (Safe for a new database; does not delete existing data.)',
811
+ initialValue: true,
812
+ });
813
+ if (clack.isCancel(applySchema)) {
814
+ handleWizardCancel('Setup cancelled.');
815
+ }
404
816
 
405
- // 2. Push the schema using the linked state
406
- dbPushSpinner.message('Pushing database schema...');
407
- const pushArgs =
408
- supabaseBin === 'npx' ? ['supabase', 'db', 'push'] : ['db', 'push'];
409
- pushArgs.push('--include-all');
817
+ const dbSpinner = clack.spinner();
818
+ dbSpinner.start('Linking to your Supabase project...');
819
+ try {
820
+ await execa(
821
+ command,
822
+ sbArgs(['link', '--project-ref', projectId, '--password', dbPassword]),
823
+ { stdio: 'inherit', cwd: projectPath, env: supabaseEnv },
824
+ );
410
825
 
411
- await execa(command, pushArgs, {
826
+ if (applySchema) {
827
+ dbSpinner.message('Pushing database schema...');
828
+ await execa(command, sbArgs(['db', 'push', '--include-all']), {
412
829
  stdio: ['pipe', 'inherit', 'inherit'],
413
830
  cwd: projectPath,
414
831
  input: 'y\n', // Auto-confirm the push prompt
415
- env: {
416
- ...process.env,
417
- SUPABASE_DB_PASSWORD: dbPassword,
418
- },
832
+ env: supabaseEnv,
419
833
  });
420
834
 
421
- // 3. Push the config (for Auth settings like site_url)
422
- dbPushSpinner.message('Pushing Supabase config (auth settings)...');
423
- const configPushArgs =
424
- supabaseBin === 'npx'
425
- ? ['supabase', 'config', 'push']
426
- : ['config', 'push'];
427
-
428
- await execa(command, configPushArgs, {
835
+ dbSpinner.message('Pushing Supabase config (auth settings)...');
836
+ await execa(command, sbArgs(['config', 'push']), {
429
837
  stdio: ['pipe', 'inherit', 'inherit'],
430
838
  cwd: projectPath,
431
- env: {
432
- ...process.env,
433
- SUPABASE_DB_PASSWORD: dbPassword,
434
- // Ensure NEXT_PUBLIC_URL is available for env() substitution in config.toml
435
- NEXT_PUBLIC_URL: siteUrl,
436
- },
839
+ env: supabaseEnv,
437
840
  });
438
841
 
439
- dbPushSpinner.stop('Database schema and config pushed successfully!');
842
+ dbSpinner.stop('Database schema and config applied.');
843
+ } else {
844
+ dbSpinner.stop(
845
+ 'Linked. Skipped schema push — run `npx supabase db push --include-all` when ready.',
846
+ );
440
847
  }
441
848
  } catch (error) {
442
- dbPushSpinner.stop(
443
- 'Database push failed. Please run `npx supabase db push` manually.',
849
+ dbSpinner.stop(
850
+ 'Database setup failed. You can run `npx supabase db push --include-all` manually.',
444
851
  );
445
852
  if (error instanceof Error) {
446
853
  clack.note(error.message);
447
854
  }
448
855
  }
449
856
 
450
- clack.note(
451
- 'Optional Cloudflare R2 Setup:\nHave your Account ID, API token (Access + Secret), bucket name, and public bucket URL handy if you want media storage ready now.',
452
- );
453
- const setupR2 = await clack.confirm({
454
- message:
455
- 'Do you want to set up Cloudflare R2 for media storage now? (Optional > populate .env keys)',
857
+ // 7. Sync hosted Supabase Auth: custom SMTP + branded email templates. SMTP and the
858
+ // access token are required, so this always runs (matching setup.mjs). This is what
859
+ // lets Supabase email your first admin their confirmation link.
860
+ await enableSupabaseSmtpConfig(projectPath);
861
+ await configureHostedSupabaseAuth(projectPath, {
862
+ projectId,
863
+ siteUrl,
864
+ accessToken: supabase.accessToken,
865
+ smtpValues,
456
866
  });
457
- if (clack.isCancel(setupR2)) {
458
- handleWizardCancel('Setup cancelled.');
459
- }
460
-
461
- let r2Values = {
462
- publicBaseUrl: '',
463
- accountId: '',
464
- bucketName: '',
465
- accessKey: '',
466
- secretKey: '',
467
- };
468
-
469
- if (setupR2) {
470
- clack.note(
471
- 'I will open your browser to the R2 dashboard.\nYou need to create a bucket and an R2 API Token.',
472
- );
473
- await open('https://dash.cloudflare.com/?to=/:account/r2', { wait: false });
474
-
475
- const r2Keys = await clack.group(
476
- {
477
- accountId: () =>
478
- clack.text({
479
- message:
480
- 'R2: Paste your Cloudflare Account ID (Overview > Account Details - Bottom right):',
481
- validate: (val) => (!val ? 'Account ID is required' : undefined),
482
- }),
483
- bucketName: () =>
484
- clack.text({
485
- message: 'R2: Paste your Bucket Name:',
486
- validate: (val) => (!val ? 'Bucket name is required' : undefined),
487
- }),
488
- accessKey: () =>
489
- clack.password({
490
- message: 'R2: Paste your Access Key ID (create API tokens):',
491
- validate: (val) => (!val ? 'Access Key ID is required' : undefined),
492
- }),
493
- secretKey: () =>
494
- clack.password({
495
- message: 'R2: Paste your Secret Access Key:',
496
- validate: (val) =>
497
- !val ? 'Secret Access Key is required' : undefined,
498
- }),
499
- publicBaseUrl: () =>
500
- clack.text({
501
- message:
502
- 'R2: Public Base URL (Bucket > Settings > Public Development URL-Enable: e.g., https://pub-xxx.r2.dev)',
503
- validate: (val) =>
504
- !val ? 'Public base URL is required' : undefined,
505
- }),
506
- },
507
- { onCancel: () => handleWizardCancel('Setup cancelled.') },
508
- );
509
867
 
510
- r2Values = {
511
- publicBaseUrl: r2Keys.publicBaseUrl,
512
- accountId: r2Keys.accountId,
513
- bucketName: r2Keys.bucketName,
514
- accessKey: r2Keys.accessKey,
515
- secretKey: r2Keys.secretKey,
516
- };
517
- }
518
-
519
- await appendEnvBlock('Cloudflare R2', [
520
- '',
521
- '# Cloudflare',
522
- `NEXT_PUBLIC_R2_BASE_URL=${r2Values.publicBaseUrl}`,
523
- `R2_ACCOUNT_ID=${r2Values.accountId}`,
524
- `R2_BUCKET_NAME=${r2Values.bucketName}`,
525
- `R2_ACCESS_KEY_ID=${r2Values.accessKey}`,
526
- `R2_SECRET_ACCESS_KEY=${r2Values.secretKey}`,
527
- '',
528
- ]);
529
- if (setupR2) {
530
- clack.note('Cloudflare R2 configuration saved!');
531
- } else if (canWriteEnv) {
532
- clack.note(
533
- 'Cloudflare R2 placeholders added to .env. Configure them later when ready.',
534
- );
535
- }
536
-
537
- clack.note(
538
- 'Optional SMTP Setup:\nProvide the host, port, credentials, and from details for your email provider (e.g., Resend, Postmark) to send transactional emails immediately.',
539
- );
540
- const setupSMTP = await clack.confirm({
541
- message: 'Do you want to set up an SMTP server for emails now? (Optional)',
868
+ // 8. Optional premium modules (CLI-specific; requires a license + registry access).
869
+ const setupPremium = await clack.confirm({
870
+ message: 'Do you have a license and want to install premium modules now?',
871
+ initialValue: false,
542
872
  });
543
- if (clack.isCancel(setupSMTP)) {
873
+ if (clack.isCancel(setupPremium)) {
544
874
  handleWizardCancel('Setup cancelled.');
545
875
  }
546
-
547
- let smtpValues = {
548
- host: '',
549
- port: '',
550
- user: '',
551
- pass: '',
552
- fromEmail: '',
553
- fromName: '',
554
- };
555
-
556
- if (setupSMTP) {
557
- const smtpKeys = await clack.group(
876
+ if (setupPremium) {
877
+ clack.note('Installing @nextblock-cms/ecommerce...');
878
+ await execa(
879
+ 'npm',
880
+ ['install', '@nextblock-cms/ecommerce@npm:@nextblock-cms/ecom@latest'],
558
881
  {
559
- host: () =>
560
- clack.text({
561
- message: 'SMTP: Host (e.g., smtp.resend.com):',
562
- validate: (val) => (!val ? 'SMTP host is required' : undefined),
563
- }),
564
- port: () =>
565
- clack.text({
566
- message: 'SMTP: Port (e.g., 465):',
567
- validate: (val) => (!val ? 'SMTP port is required' : undefined),
568
- }),
569
- user: () =>
570
- clack.text({
571
- message: 'SMTP: User (e.g., apikey):',
572
- validate: (val) => (!val ? 'SMTP user is required' : undefined),
573
- }),
574
- pass: () =>
575
- clack.password({
576
- message: 'SMTP: Password:',
577
- validate: (val) => (!val ? 'SMTP password is required' : undefined),
578
- }),
579
- fromEmail: () =>
580
- clack.text({
581
- message: 'SMTP: From Email (e.g., onboarding@my.site):',
582
- validate: (val) => (!val ? 'From email is required' : undefined),
583
- }),
584
- fromName: () =>
585
- clack.text({
586
- message: 'SMTP: From Name (e.g., NextBlock):',
587
- validate: (val) => (!val ? 'From name is required' : undefined),
588
- }),
882
+ cwd: projectPath,
883
+ stdio: 'inherit',
589
884
  },
590
- { onCancel: () => handleWizardCancel('Setup cancelled.') },
591
885
  );
592
-
593
- smtpValues = {
594
- host: smtpKeys.host,
595
- port: smtpKeys.port,
596
- user: smtpKeys.user,
597
- pass: smtpKeys.pass,
598
- fromEmail: smtpKeys.fromEmail,
599
- fromName: smtpKeys.fromName,
600
- };
886
+ clack.note('Premium module installed!');
601
887
  }
602
888
 
603
- clack.note(
604
- 'Optional Premium Module Setup:\nIf you have a nextblock license, you can install the premium modules now.',
889
+ clack.outro(
890
+ [
891
+ `🎉 Your NextBlock™ project ${projectName ? `"${projectName}" ` : ''}is ready!`,
892
+ '',
893
+ 'Next steps:',
894
+ ` 1. Start the app: cd ${projectName} && npm run dev → ${siteUrl}`,
895
+ ` 2. Create your account: open ${siteUrl}/sign-up`,
896
+ ' The FIRST account to sign up automatically becomes the ADMIN.',
897
+ ' 3. Confirm your email: click the link sent to your inbox',
898
+ ` 4. Sign in — you'll land in the CMS at ${siteUrl}/cms/dashboard`,
899
+ ].join('\n'),
605
900
  );
606
- const setupPremium = await clack.confirm({
607
- message:
608
- 'Do you have a GitHub Personal Access Token (PAT) for premium modules?',
609
- initialValue: false,
610
- });
901
+ }
611
902
 
612
- if (clack.isCancel(setupPremium)) {
613
- handleWizardCancel('Setup cancelled.');
614
- }
903
+ async function enableSupabaseSmtpConfig(projectDir) {
904
+ const configPath = resolve(projectDir, 'supabase', 'config.toml');
615
905
 
616
- if (setupPremium) {
617
- const patPrompt = await clack.text({
618
- message: 'Your GitHub Personal Access Token (PAT):',
619
- placeholder: 'ghp_ or github_pat_...',
620
- validate: (val) => {
621
- if (!val) return 'PAT is required';
622
- if (!val.startsWith('ghp_') && !val.startsWith('github_pat_')) {
623
- return 'Token must start with ghp_ or github_pat_';
624
- }
625
- },
626
- });
906
+ if (!(await fs.pathExists(configPath))) {
907
+ return;
908
+ }
627
909
 
628
- if (clack.isCancel(patPrompt)) {
629
- handleWizardCancel('Setup cancelled.');
630
- }
910
+ const smtpBlock = `# [auth.email.smtp]
911
+ # host = "env(SMTP_HOST)"
912
+ # port = 587
913
+ # user = "env(SMTP_USER)"
914
+ # pass = "env(SMTP_PASS)"
915
+ # admin_email = "env(SMTP_FROM_EMAIL)"
916
+ # sender_name = "env(SMTP_FROM_NAME)"`;
631
917
 
632
- const pat = patPrompt.trim();
918
+ const enabledSmtpBlock = `[auth.email.smtp]
919
+ host = "env(SMTP_HOST)"
920
+ port = 587
921
+ user = "env(SMTP_USER)"
922
+ pass = "env(SMTP_PASS)"
923
+ admin_email = "env(SMTP_FROM_EMAIL)"
924
+ sender_name = "env(SMTP_FROM_NAME)"`;
633
925
 
634
- // Configure .npmrc for the project
635
- const npmrcPath = resolve(projectPath, '.npmrc');
636
- const npmrcContent = [
637
- '@nextblock-cms:registry=https://npm.pkg.github.com',
638
- `//npm.pkg.github.com/:_authToken=${pat}`,
639
- '',
640
- ].join('\n');
926
+ const configContents = await fs.readFile(configPath, 'utf8');
641
927
 
642
- await fs.writeFile(npmrcPath, npmrcContent);
643
- clack.note('Premium modules configured in .npmrc!');
928
+ if (configContents.includes(enabledSmtpBlock)) {
929
+ return;
930
+ }
644
931
 
645
- clack.note('Installing @nextblock-cms/ecom...');
646
- await runCommand('npm', ['install', '@nextblock-cms/ecom'], {
647
- cwd: projectPath,
648
- });
649
- clack.note('Premium module installed!');
932
+ if (!configContents.includes(smtpBlock)) {
933
+ throw new Error(
934
+ `Could not find the SMTP placeholder block in ${configPath}.`,
935
+ );
650
936
  }
651
937
 
652
- await appendEnvBlock('SMTP', [
653
- '',
654
- '# Email SMTP Configuration',
655
- `SMTP_HOST=${smtpValues.host}`,
656
- `SMTP_PORT=${smtpValues.port}`,
657
- `SMTP_USER=${smtpValues.user}`,
658
- `SMTP_PASS=${smtpValues.pass}`,
659
- `SMTP_FROM_EMAIL=${smtpValues.fromEmail}`,
660
- `SMTP_FROM_NAME=${smtpValues.fromName}`,
661
- '',
662
- ]);
663
- if (setupSMTP) {
664
- clack.note('SMTP configuration saved!');
665
- } else if (canWriteEnv) {
938
+ await fs.writeFile(
939
+ configPath,
940
+ configContents.replace(smtpBlock, enabledSmtpBlock),
941
+ 'utf8',
942
+ );
943
+ }
944
+
945
+ async function configureHostedSupabaseAuth(
946
+ projectDir,
947
+ { projectId, siteUrl, accessToken, smtpValues },
948
+ ) {
949
+ if (!projectId || !siteUrl || !accessToken) {
666
950
  clack.note(
667
- 'SMTP placeholders added to .env. Configure them later when ready.',
951
+ 'Skipped hosted Supabase Auth sync because the project ref, site URL, or access token is missing.',
668
952
  );
953
+ return;
669
954
  }
670
955
 
671
- clack.outro(
672
- `🎉 Your NextBlock project ${projectName ? `"${projectName}" ` : ''}is ready!`,
673
- );
956
+ const spinner = clack.spinner();
957
+ spinner.start('Syncing hosted Supabase Auth SMTP and branded email templates...');
958
+
959
+ try {
960
+ await execa('node', ['tools/configure-supabase-auth.js'], {
961
+ cwd: projectDir,
962
+ env: {
963
+ ...process.env,
964
+ SUPABASE_PROJECT_ID: projectId,
965
+ NEXT_PUBLIC_URL: siteUrl,
966
+ SUPABASE_ACCESS_TOKEN: accessToken,
967
+ SMTP_HOST: smtpValues.host,
968
+ SMTP_PORT: smtpValues.port,
969
+ SMTP_USER: smtpValues.user,
970
+ SMTP_PASS: smtpValues.pass,
971
+ SMTP_FROM_EMAIL: smtpValues.fromEmail,
972
+ SMTP_FROM_NAME: smtpValues.fromName,
973
+ SUPABASE_AUTH_RATE_LIMIT_EMAIL_SENT:
974
+ process.env.SUPABASE_AUTH_RATE_LIMIT_EMAIL_SENT || '30',
975
+ },
976
+ });
977
+ spinner.stop('Hosted Supabase Auth configured.');
978
+ } catch (error) {
979
+ spinner.stop(
980
+ 'Hosted Supabase Auth sync skipped. You can rerun it later with npm run configure:supabase-auth.',
981
+ );
982
+ clack.note(
983
+ error instanceof Error ? error.message : String(error),
984
+ 'Supabase Auth Sync',
985
+ );
986
+ }
674
987
  }
675
988
 
676
989
  function handleWizardCancel(message) {
@@ -875,23 +1188,30 @@ async function ensureEnvExample(projectDir) {
875
1188
  }
876
1189
  }
877
1190
 
878
- const placeholder = `# Environment variables for NextBlock CMS
1191
+ const placeholder = `# Environment variables for NextBlock CMS
879
1192
  NEXT_PUBLIC_URL=
880
- # Vercel / Supabase
1193
+
1194
+ # Supabase — the setup wizard fills this whole block.
881
1195
  SUPABASE_PROJECT_ID=
882
1196
  POSTGRES_URL=
1197
+ POSTGRES_PASSWORD=
883
1198
  NEXT_PUBLIC_SUPABASE_URL=
884
1199
  NEXT_PUBLIC_SUPABASE_ANON_KEY=
885
1200
  SUPABASE_SERVICE_ROLE_KEY=
1201
+ SUPABASE_ACCESS_TOKEN=
1202
+
1203
+ # Auto-generated by the setup wizard.
1204
+ CRON_SECRET=
1205
+ DRAFT_MODE_SECRET=
1206
+ REVALIDATE_SECRET_TOKEN=
886
1207
 
887
- # Cloudflare
1208
+ # Cloudflare R2 — setup writes the public URL to both keys.
1209
+ NEXT_PUBLIC_R2_PUBLIC_URL=
888
1210
  NEXT_PUBLIC_R2_BASE_URL=
1211
+ R2_ACCOUNT_ID=
1212
+ R2_BUCKET_NAME=
889
1213
  R2_ACCESS_KEY_ID=
890
1214
  R2_SECRET_ACCESS_KEY=
891
- R2_BUCKET_NAME=
892
- R2_ACCOUNT_ID=
893
-
894
- REVALIDATE_SECRET_TOKEN=
895
1215
 
896
1216
  # Email SMTP Configuration
897
1217
  SMTP_HOST=
@@ -900,6 +1220,7 @@ SMTP_USER=
900
1220
  SMTP_PASS=
901
1221
  SMTP_FROM_EMAIL=
902
1222
  SMTP_FROM_NAME=
1223
+ SUPABASE_AUTH_RATE_LIMIT_EMAIL_SENT=30
903
1224
  `;
904
1225
 
905
1226
  await fs.writeFile(destination, placeholder);
@@ -949,6 +1270,19 @@ async function ensureSupabaseAssets(projectDir, options = {}) {
949
1270
  migrationsCopied = true;
950
1271
  }
951
1272
 
1273
+ // Branded Auth email templates. configure-supabase-auth.js resolves the supabase dir by
1274
+ // requiring a templates/ subdir, and uploads these via the Management API. Without them
1275
+ // the hosted-auth + SMTP sync silently skips and the first admin never gets a
1276
+ // confirmation email — so copy them alongside config.toml + migrations.
1277
+ const sourceTemplates = resolve(packageSupabaseDir, 'templates');
1278
+ const destTemplates = resolve(destSupabaseDir, 'templates');
1279
+ if (await fs.pathExists(sourceTemplates)) {
1280
+ await fs.copy(sourceTemplates, destTemplates, {
1281
+ overwrite: true,
1282
+ errorOnExist: false,
1283
+ });
1284
+ }
1285
+
952
1286
  if (required) {
953
1287
  if (!configCopied) {
954
1288
  throw new Error(
@@ -1027,32 +1361,6 @@ async function resolvePackageSupabaseDir(projectDir) {
1027
1361
  return { dir: null, triedPaths };
1028
1362
  }
1029
1363
 
1030
- async function readSupabaseProjectRef(projectDir) {
1031
- const projectRefPath = resolve(
1032
- projectDir,
1033
- 'supabase',
1034
- '.temp',
1035
- 'project-ref',
1036
- );
1037
- if (await fs.pathExists(projectRefPath)) {
1038
- const value = (await fs.readFile(projectRefPath, 'utf8')).trim();
1039
- if (/^[a-z0-9]{20,}$/i.test(value)) {
1040
- return value;
1041
- }
1042
- }
1043
-
1044
- return null;
1045
- }
1046
-
1047
- async function resetSupabaseProjectRef(projectDir) {
1048
- const tempDir = resolve(projectDir, 'supabase', '.temp');
1049
- await fs.ensureDir(tempDir);
1050
- const projectRefPath = resolve(tempDir, 'project-ref');
1051
- if (await fs.pathExists(projectRefPath)) {
1052
- await fs.remove(projectRefPath);
1053
- }
1054
- }
1055
-
1056
1364
  async function ensureClientComponents(projectDir) {
1057
1365
  const relativePaths = [
1058
1366
  'components/env-var-warning.tsx',
@@ -1237,7 +1545,6 @@ async function sanitizeLayout(projectDir) {
1237
1545
 
1238
1546
  const requiredImports = [
1239
1547
  "import '@nextblock-cms/ui/styles/globals.css';",
1240
- "import '@nextblock-cms/editor/styles/editor.css';",
1241
1548
  ];
1242
1549
 
1243
1550
  const content = await fs.readFile(layoutPath, 'utf8');
@@ -1508,6 +1815,32 @@ async function transformPackageJson(projectDir) {
1508
1815
  }
1509
1816
  }
1510
1817
 
1818
+ // Mirror the monorepo's defensive dependency overrides into the generated project so a
1819
+ // fresh `npm install` reproduces the "0 vulnerabilities" posture and silences deprecated
1820
+ // transitive deps (e.g. uuid@10). Read live from the repo root when available (local dev /
1821
+ // `npm run test-create`); fall back to this baked-in set in the published CLI where the
1822
+ // monorepo root is not on disk. Keep the fallback in sync with the root package.json.
1823
+ const FALLBACK_OVERRIDES = {
1824
+ postcss: '^8.5.12',
1825
+ qs: '^6.15.2',
1826
+ uuid: '^11.1.1',
1827
+ glob: '^10.4.5',
1828
+ 'whatwg-encoding': 'npm:@exodus/bytes@latest',
1829
+ 'node-domexception': 'npm:domexception@latest',
1830
+ keygrip: 'npm:keygrip@latest',
1831
+ };
1832
+ let rootOverrides = FALLBACK_OVERRIDES;
1833
+ try {
1834
+ const rootPkg = await fs.readJSON(resolve(REPO_ROOT, 'package.json'));
1835
+ if (rootPkg?.overrides && Object.keys(rootPkg.overrides).length > 0) {
1836
+ rootOverrides = rootPkg.overrides;
1837
+ }
1838
+ } catch {
1839
+ // Published CLI: repo root package.json not present — keep the baked-in fallback.
1840
+ }
1841
+ // Project-specific overrides (if any) win over the inherited defaults.
1842
+ packageJson.overrides = { ...rootOverrides, ...(packageJson.overrides ?? {}) };
1843
+
1511
1844
  await fs.writeJSON(packageJsonPath, packageJson, { spaces: 2 });
1512
1845
  }
1513
1846
 
@@ -1559,35 +1892,6 @@ function runCommand(command, args, options = {}) {
1559
1892
  });
1560
1893
  }
1561
1894
 
1562
- async function runSupabaseCli(args, options = {}) {
1563
- const { cwd } = options;
1564
- const supabaseBin = await getSupabaseBinary(cwd);
1565
- const command = supabaseBin === 'npx' ? 'npx' : supabaseBin;
1566
- const cmdArgs = supabaseBin === 'npx' ? ['supabase', ...args] : args;
1567
-
1568
- return new Promise((resolve, reject) => {
1569
- const child = spawn(command, cmdArgs, {
1570
- cwd,
1571
- shell: IS_WINDOWS,
1572
- stdio: 'inherit',
1573
- });
1574
-
1575
- child.on('error', (error) => {
1576
- reject(error);
1577
- });
1578
-
1579
- child.on('close', (code) => {
1580
- if (code === 0) {
1581
- resolve();
1582
- } else {
1583
- reject(
1584
- new Error(`supabase ${args.join(' ')} exited with code ${code}`),
1585
- );
1586
- }
1587
- });
1588
- });
1589
- }
1590
-
1591
1895
  async function getSupabaseBinary(projectDir) {
1592
1896
  const binDir = resolve(projectDir, 'node_modules', '.bin');
1593
1897
  const ext = IS_WINDOWS ? '.cmd' : '';
@@ -1643,18 +1947,7 @@ function buildNextConfigContent(editorUtilNames) {
1643
1947
  ' minimumCacheTTL: 31536000,',
1644
1948
  ' dangerouslyAllowSVG: false,',
1645
1949
  " contentSecurityPolicy: \"default-src 'self'; script-src 'none'; sandbox;\",",
1646
- ' remotePatterns: [',
1647
- " { protocol: 'https', hostname: 'pub-a31e3f1a87d144898aeb489a8221f92e.r2.dev' },",
1648
- " { protocol: 'https', hostname: 'e260676f72b0b18314b868f136ed72ae.r2.cloudflarestorage.com' },",
1649
- ' ...(process.env.NEXT_PUBLIC_URL',
1650
- ' ? [',
1651
- ' {',
1652
- " protocol: /** @type {'http' | 'https'} */ (new URL(process.env.NEXT_PUBLIC_URL).protocol.slice(0, -1)),",
1653
- ' hostname: new URL(process.env.NEXT_PUBLIC_URL).hostname,',
1654
- ' },',
1655
- ' ]',
1656
- ' : []),',
1657
- ' ],',
1950
+ ' remotePatterns: getRemotePatterns(),',
1658
1951
  ' },',
1659
1952
  ' experimental: {',
1660
1953
  ' optimizeCss: true,',
@@ -1736,6 +2029,34 @@ function buildNextConfigContent(editorUtilNames) {
1736
2029
  '};',
1737
2030
  '',
1738
2031
  'module.exports = nextConfig;',
2032
+ '',
2033
+ 'function getRemotePatterns() {',
2034
+ ' /** @type {Array<{ protocol: "http" | "https", hostname: string, pathname: string }>} */',
2035
+ ' const patterns = [];',
2036
+ ' // Whitelist this project R2 public/base URLs and the site URL for next/image.',
2037
+ ' const sources = [',
2038
+ ' process.env.NEXT_PUBLIC_R2_PUBLIC_URL,',
2039
+ ' process.env.NEXT_PUBLIC_R2_BASE_URL,',
2040
+ ' process.env.NEXT_PUBLIC_URL,',
2041
+ ' ];',
2042
+ ' for (const value of sources) {',
2043
+ ' if (!value) continue;',
2044
+ ' try {',
2045
+ ' const parsed = new URL(value);',
2046
+ " if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') continue;",
2047
+ ' const hostname = parsed.hostname;',
2048
+ ' if (patterns.some((pattern) => pattern.hostname === hostname)) continue;',
2049
+ ' patterns.push({',
2050
+ " protocol: parsed.protocol === 'https:' ? 'https' : 'http',",
2051
+ ' hostname,',
2052
+ " pathname: '/**',",
2053
+ ' });',
2054
+ ' } catch {',
2055
+ ' // ignore malformed value',
2056
+ ' }',
2057
+ ' }',
2058
+ ' return patterns;',
2059
+ '}',
1739
2060
  );
1740
2061
 
1741
2062
  return lines.join('\n');