@team-codeme/crud-forge 0.0.0-stage → 1.0.0

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 (415) hide show
  1. package/.cursor/agents/generator-engineer.md +31 -0
  2. package/.cursor/agents/keycloak-engineer.md +33 -0
  3. package/.cursor/agents/qa-engineer.md +32 -0
  4. package/.cursor/agents/security-reviewer.md +30 -0
  5. package/.cursor/mcp.json +13 -0
  6. package/.cursor/rules/crudforge-precautions.mdc +16 -0
  7. package/.cursor/rules/playwright-browser.mdc +30 -0
  8. package/.cursor/rules/readme-todo-sync.mdc +13 -0
  9. package/.env.qa.example +19 -0
  10. package/.github/workflows/qa-live.yml +32 -0
  11. package/.github/workflows/qa.yml +27 -0
  12. package/.vscode/settings.json +32 -0
  13. package/QA/IAM-PLAN.md +25 -0
  14. package/QA/IAM-RESULTS.md +39 -0
  15. package/QA/IAM-TESTCASES.md +36 -0
  16. package/QA/IAM-TODO.md +7 -0
  17. package/QA/KEYCLOAK-BOOTSTRAP.md +105 -0
  18. package/QA/PLAN.md +43 -0
  19. package/QA/RESULTS.md +59 -0
  20. package/QA/TESTCASES.md +65 -0
  21. package/QA/TODO.md +14 -0
  22. package/QA/pmb-dev/README.md +49 -0
  23. package/QA/pmb-dev/config/config.yml +152 -0
  24. package/QA/pmb-dev/gdl/i.gdl +17 -0
  25. package/README.md +851 -2
  26. package/Todo.md +207 -0
  27. package/assets/branding/crudforge-logo.png +0 -0
  28. package/assets/branding/crudforge-mark.png +0 -0
  29. package/branding/poster-crudforge.jpg +0 -0
  30. package/chat/last-chat/Investigating Mock Data Configuration.md +428 -0
  31. package/cli.js +167 -0
  32. package/config/default-config.yml +245 -0
  33. package/docs/performance/README.md +217 -0
  34. package/docs/performance/after.json +111 -0
  35. package/docs/performance/before.json +111 -0
  36. package/docs/quality/README.md +276 -0
  37. package/e2e/crudforge.e2e.yml +47 -0
  38. package/e2e/crudforge.field-profile.yml +33 -0
  39. package/e2e/crudforge.iam-live.yml +124 -0
  40. package/e2e/crudforge.iam.yml +62 -0
  41. package/e2e/tests/field-profile.spec.js +152 -0
  42. package/e2e/tests/generated-crud.spec.js +251 -0
  43. package/e2e/tests/iam-live.spec.js +149 -0
  44. package/e2e/tests/iam-pack.spec.js +151 -0
  45. package/e2e/tests/list-state.spec.js +73 -0
  46. package/features.csv +27 -0
  47. package/fuse-starter-pack/branding/crudforge-mark-source.jpg +0 -0
  48. package/fuse-starter-pack/branding/crudforge-mark-source.png +0 -0
  49. package/fuse-starter-pack/branding/crudforge-mark.png +0 -0
  50. package/fuse-starter-pack/branding/crudforge-wordmark-source.jpg +0 -0
  51. package/fuse-starter-pack/branding/crudforge-wordmark-source.png +0 -0
  52. package/fuse-starter-pack/branding/crudforge-wordmark.png +0 -0
  53. package/fuse-starter-pack/fuse-v21.1/Dockerfile +14 -0
  54. package/fuse-starter-pack/fuse-v21.1/LICENSE.md +6 -0
  55. package/fuse-starter-pack/fuse-v21.1/README.md +59 -0
  56. package/fuse-starter-pack/fuse-v21.1/angular.json +113 -0
  57. package/fuse-starter-pack/fuse-v21.1/eslint.config.ts +150 -0
  58. package/fuse-starter-pack/fuse-v21.1/nginx.conf +23 -0
  59. package/fuse-starter-pack/fuse-v21.1/nginx.dev.conf +18 -0
  60. package/fuse-starter-pack/fuse-v21.1/package-lock.json +11513 -0
  61. package/fuse-starter-pack/fuse-v21.1/package.json +69 -0
  62. package/fuse-starter-pack/fuse-v21.1/postcss.config.json +5 -0
  63. package/fuse-starter-pack/fuse-v21.1/prettier.config.ts +15 -0
  64. package/fuse-starter-pack/fuse-v21.1/proxy.conf.json +8 -0
  65. package/fuse-starter-pack/fuse-v21.1/proxy.dev.conf.json +8 -0
  66. package/fuse-starter-pack/fuse-v21.1/public/favicon.png +0 -0
  67. package/fuse-starter-pack/fuse-v21.1/public/fonts/geist/Geist-Italic.woff2 +0 -0
  68. package/fuse-starter-pack/fuse-v21.1/public/fonts/geist/Geist.woff2 +0 -0
  69. package/fuse-starter-pack/fuse-v21.1/public/fonts/geist/GeistMono-Italic.woff2 +0 -0
  70. package/fuse-starter-pack/fuse-v21.1/public/fonts/geist/GeistMono.woff2 +0 -0
  71. package/fuse-starter-pack/fuse-v21.1/public/i18n/en.json +8 -0
  72. package/fuse-starter-pack/fuse-v21.1/public/i18n/es.json +8 -0
  73. package/fuse-starter-pack/fuse-v21.1/public/images/flags/ES.svg +79 -0
  74. package/fuse-starter-pack/fuse-v21.1/public/images/flags/US.svg +14 -0
  75. package/fuse-starter-pack/fuse-v21.1/public/images/flags/where-to-find-other-flags.txt +5 -0
  76. package/fuse-starter-pack/fuse-v21.1/public/images/logo/logo-mark.png +0 -0
  77. package/fuse-starter-pack/fuse-v21.1/public/images/logo/logo.png +0 -0
  78. package/fuse-starter-pack/fuse-v21.1/push.sh +27 -0
  79. package/fuse-starter-pack/fuse-v21.1/src/app/app.config.server.ts +10 -0
  80. package/fuse-starter-pack/fuse-v21.1/src/app/app.config.ts +64 -0
  81. package/fuse-starter-pack/fuse-v21.1/src/app/app.routes.server.ts +8 -0
  82. package/fuse-starter-pack/fuse-v21.1/src/app/app.routes.ts +39 -0
  83. package/fuse-starter-pack/fuse-v21.1/src/app/app.ts +22 -0
  84. package/fuse-starter-pack/fuse-v21.1/src/app/core/icons/index.ts +1 -0
  85. package/fuse-starter-pack/fuse-v21.1/src/app/core/icons/lucide.ts +1 -0
  86. package/fuse-starter-pack/fuse-v21.1/src/app/core/icons/provider.ts +23 -0
  87. package/fuse-starter-pack/fuse-v21.1/src/app/core/local-storage/index.ts +2 -0
  88. package/fuse-starter-pack/fuse-v21.1/src/app/core/local-storage/local-storage.ts +109 -0
  89. package/fuse-starter-pack/fuse-v21.1/src/app/core/local-storage/provider.ts +15 -0
  90. package/fuse-starter-pack/fuse-v21.1/src/app/core/media/index.ts +2 -0
  91. package/fuse-starter-pack/fuse-v21.1/src/app/core/media/media.ts +64 -0
  92. package/fuse-starter-pack/fuse-v21.1/src/app/core/media/provider.ts +15 -0
  93. package/fuse-starter-pack/fuse-v21.1/src/app/core/theming/index.ts +3 -0
  94. package/fuse-starter-pack/fuse-v21.1/src/app/core/theming/models/theming.ts +13 -0
  95. package/fuse-starter-pack/fuse-v21.1/src/app/core/theming/palette.ts +209 -0
  96. package/fuse-starter-pack/fuse-v21.1/src/app/core/theming/provider.ts +24 -0
  97. package/fuse-starter-pack/fuse-v21.1/src/app/core/theming/theming.ts +129 -0
  98. package/fuse-starter-pack/fuse-v21.1/src/app/core/transloco/transloco-http-loader.ts +12 -0
  99. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/layout/data/navigation.ts +176 -0
  100. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/layout/layout.ts +77 -0
  101. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/layout/ui/language-switcher.ts +91 -0
  102. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/layout/ui/navigation.ts +238 -0
  103. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/layout/ui/notifications.ts +252 -0
  104. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/layout/ui/scheme-switcher.ts +58 -0
  105. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/layout/ui/sidebar.ts +31 -0
  106. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/layout/ui/user.ts +132 -0
  107. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/academy/data/academy.ts +753 -0
  108. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/academy/data/model.ts +27 -0
  109. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/academy/features/course.ts +318 -0
  110. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/academy/features/courses.ts +228 -0
  111. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/academy/routes.ts +14 -0
  112. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/contacts/data/contacts.ts +4372 -0
  113. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/contacts/data/model.ts +34 -0
  114. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/contacts/features/contact.ts +239 -0
  115. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/contacts/features/contacts.ts +185 -0
  116. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/contacts/routes.ts +16 -0
  117. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/file-manager/data/file-manager.ts +261 -0
  118. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/file-manager/data/model.ts +12 -0
  119. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/file-manager/features/file-manager.ts +131 -0
  120. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/file-manager/routes.ts +15 -0
  121. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/file-manager/ui/list.ts +164 -0
  122. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/help-center/features/faq.ts +288 -0
  123. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/help-center/features/guide.ts +134 -0
  124. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/help-center/features/home.ts +219 -0
  125. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/help-center/features/support.ts +161 -0
  126. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/help-center/routes.ts +33 -0
  127. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/notes/data/model.ts +18 -0
  128. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/notes/data/notes.ts +324 -0
  129. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/notes/features/note.ts +152 -0
  130. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/notes/features/notes.ts +118 -0
  131. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/notes/routes.ts +16 -0
  132. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/tasks/data/model.ts +17 -0
  133. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/tasks/data/tasks.ts +562 -0
  134. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/tasks/features/task.ts +195 -0
  135. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/tasks/features/tasks.ts +271 -0
  136. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/apps/tasks/routes.ts +16 -0
  137. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/dashboards/data/analytics.ts +1620 -0
  138. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/dashboards/data/finance.ts +1065 -0
  139. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/dashboards/data/project.ts +376 -0
  140. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/dashboards/features/analytics/analytics.ts +1096 -0
  141. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/dashboards/features/finance/finance.ts +658 -0
  142. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/dashboards/features/project/budget-table.ts +188 -0
  143. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/dashboards/features/project/project.ts +565 -0
  144. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/dashboards/routes.ts +23 -0
  145. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/documentation/features/changelog/changelog.ts +932 -0
  146. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/documentation/features/getting-started/building.ts +33 -0
  147. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/documentation/features/getting-started/development.ts +42 -0
  148. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/documentation/features/getting-started/installation.ts +47 -0
  149. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/documentation/layout.ts +9 -0
  150. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/documentation/routes.ts +45 -0
  151. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/error/features/error-page.ts +70 -0
  152. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/error/routes.ts +21 -0
  153. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/notifications/features/notifications.ts +262 -0
  154. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/notifications/routes.ts +10 -0
  155. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/settings/features/account.ts +238 -0
  156. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/settings/features/notifications.ts +134 -0
  157. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/settings/features/plan-and-billing.ts +238 -0
  158. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/settings/features/security.ts +211 -0
  159. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/settings/features/team.ts +188 -0
  160. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/settings/layout.ts +109 -0
  161. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/settings/routes.ts +37 -0
  162. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/modules/extras/welcome/welcome.component.ts +22 -0
  163. package/fuse-starter-pack/fuse-v21.1/src/app/domains/admin/routes.ts +95 -0
  164. package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/features/forgot-password/forgot-password.html +54 -0
  165. package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/features/forgot-password/forgot-password.ts +51 -0
  166. package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/features/reset-password/reset-password.html +82 -0
  167. package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/features/reset-password/reset-password.ts +69 -0
  168. package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/features/sign-in/sign-in.html +132 -0
  169. package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/features/sign-in/sign-in.ts +54 -0
  170. package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/features/sign-up/sign-up.html +183 -0
  171. package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/features/sign-up/sign-up.ts +56 -0
  172. package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/layout.ts +9 -0
  173. package/fuse-starter-pack/fuse-v21.1/src/app/domains/auth/routes.ts +34 -0
  174. package/fuse-starter-pack/fuse-v21.1/src/app/domains/coming-soon/features/coming-soon.ts +156 -0
  175. package/fuse-starter-pack/fuse-v21.1/src/app/domains/coming-soon/layout.ts +9 -0
  176. package/fuse-starter-pack/fuse-v21.1/src/app/domains/coming-soon/routes.ts +16 -0
  177. package/fuse-starter-pack/fuse-v21.1/src/app/domains/website/layout/layout.ts +9 -0
  178. package/fuse-starter-pack/fuse-v21.1/src/app/domains/website/modules/home/features/home.ts +23 -0
  179. package/fuse-starter-pack/fuse-v21.1/src/app/domains/website/modules/home/routes.ts +10 -0
  180. package/fuse-starter-pack/fuse-v21.1/src/app/domains/website/routes.ts +30 -0
  181. package/fuse-starter-pack/fuse-v21.1/src/environments/environment.mock.ts +5 -0
  182. package/fuse-starter-pack/fuse-v21.1/src/environments/environment.prod.ts +5 -0
  183. package/fuse-starter-pack/fuse-v21.1/src/environments/environment.ts +5 -0
  184. package/fuse-starter-pack/fuse-v21.1/src/index.html +66 -0
  185. package/fuse-starter-pack/fuse-v21.1/src/main.server.ts +11 -0
  186. package/fuse-starter-pack/fuse-v21.1/src/main.ts +5 -0
  187. package/fuse-starter-pack/fuse-v21.1/src/server.ts +70 -0
  188. package/fuse-starter-pack/fuse-v21.1/src/styles/base/base.css +63 -0
  189. package/fuse-starter-pack/fuse-v21.1/src/styles/base/typography.css +54 -0
  190. package/fuse-starter-pack/fuse-v21.1/src/styles/components/a.css +18 -0
  191. package/fuse-starter-pack/fuse-v21.1/src/styles/components/input.css +22 -0
  192. package/fuse-starter-pack/fuse-v21.1/src/styles/components/kbd.css +55 -0
  193. package/fuse-starter-pack/fuse-v21.1/src/styles/components/material.css +1149 -0
  194. package/fuse-starter-pack/fuse-v21.1/src/styles/styles.css +31 -0
  195. package/fuse-starter-pack/fuse-v21.1/src/styles/tailwind/theme.css +147 -0
  196. package/fuse-starter-pack/fuse-v21.1/src/styles/tailwind/utilities.css +563 -0
  197. package/fuse-starter-pack/fuse-v21.1/src/styles/tailwind/variants.css +4 -0
  198. package/fuse-starter-pack/fuse-v21.1/src/styles/third-party/lucide-icons.css +12 -0
  199. package/fuse-starter-pack/fuse-v21.1/tsconfig.app.json +16 -0
  200. package/fuse-starter-pack/fuse-v21.1/tsconfig.json +37 -0
  201. package/fuse-starter-pack/fuse-v21.1/tsconfig.spec.json +10 -0
  202. package/generator/README.md +12 -0
  203. package/generator/issues/.gitkeep +0 -0
  204. package/package.json +56 -4
  205. package/playwright.config.js +29 -0
  206. package/playwright.field-profile.config.js +6 -0
  207. package/playwright.iam-live.config.js +31 -0
  208. package/playwright.iam.config.js +29 -0
  209. package/playwright.list-state.config.js +6 -0
  210. package/playwright.performance.config.js +1 -0
  211. package/run-config-tests.sh +5 -0
  212. package/scripts/e2e-paths.js +89 -0
  213. package/scripts/e2e-prepare-field-profile.js +17 -0
  214. package/scripts/e2e-prepare-iam-live.js +135 -0
  215. package/scripts/e2e-prepare-iam.js +65 -0
  216. package/scripts/e2e-prepare.js +72 -0
  217. package/scripts/e2e-serve-iam-live.js +28 -0
  218. package/scripts/e2e-serve-iam.js +28 -0
  219. package/scripts/e2e-serve.js +28 -0
  220. package/scripts/performance/generate.js +17 -0
  221. package/scripts/performance/measure.js +55 -0
  222. package/scripts/postinstall.js +9 -0
  223. package/scripts/prepare-quality-apps.js +34 -0
  224. package/scripts/run-config-tests.js +63 -0
  225. package/src/assets/logo-with-lable.jpeg +0 -0
  226. package/src/assets/logo.jpeg +0 -0
  227. package/src/commands/bootstrap.js +92 -0
  228. package/src/commands/doctor.js +403 -0
  229. package/src/commands/generate.js +301 -0
  230. package/src/commands/init-config.js +310 -0
  231. package/src/commands/keycloak-options.js +182 -0
  232. package/src/commands/validate.js +52 -0
  233. package/src/config/auth-config.js +183 -0
  234. package/src/config/default-values.js +235 -0
  235. package/src/config/env-refs.js +71 -0
  236. package/src/config/field-preferences.js +99 -0
  237. package/src/config/input-sources.js +104 -0
  238. package/src/config/load-config.js +166 -0
  239. package/src/config/proxy-routes.js +44 -0
  240. package/src/generator/access-control-tools.js +334 -0
  241. package/src/generator/ai-docs.js +691 -0
  242. package/src/generator/application-tests.js +252 -0
  243. package/src/generator/auth-pack.js +965 -0
  244. package/src/generator/embed-config.js +431 -0
  245. package/src/generator/entity-layout.js +30 -0
  246. package/src/generator/fuse-build.js +57 -0
  247. package/src/generator/fuse-runtime.js +344 -0
  248. package/src/generator/generated-banner.js +137 -0
  249. package/src/generator/index.js +874 -0
  250. package/src/generator/project-output.js +126 -0
  251. package/src/generator/team-pack.js +220 -0
  252. package/src/index.js +68 -0
  253. package/src/parser/gdl-parser.js +169 -0
  254. package/src/parser/jdl-parser.js +171 -0
  255. package/src/presets/pmb-test.js +139 -0
  256. package/src/presets/static-source.js +73 -0
  257. package/src/presets/store.js +96 -0
  258. package/src/provision/admin-service.js +148 -0
  259. package/src/provision/bootstrap.js +147 -0
  260. package/src/provision/http.js +123 -0
  261. package/src/provision/keycloak-admin-rest.js +335 -0
  262. package/src/provision/state.js +51 -0
  263. package/src/provision/token.js +52 -0
  264. package/src/templates/angular/auth/access-control.service.ts.ejs +179 -0
  265. package/src/templates/angular/auth/access-ctrl.pipe.ts.ejs +20 -0
  266. package/src/templates/angular/auth/auth.guard.ts.ejs +27 -0
  267. package/src/templates/angular/auth/auth.interceptor.ts.ejs +53 -0
  268. package/src/templates/angular/auth/auth.service.ts.ejs +594 -0
  269. package/src/templates/angular/auth/auth.utils.ts.ejs +28 -0
  270. package/src/templates/angular/auth/current-user.ts.ejs +24 -0
  271. package/src/templates/angular/auth/disabled-account-overlay.component.css.ejs +95 -0
  272. package/src/templates/angular/auth/disabled-account-overlay.component.html.ejs +33 -0
  273. package/src/templates/angular/auth/disabled-account-overlay.component.ts.ejs +23 -0
  274. package/src/templates/angular/auth/fuse-account.ts.ejs +234 -0
  275. package/src/templates/angular/auth/fuse-user.ts.ejs +264 -0
  276. package/src/templates/angular/auth/has-permission.directive.ts.ejs +51 -0
  277. package/src/templates/angular/auth/index.ts.ejs +24 -0
  278. package/src/templates/angular/auth/keycloak-admin.service.ts.ejs +670 -0
  279. package/src/templates/angular/auth/landing.guard.ts.ejs +61 -0
  280. package/src/templates/angular/auth/no-auth.guard.ts.ejs +19 -0
  281. package/src/templates/angular/auth/permission-attribute.util.ts.ejs +182 -0
  282. package/src/templates/angular/auth/sign-in.ts.ejs +60 -0
  283. package/src/templates/angular/entity/_entity_.model.ts.ejs +64 -0
  284. package/src/templates/angular/entity/_entity_.routes.ts.ejs +9 -0
  285. package/src/templates/angular/entity/components/forms/form/_entity_-form.component.html.ejs +168 -0
  286. package/src/templates/angular/entity/components/forms/form/_entity_-form.component.ts.ejs +175 -0
  287. package/src/templates/angular/entity/components/forms/form-page/_entity_-form-page.component.html.ejs +10 -0
  288. package/src/templates/angular/entity/components/forms/form-page/_entity_-form-page.component.ts.ejs +86 -0
  289. package/src/templates/angular/entity/components/forms/update/_entity_-form.service.ts.ejs +136 -0
  290. package/src/templates/angular/entity/components/interaction/actions/_entity_-actions.component.html.ejs +68 -0
  291. package/src/templates/angular/entity/components/interaction/actions/_entity_-actions.component.ts.ejs +69 -0
  292. package/src/templates/angular/entity/components/interaction/filter/_entity_-filter.component.html.ejs +111 -0
  293. package/src/templates/angular/entity/components/interaction/filter/_entity_-filter.component.ts.ejs +102 -0
  294. package/src/templates/angular/entity/components/presentation/card/_entity_-card.component.css.ejs +33 -0
  295. package/src/templates/angular/entity/components/presentation/card/_entity_-card.component.html.ejs +29 -0
  296. package/src/templates/angular/entity/components/presentation/card/_entity_-card.component.ts.ejs +93 -0
  297. package/src/templates/angular/entity/components/presentation/detail/_entity_-detail.component.css.ejs +54 -0
  298. package/src/templates/angular/entity/components/presentation/detail/_entity_-detail.component.html.ejs +58 -0
  299. package/src/templates/angular/entity/components/presentation/detail/_entity_-detail.component.ts.ejs +47 -0
  300. package/src/templates/angular/entity/components/presentation/list/_entity_-list.component.html.ejs +419 -0
  301. package/src/templates/angular/entity/components/presentation/list/_entity_-list.component.scss.ejs +48 -0
  302. package/src/templates/angular/entity/components/presentation/list/_entity_-list.component.ts.ejs +592 -0
  303. package/src/templates/angular/entity/service/_entity_.service.ts.ejs +153 -0
  304. package/src/templates/angular/entity/settings/_entity_.settings.ts.ejs +49 -0
  305. package/src/templates/angular/enums/_enum_.model.ts.ejs +11 -0
  306. package/src/templates/angular/iam/change-password.component.html.ejs +39 -0
  307. package/src/templates/angular/iam/change-password.component.ts.ejs +44 -0
  308. package/src/templates/angular/iam/no-access.component.ts.ejs +17 -0
  309. package/src/templates/angular/iam/role-list.component.html.ejs +331 -0
  310. package/src/templates/angular/iam/role-list.component.scss.ejs +107 -0
  311. package/src/templates/angular/iam/role-list.component.ts.ejs +597 -0
  312. package/src/templates/angular/iam/role-management/role-header.component.html.ejs +51 -0
  313. package/src/templates/angular/iam/role-management/role-header.component.ts.ejs +45 -0
  314. package/src/templates/angular/iam/role-management/role-inheritance.component.html.ejs +15 -0
  315. package/src/templates/angular/iam/role-management/role-inheritance.component.ts.ejs +19 -0
  316. package/src/templates/angular/iam/role-management/role-permissions-panel.component.css.ejs +24 -0
  317. package/src/templates/angular/iam/role-management/role-permissions-panel.component.html.ejs +108 -0
  318. package/src/templates/angular/iam/role-management/role-permissions-panel.component.ts.ejs +122 -0
  319. package/src/templates/angular/iam/role-management/role-users.component.html.ejs +104 -0
  320. package/src/templates/angular/iam/role-management/role-users.component.ts.ejs +53 -0
  321. package/src/templates/angular/iam/role-management/user-select-dialog.component.html.ejs +113 -0
  322. package/src/templates/angular/iam/role-management/user-select-dialog.component.ts.ejs +157 -0
  323. package/src/templates/angular/iam/role-permission-matrix.component.html.ejs +355 -0
  324. package/src/templates/angular/iam/role-permission-matrix.component.scss.ejs +217 -0
  325. package/src/templates/angular/iam/role-permission-matrix.component.ts.ejs +800 -0
  326. package/src/templates/angular/iam/screen-catalog.ts.ejs +147 -0
  327. package/src/templates/angular/iam/shared/confirm-dialog.component.ts.ejs +83 -0
  328. package/src/templates/angular/iam/shared/matrix-hierarchy.util.ts.ejs +196 -0
  329. package/src/templates/angular/iam/shared/permission-matrix-sound.ts.ejs +59 -0
  330. package/src/templates/angular/iam/shared/role-display.util.ts.ejs +43 -0
  331. package/src/templates/angular/iam/shared/role-permission-skeleton.component.ts.ejs +54 -0
  332. package/src/templates/angular/iam/shared/screen-matrix.util.ts.ejs +554 -0
  333. package/src/templates/angular/iam/shared/screen-navigation-map.ts.ejs +187 -0
  334. package/src/templates/angular/iam/shared/screen-navigation.registry.ts.ejs +64 -0
  335. package/src/templates/angular/iam/shared/text-highlight.pipe.ts.ejs +39 -0
  336. package/src/templates/angular/iam/user-access-matrix.component.html.ejs +212 -0
  337. package/src/templates/angular/iam/user-access-matrix.component.scss.ejs +34 -0
  338. package/src/templates/angular/iam/user-access-matrix.component.ts.ejs +239 -0
  339. package/src/templates/angular/iam/user-detail.component.html.ejs +143 -0
  340. package/src/templates/angular/iam/user-detail.component.ts.ejs +108 -0
  341. package/src/templates/angular/iam/user-form.component.html.ejs +63 -0
  342. package/src/templates/angular/iam/user-form.component.ts.ejs +100 -0
  343. package/src/templates/angular/iam/user-list.component.html.ejs +369 -0
  344. package/src/templates/angular/iam/user-list.component.scss.ejs +242 -0
  345. package/src/templates/angular/iam/user-list.component.ts.ejs +446 -0
  346. package/src/templates/angular/iam/user-management.routes.ts.ejs +47 -0
  347. package/src/templates/angular/iam/user-permission-matrix.component.html.ejs +231 -0
  348. package/src/templates/angular/iam/user-permission-matrix.component.scss.ejs +216 -0
  349. package/src/templates/angular/iam/user-permission-matrix.component.ts.ejs +516 -0
  350. package/src/templates/angular/shared/base/audit-user-name.pipe.ts.ejs +69 -0
  351. package/src/templates/angular/shared/base/base-filter.component.ts.ejs +245 -0
  352. package/src/templates/angular/shared/base/base-form.component.ts.ejs +109 -0
  353. package/src/templates/angular/shared/base/base-list.component.ts.ejs +507 -0
  354. package/src/templates/angular/shared/base/filter-types.ts.ejs +55 -0
  355. package/src/templates/angular/shared/base/input-format.directive.ts.ejs +47 -0
  356. package/src/templates/angular/shared/base/input-format.ts.ejs +15 -0
  357. package/src/templates/angular/shared/confirm-with-input-dialog.component.ts.ejs +91 -0
  358. package/src/templates/angular/shared/confirmation-dialog.component.ts.ejs +80 -0
  359. package/src/templates/angular/shared/list-styles.css.ejs +103 -0
  360. package/src/templates/angular/shared/loading-overlay.component.ts.ejs +24 -0
  361. package/src/templates/angular/shared/resizable.directive.ts.ejs +115 -0
  362. package/src/templates/angular/shared/snackbar-styles.css.ejs +11 -0
  363. package/src/templates/fuse-packs/access-control-tools/access-control-sync.js +819 -0
  364. package/src/templates/fuse-packs/access-control-tools/build-rename-map.js +67 -0
  365. package/src/templates/fuse-packs/access-control-tools/inject-qa-ids.js +14 -0
  366. package/src/templates/fuse-packs/access-control-tools/lib/hierarchy-scanner.js +628 -0
  367. package/src/templates/fuse-packs/access-control-tools/lib/hierarchy-scanner.test.js +323 -0
  368. package/src/templates/fuse-packs/access-control-tools/lib/html-scanner.js +362 -0
  369. package/src/templates/fuse-packs/access-control-tools/lib/id-rules.duplicate.test.js +136 -0
  370. package/src/templates/fuse-packs/access-control-tools/lib/id-rules.js +1095 -0
  371. package/src/templates/fuse-packs/access-control-tools/lib/ts-component-index.js +482 -0
  372. package/src/templates/fuse-packs/access-control-tools/output/.gitkeep +0 -0
  373. package/src/templates/fuse-packs/access-control-tools/run.sh +223 -0
  374. package/src/templates/fuse-packs/access-control-tools/scan-access-control.js +14 -0
  375. package/src/templates/fuse-packs/iam-assets/sound/apple-play.mp3 +0 -0
  376. package/src/templates/fuse-packs/iam-assets/sound/remove.mp3 +0 -0
  377. package/src/templates/generator/README.md +12 -0
  378. package/src/templates/testing/ci.cjs +45 -0
  379. package/src/templates/testing/mock.cjs +149 -0
  380. package/src/templates/testing/playwright.config.cjs +25 -0
  381. package/src/templates/testing/serve.cjs +22 -0
  382. package/src/templates/testing/smoke.spec.cjs +180 -0
  383. package/src/utils/branding.js +39 -0
  384. package/src/utils/colors.js +18 -0
  385. package/src/utils/diff.js +109 -0
  386. package/src/utils/merger.js +115 -0
  387. package/src/validator/backend-diagnostics.js +30 -0
  388. package/src/validator/blueprint-validator.js +276 -0
  389. package/src/validator/diagnostics.js +608 -0
  390. package/src/validator/pattern.js +28 -0
  391. package/test/auth-interceptor.js +84 -0
  392. package/test/auth-provision-checks.js +297 -0
  393. package/test/field-preferences.js +145 -0
  394. package/test/fixtures/field-profile.gdl +7 -0
  395. package/test/fixtures/mock.jdl +17 -0
  396. package/test/fixtures/mock.png +0 -0
  397. package/test/fixtures/performance/config.yml +33 -0
  398. package/test/fixtures/performance/large.gdl +199 -0
  399. package/test/fixtures/performance/small.gdl +7 -0
  400. package/test/fixtures/quality/custom.yml +22 -0
  401. package/test/fixtures/quality/go-duck.yml +22 -0
  402. package/test/fixtures/quality/jhipster.yml +30 -0
  403. package/test/fixtures/quality/representative.gdl +14 -0
  404. package/test/list-state.js +138 -0
  405. package/test/micro-functions-layout.js +95 -0
  406. package/test/performance-generation.js +32 -0
  407. package/test/pmb-preset.js +127 -0
  408. package/test/project-workflow.js +111 -0
  409. package/test/quality-features.js +250 -0
  410. package/test/verify-commands.js +83 -0
  411. package/test/verify.js +959 -0
  412. package/test-files/GDL/example.gdl +15 -0
  413. package/test-files/JDL/example.jdl +15 -0
  414. package/test-files/config/crudforge.config.yml +127 -0
  415. package/test-run.sh +272 -0
@@ -0,0 +1,874 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const ejs = require('ejs');
4
+ const _ = require('lodash');
5
+
6
+ const { entityFolder: outputEntityFolder, serviceFolder, relativeEntityPath } = require('./entity-layout');
7
+ const { isKeycloakAuthEnabled } = require('./auth-pack');
8
+
9
+ const helpers = {
10
+ patternExpression: require('../validator/pattern').patternExpression,
11
+ toKebabCase: (str) => _.kebabCase(str),
12
+ toCamelCase: (str) => _.camelCase(str),
13
+ toPascalCase: (str) => _.upperFirst(_.camelCase(str)),
14
+ };
15
+
16
+ /**
17
+ * Executes code generation for all entities and enums.
18
+ * @param {object} parsedData - Entities, enums, and relationships.
19
+ * @param {object} config - Configuration preferences.
20
+ */
21
+ async function generate(parsedData, config) {
22
+ require('../config/field-preferences').validateFieldPreferences(config, parsedData.entities, config._configPath || 'fields configuration');
23
+ if (config.auth) {
24
+ const { stripSecretsForGenerate, deriveAuthUrls } = require('../config/auth-config');
25
+ deriveAuthUrls(config.auth);
26
+ config.auth = stripSecretsForGenerate(config.auth);
27
+ }
28
+
29
+ const { entities, enums, relationships } = parsedData;
30
+ if (config.testing?.ci && !config.generate?.globals?.tests) throw new Error('testing.ci requires generate.globals.tests: true.');
31
+ if (config.generate?.globals?.tests) {
32
+ if (!config.fuse?.enabled) throw new Error('generate.globals.tests requires fuse.enabled.');
33
+ if (config.generate.components.list === false || config.generate.forms.form === false || config.generate.routes === false) throw new Error('Generated smoke tests require list components, forms and routes.');
34
+ for (const entity of entities) for (const field of entity.fields) require('./application-tests').fixtureValue(field, enums, config.testing?.fixtures?.[entity.name]?.[field.fieldName]);
35
+ }
36
+ const outputDir = config.output.directory;
37
+
38
+ const isGdl = config.fileType === 'gdl' || (config.input?.file && String(config.input.file).endsWith('.gdl'));
39
+ const defaultFolderName = config.fuse?.folderType || (isGdl ? 'micro-functions' : 'microservice');
40
+ // Prefer first multi-source folder when present; single-file mode keeps micro-functions / microservice.
41
+ const folderName =
42
+ (Array.isArray(config._inputSources) && config._inputSources[0]?.folder) || defaultFolderName;
43
+
44
+ // Validate and copy Fuse starter pack if enabled
45
+ if (config.fuse && config.fuse.enabled) {
46
+ const starterPackPath = config.fuse.starterPackPath || '/Users/abdulla/Documents/HC/crudforge-cli/fuse-starter-pack/fuse-v21.1';
47
+ if (!fs.existsSync(starterPackPath)) {
48
+ throw new Error(`Fuse Starter Pack Error: Source path "${starterPackPath}" does not exist.`);
49
+ }
50
+ const routesFilePath = path.join(starterPackPath, config.fuse.routesFile || 'src/app/domains/admin/routes.ts');
51
+ const navFilePath = path.join(starterPackPath, config.fuse.navigationFile || 'src/app/domains/admin/layout/data/navigation.ts');
52
+ if (!fs.existsSync(routesFilePath)) {
53
+ throw new Error(`Fuse Starter Pack Error: Required routes file "${routesFilePath}" does not exist.`);
54
+ }
55
+ if (!fs.existsSync(navFilePath)) {
56
+ throw new Error(`Fuse Starter Pack Error: Required navigation file "${navFilePath}" does not exist.`);
57
+ }
58
+
59
+ if (!config.dryRun) {
60
+ console.log(` -> Scaffolding Fuse Angular project from: ${starterPackPath}`);
61
+ const filterFn = (src, dest) => {
62
+ const basename = path.basename(src);
63
+ if (['node_modules', '.git', '.angular', '.DS_Store'].includes(basename)) return false;
64
+ const relativeFile = path.relative(starterPackPath, src).replace(/\\/g, '/');
65
+ if ((config.fuse.rendering || 'spa') === 'spa' && ['src/main.server.ts', 'src/server.ts', 'src/app/app.config.server.ts', 'src/app/app.routes.server.ts'].includes(relativeFile)) return false;
66
+
67
+ if (config.fuse?.includePrebuilt === false) {
68
+ const relativeSrc = path.relative(starterPackPath, src).replace(/\\/g, '/');
69
+ const prebuiltPaths = [
70
+ 'src/app/domains/admin/modules/apps',
71
+ 'src/app/domains/admin/modules/dashboards',
72
+ 'src/app/domains/admin/modules/documentation'
73
+ ];
74
+ if (prebuiltPaths.some(p => relativeSrc === p || relativeSrc.startsWith(p + '/'))) {
75
+ return false;
76
+ }
77
+ }
78
+ return true;
79
+ };
80
+ fs.copySync(starterPackPath, outputDir, { filter: filterFn });
81
+ cleanupStaleKeycloakArtifacts(outputDir, config);
82
+ }
83
+ }
84
+
85
+ if (!config.dryRun) {
86
+ fs.ensureDirSync(outputDir);
87
+ }
88
+ let fileCount = 0;
89
+
90
+ const dryRunReport = {
91
+ newFiles: [],
92
+ modifiedFiles: []
93
+ };
94
+
95
+ const conflicts = [];
96
+ const { stampGeneratedBanner } = require('./generated-banner');
97
+
98
+ const writeOrPreview = (filePath, rawContent) => {
99
+ const content = stampGeneratedBanner(filePath, rawContent);
100
+ const relativePath = path.relative(outputDir, filePath);
101
+ const exists = fs.existsSync(filePath);
102
+
103
+ if (config.dryRun) {
104
+ if (exists) {
105
+ const existingContent = fs.readFileSync(filePath, 'utf8');
106
+ if (existingContent !== content) {
107
+ dryRunReport.modifiedFiles.push({
108
+ path: relativePath,
109
+ content: content,
110
+ oldContent: existingContent
111
+ });
112
+ }
113
+ } else {
114
+ dryRunReport.newFiles.push({
115
+ path: relativePath,
116
+ content: content
117
+ });
118
+ }
119
+ return;
120
+ }
121
+
122
+ // Standard run with Overwrite Policy
123
+ const policy = config.output?.overwritePolicy || 'overwrite';
124
+
125
+ if (exists) {
126
+ const existingContent = fs.readFileSync(filePath, 'utf8');
127
+ if (existingContent === content) {
128
+ return; // Identical, no changes
129
+ }
130
+
131
+ if (policy === 'skip') {
132
+ console.log(` ⚠️ Skipped (protected): ${relativePath}`);
133
+ return;
134
+ }
135
+
136
+ if (policy === 'fail') {
137
+ throw new Error(`Conflict Error: File "${relativePath}" already exists and differs. Aborting generation.`);
138
+ }
139
+
140
+ if (policy === 'merge') {
141
+ const { mergeContent } = require('../utils/merger');
142
+ const merged = mergeContent(existingContent, content);
143
+ if (merged !== existingContent) {
144
+ fs.ensureDirSync(path.dirname(filePath));
145
+ fs.writeFileSync(filePath, merged);
146
+ console.log(` ✨ Merged custom changes: ${relativePath}`);
147
+ fileCount++;
148
+ }
149
+ return;
150
+ }
151
+
152
+ if (policy === 'ask') {
153
+ conflicts.push({
154
+ filePath,
155
+ relativePath,
156
+ content,
157
+ oldContent: existingContent
158
+ });
159
+ return;
160
+ }
161
+
162
+ // Default: 'overwrite'
163
+ fs.ensureDirSync(path.dirname(filePath));
164
+ fs.writeFileSync(filePath, content);
165
+ fileCount++;
166
+ } else {
167
+ fs.ensureDirSync(path.dirname(filePath));
168
+ fs.writeFileSync(filePath, content);
169
+ fileCount++;
170
+ }
171
+ };
172
+
173
+ const templatesDir = path.join(__dirname, '..', 'templates', 'angular');
174
+
175
+ // Helper to map type mappings in JS for template pre-parsing (fallback)
176
+ const getTsType = (jdlType) => {
177
+ const typeMappings = {
178
+ String: 'string',
179
+ Integer: 'number',
180
+ Long: 'number',
181
+ BigDecimal: 'number',
182
+ Float: 'number',
183
+ Double: 'number',
184
+ Boolean: 'boolean',
185
+ LocalDate: 'dayjs.Dayjs',
186
+ Instant: 'dayjs.Dayjs',
187
+ ZonedDateTime: 'dayjs.Dayjs',
188
+ TextBlob: 'string',
189
+ UUID: 'string',
190
+ };
191
+ return typeMappings[jdlType] || jdlType;
192
+ };
193
+
194
+ // Keep the module skeleton even when enums are absent or generation is disabled.
195
+ // A placeholder survives staged publishing and Git, which omit empty folders.
196
+ if (config.fuse?.enabled) {
197
+ const modules = new Set([
198
+ ...entities.map(entity => serviceFolder(entity, config)),
199
+ ...(config._inputSources || []).map(source => serviceFolder({
200
+ folderName: source.folder, sourceName: source.name, microservice: source.microservice,
201
+ }, config)),
202
+ ]);
203
+ for (const module of modules) {
204
+ writeOrPreview(path.join(outputDir, 'src/app/domains/admin/modules/apps', module, 'enum/.gitkeep'), '');
205
+ }
206
+ }
207
+
208
+ // 1. Generate Enums if enabled
209
+ if (config.generate.enums && enums.length > 0) {
210
+ const enumTemplatePath = path.join(templatesDir, 'enums', '_enum_.model.ts.ejs');
211
+
212
+ for (const enumDef of enums) {
213
+ const enumConfig = {
214
+ ...helpers,
215
+ enumDef,
216
+ preferences: config
217
+ };
218
+ console.log(` -> Generating Enum: ${enumDef.name}`);
219
+ const fileContent = await ejs.renderFile(enumTemplatePath, { ...enumConfig, cache: true });
220
+ const outputFileName = `${helpers.toKebabCase(enumDef.name)}.model.ts`;
221
+ // CLI schemas carry enum ownership. Programmatic inputs without ownership
222
+ // place each enum in the modules that use it (unused enums in every module).
223
+ const users = entities.filter(entity => entity.fields.some(field => field.fieldType === enumDef.name));
224
+ const owners = enumDef.sourceName || enumDef.folderName || enumDef.microservice
225
+ ? [enumDef] : users.length ? users : entities;
226
+ const directories = config.fuse?.enabled
227
+ ? [...new Set(owners.map(owner => path.join(outputDir,
228
+ 'src/app/domains/admin/modules/apps', serviceFolder(owner, config), 'enum')))]
229
+ : [path.join(outputDir, 'enums')];
230
+ for (const directory of directories) {
231
+ if (!config.dryRun) fs.ensureDirSync(directory);
232
+ writeOrPreview(path.join(directory, outputFileName), fileContent);
233
+ }
234
+ }
235
+ }
236
+
237
+ // 2. Hoist shared UI once for both supported output layouts.
238
+ const fuseShared = !!config.fuse?.enabled;
239
+ config.architecture = { ...(config.architecture || {}), centralizedSharedFolder: true };
240
+ if (config.generate.globals?.sharedAssets !== false) {
241
+ console.log(` -> Generating Centralized Shared Folder...`);
242
+ const sharedBase = fuseShared
243
+ ? path.join(outputDir, 'src/app/domains/admin/modules/apps/shared')
244
+ : path.join(outputDir, 'shared');
245
+ const sharedDirectivesDir = path.join(sharedBase, 'directives');
246
+ const sharedComponentsDir = path.join(sharedBase, 'components');
247
+ const sharedStylesDir = path.join(sharedBase, 'styles');
248
+ const sharedBaseDir = path.join(sharedBase, 'base');
249
+
250
+ if (!config.dryRun) {
251
+ fs.ensureDirSync(sharedDirectivesDir);
252
+ fs.ensureDirSync(sharedComponentsDir);
253
+ fs.ensureDirSync(sharedStylesDir);
254
+ fs.ensureDirSync(sharedBaseDir);
255
+ }
256
+
257
+ const sharedLocals = { preferences: config, cache: true };
258
+ for (const name of ['input-format', 'input-format.directive']) {
259
+ writeOrPreview(path.join(sharedBaseDir, `${name}.ts`), await ejs.renderFile(path.join(templatesDir, `shared/base/${name}.ts.ejs`), sharedLocals));
260
+ }
261
+ const auditNamesContent = await ejs.renderFile(path.join(templatesDir, 'shared/base/audit-user-name.pipe.ts.ejs'), sharedLocals);
262
+ writeOrPreview(path.join(sharedBaseDir, 'audit-user-name.pipe.ts'), auditNamesContent);
263
+ const resizableTpl = path.join(templatesDir, 'shared', 'resizable.directive.ts.ejs');
264
+ const confirmTpl = path.join(templatesDir, 'shared', 'confirmation-dialog.component.ts.ejs');
265
+ const typedConfirmTpl = path.join(templatesDir, 'shared', 'confirm-with-input-dialog.component.ts.ejs');
266
+ const overlayTpl = path.join(templatesDir, 'shared', 'loading-overlay.component.ts.ejs');
267
+ const snackbarTpl = path.join(templatesDir, 'shared', 'snackbar-styles.css.ejs');
268
+ const baseListTpl = path.join(templatesDir, 'shared', 'base', 'base-list.component.ts.ejs');
269
+ const baseFilterTpl = path.join(templatesDir, 'shared', 'base', 'base-filter.component.ts.ejs');
270
+ const baseFormTpl = path.join(templatesDir, 'shared', 'base', 'base-form.component.ts.ejs');
271
+ const filterTypesTpl = path.join(templatesDir, 'shared', 'base', 'filter-types.ts.ejs');
272
+
273
+ const resizableContent = await ejs.renderFile(resizableTpl, sharedLocals);
274
+ const confirmContent = await ejs.renderFile(confirmTpl, sharedLocals);
275
+ const typedConfirmContent = await ejs.renderFile(typedConfirmTpl, sharedLocals);
276
+ const overlayContent = await ejs.renderFile(overlayTpl, sharedLocals);
277
+ const snackbarContent = await ejs.renderFile(snackbarTpl, sharedLocals);
278
+ const baseListContent = await ejs.renderFile(baseListTpl, sharedLocals);
279
+ const baseFilterContent = await ejs.renderFile(baseFilterTpl, sharedLocals);
280
+ const baseFormContent = await ejs.renderFile(baseFormTpl, sharedLocals);
281
+ const filterTypesContent = await ejs.renderFile(filterTypesTpl, sharedLocals);
282
+
283
+ writeOrPreview(path.join(sharedDirectivesDir, 'resizable.directive.ts'), resizableContent);
284
+ writeOrPreview(path.join(sharedComponentsDir, 'confirmation-dialog.component.ts'), confirmContent);
285
+ writeOrPreview(path.join(sharedComponentsDir, 'confirm-with-input-dialog.component.ts'), typedConfirmContent);
286
+ writeOrPreview(path.join(sharedComponentsDir, 'loading-overlay.component.ts'), overlayContent);
287
+ writeOrPreview(path.join(sharedStylesDir, 'snackbar-styles.css'), snackbarContent);
288
+ writeOrPreview(path.join(sharedStylesDir, 'list-styles.css'), await ejs.renderFile(path.join(templatesDir, 'shared/list-styles.css.ejs'), sharedLocals));
289
+ if (fuseShared) {
290
+ const styleFile = path.join(outputDir, 'src/styles/styles.css');
291
+ let styles = fs.readFileSync(styleFile, 'utf8');
292
+ for (const name of ['snackbar-styles', 'list-styles']) {
293
+ const reference = `../app/domains/admin/modules/apps/shared/styles/${name}.css`;
294
+ if (!styles.includes(reference)) styles += `\n@import '${reference}';\n`;
295
+ }
296
+ writeOrPreview(styleFile, styles);
297
+ }
298
+ writeOrPreview(path.join(sharedBaseDir, 'base-list.component.ts'), baseListContent);
299
+ writeOrPreview(path.join(sharedBaseDir, 'base-filter.component.ts'), baseFilterContent);
300
+ writeOrPreview(path.join(sharedBaseDir, 'base-form.component.ts'), baseFormContent);
301
+ writeOrPreview(path.join(sharedBaseDir, 'filter-types.ts'), filterTypesContent);
302
+ }
303
+
304
+ // 3. Generate Entities
305
+ for (const entity of entities) {
306
+ console.log(` -> Generating Entity Components: ${entity.name}`);
307
+
308
+ // Pre-calculate ts types
309
+ entity.fields.forEach(field => {
310
+ field.tsType = getTsType(field.fieldType);
311
+ });
312
+
313
+ const entityKebab = helpers.toKebabCase(entity.name);
314
+ const entityFolder = entity.folderName || folderName;
315
+ const fieldPreferences = require('../config/field-preferences');
316
+ const entityConfig = {
317
+ ...helpers,
318
+ entity,
319
+ entityImportPath: (name) => relativeEntityPath(entity, entities.find(e => e.name === name) || { name }, config),
320
+ config,
321
+ preferences: config,
322
+ fieldSettings: fieldPreferences.entityFieldSettings(config, entity),
323
+ technicalFieldNames: fieldPreferences.technicalFieldNames(config),
324
+ isTechnicalField: (fieldName) => fieldPreferences.isTechnicalField(fieldName, config),
325
+ isHiddenFromUi: (fieldName, surface) => fieldPreferences.isHiddenFromUi(fieldName, config, surface),
326
+ iamAcl: isKeycloakAuthEnabled(config),
327
+ // Match navigation.ts ids (`micro-functions/region` or `identity-access/region`).
328
+ folderName: entityFolder,
329
+ entityScreenId: config.fuse?.enabled ? `${entityFolder}/${entityKebab}` : entityKebab,
330
+ relationships: relationships
331
+ .filter(rel => rel.from.name === entity.name)
332
+ .map(rel => ({
333
+ ...rel,
334
+ otherEntityName: rel.to.name,
335
+ otherEntityNamePascalCase: helpers.toPascalCase(rel.to.name),
336
+ otherEntityNamePlural: `${rel.to.name.toLowerCase()}s`,
337
+ })),
338
+ };
339
+
340
+ const entityBaseDir = config.fuse?.enabled
341
+ ? path.join(outputDir, 'src/app/domains/admin/modules/apps', outputEntityFolder(entity, config), helpers.toKebabCase(entity.name))
342
+ : path.join(outputDir, 'entities', helpers.toKebabCase(entity.name));
343
+ if (!config.dryRun) {
344
+ fs.ensureDirSync(entityBaseDir);
345
+ }
346
+
347
+ // List of templates to map
348
+ const templates = [
349
+ { template: '_entity_.settings.ts.ejs', subfolder: 'settings', outputPattern: 'settings.config.ts', enabled: true },
350
+ { template: '_entity_.model.ts.ejs', subfolder: '', outputPattern: '<%= entityName %>.model.ts', enabled: config.generate.models },
351
+ { template: '_entity_.routes.ts.ejs', subfolder: '', outputPattern: '<%= entityName %>.routes.ts', enabled: config.generate.routes },
352
+ { template: '_entity_.service.ts.ejs', subfolder: 'service', outputPattern: '<%= entityName %>.service.ts', enabled: config.generate.services },
353
+ { template: '_entity_-form.service.ts.ejs',subfolder: 'components/forms/update', outputPattern: '<%= entityName %>-form.service.ts', enabled: config.generate.formServices },
354
+
355
+ // Standalone components
356
+ { template: '_entity_-form-page.component.ts.ejs', subfolder: 'components/forms/form-page', outputPattern: '<%= entityName %>-form-page.component.ts', enabled: config.generate.forms.formPage },
357
+ { template: '_entity_-form-page.component.html.ejs', subfolder: 'components/forms/form-page', outputPattern: '<%= entityName %>-form-page.component.html', enabled: config.generate.forms.formPage },
358
+ { template: '_entity_-detail.component.ts.ejs', subfolder: 'components/presentation/detail', outputPattern: '<%= entityName %>-detail.component.ts', enabled: config.generate.components.detail },
359
+ { template: '_entity_-detail.component.html.ejs', subfolder: 'components/presentation/detail', outputPattern: '<%= entityName %>-detail.component.html', enabled: config.generate.components.detail },
360
+ { template: '_entity_-detail.component.css.ejs', subfolder: 'components/presentation/detail', outputPattern: '<%= entityName %>-detail.component.css', enabled: config.generate.components.detail },
361
+ { template: '_entity_-card.component.ts.ejs', subfolder: 'components/presentation/card', outputPattern: '<%= entityName %>-card.component.ts', enabled: config.generate.components.card },
362
+ { template: '_entity_-card.component.html.ejs', subfolder: 'components/presentation/card', outputPattern: '<%= entityName %>-card.component.html', enabled: config.generate.components.card },
363
+ { template: '_entity_-card.component.css.ejs', subfolder: 'components/presentation/card', outputPattern: '<%= entityName %>-card.component.css', enabled: config.generate.components.card },
364
+ { template: '_entity_-filter.component.ts.ejs', subfolder: 'components/interaction/filter', outputPattern: '<%= entityName %>-filter.component.ts', enabled: config.generate.interaction.filter },
365
+ { template: '_entity_-filter.component.html.ejs', subfolder: 'components/interaction/filter', outputPattern: '<%= entityName %>-filter.component.html', enabled: config.generate.interaction.filter },
366
+ { template: '_entity_-actions.component.ts.ejs', subfolder: 'components/interaction/actions', outputPattern: '<%= entityName %>-actions.component.ts', enabled: config.generate.interaction.actions },
367
+ { template: '_entity_-actions.component.html.ejs', subfolder: 'components/interaction/actions', outputPattern: '<%= entityName %>-actions.component.html', enabled: config.generate.interaction.actions },
368
+ { template: '_entity_-form.component.ts.ejs', subfolder: 'components/forms/form', outputPattern: '<%= entityName %>-form.component.ts', enabled: config.generate.forms.form },
369
+ { template: '_entity_-form.component.html.ejs', subfolder: 'components/forms/form', outputPattern: '<%= entityName %>-form.component.html', enabled: config.generate.forms.form },
370
+ { template: '_entity_-list.component.ts.ejs', subfolder: 'components/presentation/list', outputPattern: '<%= entityName %>-list.component.ts', enabled: config.generate.components.list },
371
+ { template: '_entity_-list.component.html.ejs', subfolder: 'components/presentation/list', outputPattern: '<%= entityName %>-list.component.html', enabled: config.generate.components.list },
372
+ { template: '_entity_-list.component.scss.ejs', subfolder: 'components/presentation/list', outputPattern: '<%= entityName %>-list.component.scss', enabled: config.generate.components.list }
373
+ ];
374
+
375
+ for (const t of templates) {
376
+ if (t.enabled === false) continue;
377
+
378
+ let templatePath;
379
+ if (t.subfolder.startsWith('shared')) {
380
+ const subDirs = t.subfolder.split('/').slice(1); // remove 'shared'
381
+ templatePath = path.join(templatesDir, 'shared', ...subDirs, t.template);
382
+ } else {
383
+ templatePath = path.join(templatesDir, 'entity', t.subfolder, t.template);
384
+ }
385
+ const fileContent = await ejs.renderFile(templatePath, { ...entityConfig, cache: true });
386
+
387
+ const finalSubfolder = t.subfolder ? path.join(entityBaseDir, t.subfolder) : entityBaseDir;
388
+ const outputFileName = ejs.render(t.outputPattern, { entityName: helpers.toKebabCase(entity.name) });
389
+ writeOrPreview(path.join(finalSubfolder, outputFileName), fileContent);
390
+ }
391
+
392
+ if (config.mock && config.mock.enabled) {
393
+ console.log(` -> Generating Mock Data: ${entity.name}`);
394
+ const mockRecords = [];
395
+ const recordsCount = config.mock.recordsPerEntity || 10;
396
+ for (let i = 0; i < recordsCount; i++) {
397
+ const record = { id: i + 1 };
398
+ entity.fields.forEach(field => {
399
+ const type = field.fieldType;
400
+ if (['String', 'TextBlob', 'UUID'].includes(type)) {
401
+ record[field.fieldName] = `${config.mock.values?.stringPrefix || 'Mock '}${field.fieldName} ${i + 1}`;
402
+ } else if (['Integer', 'Long', 'BigDecimal', 'Float', 'Double'].includes(type)) {
403
+ const min = config.mock.values?.numberMin || 1;
404
+ record[field.fieldName] = min + i;
405
+ } else if (type === 'Boolean') {
406
+ record[field.fieldName] = config.mock.values?.booleanDefault !== undefined ? config.mock.values.booleanDefault : true;
407
+ } else if (['LocalDate', 'Instant', 'ZonedDateTime'].includes(type)) {
408
+ record[field.fieldName] = new Date().toISOString();
409
+ } else {
410
+ record[field.fieldName] = `Mock ${field.fieldName} ${i + 1}`;
411
+ }
412
+ });
413
+ mockRecords.push(record);
414
+ }
415
+ const mockFilePath = path.join(entityBaseDir, 'mock', 'data.ts');
416
+ const content = `export const mockData = ${JSON.stringify(mockRecords, null, 2)};\n`;
417
+ writeOrPreview(mockFilePath, content);
418
+ }
419
+ }
420
+
421
+ // Update Fuse routes and navigation files if enabled
422
+ if (config.fuse && config.fuse.enabled) {
423
+ console.log(` -> Registering Fuse routes and navigation menu entries...`);
424
+ const routesRelativePath = config.fuse.routesFile || 'src/app/domains/admin/routes.ts';
425
+ const navRelativePath = config.fuse.navigationFile || 'src/app/domains/admin/layout/data/navigation.ts';
426
+
427
+ const routesFilePath = path.join(outputDir, routesRelativePath);
428
+ const navFilePath = path.join(outputDir, navRelativePath);
429
+
430
+ const backupFuseFile = (filePath) => {
431
+ if (config.dryRun || !fs.existsSync(filePath)) {
432
+ return;
433
+ }
434
+ const backupPath = `${filePath}.bak`;
435
+ if (!fs.existsSync(backupPath)) {
436
+ fs.copyFileSync(filePath, backupPath);
437
+ }
438
+ };
439
+
440
+ backupFuseFile(routesFilePath);
441
+ backupFuseFile(navFilePath);
442
+
443
+ // 1. Update Routes File
444
+ if (config.generate.registrations?.routes !== false && fs.existsSync(routesFilePath)) {
445
+
446
+
447
+ let content = fs.readFileSync(routesFilePath, 'utf8');
448
+
449
+ if (config.fuse?.includePrebuilt === false && !config.fuse?.cleanLayout) {
450
+ // Strip the Dashboards, General, and Documentation sections from routes.ts
451
+ content = content.replace(/\s*\/\/\s*-+\n\s*\/\/\s*(Dashboards|General|Documentation)\n\s*\/\/\s*-+\n(?:[\s\S]*?(?=\s*\/\/\s*-+|\s*\/\/\s*404|\]))/g, '\n');
452
+ }
453
+
454
+ const routesBlock = entities.map(entity => {
455
+ const kebabName = helpers.toKebabCase(entity.name);
456
+ return ` {\n path: '${kebabName}',\n loadChildren: () => import('./modules/apps/${outputEntityFolder(entity, config)}/${kebabName}/${kebabName}.routes'),\n },`;
457
+ }).join('\n');
458
+
459
+ if (config.fuse.cleanLayout) {
460
+ let content = fs.readFileSync(routesFilePath, 'utf8').replace(/\r\n/g, '\n');
461
+ const childrenMatch = /children:\s*\[/.exec(content);
462
+ if (childrenMatch) {
463
+ const cleanChildrenBlock = `children: [
464
+ { path: '', pathMatch: 'full', redirectTo: '/auth/sign-in' },
465
+ {
466
+ path: 'welcome',
467
+ loadComponent: () => import('./modules/extras/welcome/welcome.component')
468
+ },
469
+ // cforge-routes-start
470
+ ${routesBlock}
471
+ // cforge-routes-end
472
+ {
473
+ path: '404',
474
+ pathMatch: 'full',
475
+ loadComponent: () => import('./modules/extras/error/features/error-page'),
476
+ data: { errorCode: 404, title: 'Page not found' }
477
+ },
478
+ {
479
+ path: '**',
480
+ loadComponent: () => import('./modules/extras/error/features/error-page'),
481
+ data: { errorCode: 404, title: 'Page not found' }
482
+ },
483
+ ]`;
484
+ const routesArrayEnd = content.indexOf(' ],\n },\n];');
485
+ if (routesArrayEnd !== -1) {
486
+ const newContent = content.slice(0, childrenMatch.index) + cleanChildrenBlock + content.slice(routesArrayEnd + 6);
487
+ writeOrPreview(routesFilePath, newContent);
488
+ }
489
+ }
490
+ } else {
491
+ // Ensure markers exist
492
+ if (!content.includes('// cforge-routes-start') || !content.includes('// cforge-routes-end')) {
493
+ const childrenMatch = /children:\s*\[/.exec(content);
494
+ if (childrenMatch) {
495
+ const insertIndex = childrenMatch.index + childrenMatch[0].length;
496
+ content = content.slice(0, insertIndex) + '\n // cforge-routes-start\n // cforge-routes-end' + content.slice(insertIndex);
497
+ }
498
+ }
499
+
500
+ const startIdx = content.indexOf('// cforge-routes-start');
501
+ const endIdx = content.indexOf('// cforge-routes-end');
502
+
503
+ if (startIdx !== -1 && endIdx !== -1) {
504
+ const startMarker = '// cforge-routes-start';
505
+ const endMarker = '// cforge-routes-end';
506
+ const newContent = content.slice(0, startIdx + startMarker.length) + '\n' + routesBlock + '\n ' + content.slice(endIdx);
507
+ writeOrPreview(routesFilePath, newContent);
508
+ }
509
+ }
510
+ }
511
+
512
+ // 2. Update Navigation File
513
+ if (config.generate.registrations?.navigation !== false && fs.existsSync(navFilePath)) {
514
+
515
+
516
+ let content = fs.readFileSync(navFilePath, 'utf8');
517
+
518
+ if (config.fuse?.includePrebuilt === false && !config.fuse?.cleanLayout) {
519
+ content = content.replace(/\s*{\s*id:\s*'(dashboards|general|extras|documentation)'[\s\S]*?(?=\s*{\s*id:\s*'(dashboards|general|extras|documentation)'|\s*\];)/g, '');
520
+ }
521
+
522
+ const groups = new Map();
523
+ for (const entity of entities) {
524
+ const entityFolder = entity.folderName || folderName;
525
+ const groupKey = entityFolder;
526
+ if (!groups.has(groupKey)) {
527
+ groups.set(groupKey, {
528
+ folder: entityFolder,
529
+ label:
530
+ entity.sourceName ||
531
+ (entityFolder === 'micro-functions'
532
+ ? 'Micro Functions'
533
+ : entityFolder === 'microservice'
534
+ ? 'Microservice'
535
+ : helpers.toPascalCase(entityFolder)),
536
+ items: [],
537
+ });
538
+ }
539
+ const entityConfig = (config.entities && config.entities[entity.name]) || {};
540
+ groups.get(groupKey).items.push({
541
+ id: `${entityFolder}/${helpers.toKebabCase(entity.name)}`,
542
+ label: entityConfig.label || helpers.toPascalCase(entity.name) + 's',
543
+ icon: entityConfig.icon || 'database',
544
+ route: entityConfig.route || `/admin/${helpers.toKebabCase(entity.name)}`,
545
+ order: entityConfig.order !== undefined ? entityConfig.order : 100,
546
+ permissions: entityConfig.permissions || null,
547
+ });
548
+ }
549
+
550
+ const navigationBlock = Array.from(groups.values())
551
+ .map((group) => {
552
+ group.items.sort((a, b) => {
553
+ if (a.order !== b.order) return a.order - b.order;
554
+ return a.label.localeCompare(b.label);
555
+ });
556
+ const formattedItems = group.items
557
+ .map((item) => {
558
+ let str = ` {\n`;
559
+ str += ` id: '${item.id}',\n`;
560
+ str += ` label: '${item.label}',\n`;
561
+ str += ` icon: '${item.icon}',\n`;
562
+ str += ` route: '${item.route}'`;
563
+ if (item.permissions) {
564
+ str += `,\n meta: { permissions: ${JSON.stringify(item.permissions)} }`;
565
+ }
566
+ str += `\n }`;
567
+ return str;
568
+ })
569
+ .join(',\n');
570
+ return ` {\n id: '${group.folder}-group',\n label: '${group.label}',\n description: 'Generated CRUD Modules',\n icon: 'database',\n children: [\n${formattedItems}\n ]\n },`;
571
+ })
572
+ .join('\n');
573
+
574
+ if (config.fuse.cleanLayout) {
575
+ const normalized = content.replace(/\r\n/g, '\n');
576
+ const startArrayIdx = normalized.indexOf('export const NAVIGATION: NavigationItem[] = [');
577
+ if (startArrayIdx !== -1) {
578
+ const arrayEndIdx = normalized.lastIndexOf('];');
579
+ if (arrayEndIdx !== -1) {
580
+ const newContent = content.slice(0, startArrayIdx) + 'export const NAVIGATION: NavigationItem[] = [\n' + navigationBlock + '\n];' + content.slice(arrayEndIdx + 2);
581
+ writeOrPreview(navFilePath, newContent);
582
+ }
583
+ }
584
+ } else {
585
+ // Ensure markers exist
586
+ if (!content.includes('// cforge-navigation-start') || !content.includes('// cforge-navigation-end')) {
587
+ const navArrayMatch = /export const NAVIGATION: NavigationItem\[\] = \[/.exec(content);
588
+ if (navArrayMatch) {
589
+ const insertIndex = navArrayMatch.index + navArrayMatch[0].length;
590
+ content = content.slice(0, insertIndex) + '\n // cforge-navigation-start\n // cforge-navigation-end' + content.slice(insertIndex);
591
+ }
592
+ }
593
+
594
+ const startIdx = content.indexOf('// cforge-navigation-start');
595
+ const endIdx = content.indexOf('// cforge-navigation-end');
596
+
597
+ if (startIdx !== -1 && endIdx !== -1) {
598
+ const startMarker = '// cforge-navigation-start';
599
+ const endMarker = '// cforge-navigation-end';
600
+ const newContent = content.slice(0, startIdx + startMarker.length) + '\n' + navigationBlock + '\n ' + content.slice(endIdx);
601
+ writeOrPreview(navFilePath, newContent);
602
+ }
603
+ }
604
+ }
605
+
606
+ // 3. Update Sign-In HTML File Branding (idempotent; starter already ships clean CRUDForge copy)
607
+ const signInFilePath = path.join(outputDir, 'src/app/domains/auth/features/sign-in/sign-in.html');
608
+ if (fs.existsSync(signInFilePath)) {
609
+ let html = fs.readFileSync(signInFilePath, 'utf8');
610
+ html = html.replace('alt="Fuse logo"', 'alt="CRUDForge logo"');
611
+ html = html.replace('Welcome to our community', 'Welcome to CRUDForge');
612
+ html = html.replace(
613
+ /Fuse helps developers to build organized[\s\S]*?application today\./,
614
+ 'CRUDForge helps you to generate organized and well-structured Angular applications featuring robust standalone forms, presentation lists, and service collections.'
615
+ );
616
+ html = html.replace("More than 17k people joined us, it's your turn", 'Create code, generate apps, run instantly');
617
+ writeOrPreview(signInFilePath, html);
618
+ }
619
+
620
+ // 4. Layout modifications
621
+ const layoutFilePath = path.join(outputDir, 'src/app/domains/admin/layout/layout.ts');
622
+ if (fs.existsSync(layoutFilePath)) {
623
+ let content = fs.readFileSync(layoutFilePath, 'utf8').replace(/\r\n/g, '\n');
624
+
625
+ if (config.fuse.cleanLayout) {
626
+ // Remove Banner
627
+ content = content.replace(/<!-- Banner -->[\s\S]*?<\/div>\s*<!-- Toolbar -->/, '<!-- Toolbar -->');
628
+ // Remove Language Switcher
629
+ content = content.replace('<language-switcher />', '');
630
+ // Remove Language Switcher Imports
631
+ content = content.replace('import { LanguageSwitcher } from \'@/app/domains/admin/layout/ui/language-switcher\';\n', '');
632
+ content = content.replace('LanguageSwitcher,\n', '');
633
+ }
634
+
635
+ // Apply sidebar theme
636
+ if (config.fuse.sidebarTheme === 'dark') {
637
+ content = content.replace('bg-white', 'scheme-dark');
638
+ }
639
+
640
+ writeOrPreview(layoutFilePath, content);
641
+ }
642
+
643
+ applyFuseTheme(outputDir, config, writeOrPreview);
644
+
645
+ require('./fuse-build').applyFuseBuild({ outputDir, config, writeOrPreview });
646
+
647
+ const { applyAccessControlToolsPack } = require('./access-control-tools');
648
+ const aclTools = applyAccessControlToolsPack({ outputDir, config, writeOrPreview });
649
+ if (aclTools.applied && !config.dryRun) {
650
+ console.log(` -> Access-control tools packed at ${aclTools.dest}`);
651
+ }
652
+
653
+ const { applyAuthPack } = require('./auth-pack');
654
+ await applyAuthPack({ outputDir, config, writeOrPreview, folderName, entities });
655
+
656
+ const { applyFuseRuntimePack } = require('./fuse-runtime');
657
+ const runtime = applyFuseRuntimePack({ outputDir, config, writeOrPreview });
658
+ if (runtime.applied && !config.dryRun) {
659
+ const bits = [];
660
+ if (runtime.services) bits.push(`${runtime.services} service proxy(ies)`);
661
+ if (runtime.proxyFiles?.length) bits.push(`proxy: ${runtime.proxyFiles.join(', ')}`);
662
+ if (bits.length) {
663
+ console.log(` -> Fuse runtime env/proxy updated (${bits.join('; ')})`);
664
+ }
665
+ }
666
+
667
+ if (config.fuse.cleanLayout) {
668
+ const sidebarFilePath = path.join(outputDir, 'src/app/domains/admin/layout/ui/sidebar.ts');
669
+ if (fs.existsSync(sidebarFilePath)) {
670
+ let content = fs.readFileSync(sidebarFilePath, 'utf8').replace(/\r\n/g, '\n');
671
+ // Remove Trial Alert Card
672
+ content = content.replace(/<!-- Sidebar notification -->[\s\S]*?<\/div>\s*<!-- Footer -->/, '<!-- Footer -->');
673
+
674
+ // Remove Unused MatButton and MatIcon imports left over from the Trial Alert Card
675
+ content = content.replace('import { MatButton } from \'@angular/material/button\';\n', '');
676
+ content = content.replace('import { MatIcon } from \'@angular/material/icon\';\n', '');
677
+ content = content.replace('imports: [Navigation, User, MatButton, MatIcon],', 'imports: [Navigation, User],');
678
+
679
+ // Branding: wordmark logo already ships in fuse-starter; keep alt text consistent.
680
+ content = content.replace('alt="Fuse logo"', 'alt="CRUDForge logo"');
681
+ content = content.replace(/\/images\/logo\/logo\.jpeg/g, '/images/logo/logo.png');
682
+ writeOrPreview(sidebarFilePath, content);
683
+ }
684
+ }
685
+ }
686
+
687
+ writeOrPreview(path.join(outputDir, 'generator/README.md'),
688
+ fs.readFileSync(path.join(__dirname, '../templates/generator/README.md'), 'utf8'));
689
+ writeOrPreview(path.join(outputDir, 'generator/issues/.gitkeep'), '');
690
+
691
+ await require('./application-tests').applyApplicationTests({outputDir, config, parsedData, writeOrPreview});
692
+
693
+ // Embed config + AI docs (GO-DUCK-style) + keep-team agents + optional git remote.
694
+ if (config.fuse?.enabled) {
695
+ const { embedConfigInOutput } = require('./embed-config');
696
+ const { applyAiDocsPack } = require('./ai-docs');
697
+ const { applyTeamPack, initGitRepository } = require('./team-pack');
698
+ const embedded = embedConfigInOutput(outputDir, config, writeOrPreview);
699
+ if (embedded.written && !config.dryRun) {
700
+ console.log(' -> Embedded config/crudforge.config.yml');
701
+ }
702
+ const aiDocs = applyAiDocsPack({
703
+ outputDir,
704
+ config,
705
+ parsedData: { entities, enums, relationships },
706
+ writeOrPreview,
707
+ });
708
+ if (aiDocs.applied && aiDocs.files?.length && !config.dryRun) {
709
+ console.log(' -> Seeded AGENT_README.md + docs/ai/* + .crudforge/ (GO-DUCK-style)');
710
+ }
711
+ const team = applyTeamPack(outputDir, config, writeOrPreview);
712
+ if (team.applied && team.files?.length && !config.dryRun) {
713
+ console.log(' -> Seeded keep-team folders (.cursor/agents, .codex/agents, .agents, .vscode/extensions)');
714
+ }
715
+ const gitResult = initGitRepository(outputDir, config);
716
+ if (gitResult.dryRun) {
717
+ console.log(` -> [dry-run] Would git init / set origin ${gitResult.wouldUse}`);
718
+ } else if (gitResult.initialized && gitResult.createdRemote) {
719
+ console.log(` -> git remote origin → ${gitResult.url}`);
720
+ } else if (gitResult.warning) {
721
+ console.log(` -> ${gitResult.warning}`);
722
+ }
723
+ ensureGeneratedGitignore(outputDir, writeOrPreview);
724
+ }
725
+
726
+ return {
727
+ fileCount,
728
+ entityCount: entities.length,
729
+ dryRun: !!config.dryRun,
730
+ dryRunReport: config.dryRun ? dryRunReport : null,
731
+ conflicts
732
+ };
733
+ }
734
+
735
+ function cleanupStaleKeycloakArtifacts(outputDir, config) {
736
+ // copySync does not delete leftovers. A prior IAM generate can leave
737
+ // src/app/core/auth + user-management while a non-Keycloak prepare
738
+ // overwrites environment.ts — that combo breaks ng serve (TS2339).
739
+ if (config.auth?.provider === 'keycloak') {
740
+ return;
741
+ }
742
+
743
+ const staleDirs = [
744
+ path.join(outputDir, 'src/app/core/auth'),
745
+ path.join(outputDir, 'src/app/core/user-management'),
746
+ ];
747
+ // Legacy location before IAM moved to core/
748
+ const appsRoot = path.join(outputDir, 'src/app/domains/admin/modules/apps');
749
+ if (fs.existsSync(appsRoot)) {
750
+ for (const entry of fs.readdirSync(appsRoot)) {
751
+ staleDirs.push(path.join(appsRoot, entry, 'identity'));
752
+ }
753
+ }
754
+
755
+ for (const dir of staleDirs) {
756
+ if (fs.existsSync(dir)) {
757
+ fs.removeSync(dir);
758
+ console.log(` -> Removed stale Keycloak leftover: ${path.relative(outputDir, dir)}`);
759
+ }
760
+ }
761
+
762
+ const routesFilePath = path.join(outputDir, config.fuse?.routesFile || 'src/app/domains/admin/routes.ts');
763
+ if (fs.existsSync(routesFilePath)) {
764
+ let routes = fs.readFileSync(routesFilePath, 'utf8');
765
+ const before = routes;
766
+ routes = routes.replace(/\n\s*\{\s*path:\s*'user-management'[\s\S]*?\},?\n/g, '\n');
767
+ routes = routes.replace(/import\s*\{\s*AuthGuard\s*\}\s*from\s*'[^']+auth\.guard';\s*\n?/g, '');
768
+ routes = routes.replace(/import\s*\{\s*LandingGuard\s*\}\s*from\s*'[^']+landing\.guard';\s*\n?/g, '');
769
+ routes = routes.replace(/\n\s*canActivate:\s*\[AuthGuard\],\s*\n\s*canActivateChild:\s*\[AuthGuard\],/g, '');
770
+ routes = routes.replace(/\n\s*canActivate:\s*\[LandingGuard\],/g, '');
771
+ routes = routes.replace(/\n\s*\{\s*path:\s*'no-access'[\s\S]*?\},?\n/g, '\n');
772
+ if (routes !== before) {
773
+ fs.writeFileSync(routesFilePath, routes);
774
+ }
775
+ }
776
+
777
+ const appConfigPath = path.join(outputDir, 'src/app/app.config.ts');
778
+ if (fs.existsSync(appConfigPath)) {
779
+ let cfg = fs.readFileSync(appConfigPath, 'utf8');
780
+ const before = cfg;
781
+ cfg = cfg.replace(
782
+ /import\s*\{\s*provideHttpClient,\s*withFetch,\s*withInterceptors\s*\}\s*from\s*'@angular\/common\/http';/,
783
+ "import { provideHttpClient, withFetch } from '@angular/common/http';"
784
+ );
785
+ cfg = cfg.replace(/import\s*\{\s*authInterceptor\s*\}\s*from\s*'[^']+';\s*\n?/g, '');
786
+ cfg = cfg.replace(
787
+ 'provideHttpClient(withFetch(), withInterceptors([authInterceptor]))',
788
+ 'provideHttpClient(withFetch())'
789
+ );
790
+ if (cfg !== before) {
791
+ fs.writeFileSync(appConfigPath, cfg);
792
+ }
793
+ }
794
+
795
+ const noAccess = path.join(
796
+ outputDir,
797
+ 'src/app/domains/admin/modules/extras/no-access/no-access.component.ts'
798
+ );
799
+ if (fs.existsSync(noAccess)) {
800
+ fs.removeSync(path.dirname(noAccess));
801
+ }
802
+ }
803
+
804
+ function ensureGeneratedGitignore(outputDir, writeOrPreview) {
805
+ const target = path.join(outputDir, '.gitignore');
806
+ const required = [
807
+ 'node_modules/',
808
+ 'dist/',
809
+ '.angular/',
810
+ '.env',
811
+ '.env.qa',
812
+ '.crudforge/provision-state.json',
813
+ '*.log',
814
+ '.DS_Store',
815
+ ];
816
+ let existing = '';
817
+ if (fs.existsSync(target)) {
818
+ existing = fs.readFileSync(target, 'utf8');
819
+ }
820
+ const lines = existing ? existing.replace(/\s+$/, '').split(/\r?\n/) : [];
821
+ // Prefer fine-grained provision-state ignore over ignoring all of .crudforge/
822
+ const filtered = lines.filter((line) => line.trim() !== '.crudforge/');
823
+ let changed = !existing || filtered.length !== lines.length;
824
+ const next = filtered.length ? filtered : lines;
825
+ for (const entry of required) {
826
+ if (!next.includes(entry)) {
827
+ next.push(entry);
828
+ changed = true;
829
+ }
830
+ }
831
+ if (changed) {
832
+ writeOrPreview(target, `${next.join('\n')}\n`);
833
+ }
834
+ }
835
+
836
+ function applyFuseTheme(outputDir, config, writeOrPreview) {
837
+ const theme = config.fuse?.theme || {};
838
+ const primary = theme.primary || '#0D1E3A';
839
+ const error = theme.error || '#dc2626';
840
+ const scheme = theme.scheme || 'system';
841
+
842
+ const appTsPath = path.join(outputDir, 'src/app/app.ts');
843
+ if (fs.existsSync(appTsPath)) {
844
+ let content = fs.readFileSync(appTsPath, 'utf8').replace(/\r\n/g, '\n');
845
+ if (content.includes('export const fuseTheme')) {
846
+ content = content.replace(/scheme:\s*'[^']*'/, `scheme: '${scheme}'`);
847
+ content = content.replace(/primary:\s*'[^']*'/, `primary: '${primary}'`);
848
+ content = content.replace(/error:\s*'[^']*'/, `error: '${error}'`);
849
+ } else {
850
+ const lastImport = content.lastIndexOf('import ');
851
+ const lastImportEnd = lastImport === -1 ? 0 : content.indexOf('\n', lastImport) + 1;
852
+ const themeBlock = `import { ThemeConfig } from '@/app/core/theming/models/theming';\n\n/** Root Fuse theme. Change these values to recolor buttons, links, and Material accents. */\nexport const fuseTheme: ThemeConfig = {\n scheme: '${scheme}',\n primary: '${primary}',\n error: '${error}',\n};\n\n`;
853
+ content = content.slice(0, lastImportEnd) + themeBlock + content.slice(lastImportEnd);
854
+ }
855
+ writeOrPreview(appTsPath, content);
856
+ }
857
+
858
+ const appConfigPath = path.join(outputDir, 'src/app/app.config.ts');
859
+ if (fs.existsSync(appConfigPath)) {
860
+ let content = fs.readFileSync(appConfigPath, 'utf8').replace(/\r\n/g, '\n');
861
+ if (!content.includes("import { fuseTheme }")) {
862
+ content = content.replace(
863
+ /import \{ routes \} from '\.\/app\.routes';/,
864
+ "import { fuseTheme } from './app';\nimport { routes } from './app.routes';"
865
+ );
866
+ }
867
+ content = content.replace(/provideTheming\(\{[\s\S]*?\}\)/, 'provideTheming(fuseTheme)');
868
+ writeOrPreview(appConfigPath, content);
869
+ }
870
+ }
871
+
872
+ module.exports = {
873
+ generate
874
+ };