create-mithaq 0.1.1 → 0.2.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 (348) hide show
  1. package/LICENSES/openstatus-time-picker.txt +21 -0
  2. package/index.js +457 -63
  3. package/package.json +1 -1
  4. package/template/apps/{asas-platform → asas-backoffice}/package.json +5 -3
  5. package/template/apps/{asas-platform → asas-backoffice}/src/api.ts +1 -1
  6. package/template/apps/{asas-platform → asas-backoffice}/src/auth-frame.tsx +12 -12
  7. package/template/apps/asas-backoffice/src/components.ts +35 -0
  8. package/template/apps/asas-backoffice/src/first-user.ts +14 -0
  9. package/template/apps/{asas-platform → asas-backoffice}/src/links.ts +1 -1
  10. package/template/apps/{asas-platform → asas-backoffice}/src/locale-switch.tsx +7 -5
  11. package/template/apps/asas-backoffice/src/no-organization.tsx +5 -0
  12. package/template/apps/asas-backoffice/src/redirect.ts +1 -0
  13. package/template/apps/{asas-platform → asas-backoffice}/src/request-headers.ts +1 -1
  14. package/template/apps/asas-backoffice/src/routeTree.gen.ts +206 -0
  15. package/template/apps/asas-backoffice/src/router.tsx +18 -0
  16. package/template/apps/asas-backoffice/src/routes/(screens)/backoffice/files.ts +10 -0
  17. package/template/apps/{asas-platform/src/routes/(screens)/platform → asas-backoffice/src/routes/(screens)/backoffice}/screen.json +426 -64
  18. package/template/apps/{asas-platform/src/routes/(screens)/platform → asas-backoffice/src/routes/(screens)/backoffice}/types.ts +18 -0
  19. package/template/apps/{asas-platform/src/routes/(screens)/platform.$.tsx → asas-backoffice/src/routes/(screens)/backoffice.$.tsx} +4 -4
  20. package/template/apps/{asas-platform/src/routes/(screens)/platform.index.tsx → asas-backoffice/src/routes/(screens)/backoffice.index.tsx} +3 -3
  21. package/template/apps/{asas-platform/src/routes/(screens)/platform.tsx → asas-backoffice/src/routes/(screens)/backoffice.tsx} +3 -3
  22. package/template/apps/{asas-platform → asas-backoffice}/src/routes/__root.tsx +2 -2
  23. package/template/apps/asas-backoffice/src/routes/api/$.ts +34 -0
  24. package/template/apps/asas-backoffice/src/routes/auth.create-first-user.tsx +113 -0
  25. package/template/apps/{asas-platform → asas-backoffice}/src/routes/auth.sign-in.tsx +9 -4
  26. package/template/apps/{asas-platform → asas-backoffice}/src/routes/unauthorized.tsx +24 -12
  27. package/template/apps/{asas-platform → asas-backoffice}/src/screen-form.tsx +5 -3
  28. package/template/apps/{asas-platform → asas-backoffice}/src/screen-list.tsx +6 -5
  29. package/template/apps/{asas-platform → asas-backoffice}/src/screen-names.ts +1 -1
  30. package/template/apps/{asas-platform → asas-backoffice}/src/screens.tsx +46 -26
  31. package/template/apps/asas-backoffice/src/server.ts +30 -0
  32. package/template/apps/{asas-platform → asas-backoffice}/src/slide.ts +1 -1
  33. package/template/apps/{asas-platform → asas-backoffice}/src/start.ts +1 -1
  34. package/template/apps/{asas-platform → asas-backoffice}/src/styles.css +9 -7
  35. package/template/apps/{asas-platform → asas-backoffice}/tsconfig.json +2 -2
  36. package/template/apps/{asas-platform → asas-backoffice}/vite.config.ts +7 -1
  37. package/template/apps/asas-tanstack/package.json +4 -2
  38. package/template/apps/asas-tanstack/src/api.ts +1 -1
  39. package/template/apps/asas-tanstack/src/auth-frame.tsx +12 -12
  40. package/template/apps/asas-tanstack/src/choose-password.tsx +150 -0
  41. package/template/apps/asas-tanstack/src/components.ts +35 -0
  42. package/template/apps/asas-tanstack/src/copy.ts +416 -79
  43. package/template/apps/asas-tanstack/src/first-run.ts +16 -0
  44. package/template/apps/asas-tanstack/src/links.ts +1 -1
  45. package/template/apps/asas-tanstack/src/locale-switch.tsx +7 -5
  46. package/template/apps/asas-tanstack/src/no-organization.tsx +108 -0
  47. package/template/apps/asas-tanstack/src/redirect.ts +20 -1
  48. package/template/apps/asas-tanstack/src/request-headers.ts +1 -1
  49. package/template/apps/asas-tanstack/src/routeTree.gen.ts +42 -0
  50. package/template/apps/asas-tanstack/src/router.tsx +12 -1
  51. package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/files.ts +9 -2
  52. package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/screen.json +475 -116
  53. package/template/apps/asas-tanstack/src/routes/(screens)/dashboard/types.ts +4 -2
  54. package/template/apps/asas-tanstack/src/routes/(screens)/portal/files.ts +7 -2
  55. package/template/apps/asas-tanstack/src/routes/(screens)/portal/screen.json +83 -319
  56. package/template/apps/asas-tanstack/src/routes/(screens)/portal/types.ts +3 -1
  57. package/template/apps/asas-tanstack/src/routes/__root.tsx +1 -1
  58. package/template/apps/asas-tanstack/src/routes/auth.accept-invitation.tsx +14 -7
  59. package/template/apps/asas-tanstack/src/routes/auth.first-run.tsx +114 -0
  60. package/template/apps/asas-tanstack/src/routes/auth.forgot-password.tsx +2 -2
  61. package/template/apps/asas-tanstack/src/routes/auth.handoff.ts +5 -5
  62. package/template/apps/asas-tanstack/src/routes/auth.reset-password.tsx +7 -100
  63. package/template/apps/asas-tanstack/src/routes/auth.set-password.tsx +16 -0
  64. package/template/apps/asas-tanstack/src/routes/auth.sign-in.tsx +27 -15
  65. package/template/apps/asas-tanstack/src/routes/auth.sign-up.tsx +59 -38
  66. package/template/apps/asas-tanstack/src/routes/index.tsx +18 -14
  67. package/template/apps/asas-tanstack/src/routes/unauthorized.tsx +24 -12
  68. package/template/apps/asas-tanstack/src/screen-form.tsx +5 -3
  69. package/template/apps/asas-tanstack/src/screen-list.tsx +6 -5
  70. package/template/apps/asas-tanstack/src/screen-names.ts +1 -1
  71. package/template/apps/asas-tanstack/src/screens.tsx +46 -26
  72. package/template/apps/asas-tanstack/src/server.ts +10 -0
  73. package/template/apps/asas-tanstack/src/slide.ts +1 -1
  74. package/template/apps/asas-tanstack/src/styles.css +7 -3
  75. package/template/apps/asas-tanstack/tsconfig.json +2 -2
  76. package/template/apps/asas-tanstack/vite.config.ts +7 -1
  77. package/template/package.json +3 -3
  78. package/template/templates/asas/.claude/skills/add-field/SKILL.md +27 -0
  79. package/template/templates/asas/.claude/skills/add-kayan/SKILL.md +26 -0
  80. package/template/templates/asas/.claude/skills/eject-admin-piece/SKILL.md +28 -0
  81. package/template/templates/asas/.claude/skills/upgrade-mithaq/SKILL.md +35 -0
  82. package/template/templates/asas/.env.example +22 -5
  83. package/template/templates/asas/AGENTS.md +101 -0
  84. package/template/templates/asas/CLAUDE.md +1 -0
  85. package/template/templates/asas/README.md +1094 -0
  86. package/template/templates/asas/package.json +19 -4
  87. package/template/templates/asas/src/asas.ts +81 -56
  88. package/template/templates/asas/src/{platform-http.ts → backoffice-http.ts} +48 -27
  89. package/template/templates/asas/src/dev-setup.ts +112 -0
  90. package/template/templates/asas/src/local-database.ts +22 -0
  91. package/template/templates/asas/src/messages/account.json +129 -103
  92. package/template/templates/asas/src/messages/auditLog.json +223 -105
  93. package/template/templates/asas/src/messages/backofficeLog.json +132 -0
  94. package/template/templates/asas/src/messages/courses.json +256 -214
  95. package/template/templates/asas/src/messages/enrollments.json +45 -27
  96. package/template/templates/asas/src/messages/invitation.json +72 -56
  97. package/template/templates/asas/src/messages/localUsers.json +34 -28
  98. package/template/templates/asas/src/messages/media.json +31 -21
  99. package/template/templates/asas/src/messages/member.json +59 -39
  100. package/template/templates/asas/src/messages/memberRoles.json +37 -31
  101. package/template/templates/asas/src/messages/mithaq.json +206 -162
  102. package/template/templates/asas/src/messages/organization.json +63 -41
  103. package/template/templates/asas/src/messages/organizationRole.json +59 -47
  104. package/template/templates/asas/src/messages/roles.json +59 -45
  105. package/template/templates/asas/src/messages/session.json +99 -79
  106. package/template/templates/asas/src/messages/sessions.json +110 -80
  107. package/template/templates/asas/src/messages/settings.json +51 -53
  108. package/template/templates/asas/src/messages/tags.json +31 -21
  109. package/template/templates/asas/src/messages/user.json +109 -87
  110. package/template/templates/asas/src/messages/verification.json +59 -47
  111. package/template/templates/asas/src/migrations/20261014221355_tags_restrict/migration.sql +1 -0
  112. package/template/templates/asas/src/migrations/20261014221355_tags_restrict/snapshot.json +3881 -0
  113. package/template/templates/asas/src/migrations/20261014222218_one_seat_per_session/migration.sql +1 -0
  114. package/template/templates/asas/src/migrations/20261014222218_one_seat_per_session/snapshot.json +3894 -0
  115. package/template/templates/asas/src/migrations/20261014223522_session_archive_reason/migration.sql +1 -0
  116. package/template/templates/asas/src/migrations/20261014223522_session_archive_reason/snapshot.json +3907 -0
  117. package/template/templates/asas/src/migrations/20261014225728_course_archive_by_operation/migration.sql +1 -0
  118. package/template/templates/asas/src/migrations/20261014225728_course_archive_by_operation/snapshot.json +3907 -0
  119. package/template/templates/asas/src/migrations/20261015042444_organization_names/migration.sql +1 -0
  120. package/template/templates/asas/src/migrations/20261015042444_organization_names/snapshot.json +3920 -0
  121. package/template/templates/asas/src/migrations/20261015053212_member_last_seen/migration.sql +1 -0
  122. package/template/templates/asas/src/migrations/20261015053212_member_last_seen/snapshot.json +3933 -0
  123. package/template/templates/asas/src/migrations/20261015093203_activity_and_staff_history/migration.sql +15 -0
  124. package/template/templates/asas/src/migrations/20261015093203_activity_and_staff_history/snapshot.json +4113 -0
  125. package/template/templates/asas/src/migrations/20261015095752_portal/migration.sql +2 -0
  126. package/template/templates/asas/src/migrations/20261015095752_portal/snapshot.json +4113 -0
  127. package/template/templates/asas/src/migrations/20261015135529_better_auth_indexes/migration.sql +9 -0
  128. package/template/templates/asas/src/migrations/20261015135529_better_auth_indexes/snapshot.json +4302 -0
  129. package/template/templates/asas/src/migrations/20261015162438_audit_changes_json/migration.sql +27 -0
  130. package/template/templates/asas/src/migrations/20261015162438_audit_changes_json/snapshot.json +4289 -0
  131. package/template/templates/asas/src/migrations/schema.ts +1 -0
  132. package/template/templates/asas/src/mithaq.ts +4 -406
  133. package/template/templates/asas/src/organizations.ts +3 -0
  134. package/template/templates/asas/src/seed.ts +147 -45
  135. package/template/templates/asas/src/{ui-platform → ui-backoffice}/home.tsx +9 -9
  136. package/template/templates/asas/src/ui-backoffice/organization-actions.tsx +121 -0
  137. package/template/templates/asas/src/{ui-platform → ui-backoffice}/record-page.tsx +4 -4
  138. package/template/templates/asas/src/{ui-platform → ui-backoffice}/role-cells.tsx +13 -6
  139. package/template/templates/asas/src/{ui-platform → ui-backoffice}/user-actions.tsx +46 -48
  140. package/template/templates/asas/src/ui-dashboard/dashboard.tsx +10 -4
  141. package/template/templates/asas/src/ui-dashboard/home.tsx +73 -0
  142. package/template/templates/asas/src/ui-dashboard/members-card.tsx +147 -0
  143. package/template/templates/asas/src/ui-dashboard/members.tsx +637 -438
  144. package/template/templates/asas/src/ui-dashboard/versions.tsx +22 -8
  145. package/template/templates/asas/src/ui-portal/courses-home.tsx +96 -0
  146. package/template/templates/asas/src/ui-portal/home.tsx +9 -13
  147. package/template/templates/features/audit-log/audit-log/audit-log.ts +284 -0
  148. package/template/templates/features/audit-log/backoffice-log/backoffice-log.ts +67 -0
  149. package/template/templates/features/audit-log/index.ts +4 -0
  150. package/template/templates/features/audit-log/package.json +13 -0
  151. package/template/templates/features/auth/index.ts +11 -0
  152. package/template/templates/{asas/src → features/auth}/local-users/local-users.ts +7 -7
  153. package/template/templates/{auth → features/auth}/package.json +2 -3
  154. package/template/templates/{asas/src/kayan → features}/auth/user/account.access.ts +2 -2
  155. package/template/templates/{asas/src/kayan → features}/auth/user/account.ts +2 -2
  156. package/template/templates/{asas/src/kayan → features}/auth/user/invitation.access.ts +1 -1
  157. package/template/templates/{asas/src/kayan → features}/auth/user/invitation.ts +3 -3
  158. package/template/templates/{asas/src/kayan → features}/auth/user/member.access.ts +4 -4
  159. package/template/templates/{asas/src/kayan → features}/auth/user/member.screens.ts +5 -5
  160. package/template/templates/{asas/src/kayan → features}/auth/user/member.ts +4 -3
  161. package/template/templates/{asas/src/kayan → features}/auth/user/organization.access.ts +6 -6
  162. package/template/templates/{asas/src/kayan → features}/auth/user/organization.screens.ts +6 -6
  163. package/template/templates/{asas/src/kayan → features}/auth/user/organization.ts +2 -1
  164. package/template/templates/{asas/src/kayan → features}/auth/user/organizationRole.access.ts +2 -2
  165. package/template/templates/{asas/src/kayan → features}/auth/user/organizationRole.ts +3 -3
  166. package/template/templates/{asas/src/kayan → features}/auth/user/session.access.ts +2 -2
  167. package/template/templates/{asas/src/kayan → features}/auth/user/session.ts +2 -2
  168. package/template/templates/{asas/src/kayan → features}/auth/user/user.access.ts +8 -7
  169. package/template/templates/{asas/src/kayan → features}/auth/user/user.screens.ts +9 -9
  170. package/template/templates/{asas/src/kayan → features}/auth/user/user.ts +2 -2
  171. package/template/templates/{asas/src/kayan → features}/auth/user/verification.access.ts +2 -2
  172. package/template/templates/{asas/src/kayan → features}/auth/user/verification.ts +2 -2
  173. package/template/templates/{asas/src → features/courses}/courses/courses.ts +122 -133
  174. package/template/templates/features/courses/courses/cover-reads.ts +33 -0
  175. package/template/templates/features/courses/courses/ui/archived-badge.tsx +9 -0
  176. package/template/templates/{asas/src → features/courses}/courses/ui/fee-cell.tsx +1 -1
  177. package/template/templates/{asas/src → features/courses}/courses/ui/status-cell.tsx +5 -3
  178. package/template/templates/features/courses/courses/ui/unpublish-button.tsx +15 -0
  179. package/template/templates/features/courses/domain/archive.ts +39 -0
  180. package/template/templates/features/courses/domain/on-offer.ts +6 -0
  181. package/template/templates/features/courses/enrollments/enrollments.ts +309 -0
  182. package/template/templates/{asas/src → features/courses}/enrollments/ui/session-cell.tsx +17 -7
  183. package/template/templates/{asas/src → features/courses/enrollments}/ui/session-labels.tsx +3 -3
  184. package/template/templates/features/courses/index.ts +8 -0
  185. package/template/templates/features/courses/package.json +24 -0
  186. package/template/templates/features/courses/portal/ui/catalogue.tsx +86 -0
  187. package/template/templates/features/courses/portal/ui/course.tsx +176 -0
  188. package/template/templates/features/courses/portal/ui/enrollments.tsx +170 -0
  189. package/template/templates/features/courses/portal/ui/parts.tsx +95 -0
  190. package/template/templates/features/courses/portal/ui/reads.ts +76 -0
  191. package/template/templates/features/courses/portal/ui/rules.ts +87 -0
  192. package/template/templates/features/courses/portal/ui/shell.tsx +194 -0
  193. package/template/templates/features/courses/sessions/seats.ts +42 -0
  194. package/template/templates/{asas/src → features/courses}/sessions/sessions.ts +45 -14
  195. package/template/templates/features/media/index.ts +10 -0
  196. package/template/templates/{asas/src → features/media}/media/media.ts +34 -40
  197. package/template/templates/{asas/src → features/media}/media/ui/preview.tsx +2 -2
  198. package/template/templates/features/media/package.json +19 -0
  199. package/template/templates/features/roles/index.ts +4 -0
  200. package/template/templates/{asas/src → features/roles}/member-roles/member-roles.ts +15 -15
  201. package/template/templates/features/roles/package.json +21 -0
  202. package/template/templates/{asas/src → features/roles}/roles/roles.ts +23 -23
  203. package/template/templates/{asas/src → features/roles}/roles/ui/role-form.tsx +65 -24
  204. package/template/templates/{asas/src → features/roles}/roles/ui/roles.tsx +49 -25
  205. package/template/templates/features/settings/index.ts +3 -0
  206. package/template/templates/features/settings/package.json +13 -0
  207. package/template/templates/{asas/src → features/settings}/settings/settings.ts +12 -17
  208. package/template/templates/features/tags/index.ts +3 -0
  209. package/template/templates/features/tags/package.json +14 -0
  210. package/template/templates/{asas/src → features/tags}/tags/tags.ts +15 -15
  211. package/template/templates/mithaq/package.json +33 -0
  212. package/template/templates/mithaq/src/mail/index.ts +32 -0
  213. package/template/templates/mithaq/src/mail/mailers.ts +36 -0
  214. package/template/templates/{asas/src/audit-log/sink.ts → mithaq/src/mithaq/audit-sink.ts} +31 -9
  215. package/template/templates/{auth/src/platform-roles.ts → mithaq/src/mithaq/auth/backoffice-roles.ts} +44 -23
  216. package/template/templates/mithaq/src/mithaq/auth/index.ts +1859 -0
  217. package/template/templates/mithaq/src/mithaq/auth/organizations.ts +80 -0
  218. package/template/templates/{asas/src → mithaq/src/mithaq}/pages.ts +16 -7
  219. package/template/templates/mithaq/src/mithaq/rich-text-nodes.ts +19 -0
  220. package/template/templates/mithaq/src/mithaq.ts +593 -0
  221. package/template/templates/mithaq/src/roles/default-member-roles.ts +29 -0
  222. package/template/templates/{asas → mithaq}/src/roles/grants.ts +1 -1
  223. package/template/templates/mithaq/src/roles/member-access.ts +11 -0
  224. package/template/templates/{asas/src → mithaq/src/roles}/statements.ts +6 -4
  225. package/template/templates/mithaq/src/roles/system-roles.ts +16 -0
  226. package/template/templates/{asas/src → mithaq/src/roles}/tenant-access.ts +96 -57
  227. package/template/templates/{asas/src → mithaq/src/roles}/tenant-roles.ts +84 -25
  228. package/template/templates/{asas → mithaq}/src/steps/expected-updated-at.ts +2 -2
  229. package/template/templates/{asas → mithaq}/src/storage/index.ts +3 -1
  230. package/template/templates/mithaq/src/ui/activity-page.tsx +33 -0
  231. package/template/templates/mithaq/src/ui/activity.tsx +527 -0
  232. package/template/templates/{asas → mithaq}/src/ui/app-shell.tsx +27 -19
  233. package/template/templates/mithaq/src/ui/rich-text-nodes.tsx +33 -0
  234. package/template/templates/{asas → mithaq}/src/ui/screen-nav.tsx +1 -1
  235. package/template/templates/{asas → mithaq}/src/ui/top-shell.tsx +8 -8
  236. package/template/apps/asas-platform/src/components/mithaq/cells.tsx +0 -112
  237. package/template/apps/asas-platform/src/components/mithaq/controls.tsx +0 -650
  238. package/template/apps/asas-platform/src/components/mithaq/field-shell.tsx +0 -53
  239. package/template/apps/asas-platform/src/components/mithaq/filter-operators.ts +0 -108
  240. package/template/apps/asas-platform/src/components/mithaq/form.tsx +0 -1467
  241. package/template/apps/asas-platform/src/components/mithaq/list-columns.tsx +0 -108
  242. package/template/apps/asas-platform/src/components/mithaq/list-filters.tsx +0 -368
  243. package/template/apps/asas-platform/src/components/mithaq/list-pagination.tsx +0 -153
  244. package/template/apps/asas-platform/src/components/mithaq/list-query.ts +0 -432
  245. package/template/apps/asas-platform/src/components/mithaq/list-selection.tsx +0 -74
  246. package/template/apps/asas-platform/src/components/mithaq/list.tsx +0 -865
  247. package/template/apps/asas-platform/src/components/mithaq/nav.tsx +0 -871
  248. package/template/apps/asas-platform/src/components/mithaq/nodes.tsx +0 -1024
  249. package/template/apps/asas-platform/src/components/mithaq/relationship-cell.tsx +0 -127
  250. package/template/apps/asas-platform/src/components/mithaq/relationship-control.tsx +0 -400
  251. package/template/apps/asas-platform/src/components/mithaq/rich-text-nodes.ts +0 -6
  252. package/template/apps/asas-platform/src/components/mithaq/rich-text.tsx +0 -1083
  253. package/template/apps/asas-platform/src/components/mithaq/upload.tsx +0 -686
  254. package/template/apps/asas-platform/src/components/ui/drawer.tsx +0 -216
  255. package/template/apps/asas-platform/src/redirect.ts +0 -1
  256. package/template/apps/asas-platform/src/routeTree.gen.ts +0 -181
  257. package/template/apps/asas-platform/src/router.tsx +0 -7
  258. package/template/apps/asas-platform/src/routes/(screens)/platform/files.ts +0 -4
  259. package/template/apps/asas-platform/src/routes/api/$.ts +0 -30
  260. package/template/apps/asas-platform/src/server.ts +0 -20
  261. package/template/apps/asas-tanstack/src/components/mithaq/cells.tsx +0 -112
  262. package/template/apps/asas-tanstack/src/components/mithaq/controls.tsx +0 -650
  263. package/template/apps/asas-tanstack/src/components/mithaq/field-shell.tsx +0 -53
  264. package/template/apps/asas-tanstack/src/components/mithaq/filter-operators.ts +0 -108
  265. package/template/apps/asas-tanstack/src/components/mithaq/form.tsx +0 -1467
  266. package/template/apps/asas-tanstack/src/components/mithaq/list-columns.tsx +0 -108
  267. package/template/apps/asas-tanstack/src/components/mithaq/list-filters.tsx +0 -368
  268. package/template/apps/asas-tanstack/src/components/mithaq/list-pagination.tsx +0 -153
  269. package/template/apps/asas-tanstack/src/components/mithaq/list-query.ts +0 -432
  270. package/template/apps/asas-tanstack/src/components/mithaq/list-selection.tsx +0 -74
  271. package/template/apps/asas-tanstack/src/components/mithaq/list.tsx +0 -865
  272. package/template/apps/asas-tanstack/src/components/mithaq/nav.tsx +0 -871
  273. package/template/apps/asas-tanstack/src/components/mithaq/nodes.tsx +0 -1024
  274. package/template/apps/asas-tanstack/src/components/mithaq/relationship-cell.tsx +0 -127
  275. package/template/apps/asas-tanstack/src/components/mithaq/relationship-control.tsx +0 -400
  276. package/template/apps/asas-tanstack/src/components/mithaq/rich-text-nodes.ts +0 -6
  277. package/template/apps/asas-tanstack/src/components/mithaq/rich-text.tsx +0 -1083
  278. package/template/apps/asas-tanstack/src/components/mithaq/upload.tsx +0 -686
  279. package/template/apps/asas-tanstack/src/components/ui/drawer.tsx +0 -215
  280. package/template/templates/asas/src/audit-log/audit-log.ts +0 -187
  281. package/template/templates/asas/src/audit-log/ui/activity.tsx +0 -243
  282. package/template/templates/asas/src/courses/ui/archive-button.tsx +0 -25
  283. package/template/templates/asas/src/courses/ui/no-courses-on-offer.tsx +0 -11
  284. package/template/templates/asas/src/courses/ui/on-offer.ts +0 -6
  285. package/template/templates/asas/src/enrollments/enrollments.ts +0 -137
  286. package/template/templates/asas/src/rich-text-nodes.ts +0 -11
  287. package/template/templates/asas/src/roles.ts +0 -16
  288. package/template/templates/asas/src/ui/rich-text-nodes.tsx +0 -15
  289. package/template/templates/asas/src/ui-platform/organization-actions.tsx +0 -114
  290. package/template/templates/auth/src/index.ts +0 -690
  291. /package/template/apps/{asas-platform → asas-backoffice}/components.json +0 -0
  292. /package/template/apps/{asas-platform → asas-backoffice}/gitignore +0 -0
  293. /package/template/apps/{asas-platform → asas-backoffice}/public/favicon.svg +0 -0
  294. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/accordion.tsx +0 -0
  295. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/alert-dialog.tsx +0 -0
  296. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/avatar.tsx +0 -0
  297. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/badge.tsx +0 -0
  298. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/breadcrumb.tsx +0 -0
  299. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/button-group.tsx +0 -0
  300. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/button.tsx +0 -0
  301. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/calendar.tsx +0 -0
  302. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/card.tsx +0 -0
  303. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/chart.tsx +0 -0
  304. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/checkbox.tsx +0 -0
  305. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/collapsible.tsx +0 -0
  306. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/combobox.tsx +0 -0
  307. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/dialog.tsx +0 -0
  308. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/direction.tsx +0 -0
  309. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/directional-icon.tsx +0 -0
  310. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/dropdown-menu.tsx +0 -0
  311. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/field.tsx +0 -0
  312. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/input-group.tsx +0 -0
  313. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/input-otp.tsx +0 -0
  314. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/input.tsx +0 -0
  315. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/label.tsx +0 -0
  316. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/popover.tsx +0 -0
  317. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/radio-group.tsx +0 -0
  318. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/resizable.tsx +0 -0
  319. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/scroll-area.tsx +0 -0
  320. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/select.tsx +0 -0
  321. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/separator.tsx +0 -0
  322. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/sheet.tsx +0 -0
  323. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/sidebar.tsx +0 -0
  324. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/skeleton.tsx +0 -0
  325. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/slider.tsx +0 -0
  326. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/spinner.tsx +0 -0
  327. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/switch.tsx +0 -0
  328. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/table.tsx +0 -0
  329. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/tabs.tsx +0 -0
  330. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/textarea.tsx +0 -0
  331. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/toggle-group.tsx +0 -0
  332. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/toggle.tsx +0 -0
  333. /package/template/apps/{asas-platform → asas-backoffice}/src/components/ui/tooltip.tsx +0 -0
  334. /package/template/apps/{asas-platform → asas-backoffice}/src/copy.ts +0 -0
  335. /package/template/apps/{asas-platform → asas-backoffice}/src/hooks/use-mobile.ts +0 -0
  336. /package/template/apps/{asas-platform → asas-backoffice}/src/lib/utils.ts +0 -0
  337. /package/template/apps/{asas-platform → asas-backoffice}/src/theme.ts +0 -0
  338. /package/template/templates/{asas/src/kayan → features}/auth/user/index.ts +0 -0
  339. /package/template/templates/{asas/src → features/courses}/courses/ui/no-courses.tsx +0 -0
  340. /package/template/templates/{asas → mithaq}/components.json +0 -0
  341. /package/template/templates/{asas → mithaq}/src/storage/defaults.ts +0 -0
  342. /package/template/templates/{asas → mithaq}/src/storage/local.ts +0 -0
  343. /package/template/templates/{asas → mithaq}/src/storage/r2.ts +0 -0
  344. /package/template/templates/{asas → mithaq}/src/storage/s3.ts +0 -0
  345. /package/template/templates/{asas → mithaq}/src/storage/uploadthing.ts +0 -0
  346. /package/template/templates/{asas → mithaq}/src/ui/kpi-card.tsx +0 -0
  347. /package/template/templates/{asas → mithaq}/src/ui/record-page.tsx +0 -0
  348. /package/template/templates/{asas → mithaq}/src/ui/table-page.tsx +0 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 OpenStatus
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/index.js CHANGED
@@ -1,61 +1,361 @@
1
1
  #!/usr/bin/env node
2
- // `bun create mithaq my-app`: lays a new project down from the template this
3
- // package carries, at the version of `@drhomet/mithaq` this package was
4
- // published with. Plain JavaScript on Node's standard library, so `bunx` and
5
- // `npx` run it with nothing to install first.
2
+ // `npm create mithaq my-app` (or pnpm, yarn or bun): lays a new project down from
3
+ // the template this package carries, at the version of `@drhomet/mithaq` this
4
+ // package was published with. Plain JavaScript on Node's standard library, so
5
+ // every package manager runs it with nothing to install first.
6
6
  import { spawnSync } from 'node:child_process'
7
7
  import { randomBytes } from 'node:crypto'
8
- import { cpSync, existsSync, readdirSync, readFileSync, realpathSync, renameSync, writeFileSync } from 'node:fs'
9
- import { basename, dirname, join } from 'node:path'
8
+ import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from 'node:fs'
9
+ import { basename, dirname, join, relative } from 'node:path'
10
10
  import { createInterface } from 'node:readline/promises'
11
11
  import { fileURLToPath } from 'node:url'
12
12
  import { parseArgs } from 'node:util'
13
13
 
14
14
  const readJson = (file) => JSON.parse(readFileSync(file, 'utf8'))
15
15
  const writeJson = (file, value) => writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`)
16
+ const edit = (file, change) => writeFileSync(file, change(readFileSync(file, 'utf8')))
16
17
 
17
18
  const generateSecret = () => randomBytes(32).toString('hex')
18
19
 
20
+ /** The package manager `npm_config_user_agent` names (`pnpm/10.15.0 npm/? node/v24…`), when it is one of the four. */
21
+ export const managerOf = (agent = '') => ['npm', 'pnpm', 'yarn', 'bun'].find((name) => agent.startsWith(`${name}/`))
22
+
19
23
  /**
20
- * Pins the template's manifests to what a published project installs. It edits
21
- * `devDependencies` too, where the template keeps `mithaq-admin`. `mithaq`
22
- * becomes the published package under the name every import uses, and
23
- * `mithaq-admin`, which only the framework's own repository has, goes, since a
24
- * project reads the registry through `@mithaq`; `asas` and `@asas/auth` stay
25
- * `"*"`, the project's own workspaces. Any other `workspace:`, `link:` or
26
- * `file:` spec is an error rather than a pin, because nothing published stands
27
- * behind it.
24
+ * Pins the template's manifests to what a published project installs, in
25
+ * `dependencies` and `devDependencies` alike. `mithaq` and `mithaq-admin`
26
+ * become the published packages under the names every import uses; `asas` and
27
+ * `@asas/auth` stay `"*"`, the project's own workspaces. Any other
28
+ * `workspace:`, `link:` or `file:` spec is an error rather than a pin,
29
+ * because nothing published stands behind it.
28
30
  */
29
- export function updatePackageJSONDependencies({ mithaq, packageJson }) {
31
+ export function updatePackageJSONDependencies({ mithaq, mithaqAdmin, packageJson }) {
30
32
  for (const dependencies of [packageJson.dependencies, packageJson.devDependencies]) {
31
33
  if (!dependencies) continue
32
34
  if ('mithaq' in dependencies) dependencies.mithaq = mithaq
33
- delete dependencies['mithaq-admin']
35
+ if ('mithaq-admin' in dependencies) dependencies['mithaq-admin'] = mithaqAdmin
34
36
  for (const [name, spec] of Object.entries(dependencies)) {
35
- if (spec !== mithaq && /^(workspace|link|file):/.test(spec)) {
37
+ if (spec !== mithaq && spec !== mithaqAdmin && /^(workspace|link|file):/.test(spec)) {
36
38
  throw new Error(`The template's dependency ${name} is ${spec}, which only resolves inside the mithaq repository.`)
37
39
  }
38
40
  }
39
41
  }
40
42
  }
41
43
 
44
+ /**
45
+ * Replaces `from`, a string or a pattern with no group, where the template holds it exactly `count` times, so a
46
+ * template line that moved fails creation instead of shipping a wrong path.
47
+ */
48
+ const once = (text, from, to, count = 1) => {
49
+ if (text.split(from).length !== count + 1) throw new Error(`The template no longer holds ${from} ${count === 1 ? 'exactly once' : `${count} times`}.`)
50
+ // A function, so a replacement pattern such as `$'` or `$&` in what a person typed, an organization's name among it, is written as typed.
51
+ return typeof from === 'string' ? text.replaceAll(from, () => to) : text.replace(from, () => to)
52
+ }
53
+ /**
54
+ * What a project may hold beyond the language, the asas, the `auth` feature and the `tags` example. Each add-on names the
55
+ * add-ons it needs, the files that are its alone (removed when it is left out), the files that stand in for it (removed
56
+ * when it is chosen), the npm packages only it uses (a name ending in `/` is a whole scope), the keys of its words in
57
+ * the shared copy file, `mithaq.json`, which can hold no region, and whether it brings
58
+ * tables, without which the template's migrations, written for every table, do not fit the project. Its lines in a
59
+ * shared file sit in `// add-on: <name>` regions, which `keepAddOns` reads. `rich-text` and `uploads` bring words of the
60
+ * language, kinds and a target, so a project's vocabulary follows the add-ons it holds.
61
+ */
62
+ export const addOns = {
63
+ roles: {
64
+ needs: [],
65
+ files: ['packages/features/roles', 'packages/mithaq/src/roles/grants.ts', 'packages/asas/src/ui-dashboard/members.tsx', 'packages/asas/src/ui-dashboard/members-card.tsx', 'packages/asas/src/messages/roles.json', 'packages/asas/src/messages/memberRoles.json'],
66
+ without: ['packages/mithaq/src/roles/default-member-roles.ts', 'packages/mithaq/src/roles/member-access.ts'],
67
+ packages: [],
68
+ copy: [],
69
+ tables: true,
70
+ },
71
+ 'audit-log': {
72
+ needs: [],
73
+ files: ['packages/features/audit-log', 'packages/mithaq/src/mithaq/audit-sink.ts', 'packages/mithaq/src/ui/activity.tsx', 'packages/mithaq/src/ui/activity-page.tsx', 'packages/asas/src/messages/auditLog.json', 'packages/asas/src/messages/backofficeLog.json'],
74
+ without: [],
75
+ packages: [],
76
+ copy: [],
77
+ tables: true,
78
+ },
79
+ settings: { needs: [], files: ['packages/features/settings', 'packages/asas/src/messages/settings.json'], without: [], packages: [], copy: [], tables: true },
80
+ // The `richText` kind, its Lexical nodes and editor.
81
+ 'rich-text': {
82
+ needs: [],
83
+ files: ['packages/mithaq/src/mithaq/rich-text-nodes.ts', 'packages/mithaq/src/ui/rich-text-nodes.tsx'],
84
+ without: [],
85
+ packages: ['lexical', '@lexical/'],
86
+ copy: ['kinds.richText', 'kinds.richText.invalid'],
87
+ tables: false,
88
+ },
89
+ // The `upload` kind, the storage target and its adapters.
90
+ uploads: { needs: [], files: ['packages/mithaq/src/storage'], without: [], packages: ['aws4fetch', 'file-type', 'image-dimensions'], copy: ['kinds.upload', 'kinds.upload.invalid'], tables: false },
91
+ // The organization's images, which an upload field points at.
92
+ media: { needs: ['uploads'], files: ['packages/features/media', 'packages/asas/src/messages/media.json'], without: [], packages: [], copy: [], tables: true },
93
+ // The worked example: its business roles are the roles add-on's, its edit pages draw the audit log's activity, its
94
+ // details are rich text and a course's cover is one of the media.
95
+ courses: {
96
+ needs: ['media', 'roles', 'audit-log', 'rich-text'],
97
+ files: ['packages/features/courses', 'packages/asas/src/ui-dashboard/dashboard.tsx', 'packages/asas/src/ui-portal/courses-home.tsx', ...['courses', 'sessions', 'enrollments'].map((one) => `packages/asas/src/messages/${one}.json`)],
98
+ without: ['packages/asas/src/ui-dashboard/home.tsx', 'packages/asas/src/ui-portal/home.tsx'],
99
+ packages: [],
100
+ copy: [],
101
+ tables: true,
102
+ },
103
+ backoffice: { needs: [], files: ['apps/asas-backoffice'], without: [], packages: [], copy: [], tables: false },
104
+ }
105
+
106
+ /**
107
+ * The add-ons `asked` names with every one they need, in the table's order, and the ones added for that. A name the
108
+ * table does not hold is refused with the list, so a typo never makes a smaller project than asked for.
109
+ */
110
+ export function resolveAddOns(asked) {
111
+ const unknown = asked.filter((name) => !Object.hasOwn(addOns, name))
112
+ if (unknown.length) throw new Error(`No add-on is named ${unknown.map((name) => `"${name}"`).join(', ')}. The add-ons are ${Object.keys(addOns).join(', ')}.`)
113
+ const chosen = new Set()
114
+ const take = (name) => {
115
+ if (chosen.has(name)) return
116
+ chosen.add(name)
117
+ for (const one of addOns[name].needs) take(one)
118
+ }
119
+ for (const name of asked) take(name)
120
+ const ordered = Object.keys(addOns).filter((name) => chosen.has(name))
121
+ return { chosen: ordered, added: ordered.filter((name) => !asked.includes(name)) }
122
+ }
123
+
124
+ /**
125
+ * Keeps the lines of `text` the chosen add-ons hold. A region from `// add-on: <names>` to `// end add-on: <names>` keeps
126
+ * its lines when every add-on it names is chosen; one from `// without add-on: <names>` to its end holds commented lines,
127
+ * uncommented when they are not. The marker lines go either way, so a project reads as if written for its add-ons. A
128
+ * marker that is unbalanced or names no add-on fails creation, as a template line that moved does.
129
+ */
130
+ export function keepAddOns(text, chosen) {
131
+ const open = []
132
+ const kept = []
133
+ for (const line of text.split('\n')) {
134
+ const marker = /^\s*\/\/ (end )?(without )?add-on: (.+)$/.exec(line)
135
+ // A misspelled marker would otherwise read as a comment, and leave its region in every project.
136
+ if (!marker && /^\s*\/\/.*\badd-?on\b:/.test(line)) throw new Error(`The template holds "${line.trim()}", which reads like an add-on marker and is none.`)
137
+ if (!marker) {
138
+ if (open.every((one) => one.keep)) kept.push(open.some((one) => one.without) ? line.replace(/^(\s*)\/\/ ?/, '$1') : line)
139
+ continue
140
+ }
141
+ const [, end, without, names] = marker
142
+ const label = `${without ?? ''}add-on: ${names}`
143
+ if (end) {
144
+ if (open.pop()?.label !== label) throw new Error(`The template closes "${label}" where no such region is open.`)
145
+ continue
146
+ }
147
+ const named = names.split(', ')
148
+ for (const name of named) if (!Object.hasOwn(addOns, name)) throw new Error(`The template marks a region of the add-on "${name}", which the table does not hold.`)
149
+ const all = named.every((name) => chosen.includes(name))
150
+ open.push({ label, without: Boolean(without), keep: without ? !all : all })
151
+ }
152
+ if (open.length) throw new Error(`The template leaves "${open.at(-1).label}" open.`)
153
+ return kept.join('\n')
154
+ }
155
+
156
+ /** Every text file a project's own commands or imports read, outside what an install writes. */
157
+ const textsUnder = (folder) =>
158
+ readdirSync(folder, { recursive: true, encoding: 'utf8' })
159
+ .filter((file) => !file.split(/[\\/]/).includes('node_modules') && /\.(tsx?|jsx?|json|css|md)$/.test(file))
160
+ .map((file) => join(folder, file))
161
+
162
+ /**
163
+ * Whether a package's `exports` names `sub` (`''` for the package itself) as Node resolves it: the key equal to it, else
164
+ * the longest `*` pattern it lies under, and not where that key's value is `null`, an exclusion. A `.` or `..` segment
165
+ * names nothing, since it would climb out of what the key exports.
166
+ */
167
+ export const exportsPath = (exports, sub) => {
168
+ if (sub.split('/').some((segment) => segment === '.' || segment === '..')) return false
169
+ const asked = sub === '' ? '.' : `.${sub}`
170
+ const [, value] = Object.entries(exports ?? {})
171
+ .filter(([key]) => (key.endsWith('*') ? asked.startsWith(key.slice(0, -1)) : key === asked))
172
+ .sort(([a], [b]) => (b === asked) - (a === asked) || b.length - a.length)[0] ?? [, null]
173
+ return value !== null
174
+ }
175
+
176
+ /**
177
+ * Folds the monorepo into one package at `project`: the language's and the asas's `src/` side by side, each feature
178
+ * under `src/features/`, and the tenant app, whose `src/` holds no file of the same name as theirs. The package keeps
179
+ * the name `asas`, because the screen host imports it by that name and a package reaches itself through its own
180
+ * `exports`, which also keeps it out of Vite's server bundle, so `import.meta.url` and each `ui()` file stay real.
181
+ * Every import of the language or a feature becomes a path.
182
+ */
183
+ const foldIntoOnePackage = (project) => {
184
+ const at = (...path) => join(project, ...path)
185
+ const features = readdirSync(at('packages/features'))
186
+ const [root, language, asas, app, ...ofFeatures] = ['', 'packages/mithaq', 'packages/asas', 'apps/asas-tanstack', ...features.map((one) => `packages/features/${one}`)].map((folder) => readJson(at(folder, 'package.json')))
187
+ // Each path the language names from the asas's folder becomes the package's own, before any import is rewritten.
188
+ edit(at('packages/mithaq/src/mithaq.ts'), (text) =>
189
+ [
190
+ ["new URL('../../asas/src/', import.meta.url)", "new URL('./', import.meta.url)"],
191
+ ["new URL('../../features/', import.meta.url)", "new URL('./features/', import.meta.url)"],
192
+ ["language: '@asas/mithaq',", "language: '../../../mithaq.ts',"],
193
+ ["features: '../features/',", "features: 'src/features/',"],
194
+ ["auth: '../features/auth/',", "auth: 'src/features/auth/',"],
195
+ ["providers: '@asas/mithaq/auth',", "providers: '../../../mithaq/auth/index.ts',"],
196
+ // The dashboard's and the portal's, which the tenant app serves.
197
+ ["'../../apps/asas-tanstack/src/routes/(screens)/'", "'src/routes/(screens)/'", 2],
198
+ ].reduce((one, [from, to, count]) => once(one, from, to, count), text),
199
+ )
200
+ for (const entry of readdirSync(at('packages/asas'))) if (!['package.json', 'components.json', 'src'].includes(entry)) renameSync(at('packages/asas', entry), at(entry))
201
+ renameSync(at('packages/asas/src'), at('src'))
202
+ // The asas's `src/mithaq.ts` is where the mithaq command finds the language; the language's own takes its place.
203
+ rmSync(at('src/mithaq.ts'))
204
+ for (const entry of readdirSync(at('packages/mithaq/src'))) renameSync(at('packages/mithaq/src', entry), at('src', entry))
205
+ for (const one of features) {
206
+ mkdirSync(at('src/features', one), { recursive: true })
207
+ for (const entry of readdirSync(at('packages/features', one))) if (entry !== 'package.json') renameSync(at('packages/features', one, entry), at('src/features', one, entry))
208
+ }
209
+ for (const entry of readdirSync(at('apps/asas-tanstack/src'))) renameSync(at('apps/asas-tanstack/src', entry), at('src', entry))
210
+ for (const entry of ['public', 'vite.config.ts', 'tsconfig.json', 'components.json']) renameSync(at('apps/asas-tanstack', entry), at(entry))
211
+ rmSync(at('packages'), { recursive: true })
212
+ rmSync(at('apps'), { recursive: true })
213
+ const own = (dependencies) => Object.fromEntries(Object.entries(dependencies).filter(([name]) => !name.startsWith('@asas/') && name !== 'asas'))
214
+ const lockCheck = (script) => script.replace('(cd ../../packages/asas && mithaq lock --check)', 'mithaq lock --check')
215
+ writeJson(at('package.json'), {
216
+ name: 'asas',
217
+ private: true,
218
+ type: 'module',
219
+ engines: root.engines,
220
+ scripts: { ...Object.fromEntries(Object.entries(app.scripts).map(([name, script]) => [name, lockCheck(script)])), ...asas.scripts },
221
+ exports: asas.exports,
222
+ dependencies: own(Object.assign({}, language.dependencies, ...ofFeatures.map((one) => one.dependencies), asas.dependencies, app.dependencies)),
223
+ devDependencies: own(Object.assign({}, language.devDependencies, ...ofFeatures.map((one) => one.devDependencies), asas.devDependencies, app.devDependencies, root.devDependencies)),
224
+ })
225
+ const exportsOf = Object.fromEntries(ofFeatures.map((one) => [one.name.slice('@asas/'.length), one.exports]))
226
+ // A feature is reached through what its `exports` say, as it was while it was a package.
227
+ const targetOf = (name, sub) => {
228
+ if (name === 'mithaq') return sub === '' ? 'src/mithaq.ts' : sub === '/auth' ? 'src/mithaq/auth/index.ts' : ['/auth/backoffice-roles', '/auth/organizations'].includes(sub) ? `src/mithaq${sub}.ts` : `src${sub}`
229
+ if (sub === '') return `src/features/${name}/index.ts`
230
+ if (!exportsPath(exportsOf[name], sub)) throw new Error(`@asas/${name}${sub} reaches past the feature's exports.`)
231
+ return `src/features/${name}${sub}`
232
+ }
233
+ for (const file of textsUnder(at('src')).filter((one) => /\.tsx?$/.test(one))) {
234
+ edit(file, (text) =>
235
+ text.replaceAll(/(['"])@asas\/([a-z0-9._-]+)((?:\/[^'"]*)?)\1/g, (_, quote, name, sub) => {
236
+ const path = relative(dirname(file), at(targetOf(name, sub))).replaceAll('\\', '/')
237
+ return `${quote}${path.startsWith('.') ? path : `./${path}`}${quote}`
238
+ }),
239
+ )
240
+ }
241
+ // A screen draws the language's and each feature's files by package; in one package they are all its own, read
242
+ // off Vite's root as `mithaq generate screens` writes them for an app that is the project.
243
+ for (const screen of readdirSync(at('src/routes/(screens)')).filter((one) => existsSync(at('src/routes/(screens)', one, 'screen.json')))) {
244
+ const folder = at('src/routes/(screens)', screen)
245
+ edit(join(folder, 'screen.json'), (text) => text.replaceAll('"@asas/mithaq:./', '"./').replaceAll(/"@asas\/([a-z0-9._-]+):\.\//g, '"./src/features/$1/'))
246
+ const referenced = [...new Set([...readFileSync(join(folder, 'screen.json'), 'utf8').matchAll(/"(\.\/[^"#]+)#[^"]*"/g)].map(([, file]) => file))].sort()
247
+ writeFileSync(join(folder, 'files.ts'), `// Written by \`mithaq generate screens\`. Do not edit.\n/// <reference types="vite/client" />\n// Every file the \`${screen}\` screen's references name, each loaded when a page draws it.\nexport const files = import.meta.glob([${referenced.map((file) => `'${file}'`).join(', ')}], { base: '/' })\n`)
248
+ }
249
+ edit(at('src/styles.css'), (text) => once(text, /\/\*[^*]*\*\/\n@source '[^']*';\n/, "/* The project's own shells, pages and components, which the screens load\n (`src/ui/app-shell.tsx`). */\n@source './';\n"))
250
+ edit(at('tsconfig.json'), (text) => once(text, /, "\.\.\/\.\.\/packages\/[^\]]*\]/, ']'))
251
+ }
252
+
253
+ /**
254
+ * Leaves `project` holding the chosen add-ons alone: each other add-on's files, regions and package go, and so do the
255
+ * files that stand in for a chosen one. The template's migrations build every table, so a project short of any add-on
256
+ * with tables starts with none, and its first `mithaq migrate` writes them for the tables it has.
257
+ */
258
+ const keepOnly = ({ project, chosen }) => {
259
+ for (const [name, { files, without }] of Object.entries(addOns)) {
260
+ for (const file of chosen.includes(name) ? without : files) rmSync(join(project, file), { recursive: true })
261
+ }
262
+ for (const file of textsUnder(project).filter((one) => /\.tsx?$/.test(one))) {
263
+ const text = readFileSync(file, 'utf8')
264
+ if (text.includes('add-on: ')) writeFileSync(file, keepAddOns(text, chosen))
265
+ }
266
+ for (const file of textsUnder(project).filter((one) => basename(one) === 'package.json')) {
267
+ const manifest = readJson(file)
268
+ for (const [name, { packages }] of Object.entries(addOns).filter(([one]) => !chosen.includes(one))) {
269
+ for (const dependencies of [manifest.dependencies, manifest.devDependencies]) {
270
+ for (const dependency of Object.keys(dependencies ?? {})) {
271
+ if (dependency === `@asas/${name}` || packages.some((one) => (one.endsWith('/') ? dependency.startsWith(one) : dependency === one))) delete dependencies[dependency]
272
+ }
273
+ }
274
+ }
275
+ writeJson(file, manifest)
276
+ }
277
+ const copy = join(project, 'packages/asas/src/messages/mithaq.json')
278
+ const shared = readJson(copy)
279
+ for (const [name, { copy: keys }] of Object.entries(addOns)) if (!chosen.includes(name)) for (const key of keys) {
280
+ const parts = key.split('.')
281
+ const last = parts.pop()
282
+ delete parts.reduce((branch, part) => branch?.[part], shared)?.[last]
283
+ }
284
+ writeJson(copy, shared)
285
+ if (Object.entries(addOns).every(([name, { tables }]) => !tables || chosen.includes(name))) return
286
+ const migrations = join(project, 'packages/asas/src/migrations')
287
+ for (const entry of readdirSync(migrations)) if (entry !== 'drizzle.config.ts') rmSync(join(migrations, entry), { recursive: true })
288
+ // Each answer names a change between two of the migrations just removed.
289
+ writeFileSync(join(project, 'packages/asas/mithaq.hints.json'), '[]\n')
290
+ }
291
+
292
+ /**
293
+ * The organization mode and who may sign up, as `organizations.ts` writes them: one organization,
294
+ * shown by name, by invitation unless asked, and many founding one by every sign-up. Open joiners of
295
+ * the courses example are students; without the roles add-on nobody is given a role on joining. Every
296
+ * open sign-up proves its address.
297
+ */
298
+ export function organizationSettings({ organizations = 'one', showOrganization = true, signUp, chosen, organizationName }) {
299
+ if (organizations === 'none') throw new Error('--organizations is one or many, not "none": one organization never shown is --organizations one --show-organization no.')
300
+ if (!['one', 'many'].includes(organizations)) throw new Error(`--organizations is one or many, not "${organizations}".`)
301
+ if (signUp !== undefined && !['invite', 'open'].includes(signUp)) throw new Error(`--sign-up is invite or open, not "${signUp}".`)
302
+ const many = organizations === 'many'
303
+ if (many && signUp === 'invite') throw new Error('many organizations take --sign-up open: each sign-up founds its own.')
304
+ if (many && !showOrganization) throw new Error('many organizations are shown by name: a person who belongs to several switches between them.')
305
+ // One line of 1 to 100 characters, as the auth setup keeps an organization's name, so the project loads.
306
+ for (const one of Object.values(organizationName ?? {})) {
307
+ if (typeof one !== 'string' || !one.trim() || one.length > 100 || /[\p{Cc}\p{Zl}\p{Zp}\u202a-\u202e\u2066-\u2069]/u.test(one)) throw new Error('--organization-name is one line of 1 to 100 characters.')
308
+ }
309
+ return {
310
+ organizations,
311
+ showOrganization,
312
+ signUp: signUp ?? (many ? 'open' : 'invite'),
313
+ joinRole: !many && chosen.includes('courses') ? 'student' : null,
314
+ verifyEmail: true,
315
+ allowedDomains: [],
316
+ // The Arabic falls back to the English, so the language never reads an organization with no name.
317
+ ...(organizationName?.en && { organizationName: { en: organizationName.en.trim(), ar: organizationName.ar?.trim() || organizationName.en.trim() } }),
318
+ }
319
+ }
320
+
321
+ /**
322
+ * Writes `settings` over the values `organizations.ts` holds, each line the template holds once. A name is written as
323
+ * JSON, whose double quotes leave an apostrophe in it as written.
324
+ */
325
+ const writeOrganizations = (file, settings) =>
326
+ edit(file, (text) => Object.entries(settings).reduce((one, [key, value]) => once(one, new RegExp(`(?<=^export const ${key}: [^=]+ = ).+$`, 'm'), key === 'organizationName' ? JSON.stringify(value) : JSON.stringify(value).replaceAll('"', "'")), text))
327
+
42
328
  /**
43
329
  * Writes the project `name` under `cwd` from `template`, and answers its
44
- * folder. `version` is the `@drhomet/mithaq` the project installs and whose
45
- * registry it reads, pinned exactly so the two cannot drift apart and an
46
- * upgrade is a change the project makes; `mithaq` replaces the dependency
47
- * itself, which only the release check does.
330
+ * folder. `version` is the `@drhomet/mithaq` and `@drhomet/mithaq-admin` the
331
+ * project installs, pinned exactly so the two cannot drift apart and an
332
+ * upgrade is a change the project makes; `mithaq` and `mithaqAdmin` replace the
333
+ * dependencies themselves, which only the release check does. `layout` is `monorepo`, the
334
+ * language, each feature and the asas as packages, or `single`, one package with the tenant app and no
335
+ * backoffice console. `addOns` names the add-ons the project holds beside auth and the tags example, with
336
+ * every one they need; none is the minimal project.
48
337
  */
49
- export function create({ name, cwd, template, version, mithaq = `npm:@drhomet/mithaq@${version}` }) {
50
- // A package name npm accepts, so it is also one folder, inside `cwd`.
338
+ export function create({ name, cwd, template, version, mithaq = `npm:@drhomet/mithaq@${version}`, mithaqAdmin = `npm:@drhomet/mithaq-admin@${version}`, layout = 'monorepo', addOns: asked = [], organizations, showOrganization, signUp, organizationName }) {
339
+ // A package name npm accepts, so it is also one folder, inside `cwd`, and a scope.
51
340
  if (!/^[a-z0-9][a-z0-9._-]*$/.test(name) || name.length > 214) {
52
341
  throw new Error(`"${name}" is not a valid project name: use lowercase letters, digits, "-", "." and "_", starting with a letter or digit.`)
53
342
  }
343
+ if (!['monorepo', 'single'].includes(layout)) throw new Error(`The layout is monorepo or single, not "${layout}".`)
344
+ const { chosen } = resolveAddOns(asked)
345
+ const settings = organizationSettings({ organizations, showOrganization, signUp, chosen, organizationName })
346
+ const backoffice = chosen.includes('backoffice')
347
+ if (layout === 'single' && backoffice) throw new Error('A single package serves the tenant app alone. Choose the monorepo layout to include the backoffice console.')
54
348
  const project = join(cwd, name)
55
349
  if (existsSync(project) && readdirSync(project).length > 0) {
56
350
  throw new Error(`${project} already exists and is not empty. Pick another name.`)
57
351
  }
58
352
  cpSync(template, project, { recursive: true })
353
+ // The template's packages sit in `templates/` in the mithaq repository, beside the framework's own `packages/`; a
354
+ // project holds them in `packages/`, and every relative path an app names into them follows.
355
+ renameSync(join(project, 'templates'), join(project, 'packages'))
356
+ for (const file of textsUnder(project)) edit(file, (text) => text.replaceAll(/((?:\.\.\/)+)templates(?=[/'"])/g, '$1packages'))
357
+ // A snippet's marker names the repository file the docs test checks it against, a path no project holds.
358
+ for (const file of textsUnder(project).filter((one) => one.endsWith('.md'))) edit(file, (text) => text.replaceAll(/^[ \t]*<!-- from: \S+ -->\n/gm, ''))
59
359
  const files = readdirSync(project, { recursive: true })
60
360
  // npm drops every `.gitignore` from a published package, so the stage keeps
61
361
  // each as `gitignore`.
@@ -64,59 +364,153 @@ export function create({ name, cwd, template, version, mithaq = `npm:@drhomet/mi
64
364
  }
65
365
  for (const file of files.filter((file) => basename(file) === 'package.json')) {
66
366
  const packageJson = readJson(join(project, file))
67
- updatePackageJSONDependencies({ mithaq, packageJson })
367
+ updatePackageJSONDependencies({ mithaq, mithaqAdmin, packageJson })
68
368
  writeJson(join(project, file), file === 'package.json' ? { name, ...packageJson } : packageJson)
69
369
  }
70
- // shadcn reads `@mithaq/<item>` from this URL with `{name}` replaced, at the
71
- // version installed.
72
- for (const file of files.filter((file) => basename(file) === 'components.json')) {
73
- const components = readJson(join(project, file))
74
- writeJson(join(project, file), { ...components, registries: { ...components.registries, '@mithaq': `https://cdn.jsdelivr.net/npm/@drhomet/mithaq@${version}/r/{name}.json` } })
75
- }
76
- // One `.env` for the template's commands and one for each app's dev server,
77
- // the same secrets in each: the tenant app and the platform console sign
370
+ keepOnly({ project, chosen })
371
+ writeOrganizations(join(project, 'packages/mithaq/src/mithaq/auth/organizations.ts'), settings)
372
+ // One `.env` for the asas's commands and one for each app's dev server,
373
+ // the same secrets in each: the tenant app and the backoffice console sign
78
374
  // and read each other's sessions under one `BETTER_AUTH_SECRET`.
79
- const env = readFileSync(join(project, 'templates/asas/.env.example'), 'utf8').replace(/^([A-Z_]*SECRET)=$/gm, (_, key) => `${key}=${generateSecret()}`)
80
- for (const folder of ['templates/asas', 'apps/asas-tanstack', 'apps/asas-platform']) writeFileSync(join(project, folder, '.env'), env, { mode: 0o600 })
81
- // The screens find their files by globs under `node_modules/asas/`, which
82
- // only Bun's isolated installer lays out.
83
- writeFileSync(join(project, 'bunfig.toml'), '[install]\nlinker = "isolated"\n')
84
- writeFileSync(join(project, '.gitignore'), ['node_modules', '.env', 'dist', '.storage/', ''].join('\n'))
85
- writeJson(join(project, 'tsconfig.json'), {
86
- compilerOptions: { target: 'es2024', module: 'nodenext', moduleResolution: 'nodenext', strict: true, noUncheckedIndexedAccess: true, noEmit: true, skipLibCheck: true, erasableSyntaxOnly: true, allowImportingTsExtensions: true, types: ['node'] },
87
- include: ['templates/*/src/**/*.ts', 'apps/*/src/**/*.ts'],
88
- })
375
+ const env = readFileSync(join(project, 'packages/asas/.env.example'), 'utf8').replace(/^([A-Z_]*SECRET)=$/gm, (_, key) => `${key}=${generateSecret()}`)
376
+ if (layout === 'single') {
377
+ foldIntoOnePackage(project)
378
+ writeFileSync(join(project, '.env'), env, { mode: 0o600 })
379
+ } else {
380
+ for (const folder of ['packages/asas', 'apps/asas-tanstack', ...(backoffice ? ['apps/asas-backoffice'] : [])]) writeFileSync(join(project, folder, '.env'), env, { mode: 0o600 })
381
+ // An agent started at the root reads no file below it until it touches one, and the repository's page shows the
382
+ // root's README alone, so the guide, the agent files and the skills sit there, as a single package's do.
383
+ for (const file of ['AGENTS.md', 'CLAUDE.md', 'README.md', '.claude']) renameSync(join(project, 'packages/asas', file), join(project, file))
384
+ // The project's own scope, on each of its package names and every import of one.
385
+ for (const file of textsUnder(project)) edit(file, (text) => text.replaceAll('@asas/', `@${name}/`))
386
+ writeJson(join(project, 'tsconfig.json'), {
387
+ compilerOptions: { target: 'es2024', module: 'nodenext', moduleResolution: 'nodenext', strict: true, noUncheckedIndexedAccess: true, noEmit: true, skipLibCheck: true, erasableSyntaxOnly: true, allowImportingTsExtensions: true, types: ['node'] },
388
+ include: ['packages/*/src/**/*.ts', 'packages/features/**/*.ts', 'apps/*/src/**/*.ts'],
389
+ // Each server entry reads Vite's `import.meta.glob`, and each components map mithaq-admin's `.tsx`, which the app's own config checks.
390
+ exclude: ['apps/*/src/server.ts', 'apps/*/src/components.ts'],
391
+ })
392
+ }
393
+ // npm, yarn and Bun read `workspaces`; pnpm 10.6 and later read this file.
394
+ // Told nothing, pnpm installs a `"*"` workspace from the registry, where
395
+ // another package is named `asas`, and skips esbuild's install script, which
396
+ // pnpm 11 then fails on; pnpm 10 reads `onlyBuiltDependencies`, pnpm 11
397
+ // `allowBuilds`.
398
+ const { workspaces = [] } = readJson(join(project, 'package.json'))
399
+ const packages = workspaces.length ? `packages:\n${workspaces.map((folder) => ` - ${folder}\n`).join('')}linkWorkspacePackages: true\npreferWorkspacePackages: true\n` : ''
400
+ writeFileSync(join(project, 'pnpm-workspace.yaml'), `${packages}onlyBuiltDependencies:\n - esbuild\nallowBuilds:\n esbuild: true\n`)
401
+ // Yarn 2 and later would otherwise install Plug'n'Play, which neither Vite's
402
+ // externals nor Node's type stripping read.
403
+ writeFileSync(join(project, '.yarnrc.yml'), 'nodeLinker: node-modules\n')
404
+ writeFileSync(join(project, '.gitignore'), ['node_modules', '.env', 'dist', '.storage/', '.tanstack', ''].join('\n'))
89
405
  return project
90
406
  }
91
407
 
408
+ const managers = 'A project installs with npm, pnpm 10.6 or later, Yarn 1 or Bun 1.3. Yarn 2 and later install a copy of mithaq for each workspace of a monorepo, which the build refuses, so a monorepo does not take them yet.'
409
+
410
+ const help = `Usage: create-mithaq [name] [options]
411
+
412
+ --layout monorepo the language, each feature, the asas and each app as workspace packages (the default)
413
+ --layout single one package holding the language, the features, the asas and the tenant app
414
+ --with <a,b> the add-ons to hold beside auth and the tags example, with every one they need
415
+ --full every add-on: the whole worked example (a single package leaves out backoffice)
416
+ --organizations one|many
417
+ one organization, made on /auth/first-run (the default), or many, each sign-up
418
+ founding its own
419
+ --show-organization yes|no
420
+ with one organization: whether a page names it (the default), or the app never
421
+ shows one and its members are its users
422
+ --sign-up invite|open
423
+ with one organization: by invitation alone (the default), or anyone, who joins
424
+ once their address is proved (production needs a mailer); many takes open
425
+ --organization-name <name>
426
+ with one organization: its name, which lives in code; first-run asks none
427
+ --organization-name-ar <name>
428
+ its name in Arabic, the English one where not given
429
+ --help print this
430
+
431
+ With neither --with nor --full the project is minimal: auth and the tags example. The add-ons:
432
+
433
+ ${Object.entries(addOns).map(([name, { needs }]) => ` ${name}${needs.length ? ` (needs ${needs.join(', ')})` : ''}`).join('\n')}
434
+
435
+ Asked when not given, unless the input is not a terminal, where the defaults stand.
436
+
437
+ ${managers}`
438
+
439
+ /** The `mithaq` bin an install laid down for `folder`, in the nearest `node_modules/.bin` above it. */
440
+ const binAbove = (folder) => {
441
+ const at = join(folder, 'node_modules/.bin/mithaq')
442
+ if (existsSync(at)) return at
443
+ return dirname(folder) === folder ? undefined : binAbove(dirname(folder))
444
+ }
445
+
92
446
  const main = async () => {
93
- const { values, positionals } = parseArgs({ allowPositionals: true, options: { mithaq: { type: 'string' } } })
447
+ const { values, positionals } = parseArgs({
448
+ allowPositionals: true,
449
+ options: { mithaq: { type: 'string' }, 'mithaq-admin': { type: 'string' }, layout: { type: 'string' }, with: { type: 'string' }, full: { type: 'boolean' }, organizations: { type: 'string' }, 'show-organization': { type: 'string' }, 'sign-up': { type: 'string' }, 'organization-name': { type: 'string' }, 'organization-name-ar': { type: 'string' }, help: { type: 'boolean' } },
450
+ })
451
+ if (values.help) return console.log(help)
452
+ if (values.full && values.with !== undefined) throw new Error('Pass --full or --with, not both: --full is every add-on.')
94
453
  let [name] = positionals
95
- if (!name) {
96
- const prompt = createInterface({ input: process.stdin, output: process.stdout })
97
- name = (await prompt.question('Project name: ')).trim()
98
- prompt.close()
454
+ let { layout } = values
455
+ let picked = values.with?.split(',').map((one) => one.trim()).filter(Boolean)
456
+ let { organizations, 'show-organization': shown, 'sign-up': signUp, 'organization-name': named, 'organization-name-ar': namedInArabic } = values
457
+ const prompt = process.stdin.isTTY ? createInterface({ input: process.stdin, output: process.stdout }) : undefined
458
+ try {
459
+ if (!name) name = (await prompt?.question('Project name: '))?.trim() ?? ''
460
+ layout ??= (await prompt?.question('Monorepo, or a single package? (monorepo/single) [monorepo]: '))?.trim().toLowerCase() || 'monorepo'
461
+ if (!values.full && !picked && prompt) {
462
+ const answer = (await prompt.question('minimal: auth + tags / full: every add-on / pick: choose add-ons (minimal/full/pick) [minimal]: ')).trim().toLowerCase() || 'minimal'
463
+ if (answer === 'full') values.full = true
464
+ else if (answer === 'pick') picked = (await prompt.question(`Add-ons, comma-separated (${Object.keys(addOns).join(', ')}): `)).split(',').map((one) => one.trim()).filter(Boolean)
465
+ else if (answer !== 'minimal') throw new Error(`The answer is minimal, full or pick, not "${answer}".`)
466
+ }
467
+ if (!organizations && prompt) organizations = (await prompt.question('One organization, or many? (one/many) [one]: ')).trim().toLowerCase() || 'one'
468
+ if (!shown && prompt && organizations !== 'many') shown = (await prompt.question('Show the organization\'s name on its pages? (yes/no) [yes]: ')).trim().toLowerCase() || 'yes'
469
+ if (!signUp && prompt && organizations !== 'many') signUp = (await prompt.question('Who signs up: the invited, or anyone? (invite/open) [invite]: ')).trim().toLowerCase() || 'invite'
470
+ if (!named && prompt && organizations !== 'many') named = (await prompt.question('The organization\'s name [My organization]: ')).trim() || undefined
471
+ if (named && !namedInArabic && prompt) namedInArabic = (await prompt.question('Its name in Arabic [the same]: ')).trim() || undefined
472
+ } finally {
473
+ prompt?.close()
99
474
  }
475
+ if (shown !== undefined && !['yes', 'no'].includes(shown)) throw new Error(`--show-organization is yes or no, not "${shown}".`)
476
+ const showOrganization = shown !== 'no'
477
+ // A single package cannot hold the backoffice console, so its whole example is every other add-on.
478
+ const asked = values.full ? Object.keys(addOns).filter((one) => layout !== 'single' || one !== 'backoffice') : (picked ?? [])
479
+ const { chosen, added } = resolveAddOns(asked)
480
+ if (added.length) console.log(`Added ${added.join(', ')}, which ${asked.filter((one) => addOns[one].needs.length).join(', ')} need${added.length === 1 ? 's' : ''}.`)
100
481
  const { version } = readJson(new URL('package.json', import.meta.url))
101
- const project = create({ name, cwd: process.cwd(), template: fileURLToPath(new URL('template/', import.meta.url)), version, mithaq: values.mithaq })
102
- console.log(`Created ${project}.`)
103
- // `bunx` and `bun create` name Bun here (`bun/1.3.14 npm/? node/…`).
104
- const installed = process.env.npm_config_user_agent?.startsWith('bun/') && spawnSync('bun', ['install'], { cwd: project, stdio: 'inherit' }).status === 0
105
- if (!installed) {
106
- console.warn(`\nNot installed. Install with Bun: the project is a Bun workspace whose screens need Bun's isolated installer (bunfig.toml), and npm, pnpm and yarn lay node_modules out where they find nothing.`)
107
- }
482
+ const organizationName = named ? { en: named, ...(namedInArabic && { ar: namedInArabic }) } : undefined
483
+ const project = create({ name, cwd: process.cwd(), template: fileURLToPath(new URL('template/', import.meta.url)), version, mithaq: values.mithaq, mithaqAdmin: values['mithaq-admin'], layout, addOns: chosen, organizations, showOrganization, signUp, organizationName })
484
+ const settings = organizationSettings({ organizations, showOrganization, signUp, chosen, organizationName })
485
+ console.log(`Created ${project}${chosen.length ? `, with ${chosen.join(', ')}` : ', minimal'}.`)
486
+ const manager = managerOf(process.env.npm_config_user_agent)
487
+ const installed = manager !== undefined && spawnSync(manager, ['install'], { cwd: project, stdio: 'inherit', shell: process.platform === 'win32' }).status === 0
488
+ const run = manager ?? 'npm'
489
+ if (!installed) console.warn(`\nNot installed. Run ${run} install in ${project}. ${managers}`)
490
+ const single = layout === 'single'
491
+ const asas = single ? project : join(project, 'packages/asas')
492
+ const backoffice = chosen.includes('backoffice')
493
+ // Each screen's files were generated for every kayan of the template, so a project short of an add-on with tables
494
+ // writes its own once the install has laid the bin down.
495
+ const short = Object.entries(addOns).some(([one, { tables }]) => tables && !chosen.includes(one))
496
+ // With no organization shown, the Members page is named Users, which the template's screens do not say.
497
+ const regenerate = short || !settings.showOrganization
498
+ const bin = installed && regenerate ? binAbove(asas) : undefined
499
+ const generated = bin !== undefined && spawnSync(bin, ['generate', 'screens'], { cwd: asas, stdio: 'inherit', shell: process.platform === 'win32' }).status === 0
500
+ if (regenerate && !generated) console.warn(`\nThe screens are not written for this project yet. Run mithaq generate screens in ${asas}, after the install.`)
501
+ const language = single ? '' : 'packages/asas/'
502
+ if (single) console.log(`\nThe package is named asas, not ${name}: the screen host imports the language as asas, and a package reaches itself by its own name.`)
108
503
  console.log(`
109
- Next, with PostgreSQL 15 or later serving the database DATABASE_URL names in templates/asas/.env:
504
+ Next, with PostgreSQL 15 or later serving the database DATABASE_URL names in ${language}.env:
505
+
506
+ cd ${name}${installed ? '' : `\n ${run} install`}${single ? '' : '\n cd packages/asas'}
507
+ # push the schema, make the dev admin and fill its organization, development only
508
+ ${run} run dev:setup${run === 'npm' ? ' --' : ''} --seed${single ? '' : '\n cd ../../apps/asas-tanstack'}
509
+ ${run} run dev # the tenant app, http://localhost:5173${backoffice ? `\n # in a second terminal, from apps/asas-backoffice: ${run} run dev # the backoffice console, http://localhost:5174` : ''}
110
510
 
111
- cd ${name}${installed ? '' : '\n bun install'}
112
- cd templates/asas
113
- # push the schema once, development only
114
- bun -e "const { asas } = await import('./src/asas.ts'); await asas.connect(); await asas.destroy()"
115
- cd ../..
116
- bun run --cwd apps/asas-tanstack dev # the tenant app, http://localhost:5173
117
- bun run --cwd apps/asas-platform dev # in a second terminal: the platform console, http://localhost:5174
511
+ Sign in${backoffice ? ' to both apps' : ''} with DEV_ADMIN_EMAIL and DEV_ADMIN_PASSWORD from ${language}.env. A deploy is set up by hand instead: ${settings.organizations === 'many' ? 'sign up in the tenant app, prove your address by the link it mails, sign in and create an organization.' : `open /auth/first-run and make the app's owner${settings.showOrganization ? ' and its organization' : ''} with the SETUP_CODE it was started with.`} A production start needs a mailer: RESEND_API_KEY and MAIL_FROM. ${run} run seed in ${single ? 'the project' : 'packages/asas'} fills an organization with sample rows again: the first user's first, or the ones SEED_EMAIL and SEED_ORGANIZATION name.${short ? `
118
512
 
119
- Sign up admin@admin.com with the organization "Admin org", then run bun run seed in templates/asas.`)
513
+ The project holds no migration yet. Before it runs in production, write its first against an empty database with mithaq migrate --name init in ${single ? 'the project' : 'packages/asas'} (README.md, "Running in production").` : ''}`)
120
514
  }
121
515
 
122
516
  if (process.argv[1] && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-mithaq",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "bin": {