@nextsparkjs/core 0.1.0-beta.189 → 0.1.0-beta.190

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 (456) hide show
  1. package/dist/components/api/keys/CreateApiKeyDialog.d.ts.map +1 -1
  2. package/dist/components/api/keys/CreateApiKeyDialog.js +2 -1
  3. package/dist/components/app/layouts/PublicFooter.d.ts.map +1 -1
  4. package/dist/components/app/layouts/PublicFooter.js +2 -23
  5. package/dist/components/app/misc/ThemeToggle.d.ts.map +1 -1
  6. package/dist/components/app/misc/ThemeToggle.js +2 -1
  7. package/dist/components/auth/forms/LoginForm.js +1 -1
  8. package/dist/components/auth/forms/SignupForm.d.ts.map +1 -1
  9. package/dist/components/auth/forms/SignupForm.js +2 -1
  10. package/dist/components/auth/pages/AuthErrorPage.d.ts.map +1 -1
  11. package/dist/components/auth/pages/AuthErrorPage.js +2 -1
  12. package/dist/components/billing/ManageBillingButton.d.ts.map +1 -1
  13. package/dist/components/billing/ManageBillingButton.js +2 -1
  14. package/dist/components/dashboard/block-editor/array-field.d.ts.map +1 -1
  15. package/dist/components/dashboard/block-editor/array-field.js +2 -1
  16. package/dist/components/dashboard/block-editor/block-picker.d.ts.map +1 -1
  17. package/dist/components/dashboard/block-editor/block-picker.js +2 -1
  18. package/dist/components/dashboard/block-editor/builder-editor-view.d.ts.map +1 -1
  19. package/dist/components/dashboard/block-editor/builder-editor-view.js +3 -2
  20. package/dist/components/dashboard/block-editor/config-panel.d.ts.map +1 -1
  21. package/dist/components/dashboard/block-editor/config-panel.js +3 -2
  22. package/dist/components/dashboard/block-editor/dynamic-form.d.ts.map +1 -1
  23. package/dist/components/dashboard/block-editor/dynamic-form.js +2 -1
  24. package/dist/components/dashboard/block-editor/entity-fields-sidebar.d.ts.map +1 -1
  25. package/dist/components/dashboard/block-editor/entity-fields-sidebar.js +3 -2
  26. package/dist/components/dashboard/block-editor/pattern-reference-preview.d.ts.map +1 -1
  27. package/dist/components/dashboard/block-editor/pattern-reference-preview.js +2 -1
  28. package/dist/components/dashboard/layouts/TopNavbar.d.ts.map +1 -1
  29. package/dist/components/dashboard/layouts/TopNavbar.js +4 -3
  30. package/dist/components/dashboard/misc/SearchDropdown.d.ts.map +1 -1
  31. package/dist/components/dashboard/misc/SearchDropdown.js +2 -1
  32. package/dist/components/dashboard/mobile/MobileTopBar.d.ts.map +1 -1
  33. package/dist/components/dashboard/mobile/MobileTopBar.js +2 -1
  34. package/dist/components/devtools/BlockDetailViewer.d.ts.map +1 -1
  35. package/dist/components/devtools/BlockDetailViewer.js +2 -1
  36. package/dist/components/devtools/BlocksViewer.d.ts.map +1 -1
  37. package/dist/components/devtools/BlocksViewer.js +2 -1
  38. package/dist/components/devtools/ConfigViewer.d.ts.map +1 -1
  39. package/dist/components/devtools/ConfigViewer.js +3 -2
  40. package/dist/components/devtools/TestCasesViewer.d.ts.map +1 -1
  41. package/dist/components/devtools/TestCasesViewer.js +3 -2
  42. package/dist/components/devtools/TestCoverageDashboard.d.ts.map +1 -1
  43. package/dist/components/devtools/TestCoverageDashboard.js +2 -1
  44. package/dist/components/devtools/api-explorer/ApiDocsModal.d.ts.map +1 -1
  45. package/dist/components/devtools/api-explorer/ApiDocsModal.js +0 -0
  46. package/dist/components/devtools/api-explorer/ApiExplorer.d.ts.map +1 -1
  47. package/dist/components/devtools/api-explorer/ApiExplorer.js +2 -1
  48. package/dist/components/devtools/scheduled-actions/actions-table.d.ts.map +1 -1
  49. package/dist/components/devtools/scheduled-actions/actions-table.js +3 -2
  50. package/dist/components/docs/docs-sidebar.d.ts.map +1 -1
  51. package/dist/components/docs/docs-sidebar.js +3 -2
  52. package/dist/components/entities/EntityFieldRenderer.d.ts.map +1 -1
  53. package/dist/components/entities/EntityFieldRenderer.js +2 -1
  54. package/dist/components/entities/EntityList.d.ts.map +1 -1
  55. package/dist/components/entities/EntityList.js +2 -1
  56. package/dist/components/entities/EntityTable.d.ts.map +1 -1
  57. package/dist/components/entities/EntityTable.js +3 -2
  58. package/dist/components/entities/ErrorBoundary.d.ts.map +1 -1
  59. package/dist/components/entities/ErrorBoundary.js +2 -1
  60. package/dist/components/entities/wrappers/EntityDetailWrapper.d.ts.map +1 -1
  61. package/dist/components/entities/wrappers/EntityDetailWrapper.js +3 -2
  62. package/dist/components/media/MediaCard.d.ts.map +1 -1
  63. package/dist/components/media/MediaCard.js +2 -1
  64. package/dist/components/media/MediaDetailPanel.d.ts.map +1 -1
  65. package/dist/components/media/MediaDetailPanel.js +2 -1
  66. package/dist/components/media/MediaList.d.ts.map +1 -1
  67. package/dist/components/media/MediaList.js +2 -1
  68. package/dist/components/media/MediaSelector.d.ts.map +1 -1
  69. package/dist/components/media/MediaSelector.js +2 -1
  70. package/dist/components/media/MediaUploadZone.d.ts.map +1 -1
  71. package/dist/components/media/MediaUploadZone.js +2 -1
  72. package/dist/components/public/entities/PublicEntityDetail.d.ts.map +1 -1
  73. package/dist/components/public/entities/PublicEntityDetail.js +2 -1
  74. package/dist/components/teams/TeamPendingInvitations.d.ts.map +1 -1
  75. package/dist/components/teams/TeamPendingInvitations.js +3 -2
  76. package/dist/components/ui/accordion.js +1 -1
  77. package/dist/components/ui/audio-upload.d.ts.map +1 -1
  78. package/dist/components/ui/audio-upload.js +2 -1
  79. package/dist/components/ui/avatar.d.ts +17 -1
  80. package/dist/components/ui/avatar.d.ts.map +1 -1
  81. package/dist/components/ui/avatar.js +29 -2
  82. package/dist/components/ui/badge.js +1 -1
  83. package/dist/components/ui/breadcrumb.d.ts +13 -0
  84. package/dist/components/ui/breadcrumb.d.ts.map +1 -1
  85. package/dist/components/ui/breadcrumb.js +23 -10
  86. package/dist/components/ui/button.js +1 -1
  87. package/dist/components/ui/card.js +1 -1
  88. package/dist/components/ui/checkbox.js +1 -1
  89. package/dist/components/ui/file-upload.d.ts.map +1 -1
  90. package/dist/components/ui/file-upload.js +2 -1
  91. package/dist/components/ui/image-upload.d.ts.map +1 -1
  92. package/dist/components/ui/image-upload.js +4 -3
  93. package/dist/components/ui/input.js +1 -1
  94. package/dist/components/ui/label.js +1 -1
  95. package/dist/components/ui/optimized-image.d.ts.map +1 -1
  96. package/dist/components/ui/optimized-image.js +5 -0
  97. package/dist/components/ui/pagination.d.ts +1 -1
  98. package/dist/components/ui/pagination.d.ts.map +1 -1
  99. package/dist/components/ui/pagination.js +3 -0
  100. package/dist/components/ui/progress.js +1 -1
  101. package/dist/components/ui/relation-display.d.ts.map +1 -1
  102. package/dist/components/ui/relation-display.js +3 -2
  103. package/dist/components/ui/separator.js +1 -1
  104. package/dist/components/ui/simple-relation-select.d.ts.map +1 -1
  105. package/dist/components/ui/simple-relation-select.js +7 -6
  106. package/dist/components/ui/skeleton.js +1 -1
  107. package/dist/components/ui/slider.js +1 -1
  108. package/dist/components/ui/switch.js +1 -1
  109. package/dist/components/ui/tabs.js +1 -1
  110. package/dist/components/ui/textarea.js +1 -1
  111. package/dist/components/ui/user-display.d.ts.map +1 -1
  112. package/dist/components/ui/user-display.js +2 -1
  113. package/dist/components/ui/user-select.d.ts.map +1 -1
  114. package/dist/components/ui/user-select.js +2 -1
  115. package/dist/components/ui/video-upload.d.ts.map +1 -1
  116. package/dist/components/ui/video-upload.js +3 -2
  117. package/dist/components/users/tables/UsersTable.d.ts.map +1 -1
  118. package/dist/components/users/tables/UsersTable.js +3 -2
  119. package/dist/contexts/SubscriptionContext.d.ts.map +1 -1
  120. package/dist/contexts/SubscriptionContext.js +4 -3
  121. package/dist/contexts/TeamContext.d.ts.map +1 -1
  122. package/dist/contexts/TeamContext.js +3 -2
  123. package/dist/hooks/useAccountPreferencesSync.d.ts.map +1 -1
  124. package/dist/hooks/useAccountPreferencesSync.js +2 -1
  125. package/dist/hooks/useApiExplorerNavigation.d.ts.map +1 -1
  126. package/dist/hooks/useApiExplorerNavigation.js +2 -1
  127. package/dist/hooks/useAuth.d.ts.map +1 -1
  128. package/dist/hooks/useAuth.js +3 -2
  129. package/dist/hooks/useEnsureUserMetadata.d.ts.map +1 -1
  130. package/dist/hooks/useEnsureUserMetadata.js +3 -2
  131. package/dist/hooks/useEntityWithMeta.d.ts.map +1 -1
  132. package/dist/hooks/useEntityWithMeta.js +3 -2
  133. package/dist/hooks/useInvoice.d.ts.map +1 -1
  134. package/dist/hooks/useInvoice.js +2 -1
  135. package/dist/hooks/useInvoices.d.ts.map +1 -1
  136. package/dist/hooks/useInvoices.js +2 -1
  137. package/dist/hooks/useMedia.d.ts.map +1 -1
  138. package/dist/hooks/useMedia.js +10 -9
  139. package/dist/hooks/useMediaUpload.d.ts.map +1 -1
  140. package/dist/hooks/useMediaUpload.js +2 -1
  141. package/dist/hooks/useMembership.d.ts.map +1 -1
  142. package/dist/hooks/useMembership.js +3 -2
  143. package/dist/hooks/usePatternUsages.d.ts.map +1 -1
  144. package/dist/hooks/usePatternUsages.js +3 -2
  145. package/dist/hooks/usePrefetch.d.ts.map +1 -1
  146. package/dist/hooks/usePrefetch.js +3 -2
  147. package/dist/hooks/useQuota.d.ts.map +1 -1
  148. package/dist/hooks/useQuota.js +2 -1
  149. package/dist/hooks/useTeamInvitations.d.ts.map +1 -1
  150. package/dist/hooks/useTeamInvitations.js +4 -3
  151. package/dist/hooks/useTeamMembers.d.ts.map +1 -1
  152. package/dist/hooks/useTeamMembers.js +5 -4
  153. package/dist/hooks/useTeamUpdate.d.ts.map +1 -1
  154. package/dist/hooks/useTeamUpdate.js +2 -1
  155. package/dist/hooks/useUserProfile.d.ts.map +1 -1
  156. package/dist/hooks/useUserProfile.js +3 -2
  157. package/dist/hooks/useUserSettings.d.ts.map +1 -1
  158. package/dist/hooks/useUserSettings.js +3 -2
  159. package/dist/lib/actions/entity.actions.d.ts.map +1 -1
  160. package/dist/lib/actions/entity.actions.js +2 -1
  161. package/dist/lib/api/entities.d.ts.map +1 -1
  162. package/dist/lib/api/entities.js +4 -3
  163. package/dist/lib/auth/verify-email-link.d.ts +10 -0
  164. package/dist/lib/auth/verify-email-link.d.ts.map +1 -0
  165. package/dist/lib/auth/verify-email-link.js +14 -0
  166. package/dist/lib/auth-client.d.ts +2 -0
  167. package/dist/lib/auth-client.d.ts.map +1 -1
  168. package/dist/lib/auth-client.js +4 -0
  169. package/dist/lib/auth.d.ts.map +1 -1
  170. package/dist/lib/auth.js +10 -2
  171. package/dist/lib/base-path.d.ts +85 -0
  172. package/dist/lib/base-path.d.ts.map +1 -0
  173. package/dist/lib/base-path.js +79 -0
  174. package/dist/lib/billing/schema.d.ts +1 -1
  175. package/dist/lib/billing/schema.d.ts.map +1 -1
  176. package/dist/lib/billing/schema.js +1 -1
  177. package/dist/lib/blocks/sanitize-html.d.ts.map +1 -1
  178. package/dist/lib/blocks/sanitize-html.js +18 -2
  179. package/dist/lib/cache/revalidate-tag.d.ts +14 -0
  180. package/dist/lib/cache/revalidate-tag.d.ts.map +1 -0
  181. package/dist/lib/cache/revalidate-tag.js +8 -0
  182. package/dist/lib/config/dashboard-schema.d.ts +1 -1
  183. package/dist/lib/config/dashboard-schema.d.ts.map +1 -1
  184. package/dist/lib/config/dashboard-schema.js +1 -1
  185. package/dist/lib/config/types.d.ts +36 -16
  186. package/dist/lib/config/types.d.ts.map +1 -1
  187. package/dist/lib/docs/access.d.ts +28 -0
  188. package/dist/lib/docs/access.d.ts.map +1 -0
  189. package/dist/lib/docs/access.js +16 -0
  190. package/dist/lib/docs/parser.d.ts.map +1 -1
  191. package/dist/lib/docs/parser.js +2 -1
  192. package/dist/lib/docs/remark-doc-links.d.ts +14 -0
  193. package/dist/lib/docs/remark-doc-links.d.ts.map +1 -0
  194. package/dist/lib/docs/remark-doc-links.js +38 -0
  195. package/dist/lib/entities/api-generator.d.ts +1 -1
  196. package/dist/lib/entities/api-generator.d.ts.map +1 -1
  197. package/dist/lib/entities/api-generator.js +1 -1
  198. package/dist/lib/entities/schema-generator.d.ts +1 -1
  199. package/dist/lib/entities/schema-generator.d.ts.map +1 -1
  200. package/dist/lib/entities/schema-generator.js +1 -1
  201. package/dist/lib/mcp/engine.js +1 -1
  202. package/dist/lib/mcp/schema-builder.d.ts +1 -1
  203. package/dist/lib/mcp/schema-builder.d.ts.map +1 -1
  204. package/dist/lib/mcp/schema-builder.js +1 -1
  205. package/dist/lib/mcp/tool-generator.js +1 -1
  206. package/dist/lib/mcp/types.d.ts +1 -1
  207. package/dist/lib/mcp/types.d.ts.map +1 -1
  208. package/dist/lib/media/schemas.d.ts +1 -1
  209. package/dist/lib/media/schemas.d.ts.map +1 -1
  210. package/dist/lib/media/schemas.js +1 -1
  211. package/dist/lib/middleware/index.d.ts +5 -1
  212. package/dist/lib/middleware/index.d.ts.map +1 -1
  213. package/dist/lib/middleware/index.js +3 -2
  214. package/dist/lib/services/meta.service.d.ts +60 -0
  215. package/dist/lib/services/meta.service.d.ts.map +1 -1
  216. package/dist/lib/services/meta.service.js +132 -12
  217. package/dist/lib/services/middleware.service.d.ts.map +1 -1
  218. package/dist/lib/services/middleware.service.js +3 -2
  219. package/dist/lib/teams/schema.d.ts +1 -1
  220. package/dist/lib/teams/schema.d.ts.map +1 -1
  221. package/dist/lib/teams/schema.js +1 -1
  222. package/dist/lib/teams/teams.api.d.ts.map +1 -1
  223. package/dist/lib/teams/teams.api.js +4 -3
  224. package/dist/lib/user-data-client.d.ts.map +1 -1
  225. package/dist/lib/user-data-client.js +3 -2
  226. package/dist/lib/validation/invoices.d.ts +1 -1
  227. package/dist/lib/validation/invoices.d.ts.map +1 -1
  228. package/dist/lib/validation/invoices.js +1 -1
  229. package/dist/lib/validation-schemas.d.ts +1 -1
  230. package/dist/lib/validation-schemas.d.ts.map +1 -1
  231. package/dist/lib/validation-schemas.js +1 -1
  232. package/dist/lib/validation.d.ts +1 -1
  233. package/dist/lib/validation.d.ts.map +1 -1
  234. package/dist/lib/validation.js +1 -1
  235. package/dist/messages/de/footer.json +0 -1
  236. package/dist/messages/de/index.d.ts +0 -1
  237. package/dist/messages/de/index.d.ts.map +1 -1
  238. package/dist/messages/en/footer.json +0 -1
  239. package/dist/messages/en/index.d.ts +0 -1
  240. package/dist/messages/en/index.d.ts.map +1 -1
  241. package/dist/messages/es/footer.json +0 -1
  242. package/dist/messages/es/index.d.ts +0 -1
  243. package/dist/messages/es/index.d.ts.map +1 -1
  244. package/dist/messages/fr/footer.json +0 -1
  245. package/dist/messages/fr/index.d.ts +0 -1
  246. package/dist/messages/fr/index.d.ts.map +1 -1
  247. package/dist/messages/it/footer.json +0 -1
  248. package/dist/messages/it/index.d.ts +0 -1
  249. package/dist/messages/it/index.d.ts.map +1 -1
  250. package/dist/messages/pt/footer.json +0 -1
  251. package/dist/messages/pt/index.d.ts +0 -1
  252. package/dist/messages/pt/index.d.ts.map +1 -1
  253. package/dist/templates/app/(auth)/accept-invite/[token]/page.tsx +4 -3
  254. package/dist/templates/app/(auth)/verify-email/page.tsx +4 -3
  255. package/dist/templates/app/(public)/docs/[section]/[page]/page.tsx +11 -0
  256. package/dist/templates/app/api/auth/[...all]/route.ts +15 -14
  257. package/dist/templates/app/api/internal/user-metadata/route.ts +6 -8
  258. package/dist/templates/app/api/user/plan-flags/route.ts +1 -1
  259. package/dist/templates/app/api/user/profile/route.ts +12 -17
  260. package/dist/templates/app/api/v1/api-keys/route.ts +1 -1
  261. package/dist/templates/app/api/v1/auth/signup-with-invite/route.ts +2 -1
  262. package/dist/templates/app/api/v1/billing/cancel/route.ts +1 -1
  263. package/dist/templates/app/api/v1/billing/change-plan/route.ts +1 -1
  264. package/dist/templates/app/api/v1/billing/check-action/route.ts +1 -1
  265. package/dist/templates/app/api/v1/billing/checkout/route.ts +1 -1
  266. package/dist/templates/app/api/v1/blocks/validate/route.ts +1 -1
  267. package/dist/templates/app/api/v1/media/[id]/tags/route.ts +1 -1
  268. package/dist/templates/app/api/v1/post-categories/[id]/route.ts +1 -1
  269. package/dist/templates/app/api/v1/post-categories/route.ts +1 -1
  270. package/dist/templates/app/api/v1/teams/[teamId]/members/route.ts +2 -1
  271. package/dist/templates/app/api/v1/teams/[teamId]/subscription/route.ts +3 -5
  272. package/dist/templates/app/api/v1/teams/docs.md +4 -9
  273. package/dist/templates/app/api/v1/teams/switch/route.ts +1 -1
  274. package/dist/templates/app/api/v1/users/[id]/meta/[key]/route.ts +1 -1
  275. package/dist/templates/app/api/v1/users/[id]/route.ts +1 -1
  276. package/dist/templates/app/api/v1/users/route.ts +1 -1
  277. package/dist/templates/app/dashboard/(main)/media/page.tsx +3 -1
  278. package/dist/templates/app/dashboard/settings/api-keys/page.tsx +5 -4
  279. package/dist/templates/app/dashboard/settings/profile/page.tsx +3 -2
  280. package/dist/templates/app/devtools/scheduled-actions/page.tsx +2 -1
  281. package/dist/templates/app/superadmin/docs/[section]/[page]/page.tsx +16 -0
  282. package/dist/templates/app/superadmin/subscriptions/page.tsx +3 -2
  283. package/dist/templates/app/superadmin/teams/[teamId]/page.tsx +5 -4
  284. package/dist/templates/app/superadmin/users/[userId]/page.tsx +2 -1
  285. package/dist/templates/app/superadmin/users/page.tsx +2 -1
  286. package/dist/templates/blocks/cta-section/component.tsx +3 -2
  287. package/dist/templates/blocks/cta-section/schema.ts +1 -1
  288. package/dist/templates/blocks/features-grid/component.tsx +2 -1
  289. package/dist/templates/blocks/features-grid/schema.ts +1 -1
  290. package/dist/templates/blocks/hero/component.tsx +3 -2
  291. package/dist/templates/blocks/hero/schema.ts +1 -1
  292. package/dist/templates/blocks/testimonials/component.tsx +2 -1
  293. package/dist/templates/blocks/testimonials/schema.ts +1 -1
  294. package/dist/templates/blocks/text-content/component.tsx +2 -1
  295. package/dist/templates/blocks/text-content/schema.ts +1 -1
  296. package/dist/templates/ci-workflows/README.md +1 -1
  297. package/dist/templates/ci-workflows/github/cypress-regression.yml +1 -1
  298. package/dist/templates/ci-workflows/github/cypress-smoke.yml +1 -1
  299. package/dist/templates/contents/plugins/starter/plugin/api/example/route.ts.template +1 -1
  300. package/dist/templates/contents/themes/starter/blocks/cta-section/component.tsx +3 -2
  301. package/dist/templates/contents/themes/starter/blocks/cta-section/schema.ts +1 -1
  302. package/dist/templates/contents/themes/starter/blocks/features-grid/component.tsx +2 -1
  303. package/dist/templates/contents/themes/starter/blocks/features-grid/schema.ts +1 -1
  304. package/dist/templates/contents/themes/starter/blocks/hero/component.tsx +3 -2
  305. package/dist/templates/contents/themes/starter/blocks/hero/schema.ts +1 -1
  306. package/dist/templates/contents/themes/starter/blocks/testimonials/component.tsx +3 -2
  307. package/dist/templates/contents/themes/starter/blocks/testimonials/schema.ts +1 -1
  308. package/dist/templates/contents/themes/starter/blocks/text-content/component.tsx +2 -1
  309. package/dist/templates/contents/themes/starter/blocks/text-content/schema.ts +1 -1
  310. package/dist/templates/contents/themes/starter/entities/posts/components/post-header.tsx +2 -1
  311. package/dist/templates/contents/themes/starter/lib/hooks/useAnalyticsData.ts +2 -1
  312. package/dist/templates/contents/themes/starter/lib/hooks/useTaskStats.ts +2 -1
  313. package/dist/templates/eslint.config.mjs +133 -9
  314. package/dist/templates/features/blog/blocks/post-content/component.tsx +2 -1
  315. package/dist/templates/features/blog/blocks/post-content/schema.ts +1 -1
  316. package/dist/templates/features/blog/entities/posts/components/post-header.tsx +2 -1
  317. package/dist/templates/features/pages/blocks/hero/component.tsx +3 -2
  318. package/dist/templates/features/pages/blocks/hero/schema.ts +1 -1
  319. package/dist/templates/next.config.mjs +50 -29
  320. package/dist/templates/proxy.ts +100 -21
  321. package/dist/templates/public/sw.js +2 -2
  322. package/dist/templates/scripts/cy-run-prod.cjs +5 -5
  323. package/dist/types/blocks.d.ts +1 -1
  324. package/dist/types/blocks.d.ts.map +1 -1
  325. package/dist/types/blocks.js +1 -1
  326. package/package.json +9 -5
  327. package/scripts/build/docs-registry.mjs +7 -7
  328. package/scripts/build/registry/__tests__/billing-registry.test.mjs +43 -0
  329. package/scripts/build/registry/__tests__/build-output.test.mjs +209 -0
  330. package/scripts/build/registry/__tests__/copy-race-child.mjs +14 -0
  331. package/scripts/build/registry/__tests__/direct-fs-writes.mjs +203 -0
  332. package/scripts/build/registry/__tests__/globals-css-tag.test.mjs +66 -0
  333. package/scripts/build/registry/__tests__/icons-discovery.test.mjs +530 -29
  334. package/scripts/build/registry/__tests__/page-generator.test.mjs +211 -37
  335. package/scripts/build/registry/__tests__/published-signatures.test.mjs +239 -0
  336. package/scripts/build/registry/__tests__/route-file-rule.test.mjs +170 -0
  337. package/scripts/build/registry/__tests__/safe-fs.test.mjs +333 -0
  338. package/scripts/build/registry/__tests__/template-analysis.test.mjs +142 -0
  339. package/scripts/build/registry/__tests__/template-registry.test.mjs +279 -9
  340. package/scripts/build/registry/__tests__/templates-plan.test.mjs +128 -0
  341. package/scripts/build/registry/__tests__/templates-tree.test.mjs +367 -0
  342. package/scripts/build/registry/__tests__/write-places.test.mjs +425 -0
  343. package/scripts/build/registry/config.mjs +6 -65
  344. package/scripts/build/registry/discovery/icons.mjs +498 -43
  345. package/scripts/build/registry/discovery/templates.mjs +1 -1
  346. package/scripts/build/registry/generators/billing-registry.mjs +1 -1
  347. package/scripts/build/registry/generators/docs-registry.mjs +3 -4
  348. package/scripts/build/registry/generators/feature-registry.mjs +1 -1
  349. package/scripts/build/registry/generators/permissions-registry.mjs +2 -2
  350. package/scripts/build/registry/generators/template-registry.mjs +65 -49
  351. package/scripts/build/registry/index.mjs +1 -2
  352. package/scripts/build/registry/post-build/index.mjs +1 -1
  353. package/scripts/build/registry/post-build/own-gitignores.mjs +220 -0
  354. package/scripts/build/registry/post-build/page-generator.mjs +512 -268
  355. package/scripts/build/registry/post-build/route-cleanup.mjs +16 -13
  356. package/scripts/build/registry/post-build/test-fixtures.mjs +26 -8
  357. package/scripts/build/registry/project-mode.mjs +76 -0
  358. package/scripts/build/registry/shared/typescript-compiler.mjs +77 -0
  359. package/scripts/build/registry/validate-env.test.mjs +12 -8
  360. package/scripts/build/registry/watch.mjs +3 -53
  361. package/scripts/build/registry/write-places.mjs +228 -0
  362. package/scripts/build/registry.mjs +53 -30
  363. package/scripts/build/safe-fs.mjs +279 -0
  364. package/scripts/build/templates-plan.mjs +44 -0
  365. package/scripts/build/theme.mjs +46 -42
  366. package/scripts/build/update-tsconfig.mjs +16 -15
  367. package/scripts/db/cluster-changes.mjs +205 -0
  368. package/scripts/db/connection-time-limits.mjs +53 -0
  369. package/scripts/db/inspect-server.mjs +108 -0
  370. package/scripts/db/migration-time-limit.mjs +695 -0
  371. package/scripts/db/run-migrations.mjs +46 -41
  372. package/scripts/db/ssl-config.mjs +194 -0
  373. package/scripts/db/theme-location.mjs +69 -0
  374. package/scripts/db/verify-tables.mjs +3 -8
  375. package/scripts/db/verify-teams.mjs +2 -6
  376. package/scripts/db/verify-theme-migrations.mjs +169 -0
  377. package/scripts/maintenance/core-updater.mjs +1003 -0
  378. package/scripts/maintenance/step-guard.mjs +74 -0
  379. package/scripts/maintenance/update-core.mjs +14 -831
  380. package/scripts/utils/console-guard.mjs +13 -0
  381. package/scripts/utils/generated-tag.mjs +66 -0
  382. package/scripts/utils/index.mjs +9 -1
  383. package/scripts/utils/logging.mjs +140 -0
  384. package/templates/app/(auth)/accept-invite/[token]/page.tsx +4 -3
  385. package/templates/app/(auth)/verify-email/page.tsx +4 -3
  386. package/templates/app/(public)/docs/[section]/[page]/page.tsx +11 -0
  387. package/templates/app/api/auth/[...all]/route.ts +15 -14
  388. package/templates/app/api/internal/user-metadata/route.ts +6 -8
  389. package/templates/app/api/user/plan-flags/route.ts +1 -1
  390. package/templates/app/api/user/profile/route.ts +12 -17
  391. package/templates/app/api/v1/api-keys/route.ts +1 -1
  392. package/templates/app/api/v1/auth/signup-with-invite/route.ts +2 -1
  393. package/templates/app/api/v1/billing/cancel/route.ts +1 -1
  394. package/templates/app/api/v1/billing/change-plan/route.ts +1 -1
  395. package/templates/app/api/v1/billing/check-action/route.ts +1 -1
  396. package/templates/app/api/v1/billing/checkout/route.ts +1 -1
  397. package/templates/app/api/v1/blocks/validate/route.ts +1 -1
  398. package/templates/app/api/v1/media/[id]/tags/route.ts +1 -1
  399. package/templates/app/api/v1/post-categories/[id]/route.ts +1 -1
  400. package/templates/app/api/v1/post-categories/route.ts +1 -1
  401. package/templates/app/api/v1/teams/[teamId]/members/route.ts +2 -1
  402. package/templates/app/api/v1/teams/[teamId]/subscription/route.ts +3 -5
  403. package/templates/app/api/v1/teams/docs.md +4 -9
  404. package/templates/app/api/v1/teams/switch/route.ts +1 -1
  405. package/templates/app/api/v1/users/[id]/meta/[key]/route.ts +1 -1
  406. package/templates/app/api/v1/users/[id]/route.ts +1 -1
  407. package/templates/app/api/v1/users/route.ts +1 -1
  408. package/templates/app/dashboard/(main)/media/page.tsx +3 -1
  409. package/templates/app/dashboard/settings/api-keys/page.tsx +5 -4
  410. package/templates/app/dashboard/settings/profile/page.tsx +3 -2
  411. package/templates/app/devtools/scheduled-actions/page.tsx +2 -1
  412. package/templates/app/superadmin/docs/[section]/[page]/page.tsx +16 -0
  413. package/templates/app/superadmin/subscriptions/page.tsx +3 -2
  414. package/templates/app/superadmin/teams/[teamId]/page.tsx +5 -4
  415. package/templates/app/superadmin/users/[userId]/page.tsx +2 -1
  416. package/templates/app/superadmin/users/page.tsx +2 -1
  417. package/templates/blocks/cta-section/component.tsx +3 -2
  418. package/templates/blocks/cta-section/schema.ts +1 -1
  419. package/templates/blocks/features-grid/component.tsx +2 -1
  420. package/templates/blocks/features-grid/schema.ts +1 -1
  421. package/templates/blocks/hero/component.tsx +3 -2
  422. package/templates/blocks/hero/schema.ts +1 -1
  423. package/templates/blocks/testimonials/component.tsx +2 -1
  424. package/templates/blocks/testimonials/schema.ts +1 -1
  425. package/templates/blocks/text-content/component.tsx +2 -1
  426. package/templates/blocks/text-content/schema.ts +1 -1
  427. package/templates/ci-workflows/README.md +1 -1
  428. package/templates/ci-workflows/github/cypress-regression.yml +1 -1
  429. package/templates/ci-workflows/github/cypress-smoke.yml +1 -1
  430. package/templates/contents/plugins/starter/plugin/api/example/route.ts.template +1 -1
  431. package/templates/contents/themes/starter/blocks/cta-section/component.tsx +3 -2
  432. package/templates/contents/themes/starter/blocks/cta-section/schema.ts +1 -1
  433. package/templates/contents/themes/starter/blocks/features-grid/component.tsx +2 -1
  434. package/templates/contents/themes/starter/blocks/features-grid/schema.ts +1 -1
  435. package/templates/contents/themes/starter/blocks/hero/component.tsx +3 -2
  436. package/templates/contents/themes/starter/blocks/hero/schema.ts +1 -1
  437. package/templates/contents/themes/starter/blocks/testimonials/component.tsx +3 -2
  438. package/templates/contents/themes/starter/blocks/testimonials/schema.ts +1 -1
  439. package/templates/contents/themes/starter/blocks/text-content/component.tsx +2 -1
  440. package/templates/contents/themes/starter/blocks/text-content/schema.ts +1 -1
  441. package/templates/contents/themes/starter/entities/posts/components/post-header.tsx +2 -1
  442. package/templates/contents/themes/starter/lib/hooks/useAnalyticsData.ts +2 -1
  443. package/templates/contents/themes/starter/lib/hooks/useTaskStats.ts +2 -1
  444. package/templates/eslint.config.mjs +133 -9
  445. package/templates/features/blog/blocks/post-content/component.tsx +2 -1
  446. package/templates/features/blog/blocks/post-content/schema.ts +1 -1
  447. package/templates/features/blog/entities/posts/components/post-header.tsx +2 -1
  448. package/templates/features/pages/blocks/hero/component.tsx +3 -2
  449. package/templates/features/pages/blocks/hero/schema.ts +1 -1
  450. package/templates/next.config.mjs +50 -29
  451. package/templates/proxy.ts +100 -21
  452. package/templates/public/sw.js +2 -2
  453. package/templates/scripts/cy-run-prod.cjs +5 -5
  454. package/tests/jest/__mocks__/@nextsparkjs/registries/template-registry.client.ts +11 -0
  455. package/tests/jest/__mocks__/next-server.js +10 -0
  456. package/scripts/build/docs.mjs +0 -392
@@ -0,0 +1,1003 @@
1
+ /**
2
+ * Updates a NextSpark project to another release of the @nextsparkjs packages.
3
+ *
4
+ * A project takes NextSpark from npm: its package.json pins @nextsparkjs/core,
5
+ * @nextsparkjs/cli and the other @nextsparkjs packages to one version, app/ is
6
+ * kept in step with core's templates by `nextspark sync:app`, and contents/
7
+ * holds the project's own themes and plugins. So an update sets those pins to
8
+ * the new version, installs, and syncs app/. No framework source is copied into
9
+ * the project, and of package.json only the @nextsparkjs versions change.
10
+ *
11
+ * What can be checked without changing anything is checked before the first
12
+ * write, so a run that can't start says why and leaves the project as it was.
13
+ * A run only starts from a clean git tree, because a run that fails or is
14
+ * interrupted once it has started writing undoes nothing itself: pnpm install
15
+ * runs lifecycle scripts, and those can write anywhere in the project, so the
16
+ * one rollback that covers whatever was written is going back to the commit the
17
+ * run started from. It exits non-zero and prints what it got through, what git
18
+ * status shows and that rollback.
19
+ */
20
+
21
+ import { spawn, spawnSync } from 'node:child_process'
22
+ import fs from 'node:fs'
23
+ import os from 'node:os'
24
+ import path from 'node:path'
25
+ import { fileURLToPath } from 'node:url'
26
+
27
+ const SCOPE = '@nextsparkjs/'
28
+ const CORE_PACKAGE = '@nextsparkjs/core'
29
+ const CLI_PACKAGE = '@nextsparkjs/cli'
30
+ const DEPENDENCY_FIELDS = ['dependencies', 'devDependencies']
31
+ const VERSION_FILE = 'core.version.json'
32
+ const LOCKFILE = 'pnpm-lock.yaml'
33
+ const LIST_LIMIT = 20
34
+ const STATUS_SHOWN = 40
35
+ /** Signals an update stops its running step for and reports on; SIGHUP is a closed terminal */
36
+ const HANDLED_SIGNALS = ['SIGINT', 'SIGTERM', 'SIGHUP']
37
+ /** How long a stopped step's processes get to exit before they are killed */
38
+ const STOP_GRACE_MS = 5000
39
+ /** How long processes sent SIGKILL, or a released step guard, get to be gone */
40
+ const KILL_WAIT_MS = 2000
41
+ /** How long a step's group gets to empty on its own once its command exited */
42
+ const SETTLE_MS = 500
43
+ /** Runs each long step and kills it if update-core goes away first */
44
+ const STEP_GUARD = fileURLToPath(new URL('./step-guard.mjs', import.meta.url))
45
+
46
+ const VERSION_PATTERN = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/
47
+
48
+ const HELP = `
49
+ NextSpark Core Updater
50
+
51
+ Usage:
52
+ pnpm update-core [options]
53
+
54
+ Options:
55
+ --version <version> Update to this version (e.g. 0.1.0-beta.189)
56
+ --latest Update to the version npm tags latest (default)
57
+ --branch, -b Create the branch update/<version> and update there
58
+ --list List the published versions
59
+ --check Say whether a newer version is published
60
+ --current Show the installed version
61
+ --help, -h Show this help message
62
+
63
+ What gets updated:
64
+ package.json Only the versions of the @nextsparkjs packages, all set to exactly
65
+ the target: a ^ or ~ range is replaced, since pnpm would install
66
+ the newest version the range allows instead of the target
67
+ pnpm-lock.yaml, node_modules Through pnpm install, run where pnpm-lock.yaml is (in a
68
+ web-mobile project, the directory above web/)
69
+ app/ Synced with core's templates by nextspark sync:app, which keeps
70
+ files the project customized and rebuilds the registries
71
+ next.config.mjs, tsconfig.json, i18n.ts, proxy.ts or middleware.ts
72
+ Also synced by sync:app, which keeps them when customized
73
+ core.version.json Written once everything above succeeded
74
+
75
+ What update-core itself never writes:
76
+ The rest of package.json (name, scripts, other dependencies)
77
+ contents/ The project's themes and plugins
78
+ .env* Environment files
79
+ Lifecycle scripts that pnpm install runs are not bound by this list.
80
+
81
+ The update stops before changing anything when the project isn't in a git
82
+ repository with a commit, when it has uncommitted changes or no committed
83
+ pnpm-lock.yaml, when a @nextsparkjs package isn't published at the target
84
+ version, when the target is older than the installed version, when package.json
85
+ takes a @nextsparkjs package from somewhere other than the registry, or when .env
86
+ doesn't set NEXT_PUBLIC_ACTIVE_THEME, which the registry build needs. When every
87
+ @nextsparkjs package is already installed and pinned exactly to the target, there
88
+ is nothing to do and nothing is changed; a ^ or ~ range is set to the exact
89
+ target like any other pin.
90
+
91
+ If a step fails, or the run is interrupted with Ctrl-C (SIGINT), SIGTERM or
92
+ SIGHUP, after it started changing the project, update-core stops the running
93
+ step (processes still running 5 s after the signal are killed), undoes nothing,
94
+ exits non-zero and prints what it got through, what git status shows and the
95
+ command that rolls the project back to the commit it started from:
96
+ git reset --hard <commit> && git clean -fd && rm -rf node_modules && pnpm install --frozen-lockfile
97
+ That command is also printed before the first change, for a run that is killed
98
+ without a chance to report (SIGKILL). The install or sync such a run was running
99
+ is then killed by a guard process, but not processes that left that step's
100
+ process group, not while the guard is stopped (SIGSTOP), and not at all if the
101
+ guard is killed too.
102
+ While update-core or its guard is stopped (SIGSTOP, or Ctrl-Z for update-core),
103
+ none of this is enforced: the step and the processes it started keep running,
104
+ with no 5 s limit, and what update-core reports can't be relied on.
105
+ The rollback can't bring back files .gitignore ignores that a lifecycle script
106
+ overwrote, nor git refs a script changed (a submodule is put back on the branch
107
+ it was on only if that branch still points at the commit the project records),
108
+ and removes untracked files created while the update ran.
109
+ `
110
+
111
+ /** Runs a command to completion, printing its output as it goes unless `capture` asks for it back. */
112
+ export function runCommand(command, args, { cwd, capture = false }) {
113
+ const result = spawnSync(command, args, {
114
+ cwd,
115
+ encoding: 'utf8',
116
+ stdio: capture ? ['ignore', 'pipe', 'pipe'] : 'inherit',
117
+ shell: process.platform === 'win32' && !path.isAbsolute(command),
118
+ })
119
+ return {
120
+ status: result.error ? null : result.status,
121
+ stdout: result.stdout ?? '',
122
+ stderr: result.stderr ?? '',
123
+ error: result.error ?? null,
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Orders two semver versions: negative when `a` comes first, positive when `b`
129
+ * does. A release comes after its prereleases, and prerelease identifiers
130
+ * compare numerically when both are numbers.
131
+ */
132
+ export function compareVersions(a, b) {
133
+ const left = a.match(VERSION_PATTERN)
134
+ const right = b.match(VERSION_PATTERN)
135
+ for (let i = 1; i <= 3; i++) {
136
+ const difference = Number(left[i]) - Number(right[i])
137
+ if (difference !== 0) return Math.sign(difference)
138
+ }
139
+ if (!left[4] || !right[4]) return left[4] ? -1 : right[4] ? 1 : 0
140
+
141
+ const leftIds = left[4].split('.')
142
+ const rightIds = right[4].split('.')
143
+ for (let i = 0; i < Math.max(leftIds.length, rightIds.length); i++) {
144
+ if (leftIds[i] === undefined) return -1
145
+ if (rightIds[i] === undefined) return 1
146
+ const leftNumeric = /^\d+$/.test(leftIds[i])
147
+ const rightNumeric = /^\d+$/.test(rightIds[i])
148
+ if (leftNumeric && rightNumeric) {
149
+ const difference = Number(leftIds[i]) - Number(rightIds[i])
150
+ if (difference !== 0) return Math.sign(difference)
151
+ } else if (leftNumeric !== rightNumeric) {
152
+ return leftNumeric ? -1 : 1
153
+ } else if (leftIds[i] !== rightIds[i]) {
154
+ return leftIds[i] < rightIds[i] ? -1 : 1
155
+ }
156
+ }
157
+ return 0
158
+ }
159
+
160
+ function isVersion(value) {
161
+ return typeof value === 'string' && VERSION_PATTERN.test(value)
162
+ }
163
+
164
+ /** A spec with a protocol or a path takes the package from somewhere other than the registry. */
165
+ function takesFromRegistry(spec) {
166
+ return !spec.includes(':') && !spec.includes('/')
167
+ }
168
+
169
+ function parseArguments(args) {
170
+ const flags = { version: null, list: false, check: false, current: false, help: false, branch: false }
171
+ const problems = []
172
+
173
+ for (let i = 0; i < args.length; i++) {
174
+ const arg = args[i]
175
+ if (arg === '--version' || arg === '-v') {
176
+ const value = args[i + 1]
177
+ if (value === undefined || value.startsWith('-')) {
178
+ problems.push(`${arg} needs a version, e.g. ${arg} 0.1.0-beta.189`)
179
+ } else {
180
+ flags.version = value
181
+ i++
182
+ }
183
+ } else if (arg.startsWith('--version=')) {
184
+ flags.version = arg.slice('--version='.length)
185
+ } else if (arg === '--latest') {
186
+ flags.version = null
187
+ } else if (arg === '--list') {
188
+ flags.list = true
189
+ } else if (arg === '--check') {
190
+ flags.check = true
191
+ } else if (arg === '--current') {
192
+ flags.current = true
193
+ } else if (arg === '--branch' || arg === '-b') {
194
+ flags.branch = true
195
+ } else if (arg === '--help' || arg === '-h') {
196
+ flags.help = true
197
+ } else {
198
+ problems.push(`Unknown option: ${arg}`)
199
+ }
200
+ }
201
+
202
+ if (flags.version !== null) {
203
+ flags.version = flags.version.replace(/^v/, '')
204
+ if (!isVersion(flags.version)) problems.push(`Not a version: ${flags.version}`)
205
+ }
206
+
207
+ return { flags, problems }
208
+ }
209
+
210
+ function readJson(file) {
211
+ try {
212
+ return JSON.parse(fs.readFileSync(file, 'utf8'))
213
+ } catch {
214
+ return null
215
+ }
216
+ }
217
+
218
+ function readOptional(file) {
219
+ try {
220
+ return fs.readFileSync(file)
221
+ } catch {
222
+ return null
223
+ }
224
+ }
225
+
226
+ /** The version of `name` in the project's node_modules, or null. */
227
+ function installedVersion(cwd, name) {
228
+ return readJson(path.join(cwd, 'node_modules', name, 'package.json'))?.version ?? null
229
+ }
230
+
231
+ /** The project's package.json and the @nextsparkjs packages it declares, or why it can't be updated. */
232
+ function readProject(cwd) {
233
+ let manifestText
234
+ try {
235
+ manifestText = fs.readFileSync(path.join(cwd, 'package.json'), 'utf8')
236
+ } catch {
237
+ return { problem: 'No package.json here. Run update-core from the project root.' }
238
+ }
239
+
240
+ let manifest
241
+ try {
242
+ manifest = JSON.parse(manifestText)
243
+ } catch (error) {
244
+ return { problem: `package.json is not valid JSON: ${error.message}` }
245
+ }
246
+
247
+ if (readJson(path.join(cwd, 'packages', 'core', 'package.json'))?.name === CORE_PACKAGE) {
248
+ return { problem: 'This is the NextSpark monorepo, where core is developed. update-core updates a project that installs @nextsparkjs/core.' }
249
+ }
250
+
251
+ const packages = []
252
+ for (const field of DEPENDENCY_FIELDS) {
253
+ for (const [name, spec] of Object.entries(manifest[field] ?? {})) {
254
+ if (name.startsWith(SCOPE)) packages.push({ field, name, spec })
255
+ }
256
+ }
257
+
258
+ if (!packages.some(({ name }) => name === CORE_PACKAGE)) {
259
+ if (fs.existsSync(path.join(cwd, 'core'))) {
260
+ return {
261
+ problem: 'This project keeps the framework in core/, the layout from before NextSpark shipped as npm packages, and update-core only updates projects that install @nextsparkjs/core. Nothing was changed.\n' +
262
+ ' To move it over, create a project with create-nextspark-app and bring your contents/ across.',
263
+ }
264
+ }
265
+ return { problem: `package.json doesn't depend on ${CORE_PACKAGE}, so there is nothing for update-core to update.` }
266
+ }
267
+
268
+ return { manifestText, manifest, packages, installed: installedVersion(cwd, CORE_PACKAGE) }
269
+ }
270
+
271
+ /** Whatever `pnpm view <spec> <field> --json` prints, parsed, or the error it gave. */
272
+ function viewRegistry(run, cwd, spec, field) {
273
+ const result = run('pnpm', ['view', spec, field, '--json'], { cwd, capture: true })
274
+ if (result.error?.code === 'ENOENT') return { error: 'pnpm is not installed or not on PATH' }
275
+ if (result.status !== 0) {
276
+ return { error: (result.stderr || result.stdout || `pnpm view exited with ${result.status}`).trim() }
277
+ }
278
+ try {
279
+ return { value: JSON.parse(result.stdout) }
280
+ } catch {
281
+ return { error: `pnpm view ${spec} ${field} printed something other than JSON` }
282
+ }
283
+ }
284
+
285
+ function publishedVersions(run, cwd, name) {
286
+ const { value, error } = viewRegistry(run, cwd, name, 'versions')
287
+ if (error) return { error }
288
+ return { versions: (Array.isArray(value) ? value : [value]).filter(isVersion) }
289
+ }
290
+
291
+ function latestVersion(run, cwd) {
292
+ const { value, error } = viewRegistry(run, cwd, CORE_PACKAGE, 'dist-tags')
293
+ if (error) return { error }
294
+ if (!isVersion(value?.latest)) return { error: `npm has no latest tag for ${CORE_PACKAGE}` }
295
+ return { version: value.latest }
296
+ }
297
+
298
+ function realPath(file) {
299
+ try {
300
+ return fs.realpathSync(file)
301
+ } catch {
302
+ return path.resolve(file)
303
+ }
304
+ }
305
+
306
+ /**
307
+ * Where the project stands in git: the commit and branch it is on, whether
308
+ * anything is uncommitted, whether the commit has submodules and, for each
309
+ * checked-out submodule (nested ones too) that is on a branch, its path from
310
+ * `cwd`, the branch and the commit, or why git couldn't list them. Changes
311
+ * inside a submodule count as uncommitted even where .gitmodules or the git
312
+ * config tells git status to ignore them, since the rollback resets submodules
313
+ * too; so on a clean tree a submodule's commit is the one the project records
314
+ * for it.
315
+ */
316
+ function gitState(run, cwd) {
317
+ const top = run('git', ['rev-parse', '--show-toplevel'], { cwd, capture: true })
318
+ if (top.status !== 0) return { repo: false }
319
+
320
+ const head = run('git', ['rev-parse', '--verify', '--quiet', 'HEAD'], { cwd, capture: true })
321
+ const status = run('git', ['status', '--porcelain=v1', '--ignore-submodules=none'], { cwd, capture: true })
322
+ const branch = run('git', ['symbolic-ref', '--quiet', '--short', 'HEAD'], { cwd, capture: true })
323
+ const gitmodules = run('git', ['ls-files', '--', ':/.gitmodules'], { cwd, capture: true })
324
+ const submodules = gitmodules.status !== 0 || gitmodules.stdout.trim() !== ''
325
+ const onBranch = []
326
+ let submodulesUnlisted = null
327
+ if (submodules) {
328
+ const listed = run('git', ['submodule', 'foreach', '--quiet', '--recursive', 'printf "%s\\0%s\\0%s\\0" "$displaypath" "$(git symbolic-ref --quiet HEAD)" "$(git rev-parse HEAD)"'], { cwd, capture: true })
329
+ if (listed.status !== 0) {
330
+ submodulesUnlisted = `git submodule foreach ${listed.error ? listed.error.message : `exited ${listed.status}`}: ${(listed.stderr || listed.stdout).trim()}`
331
+ } else {
332
+ const fields = listed.stdout.split('\0')
333
+ for (let i = 0; i + 2 < fields.length; i += 3) {
334
+ if (fields[i + 1]) onBranch.push({ path: fields[i], ref: fields[i + 1], commit: fields[i + 2] })
335
+ }
336
+ }
337
+ }
338
+ return {
339
+ repo: true,
340
+ top: top.stdout.trim(),
341
+ head: head.status === 0 ? head.stdout.trim() : null,
342
+ dirty: status.status !== 0 || status.stdout.trim() !== '',
343
+ branch: branch.status === 0 ? branch.stdout.trim() : null,
344
+ submodules,
345
+ submodulesOnBranch: onBranch,
346
+ submodulesUnlisted,
347
+ }
348
+ }
349
+
350
+ function branchName(version) {
351
+ return `update/${version.replace(/[.+]/g, '-')}`
352
+ }
353
+
354
+ function branchExists(run, cwd, name) {
355
+ return run('git', ['rev-parse', '--verify', '--quiet', `refs/heads/${name}`], { cwd, capture: true }).status === 0
356
+ }
357
+
358
+ /** `value` as one shell word. */
359
+ function shellWord(value) {
360
+ return /^[\w./@%+=:,-]+$/.test(value) ? value : `'${value.replace(/'/g, "'\\''")}'`
361
+ }
362
+
363
+ /**
364
+ * The directory the project's pnpm install runs in: the nearest one from `cwd`
365
+ * up to the top of the repository with a pnpm-lock.yaml, which for a web-mobile
366
+ * project is the one above web/, or null when there is none. Run from a web/
367
+ * that carries a pnpm-workspace.yaml of its own, pnpm would take that file for
368
+ * the whole workspace and install web/ on its own.
369
+ */
370
+ function installRoot(cwd, top) {
371
+ const stop = realPath(top)
372
+ for (let dir = realPath(cwd); ; dir = path.dirname(dir)) {
373
+ if (fs.existsSync(path.join(dir, LOCKFILE))) return dir
374
+ if (dir === stop || path.dirname(dir) === dir) return null
375
+ }
376
+ }
377
+
378
+ /**
379
+ * The command that puts the project back at the commit the update started from.
380
+ * The tree was clean then, so resetting tracked files and removing untracked ones
381
+ * undoes whatever the update and the scripts it ran wrote in the repository;
382
+ * `git clean` only cleans below the directory it runs in, so from web/ in a
383
+ * web-mobile project it is pointed at the top of the repository. Neither goes
384
+ * into submodules: when the commit has any, each checked-out submodule is reset
385
+ * to its own HEAD, checked out again at the commit the project records for it
386
+ * if a script moved it or deleted its worktree (which leaves it detached;
387
+ * --checkout overrides an update strategy such as merge that a submodule's
388
+ * config sets), and cleaned. A submodule that was on a branch when the update
389
+ * started is put back on that branch if the branch still points at the recorded
390
+ * commit; one whose branch moved, say with a commit made inside it, stays
391
+ * detached at the recorded commit, and its branch isn't touched. That step only
392
+ * runs where the submodule's .git is: in a directory without one, `git -C`
393
+ * would find the project's own repository. With --branch,
394
+ * the update branch is deleted with update-ref, which also succeeds when the run
395
+ * stopped before creating it. node_modules is removed before installing because
396
+ * an install that was stopped can leave it holding the new versions while the
397
+ * lockfile still names the old ones, and pnpm install then finds nothing to do;
398
+ * the install is frozen to the commit's lockfile, so it fails instead of
399
+ * rewriting a lockfile that doesn't match the commit's package.json.
400
+ */
401
+ function rollbackCommand(git, { cwd, root, branch }) {
402
+ const commit = git.head.slice(0, 12)
403
+ const project = realPath(cwd)
404
+ const commands = [`git reset --hard ${commit}`]
405
+ if (git.submodules) commands.push('git submodule foreach --recursive git reset --hard', 'git submodule update --checkout --recursive')
406
+ for (const submodule of git.submodulesOnBranch ?? []) {
407
+ const dir = shellWord(submodule.path)
408
+ const ref = shellWord(submodule.ref)
409
+ // Exits 0 either way: the if leaves a submodule whose branch moved detached
410
+ commands.push(`sh -c ${shellWord(`if test -e ${shellWord(`${submodule.path}/.git`)} && test "$(git -C ${dir} rev-parse --quiet --verify ${ref})" = ${submodule.commit}; then git -C ${dir} symbolic-ref HEAD ${ref}; fi`)}`)
411
+ }
412
+ commands.push(realPath(git.top) === project ? 'git clean -fd' : 'git clean -fd :/')
413
+ if (git.submodules) commands.push('git submodule foreach --recursive git clean -fd')
414
+ if (branch) {
415
+ commands.push(git.branch ? `git checkout ${shellWord(git.branch)}` : `git checkout --detach ${commit}`, `git update-ref -d refs/heads/${branch}`)
416
+ }
417
+ const fromProject = path.relative(project, root)
418
+ commands.push(
419
+ `rm -rf ${[...(fromProject ? [path.join(fromProject, 'node_modules')] : []), 'node_modules'].map(shellWord).join(' ')}`,
420
+ fromProject ? `pnpm --dir ${shellWord(fromProject)} install --frozen-lockfile` : 'pnpm install --frozen-lockfile',
421
+ )
422
+ return commands.join(' && ')
423
+ }
424
+
425
+ /** package.json text with `manifest` in it, keeping the indentation and line endings it had. */
426
+ function manifestText(manifest, original) {
427
+ const indent = original.match(/^[ \t]+(?=")/m)?.[0] ?? 2
428
+ let text = JSON.stringify(manifest, null, indent)
429
+ if (original.endsWith('\n')) text += '\n'
430
+ if (original.includes('\r\n')) text = text.replace(/\r?\n/g, '\r\n')
431
+ return text
432
+ }
433
+
434
+ /**
435
+ * The entry point of the nextspark CLI installed in the project. Running it with
436
+ * node skips `pnpm exec`, which checks the install against the lockfile first
437
+ * and, under pnpm 11, fetches the registry metadata of every package to do so.
438
+ */
439
+ function installedCli(cwd) {
440
+ const cliDir = path.join(cwd, 'node_modules', CLI_PACKAGE)
441
+ const bin = readJson(path.join(cliDir, 'package.json'))?.bin
442
+ const entry = typeof bin === 'string' ? bin : bin?.nextspark
443
+ return entry ? path.join(cliDir, entry) : null
444
+ }
445
+
446
+ /**
447
+ * Whether the registry build sync:app runs has a theme to build: it reads
448
+ * NEXT_PUBLIC_ACTIVE_THEME from the project's .env or the environment, and
449
+ * without one it skips the build and still exits 0.
450
+ */
451
+ function hasActiveTheme(cwd, env) {
452
+ if (env.NEXT_PUBLIC_ACTIVE_THEME) return true
453
+ const text = readOptional(path.join(cwd, '.env'))?.toString('utf8') ?? ''
454
+ const lines = text.split(/\r?\n/).filter((line) => /^\s*(?:export\s+)?NEXT_PUBLIC_ACTIVE_THEME\s*=/.test(line))
455
+ if (lines.length === 0) return false
456
+ const raw = lines.at(-1).slice(lines.at(-1).indexOf('=') + 1).trim()
457
+ const quoted = raw.match(/^(["'`])(.*?)\1/)
458
+ const value = quoted ? quoted[2] : raw.replace(/\s+#.*$/, '').trim()
459
+ return value !== ''
460
+ }
461
+
462
+ function coreMigrations(cwd) {
463
+ try {
464
+ return fs.readdirSync(path.join(cwd, 'node_modules', CORE_PACKAGE, 'migrations')).filter((file) => file.endsWith('.sql'))
465
+ } catch {
466
+ return []
467
+ }
468
+ }
469
+
470
+ /** Sends `signal` to every process of the group a step runs in. */
471
+ function signalGroup(pid, signal) {
472
+ if (process.platform === 'win32') {
473
+ // Windows has no process groups: taskkill /T ends the step's process tree
474
+ spawnSync('taskkill', ['/pid', String(pid), '/T', '/F'], { stdio: 'ignore', timeout: 10_000 })
475
+ return
476
+ }
477
+ try {
478
+ process.kill(-pid, signal)
479
+ } catch {
480
+ // ESRCH: nothing left in the group
481
+ }
482
+ }
483
+
484
+ /**
485
+ * Whether any process of a step's group may still be running. EPERM counts as
486
+ * running: a process in the group can't be signalled, which on macOS is also
487
+ * the answer while its processes have exited and wait to be reaped.
488
+ */
489
+ function groupRunning(pid) {
490
+ if (process.platform === 'win32') return false
491
+ try {
492
+ process.kill(-pid, 0)
493
+ return true
494
+ } catch (error) {
495
+ return error.code === 'EPERM'
496
+ }
497
+ }
498
+
499
+ const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
500
+
501
+ /**
502
+ * `promise`'s value, or `fallback` once `ms` pass. The timer is cleared as soon
503
+ * as either settles, so a wait that ended early doesn't keep update-core running.
504
+ */
505
+ function within(promise, ms, fallback) {
506
+ let timer
507
+ const timeout = new Promise((resolve) => { timer = setTimeout(resolve, Math.max(0, ms), fallback) })
508
+ return Promise.race([promise, timeout]).finally(() => clearTimeout(timer))
509
+ }
510
+
511
+ /** Waits until the group is empty or `deadline` passes, and says whether it emptied. */
512
+ async function groupGone(pid, deadline) {
513
+ while (Date.now() < deadline) {
514
+ if (!groupRunning(pid)) return true
515
+ await pause(Math.min(100, Math.max(0, deadline - Date.now())))
516
+ }
517
+ return !groupRunning(pid)
518
+ }
519
+
520
+ /**
521
+ * Watches for the signals an update reports on while it writes. The first one
522
+ * is passed on to the running step's group, if any, and kept for the report,
523
+ * and from it the step's processes have STOP_GRACE_MS to exit. A repeat changes
524
+ * nothing: `pnpm update-core` passes on the signals it gets, so the Ctrl-C a
525
+ * terminal sends to both can arrive twice.
526
+ */
527
+ function watchSignals() {
528
+ let interrupt
529
+ const watch = {
530
+ signal: null,
531
+ /** When the processes of a step stopped by the signal get killed */
532
+ deadline: null,
533
+ /** The pid of the running step, which is its group's id, once known */
534
+ step: null,
535
+ interrupted: new Promise((resolve) => { interrupt = resolve }),
536
+ }
537
+ const onSignal = (signal) => {
538
+ if (watch.signal) return
539
+ watch.signal = signal
540
+ watch.deadline = Date.now() + STOP_GRACE_MS
541
+ if (watch.step) signalGroup(watch.step, signal)
542
+ interrupt()
543
+ }
544
+ for (const signal of HANDLED_SIGNALS) process.on(signal, onSignal)
545
+ watch.stop = () => {
546
+ for (const signal of HANDLED_SIGNALS) process.off(signal, onSignal)
547
+ }
548
+ return watch
549
+ }
550
+
551
+ /**
552
+ * Stops what is left of a step's group: sends `signal` unless it was already
553
+ * sent, waits for the group to empty until `deadline`, and then kills it.
554
+ * Returns how it stopped: 'exited' or 'killed', and whether anything was still
555
+ * running after being killed.
556
+ */
557
+ async function stopGroup(pid, { signal, deadline }) {
558
+ if (signal) signalGroup(pid, signal)
559
+ if (await groupGone(pid, deadline)) return { stop: 'exited', leftRunning: false }
560
+ signalGroup(pid, 'SIGKILL')
561
+ return { stop: 'killed', leftRunning: !(await groupGone(pid, Date.now() + KILL_WAIT_MS)) }
562
+ }
563
+
564
+ /**
565
+ * Runs a step's command and resolves once it is done with it: when it exited
566
+ * and its group emptied, or, when the run was interrupted, when its group
567
+ * emptied or its processes were killed STOP_GRACE_MS after the signal, without
568
+ * waiting for the command itself to exit. The command runs through
569
+ * step-guard.mjs, in a process group of its own that also holds the lifecycle
570
+ * scripts pnpm install starts: when the command exits, failing or not,
571
+ * whatever is still left of its group after SETTLE_MS gets SIGTERM and is
572
+ * killed STOP_GRACE_MS later, and if update-core is killed before it is done
573
+ * with the step, the guard kills the group. The guard starts the command only
574
+ * once it is told 'start', which is sent once the group's id is known and no
575
+ * signal has come, so a guard that dies before reporting the pid leaves no
576
+ * command running (except on Windows, which runs the command at once). None of
577
+ * this reaches processes that leave the group, and none of it runs while
578
+ * update-core or the guard is stopped (SIGSTOP), or once both are gone. Ctrl-C
579
+ * reaches update-core only, which passes it on. The step's stdin is closed: from
580
+ * a background group, reading the terminal would stop it instead of failing.
581
+ * `started` in the result says whether the command may have started.
582
+ */
583
+ async function runStep(command, args, { cwd, signals }) {
584
+ const guard = spawn(process.execPath, [STEP_GUARD, command, ...args], {
585
+ cwd,
586
+ stdio: ['ignore', 'inherit', 'inherit', 'ipc'],
587
+ detached: process.platform !== 'win32',
588
+ })
589
+
590
+ let started
591
+ let finished
592
+ let commandStarted = process.platform === 'win32'
593
+ const startedStep = new Promise((resolve) => { started = resolve })
594
+ const exited = new Promise((resolve) => { finished = resolve })
595
+ guard.on('message', (message) => {
596
+ if (message?.pid) {
597
+ signals.step = message.pid
598
+ if (signals.signal) {
599
+ // A signal that came before the pid did is passed on now, and the command is never started
600
+ signalGroup(message.pid, signals.signal)
601
+ } else {
602
+ guard.send('start', () => {})
603
+ commandStarted = true
604
+ }
605
+ started(message.pid)
606
+ } else if (message?.exit) {
607
+ finished({ status: message.exit.code, signal: message.exit.signal, error: null })
608
+ } else if (message?.error) {
609
+ started(null)
610
+ finished({ status: null, signal: null, error: message.error })
611
+ }
612
+ })
613
+ const guardGone = new Promise((resolve) => {
614
+ guard.on('error', (error) => resolve({ status: null, signal: null, error }))
615
+ guard.once('exit', (code, signal) => resolve({ status: null, signal: null, error: { message: `its guard process exited (${signal ?? `exit ${code}`})` } }))
616
+ })
617
+ guardGone.then((result) => {
618
+ started(null)
619
+ finished(result)
620
+ })
621
+
622
+ // A signal waits for the step's pid only until its deadline
623
+ const pid = await Promise.race([
624
+ startedStep,
625
+ signals.interrupted.then(() => within(startedStep, signals.deadline - Date.now(), undefined)),
626
+ ])
627
+ if (pid === undefined) {
628
+ // Without the pid the step's group is out of reach: closing the channel has the guard kill it, if the guard can still run
629
+ guard.kill('SIGCONT')
630
+ guard.disconnect()
631
+ if (!(await within(guardGone.then(() => true), KILL_WAIT_MS, false))) guard.kill('SIGKILL')
632
+ signals.step = null
633
+ return { status: null, signal: null, error: null, stop: 'unreached', leftRunning: false, started: commandStarted }
634
+ }
635
+
636
+ const result = await Promise.race([exited, signals.interrupted.then(() => null)])
637
+ let outcome = { stop: null, leftRunning: false }
638
+ if (pid !== null) {
639
+ if (signals.signal) {
640
+ outcome = await stopGroup(pid, { deadline: signals.deadline })
641
+ } else if (!(await groupGone(pid, Date.now() + SETTLE_MS))) {
642
+ // The command exited, failing or not, and left processes running that could still write
643
+ outcome = await stopGroup(pid, { signal: 'SIGTERM', deadline: Math.min(Date.now() + STOP_GRACE_MS, signals.deadline ?? Infinity) })
644
+ }
645
+ }
646
+ signals.step = null
647
+ guard.send('release', () => {})
648
+ if (!(await within(guardGone.then(() => true), KILL_WAIT_MS, false))) guard.kill('SIGKILL')
649
+ return { ...(result ?? { status: null, signal: null, error: null }), ...outcome, started: commandStarted }
650
+ }
651
+
652
+ function describeExit(result) {
653
+ if (result.error) return result.error.code === 'ENOENT' ? 'not found on PATH' : result.error.message
654
+ return result.signal ? `killed by ${result.signal}` : `exit ${result.status}`
655
+ }
656
+
657
+ function listReleases({ run, cwd, installed, out, err }) {
658
+ const published = publishedVersions(run, cwd, CORE_PACKAGE)
659
+ if (published.error) {
660
+ err(`Could not read the published versions of ${CORE_PACKAGE}:\n${published.error}`)
661
+ return 1
662
+ }
663
+ const latest = latestVersion(run, cwd).version
664
+
665
+ const versions = [...published.versions].sort(compareVersions).reverse()
666
+ out(`Published versions of ${CORE_PACKAGE}:\n`)
667
+ for (const version of versions.slice(0, LIST_LIMIT)) {
668
+ const marks = [version === latest && 'latest', version === installed && 'installed'].filter(Boolean)
669
+ out(` ${version}${marks.length > 0 ? ` (${marks.join(', ')})` : ''}`)
670
+ }
671
+ if (versions.length > LIST_LIMIT) out(` ... and ${versions.length - LIST_LIMIT} older`)
672
+ return 0
673
+ }
674
+
675
+ function checkUpdates({ run, cwd, installed, out, err }) {
676
+ const latest = latestVersion(run, cwd)
677
+ if (latest.error) {
678
+ err(`Could not read the latest version of ${CORE_PACKAGE}:\n${latest.error}`)
679
+ return 1
680
+ }
681
+ out(`Installed: ${installed ?? 'not installed'}`)
682
+ out(`Latest: ${latest.version}`)
683
+ if (!installed || compareVersions(latest.version, installed) > 0) {
684
+ out('\nUpdate available. Run: pnpm update-core')
685
+ } else {
686
+ out("\nYou're up to date.")
687
+ }
688
+ return 0
689
+ }
690
+
691
+ function banner(out, title) {
692
+ out('\n========================================')
693
+ out(` ${title}`)
694
+ out('========================================')
695
+ }
696
+
697
+ /**
698
+ * The report of an update that stopped after it started writing: what stopped
699
+ * it, how the processes of the step it stopped ended, the steps it finished and
700
+ * the ones it never reached, what git status shows now, and the rollback.
701
+ * Returns the exit code, 128 plus the signal's number for an interrupted run.
702
+ */
703
+ function stoppedReport({ run, cwd, err, target, rollback, signal, step, started, reason, stop, leftRunning, done, notReached }) {
704
+ const grace = `${STOP_GRACE_MS / 1000} s`
705
+ banner(err, `Update to ${target} did not finish`)
706
+ err('')
707
+ if (signal) err(` Interrupted by ${signal} ${started ? 'during' : 'before'}: ${step}`)
708
+ else err(` Failed ${started ? 'during' : 'before'}: ${step} (${reason})`)
709
+ // "during" only means the command may have started, so these speak of the step's process group, not of the command.
710
+ // Windows has no process groups to check: taskkill ran after the signal, and its result isn't known.
711
+ if (signal && started && stop === 'exited') {
712
+ err(process.platform === 'win32'
713
+ ? ` taskkill /T /F was run on ${step} and the processes under it after the signal; whether they stopped isn't checked.`
714
+ : ` No process in the process group of ${step} was left running after the signal.`)
715
+ }
716
+ if (signal && started && stop === 'killed') err(` Processes in the process group of ${step} were still running ${grace} after the signal, and were killed with SIGKILL.`)
717
+ if (signal && stop === 'unreached') {
718
+ err(started
719
+ ? ` The process that runs ${step} didn't say which process group it runs in within ${grace} of the signal, and was stopped; ${step} may still be running and write to the project.`
720
+ : ` The process that runs ${step} didn't say which process group it runs in within ${grace} of the signal, and was stopped before it started ${step}.`)
721
+ }
722
+ if (!signal && stop === 'exited') err(' Processes it started were still running after it exited, and exited after SIGTERM.')
723
+ if (!signal && stop === 'killed') err(` Processes it started were still running after it exited and ${grace} after SIGTERM, and were killed with SIGKILL.`)
724
+ if (leftRunning) err(' Some of its processes were still running after being killed, and may still write to the project.')
725
+ if (done.length > 0) {
726
+ err('\n Done before that:')
727
+ for (const line of done) err(` - ${line}`)
728
+ }
729
+ if (notReached.length > 0) {
730
+ err('\n Not reached:')
731
+ for (const line of notReached) err(` - ${line}`)
732
+ }
733
+
734
+ err('\n Nothing was undone. A step that stopped may have written part of its work, and the')
735
+ err(' lifecycle scripts pnpm install runs can write anywhere in the project.')
736
+ const status = run('git', ['status', '--short', '--ignore-submodules=none'], { cwd, capture: true })
737
+ if (status.status !== 0) {
738
+ err(` git status failed: ${(status.stderr || status.stdout).trim()}`)
739
+ } else {
740
+ const lines = status.stdout.split('\n').filter(Boolean)
741
+ if (lines.length === 0) {
742
+ err(' git status shows no changes.')
743
+ } else {
744
+ err(' git status now (files .gitignore ignores, like node_modules, are not listed):')
745
+ for (const line of lines.slice(0, STATUS_SHOWN)) err(` ${line}`)
746
+ if (lines.length > STATUS_SHOWN) err(` ... and ${lines.length - STATUS_SHOWN} more (git status lists them all)`)
747
+ }
748
+ }
749
+
750
+ err('\n To put the project back at the commit the update started from, run here:')
751
+ err(` ${rollback}`)
752
+ err(` Then ${signal ? '' : 'fix what failed and '}run update-core --version ${target} again.`)
753
+ err('')
754
+ return signal ? 128 + (os.constants.signals[signal] ?? 0) : 1
755
+ }
756
+
757
+ /**
758
+ * Runs `update-core` with `args` against the project in `cwd` and resolves to
759
+ * the exit code. `run` executes the commands that only read and `out`/`err`
760
+ * print, so a test can hand in its own.
761
+ */
762
+ export async function updateCore(args, { cwd = process.cwd(), env = process.env, run = runCommand, out = console.log, err = console.error, now = () => new Date() } = {}) {
763
+ const { flags, problems } = parseArguments(args)
764
+ if (problems.length > 0) {
765
+ for (const problem of problems) err(problem)
766
+ err('Run pnpm update-core --help for the options.')
767
+ return 1
768
+ }
769
+ if (flags.help) {
770
+ out(HELP)
771
+ return 0
772
+ }
773
+
774
+ const project = readProject(cwd)
775
+ if (project.problem) {
776
+ err(`update-core: ${project.problem}`)
777
+ return 1
778
+ }
779
+ const { manifest, packages, installed } = project
780
+
781
+ if (flags.current) {
782
+ const spec = packages.find(({ name }) => name === CORE_PACKAGE).spec
783
+ out(installed ? `${CORE_PACKAGE} ${installed}` : `${CORE_PACKAGE} is not installed (package.json asks for ${spec})`)
784
+ return 0
785
+ }
786
+ if (flags.list) return listReleases({ run, cwd, installed, out, err })
787
+ if (flags.check) return checkUpdates({ run, cwd, installed, out, err })
788
+
789
+ out('\n========================================')
790
+ out(' NextSpark Core Updater')
791
+ out('========================================\n')
792
+
793
+ // Every check comes before the first write
794
+ out('[1/5] Checking the project...')
795
+
796
+ const fromElsewhere = packages.filter(({ spec }) => !takesFromRegistry(spec))
797
+ if (fromElsewhere.length > 0) {
798
+ err(' package.json takes these from somewhere other than the registry, so there is no version for update-core to set:')
799
+ for (const { name, spec } of fromElsewhere) err(` ${name}: ${spec}`)
800
+ err(' Nothing was changed.')
801
+ return 1
802
+ }
803
+
804
+ if (!packages.some(({ name }) => name === CLI_PACKAGE)) {
805
+ err(` update-core syncs app/ through the nextspark CLI, and package.json doesn't depend on ${CLI_PACKAGE}. Nothing was changed.`)
806
+ return 1
807
+ }
808
+
809
+ if (!installed) {
810
+ err(` node_modules has no ${CORE_PACKAGE}. Run pnpm install first, so the update starts from a known version. Nothing was changed.`)
811
+ return 1
812
+ }
813
+
814
+ const git = gitState(run, cwd)
815
+ if (!git.repo || !git.head) {
816
+ err(` update-core needs the project in a git repository with a commit to start from: an update that fails or is interrupted is rolled back by returning to that commit. ${git.repo ? 'Make a first commit' : 'Run git init and commit the project'} first. Nothing was changed.`)
817
+ return 1
818
+ }
819
+ if (git.dirty) {
820
+ err(' Uncommitted changes. Commit or stash them first: an update that fails or is interrupted is rolled back by returning to the commit it started from, which would discard them. Nothing was changed.')
821
+ return 1
822
+ }
823
+ if (git.submodulesUnlisted) {
824
+ err(` Could not list the branches the submodules are on, which the rollback needs to put them back on: ${git.submodulesUnlisted}. Nothing was changed.`)
825
+ return 1
826
+ }
827
+
828
+ // The rollback installs exactly what the commit's lockfile records, so the commit needs one
829
+ const root = installRoot(cwd, git.top)
830
+ const lockfile = root && run('git', ['ls-files', '--error-unmatch', '--', path.join(root, LOCKFILE)], { cwd, capture: true })
831
+ if (!root || lockfile.status !== 0) {
832
+ err(` ${root ? `${path.relative(cwd, path.join(root, LOCKFILE)) || LOCKFILE} isn't committed` : `No ${LOCKFILE} here or above, up to the top of the repository`}. An update that fails or is interrupted is rolled back by installing what the lockfile of the commit it started from records, so commit ${LOCKFILE} first. Nothing was changed.`)
833
+ return 1
834
+ }
835
+
836
+ if (!hasActiveTheme(cwd, env)) {
837
+ err(' NEXT_PUBLIC_ACTIVE_THEME is not set in .env, and sync:app skips the registry build without it, so app/(templates) would stay on the old core. Set it and run update-core again. Nothing was changed.')
838
+ return 1
839
+ }
840
+ out(` Installed: ${CORE_PACKAGE} ${installed}`)
841
+
842
+ let target = flags.version
843
+ if (!target) {
844
+ const latest = latestVersion(run, cwd)
845
+ if (latest.error) {
846
+ err(` Could not read the latest version of ${CORE_PACKAGE}:\n${latest.error}\n Nothing was changed.`)
847
+ return 1
848
+ }
849
+ target = latest.version
850
+ }
851
+ out(` Target: ${target}`)
852
+
853
+ if (compareVersions(target, installed) < 0) {
854
+ err(` ${target} is older than the installed ${installed}. update-core doesn't downgrade: app/ would be synced back and applied migrations can't be undone. Nothing was changed.`)
855
+ return 1
856
+ }
857
+
858
+ if (packages.every(({ name, spec }) => spec === target && installedVersion(cwd, name) === target)) {
859
+ out(`\n Already on ${target}: every @nextsparkjs package in package.json is pinned to it and node_modules holds it. Nothing was changed.\n`)
860
+ return 0
861
+ }
862
+
863
+ const missing = []
864
+ for (const { name } of packages) {
865
+ const published = publishedVersions(run, cwd, name)
866
+ if (published.error) {
867
+ err(` Could not read the published versions of ${name}:\n${published.error}\n Nothing was changed.`)
868
+ return 1
869
+ }
870
+ if (!published.versions.includes(target)) missing.push(name)
871
+ }
872
+ if (missing.length > 0) {
873
+ err(` Not published at ${target}: ${missing.join(', ')}. Nothing was changed.`)
874
+ return 1
875
+ }
876
+ out(` Published: all ${packages.length} @nextsparkjs packages at ${target}`)
877
+
878
+ const branch = flags.branch ? branchName(target) : null
879
+ if (branch && branchExists(run, cwd, branch)) {
880
+ err(` Branch ${branch} already exists. Delete it (git branch -D ${branch}) or update without --branch. Nothing was changed.`)
881
+ return 1
882
+ }
883
+
884
+ const rollback = rollbackCommand(git, { cwd, root, branch })
885
+ out(`\n Starting from commit ${git.head.slice(0, 12)}. If this run is killed before it can report (SIGKILL), roll back with:`)
886
+ out(` ${rollback}`)
887
+ out(' Killing this run also kills the install or sync it is running, unless its guard process is stopped or killed too, but not processes that left that step\'s process group.')
888
+ out(' The rollback removes files created in the repository while the update runs, and can\'t bring back ignored files, like .env, that a lifecycle script overwrites.')
889
+
890
+ // From here on the project changes, and whatever stops the run goes through stoppedReport
891
+ const signals = watchSignals()
892
+ const done = []
893
+ const steps = [
894
+ ...(branch ? [`create and switch to branch ${branch}`] : []),
895
+ 'set the @nextsparkjs versions in package.json',
896
+ 'pnpm install',
897
+ 'nextspark sync:app --force',
898
+ `write ${VERSION_FILE}`,
899
+ ]
900
+ let current = 0
901
+ const stopped = ({ reason = null, started = true, stop = null, leftRunning = false } = {}) => stoppedReport({
902
+ run, cwd, err, target, rollback, reason, started, stop, leftRunning,
903
+ signal: signals.signal,
904
+ step: steps[current],
905
+ done,
906
+ notReached: steps.slice(current + 1),
907
+ })
908
+ // A signal that came in while a synchronous write ran is only delivered on the next turn of the event loop
909
+ const signalled = async () => {
910
+ await new Promise((resolve) => setImmediate(resolve))
911
+ return signals.signal !== null
912
+ }
913
+
914
+ try {
915
+ if (branch) {
916
+ out(`\n Creating branch ${branch}...`)
917
+ const created = run('git', ['checkout', '-b', branch], { cwd, capture: true })
918
+ if (created.status !== 0) {
919
+ // git can have created the branch, or switched to it, before failing
920
+ const exit = created.error ? created.error.message : `exited ${created.status}`
921
+ return stopped({ reason: `git checkout -b ${exit}: ${(created.stderr || created.stdout).trim()}` })
922
+ }
923
+ done.push(`created and switched to branch ${branch}`)
924
+ current++
925
+ if (await signalled()) return stopped({ started: false })
926
+ }
927
+
928
+ out('\n[2/5] Setting the @nextsparkjs versions in package.json...')
929
+ const bumped = []
930
+ for (const { field, name, spec } of packages) {
931
+ if (spec !== target) {
932
+ manifest[field][name] = target
933
+ bumped.push(`${name} ${spec} -> ${target}`)
934
+ }
935
+ }
936
+ if (bumped.length > 0) {
937
+ fs.writeFileSync(path.join(cwd, 'package.json'), manifestText(manifest, project.manifestText))
938
+ for (const line of bumped) out(` ${line}`)
939
+ done.push(`package.json: ${bumped.join(', ')}`)
940
+ } else {
941
+ out(` Already set to ${target}`)
942
+ }
943
+ current++
944
+ if (await signalled()) return stopped({ started: false })
945
+
946
+ out('\n[3/5] Installing...')
947
+ const migrationsBefore = new Set(coreMigrations(cwd))
948
+ const install = await runStep('pnpm', ['install'], { cwd: root, signals })
949
+ if (signals.signal) return stopped({ started: install.started, stop: install.stop, leftRunning: install.leftRunning })
950
+ if (install.status !== 0) return stopped({ reason: describeExit(install), started: install.started, stop: install.stop, leftRunning: install.leftRunning })
951
+ if (install.leftRunning) return stopped({ reason: 'it exited 0, but processes it started were still running after being killed', stop: install.stop, leftRunning: true })
952
+ if (install.stop) out(` Processes pnpm install started were still running after it exited, and were ${install.stop === 'killed' ? 'killed' : 'stopped with SIGTERM'}.`)
953
+ const offTarget = packages
954
+ .map(({ name }) => ({ name, version: installedVersion(cwd, name) }))
955
+ .filter(({ version }) => version !== target)
956
+ if (offTarget.length > 0) {
957
+ return stopped({ reason: `it exited 0, but node_modules holds ${offTarget.map(({ name, version }) => `${name} ${version ?? '(missing)'}`).join(', ')} instead of ${target}` })
958
+ }
959
+ done.push(`pnpm install: every @nextsparkjs package installed at ${target}`)
960
+ current++
961
+
962
+ out('\n[4/5] Syncing app/ with core and rebuilding the registries...')
963
+ const cli = installedCli(cwd)
964
+ if (!cli) return stopped({ reason: `node_modules has no nextspark CLI from ${CLI_PACKAGE}` })
965
+ fs.rmSync(path.join(cwd, '.next'), { recursive: true, force: true })
966
+ done.push('.next removed')
967
+ if (await signalled()) return stopped({ started: false })
968
+ const sync = await runStep(process.execPath, [cli, 'sync:app', '--force'], { cwd, signals })
969
+ if (signals.signal) return stopped({ started: sync.started, stop: sync.stop, leftRunning: sync.leftRunning })
970
+ if (sync.status !== 0) return stopped({ reason: `${describeExit(sync)}${sync.started ? '; what it reported is above' : ''}`, started: sync.started, stop: sync.stop, leftRunning: sync.leftRunning })
971
+ if (sync.leftRunning) return stopped({ reason: 'it exited 0, but processes it started were still running after being killed', stop: sync.stop, leftRunning: true })
972
+ if (sync.stop) out(` Processes nextspark sync:app started were still running after it exited, and were ${sync.stop === 'killed' ? 'killed' : 'stopped with SIGTERM'}.`)
973
+ done.push('app/ synced with core and registries rebuilt by nextspark sync:app')
974
+ current++
975
+
976
+ if (await signalled()) return stopped({ started: false })
977
+
978
+ out('\n[5/5] Recording the version...')
979
+ const record = { version: target, previousVersion: installed, updatedAt: now().toISOString() }
980
+ fs.writeFileSync(path.join(cwd, VERSION_FILE), `${JSON.stringify(record, null, 2)}\n`)
981
+ out(` ${VERSION_FILE}: ${installed} -> ${target}`)
982
+
983
+ const newMigrations = coreMigrations(cwd).filter((file) => !migrationsBefore.has(file))
984
+
985
+ banner(out, 'Update Complete')
986
+ out(`\n ${CORE_PACKAGE} ${installed} -> ${target}`)
987
+ if (branch) out(` Branch: ${branch}`)
988
+ if (newMigrations.length > 0) out(` New core migrations: ${newMigrations.length}`)
989
+ out('\n Next steps:')
990
+ const nextSteps = ['Review: git status && git diff', 'Test: pnpm build && pnpm dev']
991
+ if (newMigrations.length > 0) nextSteps.push('Migrate: pnpm db:migrate')
992
+ nextSteps.push(branch ? `Commit on ${branch} and merge it` : 'Commit the update')
993
+ nextSteps.forEach((step, index) => out(` ${index + 1}. ${step}`))
994
+ out('')
995
+ out(` Roll back: ${rollback}`)
996
+ out('')
997
+ return 0
998
+ } catch (error) {
999
+ return stopped({ reason: error.message })
1000
+ } finally {
1001
+ signals.stop()
1002
+ }
1003
+ }