@salifbiaye/create-boutique 0.1.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 (316) hide show
  1. package/README.md +1 -0
  2. package/cli/dist/commands.js +382 -0
  3. package/cli/dist/create.js +194 -0
  4. package/cli/dist/docker.js +19 -0
  5. package/cli/dist/generate.js +322 -0
  6. package/cli/dist/guide.js +72 -0
  7. package/cli/dist/index.js +156 -0
  8. package/cli/dist/paths.js +29 -0
  9. package/cli/dist/ports.js +54 -0
  10. package/cli/dist/project.js +119 -0
  11. package/cli/dist/themes.js +24 -0
  12. package/docs/fiche-client.en.html +137 -0
  13. package/docs/fiche-client.html +137 -0
  14. package/docs/guide-complet.en.html +259 -0
  15. package/docs/guide-complet.fr.html +259 -0
  16. package/package.json +51 -0
  17. package/templates/backend/.dockerignore +9 -0
  18. package/templates/backend/Dockerfile +30 -0
  19. package/templates/backend/README.md +78 -0
  20. package/templates/backend/TEMPLATE_VERSION +1 -0
  21. package/templates/backend/docker/entrypoint.sh +28 -0
  22. package/templates/backend/instrumentation.ts +24 -0
  23. package/templates/backend/medusa-config.ts +114 -0
  24. package/templates/backend/package-lock.json +16222 -0
  25. package/templates/backend/package.json +39 -0
  26. package/templates/backend/src/admin/README.md +33 -0
  27. package/templates/backend/src/admin/components/image-field.tsx +61 -0
  28. package/templates/backend/src/admin/i18n/README.md +58 -0
  29. package/templates/backend/src/admin/i18n/index.ts +8 -0
  30. package/templates/backend/src/admin/i18n/json/en.json +66 -0
  31. package/templates/backend/src/admin/i18n/json/fr.json +66 -0
  32. package/templates/backend/src/admin/lib/client.ts +9 -0
  33. package/templates/backend/src/admin/routes/boutique-campaigns/campaign-drawer.tsx +286 -0
  34. package/templates/backend/src/admin/routes/boutique-campaigns/page.tsx +113 -0
  35. package/templates/backend/src/admin/tsconfig.json +24 -0
  36. package/templates/backend/src/admin/vite-env.d.ts +1 -0
  37. package/templates/backend/src/admin/widgets/category-image.tsx +56 -0
  38. package/templates/backend/src/api/README.md +135 -0
  39. package/templates/backend/src/api/admin/boutique-campaigns/[salesChannelId]/route.ts +30 -0
  40. package/templates/backend/src/api/admin/boutique-campaigns/route.ts +24 -0
  41. package/templates/backend/src/api/middlewares.ts +28 -0
  42. package/templates/backend/src/api/rate-limit.ts +30 -0
  43. package/templates/backend/src/api/route.ts +6 -0
  44. package/templates/backend/src/api/store/campaign/route.ts +25 -0
  45. package/templates/backend/src/api/store/order-lookup/route.ts +74 -0
  46. package/templates/backend/src/emails/en.json +64 -0
  47. package/templates/backend/src/emails/fr.json +64 -0
  48. package/templates/backend/src/jobs/README.md +38 -0
  49. package/templates/backend/src/lib/emails.ts +94 -0
  50. package/templates/backend/src/lib/notify.ts +148 -0
  51. package/templates/backend/src/lib/project.ts +146 -0
  52. package/templates/backend/src/links/README.md +26 -0
  53. package/templates/backend/src/links/campaign-sales-channel.ts +9 -0
  54. package/templates/backend/src/modules/README.md +117 -0
  55. package/templates/backend/src/modules/campaign/index.ts +9 -0
  56. package/templates/backend/src/modules/campaign/migrations/.snapshot-boutique-campaign.json +272 -0
  57. package/templates/backend/src/modules/campaign/migrations/Migration20260928141440.ts +16 -0
  58. package/templates/backend/src/modules/campaign/migrations/Migration20260928145243.ts +13 -0
  59. package/templates/backend/src/modules/campaign/models/campaign.ts +28 -0
  60. package/templates/backend/src/modules/campaign/service.ts +6 -0
  61. package/templates/backend/src/modules/campaign/validators.ts +94 -0
  62. package/templates/backend/src/modules/resend/index.ts +6 -0
  63. package/templates/backend/src/modules/resend/service.ts +63 -0
  64. package/templates/backend/src/scripts/README.md +63 -0
  65. package/templates/backend/src/scripts/campaign-export.ts +15 -0
  66. package/templates/backend/src/scripts/campaign-import.ts +38 -0
  67. package/templates/backend/src/scripts/price-report.ts +34 -0
  68. package/templates/backend/src/scripts/seed.ts +418 -0
  69. package/templates/backend/src/scripts/storage.ts +63 -0
  70. package/templates/backend/src/subscribers/README.md +61 -0
  71. package/templates/backend/src/subscribers/account-emails.ts +27 -0
  72. package/templates/backend/src/subscribers/fulfillment-emails.ts +42 -0
  73. package/templates/backend/src/subscribers/order-emails.ts +12 -0
  74. package/templates/backend/src/subscribers/return-refund-emails.ts +50 -0
  75. package/templates/backend/src/workflows/README.md +81 -0
  76. package/templates/backend/src/workflows/steps/upsert-campaign.ts +53 -0
  77. package/templates/backend/src/workflows/upsert-campaign.ts +30 -0
  78. package/templates/backend/tsconfig.json +36 -0
  79. package/templates/storefront/.dockerignore +7 -0
  80. package/templates/storefront/.github/ISSUE_TEMPLATE/bug-report.yml +71 -0
  81. package/templates/storefront/.prettierrc +8 -0
  82. package/templates/storefront/Dockerfile +37 -0
  83. package/templates/storefront/LICENSE +21 -0
  84. package/templates/storefront/README.md +127 -0
  85. package/templates/storefront/locales/en.json +269 -0
  86. package/templates/storefront/locales/fr.json +269 -0
  87. package/templates/storefront/next-env.d.ts +7 -0
  88. package/templates/storefront/next-sitemap.js +19 -0
  89. package/templates/storefront/next.config.js +29 -0
  90. package/templates/storefront/package-lock.json +9031 -0
  91. package/templates/storefront/package.json +49 -0
  92. package/templates/storefront/postcss.config.js +6 -0
  93. package/templates/storefront/public/favicon.ico +0 -0
  94. package/templates/storefront/scripts/sync-design.mjs +30 -0
  95. package/templates/storefront/src/app/(checkout)/checkout/page.tsx +31 -0
  96. package/templates/storefront/src/app/(checkout)/layout.tsx +29 -0
  97. package/templates/storefront/src/app/(checkout)/not-found.tsx +20 -0
  98. package/templates/storefront/src/app/(main)/account/@dashboard/addresses/page.tsx +34 -0
  99. package/templates/storefront/src/app/(main)/account/@dashboard/loading.tsx +9 -0
  100. package/templates/storefront/src/app/(main)/account/@dashboard/orders/details/[id]/page.tsx +33 -0
  101. package/templates/storefront/src/app/(main)/account/@dashboard/orders/page.tsx +37 -0
  102. package/templates/storefront/src/app/(main)/account/@dashboard/page.tsx +22 -0
  103. package/templates/storefront/src/app/(main)/account/@dashboard/profile/page.tsx +53 -0
  104. package/templates/storefront/src/app/(main)/account/@login/page.tsx +12 -0
  105. package/templates/storefront/src/app/(main)/account/layout.tsx +20 -0
  106. package/templates/storefront/src/app/(main)/account/loading.tsx +9 -0
  107. package/templates/storefront/src/app/(main)/cart/loading.tsx +5 -0
  108. package/templates/storefront/src/app/(main)/cart/not-found.tsx +21 -0
  109. package/templates/storefront/src/app/(main)/cart/page.tsx +21 -0
  110. package/templates/storefront/src/app/(main)/categories/[...category]/page.tsx +55 -0
  111. package/templates/storefront/src/app/(main)/collections/[handle]/page.tsx +55 -0
  112. package/templates/storefront/src/app/(main)/layout.tsx +43 -0
  113. package/templates/storefront/src/app/(main)/legal/[page]/page.tsx +42 -0
  114. package/templates/storefront/src/app/(main)/not-found.tsx +21 -0
  115. package/templates/storefront/src/app/(main)/order/[id]/confirmed/loading.tsx +5 -0
  116. package/templates/storefront/src/app/(main)/order/[id]/confirmed/page.tsx +23 -0
  117. package/templates/storefront/src/app/(main)/order/[id]/transfer/[token]/accept/page.tsx +42 -0
  118. package/templates/storefront/src/app/(main)/order/[id]/transfer/[token]/decline/page.tsx +42 -0
  119. package/templates/storefront/src/app/(main)/order/[id]/transfer/[token]/page.tsx +35 -0
  120. package/templates/storefront/src/app/(main)/order/track/page.tsx +32 -0
  121. package/templates/storefront/src/app/(main)/page.tsx +68 -0
  122. package/templates/storefront/src/app/(main)/products/[handle]/page.tsx +86 -0
  123. package/templates/storefront/src/app/(main)/store/page.tsx +28 -0
  124. package/templates/storefront/src/app/api/health/route.ts +13 -0
  125. package/templates/storefront/src/app/layout.tsx +42 -0
  126. package/templates/storefront/src/app/not-found.tsx +21 -0
  127. package/templates/storefront/src/app/opengraph-image.jpg +0 -0
  128. package/templates/storefront/src/app/twitter-image.jpg +0 -0
  129. package/templates/storefront/src/instrumentation-node.ts +18 -0
  130. package/templates/storefront/src/instrumentation.ts +9 -0
  131. package/templates/storefront/src/lib/boutique/campaign.ts +116 -0
  132. package/templates/storefront/src/lib/boutique/config.ts +152 -0
  133. package/templates/storefront/src/lib/boutique/fonts.ts +29 -0
  134. package/templates/storefront/src/lib/boutique/i18n-client.tsx +34 -0
  135. package/templates/storefront/src/lib/boutique/i18n.ts +43 -0
  136. package/templates/storefront/src/lib/boutique/schema.ts +131 -0
  137. package/templates/storefront/src/lib/config.ts +39 -0
  138. package/templates/storefront/src/lib/constants.tsx +75 -0
  139. package/templates/storefront/src/lib/context/modal-context.tsx +34 -0
  140. package/templates/storefront/src/lib/data/cart.ts +434 -0
  141. package/templates/storefront/src/lib/data/categories.ts +49 -0
  142. package/templates/storefront/src/lib/data/collections.ts +59 -0
  143. package/templates/storefront/src/lib/data/cookies.ts +103 -0
  144. package/templates/storefront/src/lib/data/customer.ts +261 -0
  145. package/templates/storefront/src/lib/data/fulfillment.ts +68 -0
  146. package/templates/storefront/src/lib/data/onboarding.ts +9 -0
  147. package/templates/storefront/src/lib/data/order-lookup.ts +34 -0
  148. package/templates/storefront/src/lib/data/orders.ts +112 -0
  149. package/templates/storefront/src/lib/data/payment.ts +35 -0
  150. package/templates/storefront/src/lib/data/products.ts +127 -0
  151. package/templates/storefront/src/lib/data/regions.ts +70 -0
  152. package/templates/storefront/src/lib/data/variants.ts +38 -0
  153. package/templates/storefront/src/lib/hooks/use-in-view.tsx +29 -0
  154. package/templates/storefront/src/lib/hooks/use-toggle-state.tsx +46 -0
  155. package/templates/storefront/src/lib/util/compare-addresses.ts +28 -0
  156. package/templates/storefront/src/lib/util/get-percentage-diff.ts +6 -0
  157. package/templates/storefront/src/lib/util/get-product-price.ts +79 -0
  158. package/templates/storefront/src/lib/util/isEmpty.ts +11 -0
  159. package/templates/storefront/src/lib/util/medusa-error.ts +22 -0
  160. package/templates/storefront/src/lib/util/money.ts +35 -0
  161. package/templates/storefront/src/lib/util/product.ts +5 -0
  162. package/templates/storefront/src/lib/util/repeat.ts +5 -0
  163. package/templates/storefront/src/lib/util/sort-products.ts +50 -0
  164. package/templates/storefront/src/modules/account/components/account-info/index.tsx +140 -0
  165. package/templates/storefront/src/modules/account/components/account-nav/index.tsx +198 -0
  166. package/templates/storefront/src/modules/account/components/address-book/index.tsx +28 -0
  167. package/templates/storefront/src/modules/account/components/address-card/add-address.tsx +169 -0
  168. package/templates/storefront/src/modules/account/components/address-card/edit-address-modal.tsx +241 -0
  169. package/templates/storefront/src/modules/account/components/login/index.tsx +65 -0
  170. package/templates/storefront/src/modules/account/components/order-card/index.tsx +88 -0
  171. package/templates/storefront/src/modules/account/components/order-overview/index.tsx +47 -0
  172. package/templates/storefront/src/modules/account/components/overview/index.tsx +169 -0
  173. package/templates/storefront/src/modules/account/components/profile-billing-address/index.tsx +193 -0
  174. package/templates/storefront/src/modules/account/components/profile-email/index.tsx +75 -0
  175. package/templates/storefront/src/modules/account/components/profile-name/index.tsx +79 -0
  176. package/templates/storefront/src/modules/account/components/profile-password/index.tsx +72 -0
  177. package/templates/storefront/src/modules/account/components/profile-phone/index.tsx +74 -0
  178. package/templates/storefront/src/modules/account/components/register/index.tsx +107 -0
  179. package/templates/storefront/src/modules/account/components/transfer-request-form/index.tsx +83 -0
  180. package/templates/storefront/src/modules/account/templates/account-layout.tsx +43 -0
  181. package/templates/storefront/src/modules/account/templates/login-template.tsx +27 -0
  182. package/templates/storefront/src/modules/cart/components/cart-item-select/index.tsx +75 -0
  183. package/templates/storefront/src/modules/cart/components/empty-cart-message/index.tsx +25 -0
  184. package/templates/storefront/src/modules/cart/components/item/index.tsx +144 -0
  185. package/templates/storefront/src/modules/cart/components/sign-in-prompt/index.tsx +27 -0
  186. package/templates/storefront/src/modules/cart/templates/index.tsx +51 -0
  187. package/templates/storefront/src/modules/cart/templates/items.tsx +58 -0
  188. package/templates/storefront/src/modules/cart/templates/preview.tsx +51 -0
  189. package/templates/storefront/src/modules/cart/templates/summary.tsx +50 -0
  190. package/templates/storefront/src/modules/categories/templates/index.tsx +94 -0
  191. package/templates/storefront/src/modules/checkout/components/address-select/index.tsx +117 -0
  192. package/templates/storefront/src/modules/checkout/components/addresses/index.tsx +186 -0
  193. package/templates/storefront/src/modules/checkout/components/billing_address/index.tsx +114 -0
  194. package/templates/storefront/src/modules/checkout/components/country-select/index.tsx +50 -0
  195. package/templates/storefront/src/modules/checkout/components/discount-code/index.tsx +181 -0
  196. package/templates/storefront/src/modules/checkout/components/error-message/index.tsx +13 -0
  197. package/templates/storefront/src/modules/checkout/components/payment/index.tsx +261 -0
  198. package/templates/storefront/src/modules/checkout/components/payment-button/index.tsx +195 -0
  199. package/templates/storefront/src/modules/checkout/components/payment-container/index.tsx +130 -0
  200. package/templates/storefront/src/modules/checkout/components/payment-test/index.tsx +12 -0
  201. package/templates/storefront/src/modules/checkout/components/payment-wrapper/index.tsx +42 -0
  202. package/templates/storefront/src/modules/checkout/components/payment-wrapper/stripe-wrapper.tsx +54 -0
  203. package/templates/storefront/src/modules/checkout/components/review/index.tsx +54 -0
  204. package/templates/storefront/src/modules/checkout/components/shipping/index.tsx +417 -0
  205. package/templates/storefront/src/modules/checkout/components/shipping-address/index.tsx +219 -0
  206. package/templates/storefront/src/modules/checkout/components/submit-button/index.tsx +32 -0
  207. package/templates/storefront/src/modules/checkout/templates/checkout-form/index.tsx +38 -0
  208. package/templates/storefront/src/modules/checkout/templates/checkout-summary/index.tsx +31 -0
  209. package/templates/storefront/src/modules/collections/templates/index.tsx +44 -0
  210. package/templates/storefront/src/modules/common/components/cart-totals/index.tsx +84 -0
  211. package/templates/storefront/src/modules/common/components/checkbox/index.tsx +44 -0
  212. package/templates/storefront/src/modules/common/components/delete-button/index.tsx +42 -0
  213. package/templates/storefront/src/modules/common/components/divider/index.tsx +9 -0
  214. package/templates/storefront/src/modules/common/components/filter-radio-group/index.tsx +61 -0
  215. package/templates/storefront/src/modules/common/components/input/index.tsx +79 -0
  216. package/templates/storefront/src/modules/common/components/interactive-link/index.tsx +33 -0
  217. package/templates/storefront/src/modules/common/components/line-item-options/index.tsx +26 -0
  218. package/templates/storefront/src/modules/common/components/line-item-price/index.tsx +63 -0
  219. package/templates/storefront/src/modules/common/components/line-item-unit-price/index.tsx +62 -0
  220. package/templates/storefront/src/modules/common/components/localized-client-link/index.tsx +29 -0
  221. package/templates/storefront/src/modules/common/components/modal/index.tsx +118 -0
  222. package/templates/storefront/src/modules/common/components/native-select/index.tsx +76 -0
  223. package/templates/storefront/src/modules/common/components/radio/index.tsx +27 -0
  224. package/templates/storefront/src/modules/common/icons/back.tsx +37 -0
  225. package/templates/storefront/src/modules/common/icons/bancontact.tsx +26 -0
  226. package/templates/storefront/src/modules/common/icons/chevron-down.tsx +30 -0
  227. package/templates/storefront/src/modules/common/icons/eye-off.tsx +37 -0
  228. package/templates/storefront/src/modules/common/icons/eye.tsx +37 -0
  229. package/templates/storefront/src/modules/common/icons/fast-delivery.tsx +65 -0
  230. package/templates/storefront/src/modules/common/icons/ideal.tsx +26 -0
  231. package/templates/storefront/src/modules/common/icons/map-pin.tsx +37 -0
  232. package/templates/storefront/src/modules/common/icons/package.tsx +44 -0
  233. package/templates/storefront/src/modules/common/icons/paypal.tsx +30 -0
  234. package/templates/storefront/src/modules/common/icons/placeholder-image.tsx +44 -0
  235. package/templates/storefront/src/modules/common/icons/refresh.tsx +51 -0
  236. package/templates/storefront/src/modules/common/icons/spinner.tsx +37 -0
  237. package/templates/storefront/src/modules/common/icons/trash.tsx +51 -0
  238. package/templates/storefront/src/modules/common/icons/user.tsx +37 -0
  239. package/templates/storefront/src/modules/common/icons/x.tsx +37 -0
  240. package/templates/storefront/src/modules/layout/components/cart-mismatch-banner/index.tsx +59 -0
  241. package/templates/storefront/src/modules/order/components/help/index.tsx +26 -0
  242. package/templates/storefront/src/modules/order/components/item/index.tsx +57 -0
  243. package/templates/storefront/src/modules/order/components/items/index.tsx +44 -0
  244. package/templates/storefront/src/modules/order/components/onboarding-cta/index.tsx +28 -0
  245. package/templates/storefront/src/modules/order/components/order-details/index.tsx +64 -0
  246. package/templates/storefront/src/modules/order/components/order-summary/index.tsx +61 -0
  247. package/templates/storefront/src/modules/order/components/payment-details/index.tsx +64 -0
  248. package/templates/storefront/src/modules/order/components/shipping-details/index.tsx +74 -0
  249. package/templates/storefront/src/modules/order/components/transfer-actions/index.tsx +83 -0
  250. package/templates/storefront/src/modules/order/components/transfer-image/index.tsx +275 -0
  251. package/templates/storefront/src/modules/order/templates/order-completed-template.tsx +53 -0
  252. package/templates/storefront/src/modules/order/templates/order-details-template.tsx +46 -0
  253. package/templates/storefront/src/modules/products/components/thumbnail/index.tsx +70 -0
  254. package/templates/storefront/src/modules/products/templates/index.tsx +118 -0
  255. package/templates/storefront/src/modules/shipping/components/free-shipping-price-nudge/index.tsx +285 -0
  256. package/templates/storefront/src/modules/skeletons/components/skeleton-button/index.tsx +5 -0
  257. package/templates/storefront/src/modules/skeletons/components/skeleton-card-details/index.tsx +10 -0
  258. package/templates/storefront/src/modules/skeletons/components/skeleton-cart-item/index.tsx +35 -0
  259. package/templates/storefront/src/modules/skeletons/components/skeleton-cart-totals/index.tsx +30 -0
  260. package/templates/storefront/src/modules/skeletons/components/skeleton-code-form/index.tsx +13 -0
  261. package/templates/storefront/src/modules/skeletons/components/skeleton-line-item/index.tsx +35 -0
  262. package/templates/storefront/src/modules/skeletons/components/skeleton-order-confirmed-header/index.tsx +14 -0
  263. package/templates/storefront/src/modules/skeletons/components/skeleton-order-information/index.tsx +36 -0
  264. package/templates/storefront/src/modules/skeletons/components/skeleton-order-items/index.tsx +43 -0
  265. package/templates/storefront/src/modules/skeletons/components/skeleton-order-summary/index.tsx +15 -0
  266. package/templates/storefront/src/modules/skeletons/components/skeleton-product-preview/index.tsx +15 -0
  267. package/templates/storefront/src/modules/skeletons/templates/skeleton-cart-page/index.tsx +65 -0
  268. package/templates/storefront/src/modules/skeletons/templates/skeleton-order-confirmed/index.tsx +21 -0
  269. package/templates/storefront/src/modules/skeletons/templates/skeleton-product-grid/index.tsx +23 -0
  270. package/templates/storefront/src/modules/store/components/pagination/index.tsx +114 -0
  271. package/templates/storefront/src/modules/store/components/refinement-list/index.tsx +41 -0
  272. package/templates/storefront/src/modules/store/components/refinement-list/sort-products/index.tsx +48 -0
  273. package/templates/storefront/src/modules/store/templates/index.tsx +42 -0
  274. package/templates/storefront/src/modules/store/templates/paginated-products.tsx +80 -0
  275. package/templates/storefront/src/modules/ub/button.tsx +41 -0
  276. package/templates/storefront/src/modules/ub/campaign-blocks.tsx +197 -0
  277. package/templates/storefront/src/modules/ub/campaign-ended.tsx +28 -0
  278. package/templates/storefront/src/modules/ub/category-tiles.tsx +41 -0
  279. package/templates/storefront/src/modules/ub/countdown.tsx +46 -0
  280. package/templates/storefront/src/modules/ub/footer.tsx +56 -0
  281. package/templates/storefront/src/modules/ub/icons.tsx +42 -0
  282. package/templates/storefront/src/modules/ub/nav.tsx +90 -0
  283. package/templates/storefront/src/modules/ub/pdp/buy-box.tsx +140 -0
  284. package/templates/storefront/src/modules/ub/pdp/gallery.tsx +40 -0
  285. package/templates/storefront/src/modules/ub/product-card.tsx +66 -0
  286. package/templates/storefront/src/modules/ub/top-bar.tsx +20 -0
  287. package/templates/storefront/src/modules/ub/track-form.tsx +92 -0
  288. package/templates/storefront/src/proxy.ts +89 -0
  289. package/templates/storefront/src/styles/globals.css +98 -0
  290. package/templates/storefront/src/styles/ub/bundle.css +372 -0
  291. package/templates/storefront/src/styles/ub/fonts.css +17 -0
  292. package/templates/storefront/src/styles/ub/medusa-ui.css +90 -0
  293. package/templates/storefront/src/styles/ub/overrides.css +12 -0
  294. package/templates/storefront/src/styles/ub/tokens.css +9 -0
  295. package/templates/storefront/src/types/global.ts +24 -0
  296. package/templates/storefront/src/types/icon.ts +4 -0
  297. package/templates/storefront/tailwind.config.js +164 -0
  298. package/templates/storefront/themes/black-friday/campaign.en.json +31 -0
  299. package/templates/storefront/themes/black-friday/campaign.fr.json +31 -0
  300. package/templates/storefront/themes/black-friday/theme.json +9 -0
  301. package/templates/storefront/themes/cyber-monday/campaign.en.json +31 -0
  302. package/templates/storefront/themes/cyber-monday/campaign.fr.json +31 -0
  303. package/templates/storefront/themes/cyber-monday/theme.json +9 -0
  304. package/templates/storefront/themes/halloween/campaign.en.json +31 -0
  305. package/templates/storefront/themes/halloween/campaign.fr.json +31 -0
  306. package/templates/storefront/themes/halloween/theme.json +9 -0
  307. package/templates/storefront/themes/noel/campaign.en.json +31 -0
  308. package/templates/storefront/themes/noel/campaign.fr.json +31 -0
  309. package/templates/storefront/themes/noel/theme.json +9 -0
  310. package/templates/storefront/themes/nouvel-an/campaign.en.json +31 -0
  311. package/templates/storefront/themes/nouvel-an/campaign.fr.json +31 -0
  312. package/templates/storefront/themes/nouvel-an/theme.json +9 -0
  313. package/templates/storefront/themes/thanksgiving/campaign.en.json +31 -0
  314. package/templates/storefront/themes/thanksgiving/campaign.fr.json +31 -0
  315. package/templates/storefront/themes/thanksgiving/theme.json +9 -0
  316. package/templates/storefront/tsconfig.json +53 -0
@@ -0,0 +1,322 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { currencies, effective } from "./project.js";
5
+ const secret = () => crypto.randomBytes(32).toString("base64url");
6
+ // ─────────────────────────── docker-compose.yml ───────────────────────────
7
+ export function composeYaml(p) {
8
+ const services = p.boutiques
9
+ .map((b) => ` storefront-${b.slug}:
10
+ <<: *storefront
11
+ ports: ["${b.port}:8000"]
12
+ environment: { <<: *storefront-env, BOUTIQUE: ${b.slug} }
13
+ `)
14
+ .join("\n");
15
+ return `# GÉNÉRÉ par create-boutique à partir de config/project.json — ne pas modifier à la main.
16
+ # Pour changer quelque chose : éditer config/project.json puis \`create-boutique apply .\`
17
+ name: ${p.project}
18
+
19
+ x-storefront: &storefront
20
+ build: ./storefront
21
+ image: ${p.project}-storefront
22
+ restart: unless-stopped
23
+ depends_on:
24
+ backend: { condition: service_healthy }
25
+ environment: &storefront-env
26
+ MEDUSA_BACKEND_URL: http://backend:9000
27
+ S3_FILE_URL: \${S3_FILE_URL}
28
+ STRIPE_PUBLISHABLE_KEY: \${STRIPE_PUBLISHABLE_KEY:-}
29
+ volumes:
30
+ - ./config:/app/config:ro # project.json + campagnes (lecture seule)
31
+ - keys:/app/keys:ro # publishable keys écrites par le backend
32
+
33
+ services:
34
+ # ───────── Boutiques : 1 instance par boutique, même image ─────────
35
+ ${services}
36
+ # ───────── Backend Medusa + admin : 1 seul, partagé par les boutiques ─────────
37
+ backend:
38
+ build: ./backend
39
+ image: ${p.project}-backend
40
+ restart: unless-stopped
41
+ env_file: .env
42
+ environment:
43
+ # Postgres interne au réseau Docker, sans SSL (Medusa active le SSL par défaut hors localhost)
44
+ DATABASE_URL: postgres://medusa:\${POSTGRES_PASSWORD}@postgres:5432/medusa?sslmode=disable
45
+ REDIS_URL: redis://redis:6379
46
+ S3_ENDPOINT: http://storage:9000
47
+ ports: ["${p.apiPort}:9000"] # API + admin (/app)
48
+ volumes:
49
+ - ./config:/app/config:ro # boutiques, devises, pays → régions, sales channels, clés
50
+ - ./seed:/app/seed:ro # products.csv + images à importer
51
+ - keys:/app/keys # y écrit la publishable key de chaque boutique
52
+ depends_on:
53
+ postgres: { condition: service_healthy }
54
+ redis: { condition: service_healthy }
55
+ storage: { condition: service_healthy }
56
+
57
+ # ───────── Services internes ─────────
58
+ postgres:
59
+ image: postgres:16-alpine
60
+ restart: unless-stopped
61
+ environment:
62
+ POSTGRES_USER: medusa
63
+ POSTGRES_PASSWORD: \${POSTGRES_PASSWORD}
64
+ POSTGRES_DB: medusa
65
+ volumes: [pg_data:/var/lib/postgresql/data]
66
+ healthcheck:
67
+ test: ["CMD-SHELL", "pg_isready -U medusa"]
68
+ interval: 5s
69
+ retries: 20
70
+
71
+ redis:
72
+ image: redis:7-alpine
73
+ restart: unless-stopped
74
+ volumes: [redis_data:/data]
75
+ healthcheck:
76
+ test: ["CMD", "redis-cli", "ping"]
77
+ interval: 5s
78
+ retries: 20
79
+
80
+ storage: # RustFS : stockage S3 des images
81
+ image: rustfs/rustfs:latest
82
+ restart: unless-stopped
83
+ environment:
84
+ RUSTFS_ACCESS_KEY: \${S3_ACCESS_KEY_ID}
85
+ RUSTFS_SECRET_KEY: \${S3_SECRET_ACCESS_KEY}
86
+ # Seule exception au « pas de port public » : les navigateurs chargent les images ici.
87
+ # Le backend rend le bucket lisible (lecture seule) ; l'écriture exige les identifiants.
88
+ ports: ["${p.storagePort}:9000"]
89
+ volumes: [storage_data:/data]
90
+ healthcheck:
91
+ test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:9000/health >/dev/null || exit 1"]
92
+ interval: 5s
93
+ retries: 20
94
+
95
+ volumes:
96
+ pg_data:
97
+ redis_data:
98
+ storage_data:
99
+ keys:
100
+ `;
101
+ }
102
+ /** Variables qui dépendent de project.json (réécrites à chaque apply). Les secrets ne sont jamais régénérés. */
103
+ function derivedEnv(p) {
104
+ const origins = p.boutiques.map((b) => b.url ?? `http://localhost:${b.port}`);
105
+ const admin = `http://localhost:${p.apiPort}`;
106
+ return {
107
+ PROJECT: p.project,
108
+ STORE_CORS: origins.join(","),
109
+ ADMIN_CORS: admin,
110
+ AUTH_CORS: admin,
111
+ S3_FILE_URL: `http://localhost:${p.storagePort}/medusa`,
112
+ };
113
+ }
114
+ export function writeEnv(dir, p, s) {
115
+ const storageKey = `${p.project}-storage`;
116
+ const lines = [
117
+ "# GÉNÉRÉ par create-boutique — secrets du projet. Ne jamais committer ni publier ce fichier.",
118
+ "",
119
+ "# Base de données",
120
+ `POSTGRES_PASSWORD=${secret()}`,
121
+ "",
122
+ "# Medusa",
123
+ `JWT_SECRET=${secret()}`,
124
+ `COOKIE_SECRET=${secret()}`,
125
+ `ADMIN_EMAIL=${s.adminEmail}`,
126
+ `ADMIN_PASSWORD=${secret()}`,
127
+ "",
128
+ "# Images (RustFS, compatible S3)",
129
+ "S3_BUCKET=medusa",
130
+ "S3_REGION=us-east-1",
131
+ `S3_ACCESS_KEY_ID=${storageKey}`,
132
+ `S3_SECRET_ACCESS_KEY=${secret()}`,
133
+ "",
134
+ "# Stripe (vide = paiement manuel seulement). Après modification : docker compose restart backend",
135
+ `STRIPE_API_KEY=${s.stripeSk ?? ""}`,
136
+ `STRIPE_WEBHOOK_SECRET=${s.stripeWhsec ?? ""}`,
137
+ `STRIPE_PUBLISHABLE_KEY=${s.stripePk ?? ""}`,
138
+ "",
139
+ "# E-mails (Resend ; vide = aucun e-mail envoyé)",
140
+ `RESEND_API_KEY=${s.resendKey ?? ""}`,
141
+ `EMAIL_FROM=${s.emailFrom ?? ""}`,
142
+ "",
143
+ "# Dérivé de config/project.json (réécrit par `create-boutique apply`)",
144
+ ...Object.entries(derivedEnv(p)).map(([k, v]) => `${k}=${v}`),
145
+ "",
146
+ ];
147
+ fs.writeFileSync(path.join(dir, ".env"), lines.join("\n"), { mode: 0o600 });
148
+ }
149
+ /** Met à jour les variables dérivées de project.json sans toucher aux secrets. */
150
+ export function refreshEnv(dir, p) {
151
+ const file = path.join(dir, ".env");
152
+ let text = fs.readFileSync(file, "utf8");
153
+ for (const [k, v] of Object.entries(derivedEnv(p))) {
154
+ const re = new RegExp(`^${k}=.*$`, "m");
155
+ text = re.test(text) ? text.replace(re, `${k}=${v}`) : `${text.trimEnd()}\n${k}=${v}\n`;
156
+ }
157
+ fs.writeFileSync(file, text);
158
+ }
159
+ export function readEnv(dir) {
160
+ const file = path.join(dir, ".env");
161
+ if (!fs.existsSync(file))
162
+ return {};
163
+ return Object.fromEntries(fs
164
+ .readFileSync(file, "utf8")
165
+ .split(/\r?\n/)
166
+ .filter((l) => /^[A-Z0-9_]+=/.test(l))
167
+ .map((l) => [l.slice(0, l.indexOf("=")), l.slice(l.indexOf("=") + 1)]));
168
+ }
169
+ // ─────────────────────────── Campagnes ───────────────────────────
170
+ const THEME_ORDER = ["halloween", "thanksgiving", "black-friday", "cyber-monday", "noel", "nouvel-an"];
171
+ const DISCOUNT = {
172
+ halloween: "40%",
173
+ thanksgiving: "30%",
174
+ "black-friday": "50%",
175
+ "cyber-monday": "50%",
176
+ noel: "30%",
177
+ "nouvel-an": "35%",
178
+ };
179
+ const FREE_SHIPPING = { usd: 50, eur: 50, gbp: 50, cad: 75, xof: 25000, xaf: 25000 };
180
+ /** 4e jeudi de novembre (Thanksgiving US). */
181
+ function thanksgiving(year) {
182
+ const d = new Date(Date.UTC(year, 10, 1));
183
+ const firstThursday = 1 + ((4 - d.getUTCDay() + 7) % 7);
184
+ return new Date(Date.UTC(year, 10, firstThursday + 21));
185
+ }
186
+ const day = (d, add = 0, end = false) => {
187
+ const x = new Date(d.getTime() + add * 86400000);
188
+ x.setUTCHours(end ? 23 : 0, end ? 59 : 0, end ? 59 : 0, 0);
189
+ return x;
190
+ };
191
+ /** Dates de la prochaine occurrence de la campagne (si la saison est passée, l'année suivante). */
192
+ export function campaignDates(theme, now = new Date()) {
193
+ const build = (y) => {
194
+ const tg = thanksgiving(y);
195
+ switch (theme) {
196
+ case "halloween":
197
+ return [new Date(Date.UTC(y, 9, 20)), day(new Date(Date.UTC(y, 9, 31)), 0, true)];
198
+ case "thanksgiving":
199
+ return [day(tg, -6), day(tg, 0, true)];
200
+ case "black-friday":
201
+ return [day(tg, 1), day(tg, 3, true)];
202
+ case "cyber-monday":
203
+ return [day(tg, 4), day(tg, 4, true)];
204
+ case "noel":
205
+ return [new Date(Date.UTC(y, 11, 1)), day(new Date(Date.UTC(y, 11, 24)), 0, true)];
206
+ case "nouvel-an":
207
+ return [new Date(Date.UTC(y, 11, 26)), day(new Date(Date.UTC(y + 1, 0, 2)), 0, true)];
208
+ default:
209
+ return null;
210
+ }
211
+ };
212
+ let range = build(now.getUTCFullYear());
213
+ if (range && range[1] < now)
214
+ range = build(now.getUTCFullYear() + 1);
215
+ if (!range)
216
+ range = [day(now), day(now, 30, true)];
217
+ return { startsAt: range[0].toISOString(), endsAt: range[1].toISOString() };
218
+ }
219
+ /** Fichier de campagne initial (dates + variables) ; les textes viennent du thème. Jamais écrasé. */
220
+ export function writeCampaignFiles(dir, p, language) {
221
+ const campaignsDir = path.join(dir, "config", "campaigns");
222
+ fs.mkdirSync(campaignsDir, { recursive: true });
223
+ const locale = `${language}-${p.defaults.country.toUpperCase()}`;
224
+ // Campagne suivante : prochaine boutique dans l'ordre des saisons.
225
+ const ordered = [...p.boutiques].sort((a, b) => THEME_ORDER.indexOf(a.theme) - THEME_ORDER.indexOf(b.theme));
226
+ for (const b of p.boutiques) {
227
+ const file = path.join(campaignsDir, `${b.slug}.json`);
228
+ if (fs.existsSync(file))
229
+ continue;
230
+ const e = effective(p, b);
231
+ const i = ordered.findIndex((x) => x.slug === b.slug);
232
+ const next = ordered[i + 1]?.slug;
233
+ const threshold = FREE_SHIPPING[e.currency] ?? 50;
234
+ const freeShipping = new Intl.NumberFormat(locale, { style: "currency", currency: e.currency, maximumFractionDigits: 0 }).format(threshold);
235
+ const campaign = {
236
+ ...campaignDates(b.theme),
237
+ ...(next ? { nextCampaign: next } : {}),
238
+ vars: { discount: DISCOUNT[b.theme] ?? "20%", freeShipping },
239
+ };
240
+ fs.writeFileSync(file, JSON.stringify(campaign, null, 2) + "\n");
241
+ }
242
+ }
243
+ // ─────────────────────────── README ───────────────────────────
244
+ export function writeReadme(dir, p) {
245
+ const rows = p.boutiques
246
+ .map((b) => {
247
+ const e = effective(p, b);
248
+ return `| ${b.slug} | ${b.theme} | ${e.currency.toUpperCase()} | http://localhost:${b.port} |`;
249
+ })
250
+ .join("\n");
251
+ const text = `# ${p.defaults.storeName} — projet \`${p.project}\`
252
+
253
+ Généré par **create-boutique**. Un backend Medusa (admin) partagé par ${p.boutiques.length} boutique(s).
254
+
255
+ ## Lancer
256
+
257
+ \`\`\`bash
258
+ docker compose up -d
259
+ \`\`\`
260
+
261
+ Le premier démarrage prend quelques minutes (construction des images, base de données, import du catalogue).
262
+ Suivre l'avancement : \`docker compose logs -f backend\`.
263
+
264
+ | Boutique | Thème | Devise | Adresse locale |
265
+ | --- | --- | --- | --- |
266
+ ${rows}
267
+
268
+ - **Admin** : http://localhost:${p.apiPort}/app — e-mail \`ADMIN_EMAIL\` du fichier \`.env\`, mot de passe \`ADMIN_PASSWORD\` (transmis à part, à changer à la première connexion).
269
+ - **Images** : http://localhost:${p.storagePort}/medusa/…
270
+ - Mémoire : environ 80 Mo par boutique, plus le backend (~500 Mo) et les services.
271
+
272
+ ## Modifier le site
273
+
274
+ | Je veux changer… | Où | Ensuite |
275
+ | --- | --- | --- |
276
+ | Produits, prix, stock, promos, commandes | Admin → Produits / Commandes | rien |
277
+ | Textes, dates, visuel, logo, image promo, compte à rebours | Admin → **Campagnes** | visible en ~1 min |
278
+ | Image d'une catégorie | Admin → Produits → Catégories → (catégorie) | visible en ~1 min |
279
+ | Nom, devise, pays, pays livrés, thème, ajout de boutique | \`config/project.json\` | \`create-boutique apply .\` |
280
+ | Clés Stripe, Resend, adresse d'envoi | \`.env\` | \`docker compose restart backend\` puis \`docker compose up -d\` |
281
+ | Mise en page, styles | ne pas modifier ici : template, puis \`create-boutique update .\` | — |
282
+
283
+ Commandes utiles :
284
+
285
+ \`\`\`bash
286
+ create-boutique add . --boutique ${p.project}-soldes:black-friday # ajouter une boutique
287
+ create-boutique set . --boutique ${p.boutiques[0].slug} --name "Nouveau nom"
288
+ create-boutique apply . # après une modification de config/project.json
289
+ create-boutique update . # mettre à jour le code (config, .env et données conservés)
290
+ create-boutique backup . # sauvegarde base + images dans backups/<date>/
291
+ \`\`\`
292
+
293
+ ## Paiement Stripe
294
+
295
+ 1. Renseigner \`STRIPE_API_KEY\` (sk_…) et \`STRIPE_PUBLISHABLE_KEY\` (pk_…) dans \`.env\`.
296
+ 2. Une fois le backend en ligne en HTTPS (le webhook ne peut pas viser \`localhost\`) :
297
+ \`create-boutique stripe-webhook . --url https://<adresse du backend>\`
298
+ crée l'endpoint \`/hooks/payment/stripe_stripe\` dans Stripe, écrit \`STRIPE_WEBHOOK_SECRET\` dans \`.env\` et relance le backend.
299
+ 3. À la main si besoin : Stripe → Développeurs → Webhooks, endpoint \`<adresse du backend>/hooks/payment/stripe_stripe\`, événements
300
+ \`payment_intent.succeeded\`, \`payment_intent.payment_failed\`, \`payment_intent.amount_capturable_updated\`, \`payment_intent.partially_funded\`,
301
+ puis copier le secret \`whsec_…\` dans \`STRIPE_WEBHOOK_SECRET\` et \`docker compose restart backend\`.
302
+ 4. Apple Pay : déclarer les domaines des boutiques dans Stripe → Paramètres → Moyens de paiement.
303
+
304
+ ## Avant de livrer : checklist
305
+
306
+ - [ ] Chaque boutique s'ouvre à son adresse, avec son thème et sa devise.
307
+ - [ ] Chaque produit a un prix dans la devise de chaque boutique où il est vendu.
308
+ - [ ] Logo, textes et dates de chaque campagne vérifiés (Admin → Campagnes).
309
+ - [ ] Stripe configuré (clés + webhook) ou paiement manuel assumé.
310
+ - [ ] E-mails : \`RESEND_API_KEY\` renseignée, une commande test reçue par le client et le marchand.
311
+ - [ ] Une commande test passée dans chaque boutique (\`create-boutique check .\`).
312
+ - [ ] Sauvegarde programmée (\`create-boutique backup .\`).
313
+
314
+ ## À ne pas faire
315
+
316
+ - Ne pas modifier \`docker-compose.yml\` à la main (régénéré par \`apply\`).
317
+ - Ne pas renommer le \`slug\` d'une boutique (il relie la boutique à ses ventes, sa clé et sa campagne).
318
+ - Ne jamais publier le fichier \`.env\`.
319
+ `;
320
+ fs.writeFileSync(path.join(dir, "README.md"), text);
321
+ }
322
+ export const projectCurrencies = currencies;
@@ -0,0 +1,72 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ import { PACKAGE_ROOT } from "./paths.js";
6
+ import { currencies, effective } from "./project.js";
7
+ const escapeHtml = (s) => s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
8
+ /** Chrome / Edge / Chromium installé (CHROME_PATH prioritaire). */
9
+ function findBrowser() {
10
+ const candidates = [
11
+ process.env.CHROME_PATH,
12
+ "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe",
13
+ "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe",
14
+ "C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe",
15
+ "C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe",
16
+ "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
17
+ "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
18
+ "/usr/bin/google-chrome",
19
+ "/usr/bin/chromium",
20
+ "/usr/bin/chromium-browser",
21
+ "/usr/bin/microsoft-edge",
22
+ ];
23
+ return candidates.find((c) => c && fs.existsSync(c));
24
+ }
25
+ /** Remplit un modèle HTML (valeurs échappées ; les clés *_HTML sont du HTML construit ici) puis l'imprime en PDF. */
26
+ function render(source, out, dir, vars) {
27
+ let html = fs.readFileSync(source, "utf8");
28
+ for (const [k, v] of Object.entries(vars))
29
+ html = html.split(`{{${k}}}`).join(k.endsWith("_HTML") ? v : escapeHtml(v) || "&nbsp;");
30
+ const htmlFile = path.join(dir, `${out}.html`);
31
+ const pdfFile = path.join(dir, `${out}.pdf`);
32
+ fs.writeFileSync(htmlFile, html);
33
+ const browser = findBrowser();
34
+ if (!browser)
35
+ return { pdf: false, file: htmlFile };
36
+ const r = spawnSync(browser, ["--headless", "--disable-gpu", "--no-pdf-header-footer", `--print-to-pdf=${pdfFile}`, pathToFileURL(htmlFile).href], { stdio: "ignore", timeout: 90000 });
37
+ if (r.status === 0 && fs.existsSync(pdfFile)) {
38
+ fs.rmSync(htmlFile);
39
+ return { pdf: true, file: pdfFile };
40
+ }
41
+ return { pdf: false, file: htmlFile };
42
+ }
43
+ /**
44
+ * GUIDE-CLIENT.pdf (1 page, docs/fiche-client[.en].html) et GUIDE-COMPLET.pdf (docs/guide-complet.<lang>.html),
45
+ * remplis avec l'adresse de l'admin, l'e-mail admin et les boutiques du projet.
46
+ * Le mot de passe n'est JAMAIS imprimé (champ laissé vide, transmis à part).
47
+ * Langue = langue du projet (fr, sinon en). Sans Chrome/Edge : fichiers HTML (à imprimer en PDF).
48
+ */
49
+ export function writeGuides(dir, p, adminEmail, support = "") {
50
+ const fr = p.defaults.language === "fr";
51
+ const adminUrl = `http://localhost:${p.apiPort}/app`;
52
+ const stores = p.boutiques
53
+ .map((b) => {
54
+ const e = effective(p, b);
55
+ return `<tr><td>${escapeHtml(e.storeName)}</td><td><code>${escapeHtml(b.slug)}</code></td><td>${escapeHtml(b.theme)}</td><td>${e.currency.toUpperCase()}</td><td>${escapeHtml(b.url ?? `http://localhost:${b.port}`)}</td></tr>`;
56
+ })
57
+ .join("");
58
+ const vars = {
59
+ ADMIN_URL: adminUrl,
60
+ ADMIN_EMAIL: adminEmail,
61
+ SUPPORT: support,
62
+ PROJECT: p.project,
63
+ STORE_NAME: p.defaults.storeName,
64
+ CURRENCIES: currencies(p).map((c) => c.toUpperCase()).join(", "),
65
+ STORES_HTML: stores,
66
+ };
67
+ const docs = path.join(PACKAGE_ROOT, "docs");
68
+ return [
69
+ render(path.join(docs, fr ? "fiche-client.html" : "fiche-client.en.html"), "GUIDE-CLIENT", dir, vars),
70
+ render(path.join(docs, fr ? "guide-complet.fr.html" : "guide-complet.en.html"), "GUIDE-COMPLET", dir, vars),
71
+ ];
72
+ }
@@ -0,0 +1,156 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import { parseArgs } from "node:util";
4
+ import pc from "picocolors";
5
+ import { z } from "zod";
6
+ import { add, admin, apply, backup, campaignExport, campaignImport, check, guide, remove, set, stripeWebhook, themes, update } from "./commands.js";
7
+ import { answersSchema, create } from "./create.js";
8
+ import { ProjectError } from "./project.js";
9
+ const HELP = `create-boutique — Usine à Boutiques
10
+
11
+ create-boutique <dossier> [options] crée un projet (questions si des options manquent)
12
+ --name "<nom>" nom affiché
13
+ --email <email> e-mail admin (et du marchand)
14
+ --currency usd (défaut usd) --country us (défaut us) --language en (défaut en)
15
+ --themes a,b,c | all une boutique par thème, slug = <dossier>-<thème>
16
+ --boutique <slug>:<thème> ajoute une boutique au nom choisi (répétable)
17
+ --stripe-pk / --stripe-sk / --stripe-whsec clés Stripe (optionnelles)
18
+ --resend-key / --email-from e-mails (optionnels)
19
+ --products <csv> --images <dossier> catalogue (optionnel)
20
+ --store-port 8001 --api-port 9000 --storage-port 9100 premiers ports essayés
21
+ --config <fichier.json> toutes les options dans un fichier (mêmes clés, en camelCase)
22
+ --yes aucune question, valeurs par défaut
23
+
24
+ create-boutique apply <dossier> [--no-up] applique config/project.json
25
+ create-boutique add <dossier> --boutique <slug>:<thème>
26
+ create-boutique remove <dossier> --boutique <slug>
27
+ create-boutique set <dossier> [--boutique <slug>] [--name …] [--theme …] [--currency …] [--country …] [--language …]
28
+ create-boutique themes liste les thèmes
29
+ create-boutique update <dossier> [--no-up] met à jour le code (config, .env, données conservés)
30
+ create-boutique check <dossier> commande test dans chaque boutique
31
+ create-boutique backup <dossier> base + images dans backups/<date>/
32
+ create-boutique admin <dossier> adresse, e-mail et mot de passe de l'admin
33
+ create-boutique guide <dossier> régénère GUIDE-CLIENT.pdf et GUIDE-COMPLET.pdf
34
+ create-boutique stripe-webhook <dossier> --url https://api.marque.com crée le webhook Stripe, écrit STRIPE_WEBHOOK_SECRET
35
+ create-boutique campaign export <dossier> campagnes en base → config/campaigns/*.json
36
+ create-boutique campaign import <dossier> [--boutique <slug>] [--yes] JSON → base (écrase)
37
+ `;
38
+ const COMMANDS = new Set(["apply", "add", "remove", "set", "themes", "update", "check", "backup", "campaign", "admin", "guide", "stripe-webhook", "help"]);
39
+ async function main() {
40
+ const { values, positionals } = parseArgs({
41
+ allowPositionals: true,
42
+ options: {
43
+ name: { type: "string" },
44
+ email: { type: "string" },
45
+ currency: { type: "string" },
46
+ country: { type: "string" },
47
+ language: { type: "string" },
48
+ themes: { type: "string" },
49
+ boutique: { type: "string", multiple: true },
50
+ theme: { type: "string" },
51
+ "stripe-pk": { type: "string" },
52
+ "stripe-sk": { type: "string" },
53
+ "stripe-whsec": { type: "string" },
54
+ "resend-key": { type: "string" },
55
+ "email-from": { type: "string" },
56
+ products: { type: "string" },
57
+ images: { type: "string" },
58
+ "store-port": { type: "string" },
59
+ "api-port": { type: "string" },
60
+ "storage-port": { type: "string" },
61
+ config: { type: "string" },
62
+ url: { type: "string" },
63
+ yes: { type: "boolean", short: "y" },
64
+ "no-up": { type: "boolean" },
65
+ help: { type: "boolean", short: "h" },
66
+ },
67
+ });
68
+ const [first, ...rest] = positionals;
69
+ if (values.help || !first || first === "help")
70
+ return console.log(HELP);
71
+ const up = { up: !values["no-up"] };
72
+ const dirArg = (i = 0) => {
73
+ const d = rest[i];
74
+ if (!d)
75
+ throw new ProjectError("dossier du projet manquant (ex. create-boutique apply ./boutique-yout)");
76
+ return d;
77
+ };
78
+ const lower = (v) => v?.toLowerCase();
79
+ if (!COMMANDS.has(first)) {
80
+ // create-boutique <dossier> [options]
81
+ const fromFile = values.config ? answersSchema.parse(JSON.parse(fs.readFileSync(values.config, "utf8"))) : {};
82
+ const port = (v) => (v ? Number(v) : undefined);
83
+ const fromFlags = answersSchema.parse(Object.fromEntries(Object.entries({
84
+ name: values.name,
85
+ email: values.email,
86
+ currency: lower(values.currency),
87
+ country: lower(values.country),
88
+ language: lower(values.language),
89
+ themes: values.themes === "all" ? "all" : values.themes?.split(",").map((s) => s.trim()).filter(Boolean),
90
+ boutiques: values.boutique,
91
+ stripePk: values["stripe-pk"],
92
+ stripeSk: values["stripe-sk"],
93
+ stripeWhsec: values["stripe-whsec"],
94
+ resendKey: values["resend-key"],
95
+ emailFrom: values["email-from"],
96
+ products: values.products,
97
+ images: values.images,
98
+ storePort: port(values["store-port"]),
99
+ apiPort: port(values["api-port"]),
100
+ storagePort: port(values["storage-port"]),
101
+ }).filter(([, v]) => v !== undefined)));
102
+ return create(first, { ...fromFile, ...fromFlags }, { yes: !!values.yes });
103
+ }
104
+ switch (first) {
105
+ case "apply":
106
+ return apply(dirArg(), up);
107
+ case "add":
108
+ return add(dirArg(), values.boutique ?? [], up);
109
+ case "remove":
110
+ if (!values.boutique?.length)
111
+ throw new ProjectError("--boutique <slug> obligatoire");
112
+ return remove(dirArg(), values.boutique, up);
113
+ case "set":
114
+ return set(dirArg(), {
115
+ boutique: values.boutique?.[0],
116
+ name: values.name,
117
+ theme: values.theme,
118
+ currency: lower(values.currency),
119
+ country: lower(values.country),
120
+ language: lower(values.language),
121
+ }, up);
122
+ case "themes":
123
+ return themes();
124
+ case "update":
125
+ return update(dirArg(), up);
126
+ case "check":
127
+ return check(dirArg());
128
+ case "backup":
129
+ return backup(dirArg());
130
+ case "admin":
131
+ return admin(dirArg());
132
+ case "guide":
133
+ return guide(dirArg());
134
+ case "stripe-webhook":
135
+ return stripeWebhook(dirArg(), values.url, up);
136
+ case "campaign": {
137
+ const [action, dir] = rest;
138
+ if (!dir)
139
+ throw new ProjectError("usage : create-boutique campaign export|import <dossier>");
140
+ if (action === "export")
141
+ return campaignExport(dir);
142
+ if (action === "import")
143
+ return campaignImport(dir, values.boutique ?? [], !!values.yes);
144
+ throw new ProjectError("usage : create-boutique campaign export|import <dossier>");
145
+ }
146
+ }
147
+ }
148
+ main().catch((e) => {
149
+ if (e instanceof ProjectError)
150
+ console.error(pc.red("✖ config invalide, rien n'a été modifié :\n") + e.message);
151
+ else if (e instanceof z.ZodError)
152
+ console.error(pc.red("✖ options invalides :\n") + z.prettifyError(e));
153
+ else
154
+ console.error(pc.red(`✖ ${e instanceof Error ? e.message : String(e)}`));
155
+ process.exit(1);
156
+ });
@@ -0,0 +1,29 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ /** Racine du package create-boutique (contient templates/). */
5
+ export const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
6
+ export const TEMPLATES = path.join(PACKAGE_ROOT, "templates");
7
+ export const THEMES_DIR = path.join(TEMPLATES, "storefront", "themes");
8
+ export function templateVersion() {
9
+ const pkg = JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, "package.json"), "utf8"));
10
+ return pkg.version;
11
+ }
12
+ /** Fichiers et dossiers jamais copiés dans un projet (dépendances, builds, secrets). */
13
+ const IGNORED = new Set(["node_modules", ".next", ".medusa", "static", "uploads", ".git", ".turbo", "dist"]);
14
+ export function shouldCopy(name) {
15
+ return !IGNORED.has(name) && !name.startsWith(".env") && !name.endsWith(".log") && name !== "tsconfig.tsbuildinfo";
16
+ }
17
+ export function copyDir(from, to) {
18
+ fs.mkdirSync(to, { recursive: true });
19
+ for (const entry of fs.readdirSync(from, { withFileTypes: true })) {
20
+ if (!shouldCopy(entry.name))
21
+ continue;
22
+ const src = path.join(from, entry.name);
23
+ const dst = path.join(to, entry.name);
24
+ if (entry.isDirectory())
25
+ copyDir(src, dst);
26
+ else
27
+ fs.copyFileSync(src, dst);
28
+ }
29
+ }
@@ -0,0 +1,54 @@
1
+ import fs from "node:fs";
2
+ import net from "node:net";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ /**
6
+ * Registre local des ports réservés par les projets générés sur cette machine
7
+ * (~/.create-boutique/ports.json), pour que deux projets ne prennent pas les mêmes.
8
+ */
9
+ const REGISTRY = path.join(os.homedir(), ".create-boutique", "ports.json");
10
+ function readRegistry() {
11
+ try {
12
+ return JSON.parse(fs.readFileSync(REGISTRY, "utf8"));
13
+ }
14
+ catch {
15
+ return {};
16
+ }
17
+ }
18
+ export function reservePorts(projectDir, ports) {
19
+ const reg = readRegistry();
20
+ reg[path.resolve(projectDir)] = ports;
21
+ fs.mkdirSync(path.dirname(REGISTRY), { recursive: true });
22
+ fs.writeFileSync(REGISTRY, JSON.stringify(reg, null, 2) + "\n");
23
+ }
24
+ export function releasePorts(projectDir) {
25
+ const reg = readRegistry();
26
+ delete reg[path.resolve(projectDir)];
27
+ fs.mkdirSync(path.dirname(REGISTRY), { recursive: true });
28
+ fs.writeFileSync(REGISTRY, JSON.stringify(reg, null, 2) + "\n");
29
+ }
30
+ /** Ports réservés par les AUTRES projets (un projet existant n'est pas en conflit avec lui-même). */
31
+ export function reservedByOthers(projectDir) {
32
+ const self = path.resolve(projectDir);
33
+ const reg = readRegistry();
34
+ // On oublie les projets dont le dossier a été supprimé.
35
+ return new Set(Object.entries(reg).filter(([dir]) => dir !== self && fs.existsSync(dir)).flatMap(([, p]) => p));
36
+ }
37
+ export function isPortFree(port) {
38
+ return new Promise((resolve) => {
39
+ const srv = net.createServer();
40
+ srv.once("error", () => resolve(false));
41
+ srv.once("listening", () => srv.close(() => resolve(true)));
42
+ srv.listen(port, "0.0.0.0");
43
+ });
44
+ }
45
+ /** Premier port libre à partir de `from`, en évitant `taken` (ports déjà choisis ou réservés). */
46
+ export async function nextFreePort(from, taken) {
47
+ for (let p = from; p < 65535; p++) {
48
+ if (taken.has(p))
49
+ continue;
50
+ if (await isPortFree(p))
51
+ return p;
52
+ }
53
+ throw new Error(`aucun port libre à partir de ${from}`);
54
+ }