@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,819 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Unified access-control + QA id sync.
5
+ *
6
+ * Usage:
7
+ * node access-control-sync.js --dry-run
8
+ * node access-control-sync.js --write
9
+ * node access-control-sync.js --dry-run --qa-only
10
+ * node access-control-sync.js --dry-run --access-only
11
+ * node access-control-sync.js --write --rewrite-ids # normalize legacy noisy ids (destructive)
12
+ */
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+
17
+ const {
18
+ formatScreenNameFromPath,
19
+ extractScreenContent,
20
+ isExcludedFromContentIds,
21
+ deriveScreenId,
22
+ disambiguateDuplicateScreenIds,
23
+ generateScreenDescription,
24
+ generateComponentDescription,
25
+ } = require('./lib/id-rules');
26
+ const { parseHtmlAndInject } = require('./lib/html-scanner');
27
+ const { buildScreenNavigationHierarchy } = require('./lib/hierarchy-scanner');
28
+
29
+ const TOOLS_DIR = __dirname;
30
+ const APPS_DIR = path.join(TOOLS_DIR, '../..');
31
+ const MICRO_FUNCTIONS_DIR = path.join(APPS_DIR, 'micro-functions');
32
+ const MICROSERVICE_DIR = path.join(APPS_DIR, 'microservice');
33
+ const PAGES_DIR = path.join(APPS_DIR, 'pages');
34
+ const OUTPUT_DIR = path.join(TOOLS_DIR, 'output');
35
+ const OUTPUT_CSV_PATH = path.join(OUTPUT_DIR, 'screen-components.csv');
36
+ const OUTPUT_HIERARCHY_PATH = path.join(OUTPUT_DIR, 'screen-navigation-map.json');
37
+ const REPORT_PATH = path.join(OUTPUT_DIR, 'sync-report.json');
38
+ // CRUDForge Fuse serves static assets from public/ (also mirrored under shared for tooling).
39
+ const ASSETS_CSV_PATH = path.join(TOOLS_DIR, '../../../../../../../public/assets/screen-components.csv');
40
+ const ASSETS_HIERARCHY_PATH = path.join(
41
+ TOOLS_DIR,
42
+ '../../../../../../../public/assets/screen-navigation-map.json'
43
+ );
44
+ const SHARED_CSV_PATH = path.join(TOOLS_DIR, '../screen-components.csv');
45
+ const ADMIN_ROUTES_FILE = path.join(TOOLS_DIR, '../../../../routes.ts');
46
+
47
+ const ACCESS_CTRL_IMPORT = "import { AccessCtrlPipe } from '@/app/core/auth';\n";
48
+ const ACCESS_CTRL_PIPE = 'AccessCtrlPipe';
49
+
50
+ function parseArgs(argv) {
51
+ const args = {
52
+ dryRun: true,
53
+ write: false,
54
+ qaOnly: false,
55
+ accessOnly: false,
56
+ rewriteIds: false,
57
+ pruneCsv: false,
58
+ // Default false for CRUDForge: keep nav-aligned {folder}/{entity} SCREEN values.
59
+ forceContentIds: false,
60
+ preferExistingScreen: true,
61
+ };
62
+
63
+ for (const arg of argv.slice(2)) {
64
+ if (arg === '--write') {
65
+ args.write = true;
66
+ args.dryRun = false;
67
+ } else if (arg === '--dry-run') {
68
+ args.dryRun = true;
69
+ args.write = false;
70
+ } else if (arg === '--qa-only') {
71
+ args.qaOnly = true;
72
+ } else if (arg === '--access-only') {
73
+ args.accessOnly = true;
74
+ } else if (arg === '--rewrite-ids') {
75
+ args.rewriteIds = true;
76
+ } else if (arg === '--prune-csv') {
77
+ args.pruneCsv = true;
78
+ } else if (arg === '--force-content-ids') {
79
+ args.forceContentIds = true;
80
+ args.preferExistingScreen = false;
81
+ } else if (arg === '--keep-existing-screen') {
82
+ args.forceContentIds = false;
83
+ args.preferExistingScreen = true;
84
+ } else if (arg === '--help' || arg === '-h') {
85
+ args.help = true;
86
+ }
87
+ }
88
+
89
+ return args;
90
+ }
91
+
92
+ function printHelp() {
93
+ console.log(`
94
+ Access Control & QA ID Sync
95
+
96
+ --dry-run Report only (default). Writes output/sync-report.json
97
+ --write Apply HTML/TS/CSV changes
98
+ --qa-only Only inject QA attributes (no accessCtrl / SCREEN)
99
+ --access-only Only inject accessCtrl / SCREEN / CSV
100
+ --rewrite-ids Normalize legacy noisy component ids (use with care)
101
+ --prune-csv Drop CSV rows not rediscovered in this scan (write mode)
102
+ --force-content-ids Rebuild SCREEN from heading + view (default on)
103
+ --keep-existing-screen Do not rewrite existing SCREEN values
104
+
105
+ Excluded from content rewrite (path/filename ids kept):
106
+ dad-mock-module, accessControlModule, notificationModule
107
+
108
+ CRUDForge Fuse notes:
109
+ • Entity SCREEN ids stay {micro-functions|microservice}/{entity} (nav-aligned)
110
+ • Prefer --keep-existing-screen for generated entities; use content ids on custom pages
111
+ • Pipe import is AccessCtrlPipe from @/app/core/auth (template: accessCtrl)
112
+ `);
113
+ }
114
+
115
+ function walkDir(dir, callback) {
116
+ if (!fs.existsSync(dir)) {
117
+ return;
118
+ }
119
+ for (const file of fs.readdirSync(dir)) {
120
+ const fullPath = path.join(dir, file);
121
+ const stat = fs.statSync(fullPath);
122
+ if (stat.isDirectory()) {
123
+ walkDir(fullPath, callback);
124
+ } else {
125
+ callback(fullPath);
126
+ }
127
+ }
128
+ }
129
+
130
+ function isAngularComponentSource(filePath, fileContent) {
131
+ if (!filePath.endsWith('.ts')) {
132
+ return false;
133
+ }
134
+ const normalizedPath = filePath.replace(/\\/g, '/');
135
+ if (
136
+ normalizedPath.endsWith('.spec.ts') ||
137
+ normalizedPath.endsWith('.route.ts') ||
138
+ normalizedPath.endsWith('.routes.ts') ||
139
+ normalizedPath.includes('/services/') ||
140
+ normalizedPath.includes('/service/')
141
+ ) {
142
+ return false;
143
+ }
144
+ return fileContent.includes('@Component');
145
+ }
146
+
147
+ /**
148
+ * Unified page + micro-function include rules.
149
+ * Skips pure dialogs/confirmations; includes actions/filter/list/form/cards under pages.
150
+ */
151
+ function shouldScanComponent(filePath, sourceType) {
152
+ const normalizedPath = filePath.replace(/\\/g, '/');
153
+ const fileName = path.basename(normalizedPath);
154
+
155
+ if (sourceType === 'micro-functions') {
156
+ return fileName.endsWith('.component.ts');
157
+ }
158
+
159
+ // pages: any component ts, excluding shared confirm dialogs only
160
+ if (normalizedPath.includes('/shared/confirmation')) {
161
+ return false;
162
+ }
163
+ return (
164
+ fileName.endsWith('.component.ts') ||
165
+ fileName === 'national.ts' ||
166
+ fileName === 'regional.ts' ||
167
+ fileName === 'warehouse.ts' ||
168
+ fileName === 'warehouse-building-details.ts' ||
169
+ fileName === 'stock-transfer-approval.ts' ||
170
+ fileName === 'new-warehouse-approval.ts' ||
171
+ fileName === 'damage-loss-approval.ts'
172
+ );
173
+ }
174
+
175
+ function resolveScanRoots() {
176
+ const roots = [];
177
+ if (fs.existsSync(APPS_DIR)) {
178
+ for (const entry of fs.readdirSync(APPS_DIR)) {
179
+ if (entry === 'shared' || entry === 'enums') continue;
180
+ const full = path.join(APPS_DIR, entry);
181
+ if (!fs.statSync(full).isDirectory()) continue;
182
+ roots.push({
183
+ name: entry,
184
+ dir: full,
185
+ sourceType: entry === 'pages' ? 'pages' : 'micro-functions',
186
+ });
187
+ }
188
+ }
189
+ // Ensure known roots even if empty (stable logging).
190
+ for (const known of [
191
+ { name: 'micro-functions', dir: MICRO_FUNCTIONS_DIR, sourceType: 'micro-functions' },
192
+ { name: 'microservice', dir: MICROSERVICE_DIR, sourceType: 'micro-functions' },
193
+ { name: 'pages', dir: PAGES_DIR, sourceType: 'pages' },
194
+ ]) {
195
+ if (!roots.some((r) => r.dir === known.dir) && fs.existsSync(known.dir)) {
196
+ roots.push(known);
197
+ }
198
+ }
199
+ return roots;
200
+ }
201
+
202
+ function collectComponentFiles() {
203
+ const componentFiles = [];
204
+ const seen = new Set();
205
+
206
+ for (const root of resolveScanRoots()) {
207
+ walkDir(root.dir, (filePath) => {
208
+ if (seen.has(filePath)) return;
209
+ seen.add(filePath);
210
+ const fileContent = fs.readFileSync(filePath, 'utf8');
211
+ if (
212
+ isAngularComponentSource(filePath, fileContent) &&
213
+ shouldScanComponent(filePath, root.sourceType)
214
+ ) {
215
+ componentFiles.push({ filePath, sourceType: root.sourceType, rootDir: root.dir, fileContent });
216
+ }
217
+ });
218
+ }
219
+
220
+ return componentFiles;
221
+ }
222
+
223
+ function findComponentClassHeader(tsContent) {
224
+ // Prefer the class that follows @Component(...) so we never inject into @Pipe classes
225
+ const componentClassMatch = tsContent.match(
226
+ /@Component\s*\([\s\S]*?\)\s*export\s+class\s+(\w+)[^{]*?\{/
227
+ );
228
+ if (componentClassMatch) {
229
+ return componentClassMatch[0].match(/export\s+class\s+\w+[^{]*?\{/)?.[0] || null;
230
+ }
231
+ return null;
232
+ }
233
+
234
+ function processComponentTs(tsFilePath, tsContent, screenId, write, options = {}) {
235
+ const { screenOnly = false } = options;
236
+ let content = tsContent;
237
+ let modified = false;
238
+ const changes = [];
239
+
240
+ const componentClassHeader = findComponentClassHeader(content);
241
+
242
+ // Detect SCREEN only inside the @Component class body when possible
243
+ let existingScreenInComponent = null;
244
+ let hasScreenGetter = false;
245
+ if (componentClassHeader) {
246
+ const afterHeaderIdx = content.indexOf(componentClassHeader) + componentClassHeader.length;
247
+ // Scan until next top-level export or end — simple window of 8k chars is enough for class fields
248
+ const classWindow = content.slice(afterHeaderIdx, afterHeaderIdx + 8000);
249
+ const screenInClass = classWindow.match(/^\s*(?:readonly\s+)?SCREEN\s*=\s*['"]([^'"]+)['"]/m);
250
+ if (screenInClass) {
251
+ existingScreenInComponent = screenInClass[1];
252
+ }
253
+ // Some filters expose SCREEN via getter + @Input() screenId — never inject a field there.
254
+ hasScreenGetter = /^\s*get\s+SCREEN\s*\(/m.test(classWindow);
255
+ }
256
+
257
+ if (hasScreenGetter) {
258
+ // Keep getter; align common @Input() screenId default when present.
259
+ const afterHeaderIdx = content.indexOf(componentClassHeader) + componentClassHeader.length;
260
+ const before = content.slice(0, afterHeaderIdx);
261
+ let after = content.slice(afterHeaderIdx);
262
+ const inputRe =
263
+ /(@Input\(\)\s+screenId\s*=\s*)['"][^'"]+['"]/;
264
+ if (inputRe.test(after)) {
265
+ const prev = (after.match(inputRe) || [])[0];
266
+ after = after.replace(inputRe, `$1'${screenId}'`);
267
+ if (prev && !prev.includes(`'${screenId}'`) && !prev.includes(`"${screenId}"`)) {
268
+ modified = true;
269
+ changes.push(`@Input() screenId → '${screenId}'`);
270
+ }
271
+ }
272
+ content = before + after;
273
+ } else if (!existingScreenInComponent) {
274
+ if (componentClassHeader) {
275
+ content = content.replace(
276
+ componentClassHeader,
277
+ `${componentClassHeader}\n readonly SCREEN = '${screenId}';`
278
+ );
279
+ modified = true;
280
+ changes.push(`SCREEN='${screenId}'`);
281
+ }
282
+ } else if (existingScreenInComponent !== screenId && componentClassHeader) {
283
+ const afterHeaderIdx = content.indexOf(componentClassHeader) + componentClassHeader.length;
284
+ const before = content.slice(0, afterHeaderIdx);
285
+ let after = content.slice(afterHeaderIdx);
286
+ after = after.replace(
287
+ /((?:readonly\s+)?)SCREEN\s*=\s*['"][^'"]+['"]/,
288
+ `$1SCREEN = '${screenId}'`
289
+ );
290
+ content = before + after;
291
+ modified = true;
292
+ changes.push(`SCREEN '${existingScreenInComponent}' → '${screenId}'`);
293
+ }
294
+
295
+ if (!screenOnly) {
296
+ const hasAccessCtrlImport =
297
+ content.includes(`import { ${ACCESS_CTRL_PIPE}`) ||
298
+ content.includes(`${ACCESS_CTRL_PIPE} }`) ||
299
+ /import\s*\{[^}]*AccessCtrlPipe[^}]*\}\s*from\s*['"]@\/app\/core\/auth['"]/.test(content);
300
+ if (!hasAccessCtrlImport) {
301
+ content = ACCESS_CTRL_IMPORT + content;
302
+ modified = true;
303
+ changes.push(`import ${ACCESS_CTRL_PIPE}`);
304
+ }
305
+
306
+ const componentDecoratorMatch = content.match(/@Component\s*\(\s*\{([\s\S]*?)\}\s*\)/);
307
+ if (componentDecoratorMatch) {
308
+ let decoratorContent = componentDecoratorMatch[1];
309
+ let newDecoratorContent = decoratorContent;
310
+ let modifiedDecorator = false;
311
+
312
+ if (!decoratorContent.includes(ACCESS_CTRL_PIPE) && !decoratorContent.includes('ScreenCtrlPipe')) {
313
+ const importsMatch = newDecoratorContent.match(/imports\s*:\s*\[([\s\S]*?)\]/);
314
+ if (importsMatch) {
315
+ newDecoratorContent = newDecoratorContent.replace(
316
+ importsMatch[1],
317
+ `\n ${ACCESS_CTRL_PIPE},${importsMatch[1]}`
318
+ );
319
+ modifiedDecorator = true;
320
+ changes.push(`imports ${ACCESS_CTRL_PIPE}`);
321
+ }
322
+ }
323
+
324
+ if (!decoratorContent.includes('CommonModule') && !decoratorContent.includes('NgIf')) {
325
+ if (!content.includes("from '@angular/common'") && !content.includes('from "@angular/common"')) {
326
+ content = `import { CommonModule } from '@angular/common';\n` + content;
327
+ changes.push('import CommonModule');
328
+ } else if (!content.includes('CommonModule')) {
329
+ const commonImportMatch = content.match(/import\s*\{([^}]+)\}\s*from\s*['"]@angular\/common['"]/);
330
+ if (commonImportMatch) {
331
+ content = content.replace(
332
+ commonImportMatch[0],
333
+ `import { CommonModule, ${commonImportMatch[1].trim()} } from '@angular/common'`
334
+ );
335
+ changes.push('add CommonModule to @angular/common import');
336
+ }
337
+ }
338
+
339
+ const importsMatch = newDecoratorContent.match(/imports\s*:\s*\[([\s\S]*?)\]/);
340
+ if (importsMatch && !newDecoratorContent.includes('CommonModule')) {
341
+ newDecoratorContent = newDecoratorContent.replace(
342
+ importsMatch[1],
343
+ `\n CommonModule,${importsMatch[1]}`
344
+ );
345
+ modifiedDecorator = true;
346
+ changes.push('imports CommonModule');
347
+ }
348
+ }
349
+
350
+ if (modifiedDecorator) {
351
+ content = content.replace(decoratorContent, newDecoratorContent);
352
+ modified = true;
353
+ }
354
+ }
355
+ }
356
+
357
+ if (modified && write) {
358
+ fs.writeFileSync(tsFilePath, content, 'utf8');
359
+ }
360
+
361
+ return { modified, changes, content };
362
+ }
363
+
364
+ function readExistingCsv() {
365
+ if (!fs.existsSync(OUTPUT_CSV_PATH)) {
366
+ return [];
367
+ }
368
+ const rawCsv = fs.readFileSync(OUTPUT_CSV_PATH, 'utf8');
369
+ const lines = rawCsv.split(/\r?\n/).filter((line) => line.trim().length > 0);
370
+ const mappings = [];
371
+ for (let i = 1; i < lines.length; i++) {
372
+ const parts = parseCsvLine(lines[i]);
373
+ if (parts.length >= 5) {
374
+ mappings.push({
375
+ screenName: parts[0],
376
+ screenId: parts[1],
377
+ componentName: parts[2],
378
+ componentId: parts[3],
379
+ componentType: parts[4],
380
+ description: parts[5] || '',
381
+ });
382
+ }
383
+ }
384
+ return mappings;
385
+ }
386
+
387
+ /** Minimal CSV line parser that respects quoted commas. */
388
+ function parseCsvLine(line) {
389
+ const parts = [];
390
+ let current = '';
391
+ let inQuotes = false;
392
+ for (let i = 0; i < line.length; i++) {
393
+ const char = line[i];
394
+ if (char === '"') {
395
+ if (inQuotes && line[i + 1] === '"') {
396
+ current += '"';
397
+ i++;
398
+ } else {
399
+ inQuotes = !inQuotes;
400
+ }
401
+ } else if (char === ',' && !inQuotes) {
402
+ parts.push(current.trim());
403
+ current = '';
404
+ } else {
405
+ current += char;
406
+ }
407
+ }
408
+ parts.push(current.trim());
409
+ return parts;
410
+ }
411
+
412
+ function csvEscape(value) {
413
+ const text = String(value ?? '');
414
+ return `"${text.replace(/"/g, '""')}"`;
415
+ }
416
+
417
+ function csvLinesFormat(mappings) {
418
+ const csvLines = ['screenName,screenId,componentName,componentId,componentType,description'];
419
+ mappings.forEach((m) => {
420
+ csvLines.push(
421
+ [
422
+ csvEscape(m.screenName),
423
+ csvEscape(m.screenId),
424
+ csvEscape(m.componentName),
425
+ csvEscape(m.componentId),
426
+ csvEscape(m.componentType),
427
+ csvEscape(m.description || ''),
428
+ ].join(',')
429
+ );
430
+ });
431
+ return csvLines.join('\n') + '\n';
432
+ }
433
+
434
+ function withDescription(row) {
435
+ if (row.description && String(row.description).trim()) {
436
+ return row;
437
+ }
438
+ const description =
439
+ row.componentType === 'SCREEN' || row.componentId === row.screenId
440
+ ? generateScreenDescription(row.screenId, row.screenName)
441
+ : generateComponentDescription(
442
+ row.componentId,
443
+ row.componentName,
444
+ row.componentType,
445
+ row.screenId
446
+ );
447
+ return { ...row, description };
448
+ }
449
+
450
+ function mergeCsv(existing, discovered, prune) {
451
+ const keyOf = (m) => `${m.screenId}::${m.componentId}`;
452
+ const existingByKey = new Map(existing.map((m) => [keyOf(m), m]));
453
+ const discoveredKeys = new Set(discovered.map(keyOf));
454
+
455
+ const enrich = (disc) => {
456
+ const prev = existingByKey.get(keyOf(disc));
457
+ // Prefer freshly generated description from scan (includes on-screen heading).
458
+ const next = withDescription({
459
+ ...disc,
460
+ description: disc.description && String(disc.description).trim() ? disc.description : '',
461
+ });
462
+ if (prev && prev.description && String(prev.description).startsWith('[manual] ')) {
463
+ return { ...next, description: prev.description };
464
+ }
465
+ return next;
466
+ };
467
+
468
+ let finalMappings;
469
+ if (prune) {
470
+ finalMappings = discovered.map(enrich);
471
+ } else {
472
+ finalMappings = existing.map((ext) => {
473
+ if (!ext.description || !String(ext.description).trim()) {
474
+ return withDescription(ext);
475
+ }
476
+ return ext;
477
+ });
478
+ for (const disc of discovered) {
479
+ const key = keyOf(disc);
480
+ const idx = finalMappings.findIndex((ext) => keyOf(ext) === key);
481
+ if (idx === -1) {
482
+ finalMappings.push(enrich(disc));
483
+ } else {
484
+ finalMappings[idx] = enrich({ ...finalMappings[idx], ...disc, description: disc.description || '' });
485
+ }
486
+ }
487
+ }
488
+
489
+ finalMappings.sort((a, b) => {
490
+ if (a.screenName !== b.screenName) {
491
+ return a.screenName.localeCompare(b.screenName);
492
+ }
493
+ if (a.componentType === 'SCREEN' && b.componentType !== 'SCREEN') return -1;
494
+ if (a.componentType !== 'SCREEN' && b.componentType === 'SCREEN') return 1;
495
+ return a.componentId.localeCompare(b.componentId);
496
+ });
497
+
498
+ const existingKeys = new Set(existing.map(keyOf));
499
+ const added = discovered.filter((d) => !existingKeys.has(keyOf(d)));
500
+ const stale = existing.filter((e) => !discoveredKeys.has(keyOf(e)));
501
+
502
+ return { finalMappings, added, stale };
503
+ }
504
+
505
+ function scan(args) {
506
+ const injectAccess = !args.qaOnly;
507
+ const injectQa = !args.accessOnly;
508
+
509
+ console.log(
510
+ `Access-control sync (${args.write ? 'WRITE' : 'DRY-RUN'})` +
511
+ ` access=${injectAccess} qa=${injectQa} rewriteIds=${args.rewriteIds}`
512
+ );
513
+
514
+ const componentFiles = collectComponentFiles();
515
+ console.log(`Found ${componentFiles.length} component files`);
516
+
517
+ const report = {
518
+ mode: args.write ? 'write' : 'dry-run',
519
+ scannedComponents: 0,
520
+ templatesUpdated: [],
521
+ templatesWouldUpdate: [],
522
+ tsUpdated: [],
523
+ tsWouldUpdate: [],
524
+ skippedNoise: 0,
525
+ discoveries: [],
526
+ screenIds: new Set(),
527
+ duplicateScreenIds: {},
528
+ errors: [],
529
+ };
530
+
531
+ const allDiscoveries = [];
532
+ const workItems = [];
533
+
534
+ // Phase 1: resolve provisional screen ids (content-based where allowed).
535
+ for (const entry of componentFiles) {
536
+ const { filePath: tsFile, fileContent, sourceType } = entry;
537
+ const classMatch = fileContent.match(/export\s+class\s+(\w+)[^{]*?\{/);
538
+ if (!classMatch) {
539
+ continue;
540
+ }
541
+ const className = classMatch[1];
542
+ if (className.toLowerCase().includes('confirm') && className.toLowerCase().includes('dialog')) {
543
+ continue;
544
+ }
545
+
546
+ report.scannedComponents++;
547
+
548
+ const templateUrlMatch = fileContent.match(/templateUrl\s*:\s*['"]([^'"]+)['"]/);
549
+ let templateContent = '';
550
+ let htmlPath = '';
551
+ if (templateUrlMatch) {
552
+ htmlPath = path.join(path.dirname(tsFile), templateUrlMatch[1]);
553
+ if (fs.existsSync(htmlPath)) {
554
+ templateContent = fs.readFileSync(htmlPath, 'utf8');
555
+ } else {
556
+ report.errors.push(`Missing template: ${htmlPath}`);
557
+ }
558
+ }
559
+
560
+ const excluded = isExcludedFromContentIds(tsFile);
561
+ const content = extractScreenContent(templateContent);
562
+ const deriveOpts = {
563
+ htmlContent: templateContent,
564
+ forceContentIds: args.forceContentIds && !excluded,
565
+ preferExistingScreen: excluded || args.preferExistingScreen,
566
+ };
567
+ const screenId = deriveScreenId(fileContent, tsFile, className, path, deriveOpts);
568
+ const pathScreenName = formatScreenNameFromPath(entry.rootDir || APPS_DIR, tsFile, path);
569
+ const screenName = content.title
570
+ ? `I-${content.title.replace(/^I-\s*/i, '').trim()}`
571
+ : pathScreenName;
572
+
573
+ workItems.push({
574
+ tsFile,
575
+ fileContent,
576
+ className,
577
+ templateContent,
578
+ htmlPath,
579
+ excluded,
580
+ content,
581
+ deriveOpts,
582
+ screenId,
583
+ screenName,
584
+ sourceType,
585
+ });
586
+ }
587
+
588
+ // Phase 2: soft disambiguate content collisions with entity prefix (non-excluded).
589
+ const idCounts = new Map();
590
+ for (const item of workItems) {
591
+ idCounts.set(item.screenId, (idCounts.get(item.screenId) || 0) + 1);
592
+ }
593
+ for (const item of workItems) {
594
+ if ((idCounts.get(item.screenId) || 0) <= 1 || item.excluded) {
595
+ continue;
596
+ }
597
+ const disambiguated = deriveScreenId(item.fileContent, item.tsFile, item.className, path, {
598
+ ...item.deriveOpts,
599
+ disambiguateWithEntity: true,
600
+ });
601
+ if (disambiguated && disambiguated !== item.screenId) {
602
+ item.screenId = disambiguated;
603
+ }
604
+ }
605
+
606
+ // Phase 2b: hard disambiguate remaining clones (cross-module + excluded collisions).
607
+ const dupFix = disambiguateDuplicateScreenIds(workItems, path);
608
+ report.duplicateScreenIdResolutions = dupFix.allResolutions
609
+ .filter((r) => !r.keptPrimary)
610
+ .map((r) => ({
611
+ from: r.from,
612
+ to: r.to,
613
+ file: path.relative(process.cwd(), r.tsFile),
614
+ }));
615
+
616
+ report.duplicateScreenIds = { ...dupFix.remainingDuplicates };
617
+ for (const item of workItems) {
618
+ report.screenIds.add(item.screenId);
619
+ }
620
+
621
+ // Phase 3: inject HTML/TS + collect discoveries.
622
+ for (const item of workItems) {
623
+ const { tsFile, fileContent, templateContent, htmlPath, content, screenId, screenName } = item;
624
+ let htmlModified = false;
625
+ let discoveries = [];
626
+
627
+ if (templateContent && htmlPath) {
628
+ const parsed = parseHtmlAndInject(templateContent, screenId, screenName, {
629
+ injectAccess,
630
+ injectQa,
631
+ rewriteIds: args.rewriteIds,
632
+ });
633
+
634
+ report.skippedNoise += parsed.skipped.length;
635
+ discoveries = parsed.discoveries;
636
+ htmlModified = parsed.modified;
637
+
638
+ if (htmlModified) {
639
+ const rel = path.relative(process.cwd(), htmlPath);
640
+ if (args.write) {
641
+ fs.writeFileSync(htmlPath, parsed.html, 'utf8');
642
+ report.templatesUpdated.push(rel);
643
+ } else {
644
+ report.templatesWouldUpdate.push(rel);
645
+ }
646
+ }
647
+ }
648
+
649
+ if (discoveries.length > 0 || /SCREEN\s*=/.test(fileContent) || htmlModified) {
650
+ allDiscoveries.push({
651
+ screenName,
652
+ screenId,
653
+ componentName: 'Screen Access',
654
+ componentId: screenId,
655
+ componentType: 'SCREEN',
656
+ description: generateScreenDescription(screenId, screenName, content.title),
657
+ });
658
+ allDiscoveries.push(...discoveries);
659
+ }
660
+
661
+ if (injectAccess) {
662
+ const existingScreen = (fileContent.match(/^\s*(?:readonly\s+)?SCREEN\s*=\s*['"]([^'"]+)['"]/im) || [])[1];
663
+ const hasScreenGetter = /^\s*get\s+SCREEN\s*\(/m.test(fileContent);
664
+ const needsScreenFix = existingScreen && existingScreen !== screenId;
665
+ const needsTsBootstrap = htmlModified;
666
+ // Getter-based SCREEN (e.g. filter screenId Input) — still allow default realignment.
667
+ const needsScreenWrite = hasScreenGetter
668
+ ? true
669
+ : !existingScreen || needsScreenFix;
670
+
671
+ if (needsScreenWrite || needsTsBootstrap) {
672
+ const tsResult = processComponentTs(tsFile, fileContent, screenId, args.write, {
673
+ screenOnly: needsScreenWrite && !needsTsBootstrap,
674
+ });
675
+ if (tsResult.modified) {
676
+ const tsRel = path.relative(process.cwd(), tsFile);
677
+ if (args.write) {
678
+ report.tsUpdated.push({ file: tsRel, changes: tsResult.changes });
679
+ } else {
680
+ report.tsWouldUpdate.push({ file: tsRel, changes: tsResult.changes });
681
+ }
682
+ }
683
+ }
684
+ }
685
+ }
686
+
687
+ // Dedupe discoveries
688
+ const seen = new Set();
689
+ const uniqueDiscoveries = [];
690
+ for (const d of allDiscoveries) {
691
+ const key = `${d.screenId}::${d.componentId}`;
692
+ if (seen.has(key)) {
693
+ continue;
694
+ }
695
+ seen.add(key);
696
+ uniqueDiscoveries.push(d);
697
+ }
698
+
699
+ const existing = readExistingCsv();
700
+ const { finalMappings, added, stale } = mergeCsv(existing, uniqueDiscoveries, args.pruneCsv && args.write);
701
+
702
+ // Phase 4: route + template hierarchy (page hosts → embedded SCREEN children)
703
+ const hierarchy = buildScreenNavigationHierarchy({
704
+ workItems,
705
+ appsRoot: APPS_DIR,
706
+ adminRoutesFile: ADMIN_ROUTES_FILE,
707
+ });
708
+
709
+ fs.mkdirSync(OUTPUT_DIR, { recursive: true });
710
+ fs.writeFileSync(OUTPUT_HIERARCHY_PATH, JSON.stringify(hierarchy, null, 2), 'utf8');
711
+
712
+ if (args.write && injectAccess) {
713
+ const csvContent = csvLinesFormat(finalMappings);
714
+ fs.writeFileSync(OUTPUT_CSV_PATH, csvContent, 'utf8');
715
+ fs.mkdirSync(path.dirname(ASSETS_CSV_PATH), { recursive: true });
716
+ fs.writeFileSync(ASSETS_CSV_PATH, csvContent, 'utf8');
717
+ fs.writeFileSync(ASSETS_HIERARCHY_PATH, JSON.stringify(hierarchy, null, 2), 'utf8');
718
+ try {
719
+ fs.writeFileSync(SHARED_CSV_PATH, csvContent, 'utf8');
720
+ } catch {
721
+ // optional mirror next to tools
722
+ }
723
+ } else if (injectAccess) {
724
+ const assetsDir = path.dirname(ASSETS_HIERARCHY_PATH);
725
+ if (fs.existsSync(assetsDir)) {
726
+ fs.writeFileSync(ASSETS_HIERARCHY_PATH, JSON.stringify(hierarchy, null, 2), 'utf8');
727
+ }
728
+ }
729
+
730
+ const serializableReport = {
731
+ ...report,
732
+ screenIds: [...report.screenIds].sort(),
733
+ discoveryCount: uniqueDiscoveries.length,
734
+ screenCount: uniqueDiscoveries.filter((d) => d.componentType === 'SCREEN').length,
735
+ buttonCount: uniqueDiscoveries.filter((d) => d.componentType === 'BUTTON').length,
736
+ csvWouldAdd: added.slice(0, 50),
737
+ csvWouldAddCount: added.length,
738
+ csvStaleCount: stale.length,
739
+ csvStaleSample: stale.slice(0, 30),
740
+ duplicateScreenIdCount: Object.keys(report.duplicateScreenIds).length,
741
+ duplicateScreenIdResolutions: report.duplicateScreenIdResolutions || [],
742
+ templatesWouldUpdateCount: report.templatesWouldUpdate.length,
743
+ templatesUpdatedCount: report.templatesUpdated.length,
744
+ sampleDiscoveries: uniqueDiscoveries.filter((d) => d.componentType === 'BUTTON').slice(0, 40),
745
+ hierarchy: {
746
+ pageCount: hierarchy.stats.pageCount,
747
+ embedEdgeCount: hierarchy.stats.embedEdgeCount,
748
+ unresolvedCount: hierarchy.stats.unresolvedCount,
749
+ resolvedWithoutScreenCount: hierarchy.stats.resolvedWithoutScreenCount || 0,
750
+ astComponentCount: hierarchy.stats.astComponentCount || 0,
751
+ classification: hierarchy.classification || null,
752
+ samplePages: hierarchy.pages.slice(0, 15),
753
+ unresolvedSample: hierarchy.unresolved.slice(0, 40),
754
+ resolvedWithoutScreenSample: (hierarchy.resolvedWithoutScreen || []).slice(0, 40),
755
+ output: path.relative(process.cwd(), OUTPUT_HIERARCHY_PATH),
756
+ },
757
+ };
758
+
759
+ fs.writeFileSync(REPORT_PATH, JSON.stringify(serializableReport, null, 2), 'utf8');
760
+
761
+ console.log('');
762
+ console.log('── Summary ──────────────────────────────────');
763
+ console.log(`Components scanned: ${report.scannedComponents}`);
764
+ console.log(`Unique screenIds: ${report.screenIds.size}`);
765
+ console.log(`Duplicate screenIds: ${Object.keys(report.duplicateScreenIds).length}`);
766
+ if ((report.duplicateScreenIdResolutions || []).length > 0) {
767
+ console.log(
768
+ `Duplicate ids resolved: ${report.duplicateScreenIdResolutions.length} (module/path uniquified)`
769
+ );
770
+ }
771
+ console.log(`Discoveries (unique): ${uniqueDiscoveries.length}`);
772
+ console.log(` SCREEN rows: ${serializableReport.screenCount}`);
773
+ console.log(` BUTTON rows: ${serializableReport.buttonCount}`);
774
+ console.log(`Noise skipped: ${report.skippedNoise}`);
775
+ console.log(
776
+ `Templates ${args.write ? 'updated' : 'would update'}: ${
777
+ args.write ? report.templatesUpdated.length : report.templatesWouldUpdate.length
778
+ }`
779
+ );
780
+ console.log(`CSV new rows: ${added.length}`);
781
+ console.log(`CSV stale rows: ${stale.length}`);
782
+ console.log(`Hierarchy pages: ${hierarchy.stats.pageCount}`);
783
+ console.log(`Hierarchy embed edges: ${hierarchy.stats.embedEdgeCount}`);
784
+ console.log(`Hierarchy unresolved: ${hierarchy.stats.unresolvedCount}`);
785
+ console.log(
786
+ `Hierarchy resolved (no SCREEN): ${hierarchy.stats.resolvedWithoutScreenCount || 0}`
787
+ );
788
+ console.log(`Hierarchy map: ${path.relative(process.cwd(), OUTPUT_HIERARCHY_PATH)}`);
789
+ console.log(`Report: ${path.relative(process.cwd(), REPORT_PATH)}`);
790
+
791
+ if (Object.keys(report.duplicateScreenIds).length > 0) {
792
+ console.log('');
793
+ console.log('⚠ Duplicate SCREEN ids remaining after disambiguation:');
794
+ for (const [id, files] of Object.entries(report.duplicateScreenIds).slice(0, 10)) {
795
+ console.log(` ${id}`);
796
+ for (const f of files) {
797
+ console.log(` - ${path.relative(process.cwd(), f)}`);
798
+ }
799
+ }
800
+ }
801
+
802
+ console.log('');
803
+ if (!args.write) {
804
+ console.log('Dry-run only. Re-run with --write to apply.');
805
+ } else {
806
+ console.log('Write complete.');
807
+ }
808
+ }
809
+
810
+ function main() {
811
+ const args = parseArgs(process.argv);
812
+ if (args.help) {
813
+ printHelp();
814
+ return;
815
+ }
816
+ scan(args);
817
+ }
818
+
819
+ main();