nodefony 7.0.2 → 10.0.0-alpha.2

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 (903) hide show
  1. package/.ai/symbols.json +36135 -0
  2. package/LICENSE +544 -0
  3. package/README.md +316 -694
  4. package/bin/nodefony +2183 -85
  5. package/dist/client/Container.js +367 -0
  6. package/dist/client/Event.js +198 -0
  7. package/dist/client/Service.js +344 -0
  8. package/dist/client/Tools.js +162 -0
  9. package/dist/client/client/ClientKernel.js +276 -0
  10. package/dist/client/client/angular/index.js +352 -0
  11. package/dist/client/client/announce.js +212 -0
  12. package/dist/client/client/debugbar/DebugBar.js +1624 -0
  13. package/dist/client/client/debugbar/format.js +103 -0
  14. package/dist/client/client/debugbar/hmr.js +37 -0
  15. package/dist/client/client/debugbar/index.js +29 -0
  16. package/dist/client/client/debugbar/model.js +133 -0
  17. package/dist/client/client/debugbar/network.js +173 -0
  18. package/dist/client/client/debugbar/profile.js +114 -0
  19. package/dist/client/client/index.js +34 -0
  20. package/dist/client/client/react/index.js +268 -0
  21. package/dist/client/client/realtime/AdaptiveRate.js +240 -0
  22. package/dist/client/client/realtime/BrowserWsTransport.js +57 -0
  23. package/dist/client/client/realtime/RealtimeClient.js +898 -0
  24. package/dist/client/client/realtime/localEvents.js +56 -0
  25. package/dist/client/client/realtime/notice.js +125 -0
  26. package/dist/client/client/realtime/observe.js +280 -0
  27. package/dist/client/client/roles/index.js +3 -0
  28. package/dist/client/client/roles/registry.js +97 -0
  29. package/dist/client/client/roles/roles.js +91 -0
  30. package/dist/client/client/shim/events.js +88 -0
  31. package/dist/client/client/shim/util.js +20 -0
  32. package/dist/client/client/svelte/index.js +319 -0
  33. package/dist/client/client/syslog/context.js +122 -0
  34. package/dist/client/client/syslog/errors.js +49 -0
  35. package/dist/client/client/syslog/uplink.js +106 -0
  36. package/dist/client/client/vue/index.js +321 -0
  37. package/dist/client/colors.js +63 -0
  38. package/dist/client/debugbar.standalone.js +4274 -0
  39. package/dist/client/realtime/IRealtimeTransport.js +32 -0
  40. package/dist/client/realtime/JsonRpcPeer.js +251 -0
  41. package/dist/client/realtime/channelRate.js +39 -0
  42. package/dist/client/realtime/platformChannels.js +155 -0
  43. package/dist/client/syslog/Pdu.js +224 -0
  44. package/dist/client/syslog/Syslog.js +1159 -0
  45. package/dist/client/syslog/drivers/pduFlow.js +106 -0
  46. package/dist/client/syslog/drivers/pduProtocol.js +28 -0
  47. package/dist/client/syslog/logColor.js +80 -0
  48. package/dist/client/types/src/Container.d.ts +221 -0
  49. package/dist/client/types/src/Event.d.ts +171 -0
  50. package/dist/client/types/src/FileClass.d.ts +184 -0
  51. package/dist/client/types/src/Service.d.ts +165 -0
  52. package/dist/client/types/src/Tools.d.ts +116 -0
  53. package/dist/client/types/src/client/ClientKernel.d.ts +90 -0
  54. package/dist/client/types/src/client/IClientKernel.d.ts +195 -0
  55. package/dist/client/types/src/client/angular/index.d.ts +273 -0
  56. package/dist/client/types/src/client/announce.d.ts +74 -0
  57. package/dist/client/types/src/client/debugbar/DebugBar.d.ts +279 -0
  58. package/dist/client/types/src/client/debugbar/format.d.ts +48 -0
  59. package/dist/client/types/src/client/debugbar/hmr.d.ts +16 -0
  60. package/dist/client/types/src/client/debugbar/index.d.ts +30 -0
  61. package/dist/client/types/src/client/debugbar/model.d.ts +158 -0
  62. package/dist/client/types/src/client/debugbar/network.d.ts +55 -0
  63. package/dist/client/types/src/client/debugbar/profile.d.ts +99 -0
  64. package/dist/client/types/src/client/index.d.ts +47 -0
  65. package/dist/client/types/src/client/react/index.d.ts +207 -0
  66. package/dist/client/types/src/client/realtime/AdaptiveRate.d.ts +138 -0
  67. package/dist/client/types/src/client/realtime/BrowserWsTransport.d.ts +27 -0
  68. package/dist/client/types/src/client/realtime/RealtimeClient.d.ts +573 -0
  69. package/dist/client/types/src/client/realtime/localEvents.d.ts +53 -0
  70. package/dist/client/types/src/client/realtime/notice.d.ts +70 -0
  71. package/dist/client/types/src/client/realtime/observe.d.ts +284 -0
  72. package/dist/client/types/src/client/roles/index.d.ts +15 -0
  73. package/dist/client/types/src/client/roles/registry.d.ts +72 -0
  74. package/dist/client/types/src/client/roles/roles.d.ts +68 -0
  75. package/dist/client/types/src/client/shim/cli-color.d.ts +2 -0
  76. package/dist/client/types/src/client/shim/events.d.ts +39 -0
  77. package/dist/client/types/src/client/shim/util.d.ts +14 -0
  78. package/dist/client/types/src/client/svelte/index.d.ts +216 -0
  79. package/dist/client/types/src/client/syslog/context.d.ts +76 -0
  80. package/dist/client/types/src/client/syslog/errors.d.ts +46 -0
  81. package/dist/client/types/src/client/syslog/uplink.d.ts +89 -0
  82. package/dist/client/types/src/client/transport/websocket.d.ts +5 -0
  83. package/dist/client/types/src/client/vue/index.d.ts +234 -0
  84. package/dist/client/types/src/colors.d.ts +32 -0
  85. package/dist/client/types/src/config/infra.d.ts +171 -0
  86. package/dist/client/types/src/kernel/bootReport.d.ts +112 -0
  87. package/dist/client/types/src/kernel/readinessRegistry.d.ts +104 -0
  88. package/dist/client/types/src/realtime/IRealtimeSocket.d.ts +142 -0
  89. package/dist/client/types/src/realtime/IRealtimeTransport.d.ts +50 -0
  90. package/dist/client/types/src/realtime/JsonRpcPeer.d.ts +251 -0
  91. package/dist/client/types/src/realtime/RealtimeEventMap.d.ts +269 -0
  92. package/dist/client/types/src/realtime/channelRate.d.ts +55 -0
  93. package/dist/client/types/src/realtime/platformChannels.d.ts +147 -0
  94. package/dist/client/types/src/syslog/Pdu.d.ts +134 -0
  95. package/dist/client/types/src/syslog/Syslog.d.ts +482 -0
  96. package/dist/client/types/src/syslog/drivers/ILogDriver.d.ts +177 -0
  97. package/dist/client/types/src/syslog/drivers/pduFlow.d.ts +53 -0
  98. package/dist/client/types/src/syslog/drivers/pduProtocol.d.ts +20 -0
  99. package/dist/client/types/src/syslog/logColor.d.ts +59 -0
  100. package/dist/client/types/src/types/ICliKernel.d.ts +28 -0
  101. package/dist/client/types/src/types/ICommand.d.ts +14 -0
  102. package/dist/client/types/src/types/IContainer.d.ts +35 -0
  103. package/dist/client/types/src/types/IKernel.d.ts +138 -0
  104. package/dist/client/types/src/types/IMcpTool.d.ts +157 -0
  105. package/dist/client/types/src/types/IModule.d.ts +73 -0
  106. package/dist/client/types/src/types/IPage.d.ts +119 -0
  107. package/dist/client/types/src/types/IService.d.ts +62 -0
  108. package/dist/client/types/src/types/ISyslog.d.ts +58 -0
  109. package/dist/client/types/src/types/ITransport.d.ts +5 -0
  110. package/dist/client/types/src/types/globals.d.ts +47 -0
  111. package/dist/node/Cli.js +750 -0
  112. package/dist/node/Container.js +367 -0
  113. package/dist/node/Error.js +309 -0
  114. package/dist/node/Event.js +198 -0
  115. package/dist/node/FileClass.js +308 -0
  116. package/dist/node/Nodefony.js +78 -0
  117. package/dist/node/Service.js +344 -0
  118. package/dist/node/Tools.js +205 -0
  119. package/dist/node/bin/resolveLocalCli.js +10 -0
  120. package/dist/node/bundler/index.js +103 -0
  121. package/dist/node/cli/agentTargets.js +461 -0
  122. package/dist/node/cli/aiMcp.js +553 -0
  123. package/dist/node/cli/aiMcpReport.js +129 -0
  124. package/dist/node/cli/aiSync.js +323 -0
  125. package/dist/node/cli/aiSyncReport.js +142 -0
  126. package/dist/node/cli/card.js +186 -0
  127. package/dist/node/cli/cardReport.js +110 -0
  128. package/dist/node/cli/completion.js +415 -0
  129. package/dist/node/cli/create.js +805 -0
  130. package/dist/node/cli/env.js +332 -0
  131. package/dist/node/cli/envReport.js +159 -0
  132. package/dist/node/cli/execPortable.js +60 -0
  133. package/dist/node/cli/gitHooks.js +191 -0
  134. package/dist/node/cli/gitHooksReport.js +119 -0
  135. package/dist/node/cli/globalFlags.js +67 -0
  136. package/dist/node/cli/helpReport.js +288 -0
  137. package/dist/node/cli/manPage.js +165 -0
  138. package/dist/node/cli/nodefonyBin.js +61 -0
  139. package/dist/node/cli/outdated.js +144 -0
  140. package/dist/node/cli/progress.js +555 -0
  141. package/dist/node/cli/projectRoot.js +32 -0
  142. package/dist/node/cli/promptPassword.js +44 -0
  143. package/dist/node/cli/prompts.js +86 -0
  144. package/dist/node/cli/scaffold/destination.js +92 -0
  145. package/dist/node/cli/scaffold/engine.js +2156 -0
  146. package/dist/node/cli/scaffold/entityFields.js +589 -0
  147. package/dist/node/cli/scaffold/format.js +243 -0
  148. package/dist/node/cli/scaffold/interactive.js +124 -0
  149. package/dist/node/cli/scaffold/moduleLayout.js +38 -0
  150. package/dist/node/cli/scaffold/reservedEntities.js +74 -0
  151. package/dist/node/cli/scaffold/spec.js +704 -0
  152. package/dist/node/cli/scaffold/steps.js +40 -0
  153. package/dist/node/cli/scaffold/userContractSource.js +133 -0
  154. package/dist/node/cli/scaffold/versions.js +57 -0
  155. package/dist/node/cli/scaffold/writer.js +172 -0
  156. package/dist/node/cli/startMenu.js +496 -0
  157. package/dist/node/cli/symbols.js +205 -0
  158. package/dist/node/cli/sysexits.js +34 -0
  159. package/dist/node/cli/tableReport.js +184 -0
  160. package/dist/node/cli/usageReport.js +144 -0
  161. package/dist/node/colors.js +63 -0
  162. package/dist/node/command/Builder.js +153 -0
  163. package/dist/node/command/Command.js +348 -0
  164. package/dist/node/config/configProvenance.js +101 -0
  165. package/dist/node/config/defaults.js +39 -0
  166. package/dist/node/config/defineConfig.js +129 -0
  167. package/dist/node/config/defineEnv.js +264 -0
  168. package/dist/node/config/envExample.js +41 -0
  169. package/dist/node/config/envOverride.js +423 -0
  170. package/dist/node/config/infra.js +163 -0
  171. package/dist/node/config/reactivity.js +23 -0
  172. package/dist/node/config/reservedEnv.js +118 -0
  173. package/dist/node/config/schema.js +134 -0
  174. package/dist/node/config/use.js +24 -0
  175. package/dist/node/finder/File.js +49 -0
  176. package/dist/node/finder/FileResult.js +86 -0
  177. package/dist/node/finder/Finder.js +201 -0
  178. package/dist/node/finder/Result.js +45 -0
  179. package/dist/node/index.js +121 -0
  180. package/dist/node/kernel/BootConfigurationError.js +54 -0
  181. package/dist/node/kernel/CliKernel.js +630 -0
  182. package/dist/node/kernel/Kernel.js +2198 -0
  183. package/dist/node/kernel/Module.js +453 -0
  184. package/dist/node/kernel/adminPlane/adminCaller.js +98 -0
  185. package/dist/node/kernel/adminPlane/adminRbac.js +51 -0
  186. package/dist/node/kernel/adminPlane/catalog.js +158 -0
  187. package/dist/node/kernel/adminPlane/executeAdmin.js +78 -0
  188. package/dist/node/kernel/checks/deep.js +335 -0
  189. package/dist/node/kernel/checks/freshness.js +139 -0
  190. package/dist/node/kernel/checks/gating.js +231 -0
  191. package/dist/node/kernel/checks/guards.js +232 -0
  192. package/dist/node/kernel/checks/lastBoot.js +136 -0
  193. package/dist/node/kernel/checks/live.js +330 -0
  194. package/dist/node/kernel/checks/packageDeps.js +292 -0
  195. package/dist/node/kernel/checks/readiness.js +153 -0
  196. package/dist/node/kernel/checks/renderReport.js +780 -0
  197. package/dist/node/kernel/checks/report.js +421 -0
  198. package/dist/node/kernel/checks/runDoctor.js +731 -0
  199. package/dist/node/kernel/checks/surface.js +364 -0
  200. package/dist/node/kernel/checks/walk.js +100 -0
  201. package/dist/node/kernel/checks/wiring.js +450 -0
  202. package/dist/node/kernel/commands/AiMcpCommand.js +65 -0
  203. package/dist/node/kernel/commands/AiSyncCommand.js +48 -0
  204. package/dist/node/kernel/commands/BuildCommand.js +49 -0
  205. package/dist/node/kernel/commands/CardCommand.js +50 -0
  206. package/dist/node/kernel/commands/ClusterCommand.js +58 -0
  207. package/dist/node/kernel/commands/CompletionCommand.js +31 -0
  208. package/dist/node/kernel/commands/CreateCommand.js +45 -0
  209. package/dist/node/kernel/commands/DevCommand.js +99 -0
  210. package/dist/node/kernel/commands/DoctorCommand.js +115 -0
  211. package/dist/node/kernel/commands/EnvCommand.js +52 -0
  212. package/dist/node/kernel/commands/GitHooksCommand.js +47 -0
  213. package/dist/node/kernel/commands/InspectCommand.js +153 -0
  214. package/dist/node/kernel/commands/InstallCommand.js +20 -0
  215. package/dist/node/kernel/commands/MenuCommand.js +261 -0
  216. package/dist/node/kernel/commands/OutdatedCommand.js +164 -0
  217. package/dist/node/kernel/commands/ProdCommand.js +60 -0
  218. package/dist/node/kernel/commands/StatusCommand.js +30 -0
  219. package/dist/node/kernel/commands/StopCommand.js +35 -0
  220. package/dist/node/kernel/commands/SymbolsCommand.js +45 -0
  221. package/dist/node/kernel/commands/runtimeLauncher.js +95 -0
  222. package/dist/node/kernel/decorators/kernelDecorator.js +110 -0
  223. package/dist/node/kernel/injector/injector.js +137 -0
  224. package/dist/node/kernel/injector/serviceOrder.js +106 -0
  225. package/dist/node/kernel/inspect/adminSubjects.js +194 -0
  226. package/dist/node/kernel/inspect/docOutline.js +86 -0
  227. package/dist/node/kernel/lifecycleTags.js +76 -0
  228. package/dist/node/kernel/moduleConfig.js +75 -0
  229. package/dist/node/kernel/moduleGating.js +68 -0
  230. package/dist/node/kernel/readinessRegistry.js +98 -0
  231. package/dist/node/kernel/resolveModuleEntry.js +61 -0
  232. package/dist/node/mcp/caller.js +40 -0
  233. package/dist/node/mcp/guard.js +51 -0
  234. package/dist/node/mcp/protocol.js +157 -0
  235. package/dist/node/mcp/server.js +216 -0
  236. package/dist/node/mcp/tools.js +861 -0
  237. package/dist/node/oauth/authorizationServer.js +221 -0
  238. package/dist/node/oauth/protectedResource.js +293 -0
  239. package/dist/node/package.js +4 -0
  240. package/dist/node/realtime/IRealtimeTransport.js +32 -0
  241. package/dist/node/realtime/JsonRpcPeer.js +251 -0
  242. package/dist/node/realtime/RealtimeEventMap.js +15 -0
  243. package/dist/node/realtime/channelRate.js +39 -0
  244. package/dist/node/realtime/platformChannels.js +155 -0
  245. package/dist/node/runtime/GcScheduler.js +92 -0
  246. package/dist/node/runtime/RequestContext.js +76 -0
  247. package/dist/node/runtime/bearer.js +84 -0
  248. package/dist/node/runtime/engineEnvironment.js +79 -0
  249. package/dist/node/runtime/loadEnv.js +105 -0
  250. package/dist/node/runtime/pageFacets.js +85 -0
  251. package/dist/node/runtime/pageFilters.js +103 -0
  252. package/dist/node/runtime/pageGuard.js +84 -0
  253. package/dist/node/runtime/pageQuery.js +160 -0
  254. package/dist/node/runtime/pageSort.js +114 -0
  255. package/dist/node/runtime/redact.js +43 -0
  256. package/dist/node/runtime/withTimeout.js +71 -0
  257. package/dist/node/service/cluster/ClusterManager.js +202 -0
  258. package/dist/node/service/cluster/ClusterProbeAggregator.js +120 -0
  259. package/dist/node/service/cluster/ClusterRelay.js +74 -0
  260. package/dist/node/service/cluster/clusterMaster.js +65 -0
  261. package/dist/node/service/cluster/clusterMessage.js +70 -0
  262. package/dist/node/service/cluster/cpuQuota.js +59 -0
  263. package/dist/node/service/cluster/instanceProbe.js +52 -0
  264. package/dist/node/service/cluster/podEnvironment.js +48 -0
  265. package/dist/node/service/cluster/processProbe.js +70 -0
  266. package/dist/node/service/cluster/richProcessProbe.js +115 -0
  267. package/dist/node/service/cluster/topology.js +71 -0
  268. package/dist/node/service/dev/BootReporter.js +490 -0
  269. package/dist/node/service/dev/DevSupervisor.js +1004 -0
  270. package/dist/node/service/dev/bootVerdict.js +158 -0
  271. package/dist/node/service/dev/detachedStart.js +372 -0
  272. package/dist/node/service/dev/devProcess.js +1137 -0
  273. package/dist/node/service/dev/devProjects.js +138 -0
  274. package/dist/node/service/dev/devStatusReport.js +395 -0
  275. package/dist/node/service/dev/devStop.js +263 -0
  276. package/dist/node/service/fetchService.js +35 -0
  277. package/dist/node/service/gitService.js +62 -0
  278. package/dist/node/syslog/Pdu.js +224 -0
  279. package/dist/node/syslog/Syslog.js +1159 -0
  280. package/dist/node/syslog/drivers/ClusterFileLogDriver.js +114 -0
  281. package/dist/node/syslog/drivers/FileLogDriver.js +127 -0
  282. package/dist/node/syslog/drivers/ILogDriver.js +26 -0
  283. package/dist/node/syslog/drivers/LokiLogDriver.js +144 -0
  284. package/dist/node/syslog/drivers/MemoryLogDriver.js +33 -0
  285. package/dist/node/syslog/drivers/OpenSearchLogDriver.js +159 -0
  286. package/dist/node/syslog/drivers/builtinLogDrivers.js +114 -0
  287. package/dist/node/syslog/drivers/filterPdus.js +71 -0
  288. package/dist/node/syslog/drivers/logDriverRegistry.js +83 -0
  289. package/dist/node/syslog/drivers/opensearchShared.js +17 -0
  290. package/dist/node/syslog/drivers/pduFlow.js +106 -0
  291. package/dist/node/syslog/drivers/pduProtocol.js +28 -0
  292. package/dist/node/syslog/httpFetch.js +43 -0
  293. package/dist/node/syslog/logColor.js +80 -0
  294. package/dist/node/syslog/sinks/FileSink.js +142 -0
  295. package/dist/node/syslog/transports/BatchingHttpTransport.js +98 -0
  296. package/dist/node/syslog/transports/ConsoleTransport.js +14 -0
  297. package/dist/node/syslog/transports/FileTransport.js +17 -0
  298. package/dist/node/syslog/transports/HttpTransport.js +44 -0
  299. package/dist/node/syslog/transports/LokiTransport.js +80 -0
  300. package/dist/node/syslog/transports/OpenSearchTransport.js +53 -0
  301. package/dist/node/syslog/transports/SyslogTransport.js +13 -0
  302. package/dist/node/syslog/transports/index.js +8 -0
  303. package/dist/node/testing/index.js +190 -0
  304. package/dist/types/Cli.d.ts +177 -0
  305. package/dist/types/Container.d.ts +221 -0
  306. package/dist/types/Error.d.ts +162 -0
  307. package/dist/types/Event.d.ts +171 -0
  308. package/dist/types/FileClass.d.ts +184 -0
  309. package/dist/types/Nodefony.d.ts +66 -0
  310. package/dist/types/Service.d.ts +165 -0
  311. package/dist/types/Tools.d.ts +116 -0
  312. package/dist/types/bin/resolveLocalCli.d.ts +49 -0
  313. package/dist/types/bundler/index.d.ts +66 -0
  314. package/dist/types/cli/agentTargets.d.ts +378 -0
  315. package/dist/types/cli/aiMcp.d.ts +239 -0
  316. package/dist/types/cli/aiMcpReport.d.ts +113 -0
  317. package/dist/types/cli/aiSync.d.ts +68 -0
  318. package/dist/types/cli/aiSyncReport.d.ts +149 -0
  319. package/dist/types/cli/card.d.ts +49 -0
  320. package/dist/types/cli/cardReport.d.ts +127 -0
  321. package/dist/types/cli/completion.d.ts +154 -0
  322. package/dist/types/cli/create.d.ts +177 -0
  323. package/dist/types/cli/env.d.ts +103 -0
  324. package/dist/types/cli/envReport.d.ts +153 -0
  325. package/dist/types/cli/execPortable.d.ts +54 -0
  326. package/dist/types/cli/gitHooks.d.ts +48 -0
  327. package/dist/types/cli/gitHooksReport.d.ts +101 -0
  328. package/dist/types/cli/globalFlags.d.ts +48 -0
  329. package/dist/types/cli/helpReport.d.ts +110 -0
  330. package/dist/types/cli/manPage.d.ts +21 -0
  331. package/dist/types/cli/nodefonyBin.d.ts +45 -0
  332. package/dist/types/cli/outdated.d.ts +119 -0
  333. package/dist/types/cli/progress.d.ts +349 -0
  334. package/dist/types/cli/projectRoot.d.ts +19 -0
  335. package/dist/types/cli/promptPassword.d.ts +22 -0
  336. package/dist/types/cli/prompts.d.ts +102 -0
  337. package/dist/types/cli/scaffold/destination.d.ts +70 -0
  338. package/dist/types/cli/scaffold/engine.d.ts +545 -0
  339. package/dist/types/cli/scaffold/entityFields.d.ts +238 -0
  340. package/dist/types/cli/scaffold/format.d.ts +78 -0
  341. package/dist/types/cli/scaffold/interactive.d.ts +11 -0
  342. package/dist/types/cli/scaffold/moduleLayout.d.ts +69 -0
  343. package/dist/types/cli/scaffold/reservedEntities.d.ts +49 -0
  344. package/dist/types/cli/scaffold/spec.d.ts +159 -0
  345. package/dist/types/cli/scaffold/steps.d.ts +24 -0
  346. package/dist/types/cli/scaffold/userContractSource.d.ts +86 -0
  347. package/dist/types/cli/scaffold/versions.d.ts +14 -0
  348. package/dist/types/cli/scaffold/writer.d.ts +100 -0
  349. package/dist/types/cli/startMenu.d.ts +195 -0
  350. package/dist/types/cli/symbols.d.ts +84 -0
  351. package/dist/types/cli/sysexits.d.ts +31 -0
  352. package/dist/types/cli/tableReport.d.ts +44 -0
  353. package/dist/types/cli/usageReport.d.ts +108 -0
  354. package/dist/types/client/ClientKernel.d.ts +90 -0
  355. package/dist/types/client/IClientKernel.d.ts +195 -0
  356. package/dist/types/client/angular/index.d.ts +273 -0
  357. package/dist/types/client/announce.d.ts +74 -0
  358. package/dist/types/client/debugbar/DebugBar.d.ts +279 -0
  359. package/dist/types/client/debugbar/format.d.ts +48 -0
  360. package/dist/types/client/debugbar/hmr.d.ts +16 -0
  361. package/dist/types/client/debugbar/index.d.ts +30 -0
  362. package/dist/types/client/debugbar/model.d.ts +158 -0
  363. package/dist/types/client/debugbar/network.d.ts +55 -0
  364. package/dist/types/client/debugbar/profile.d.ts +99 -0
  365. package/dist/types/client/index.d.ts +47 -0
  366. package/dist/types/client/react/index.d.ts +207 -0
  367. package/dist/types/client/realtime/AdaptiveRate.d.ts +138 -0
  368. package/dist/types/client/realtime/BrowserWsTransport.d.ts +27 -0
  369. package/dist/types/client/realtime/RealtimeClient.d.ts +573 -0
  370. package/dist/types/client/realtime/localEvents.d.ts +53 -0
  371. package/dist/types/client/realtime/notice.d.ts +70 -0
  372. package/dist/types/client/realtime/observe.d.ts +284 -0
  373. package/dist/types/client/roles/index.d.ts +15 -0
  374. package/dist/types/client/roles/registry.d.ts +72 -0
  375. package/dist/types/client/roles/roles.d.ts +68 -0
  376. package/dist/types/client/shim/cli-color.d.ts +2 -0
  377. package/dist/types/client/shim/events.d.ts +39 -0
  378. package/dist/types/client/shim/util.d.ts +14 -0
  379. package/dist/types/client/svelte/index.d.ts +216 -0
  380. package/dist/types/client/syslog/context.d.ts +76 -0
  381. package/dist/types/client/syslog/errors.d.ts +46 -0
  382. package/dist/types/client/syslog/uplink.d.ts +89 -0
  383. package/dist/types/client/transport/websocket.d.ts +5 -0
  384. package/dist/types/client/vue/index.d.ts +234 -0
  385. package/dist/types/colors.d.ts +32 -0
  386. package/dist/types/command/Builder.d.ts +47 -0
  387. package/dist/types/command/Command.d.ts +268 -0
  388. package/dist/types/config/configMeta.d.ts +53 -0
  389. package/dist/types/config/configProvenance.d.ts +58 -0
  390. package/dist/types/config/defaults.d.ts +33 -0
  391. package/dist/types/config/defineConfig.d.ts +70 -0
  392. package/dist/types/config/defineEnv.d.ts +170 -0
  393. package/dist/types/config/envExample.d.ts +23 -0
  394. package/dist/types/config/envOverride.d.ts +236 -0
  395. package/dist/types/config/index.d.ts +23 -0
  396. package/dist/types/config/infra.d.ts +171 -0
  397. package/dist/types/config/reactivity.d.ts +36 -0
  398. package/dist/types/config/reservedEnv.d.ts +145 -0
  399. package/dist/types/config/schema.d.ts +113 -0
  400. package/dist/types/config/types.d.ts +374 -0
  401. package/dist/types/config/use.d.ts +80 -0
  402. package/dist/types/finder/File.d.ts +26 -0
  403. package/dist/types/finder/FileResult.d.ts +14 -0
  404. package/dist/types/finder/Finder.d.ts +51 -0
  405. package/dist/types/finder/Result.d.ts +10 -0
  406. package/dist/types/index.d.ts +205 -0
  407. package/dist/types/kernel/BootConfigurationError.d.ts +50 -0
  408. package/dist/types/kernel/CliKernel.d.ts +289 -0
  409. package/dist/types/kernel/Kernel.d.ts +1255 -0
  410. package/dist/types/kernel/Module.d.ts +326 -0
  411. package/dist/types/kernel/adminPlane/adminCaller.d.ts +105 -0
  412. package/dist/types/kernel/adminPlane/adminRbac.d.ts +44 -0
  413. package/dist/types/kernel/adminPlane/catalog.d.ts +122 -0
  414. package/dist/types/kernel/adminPlane/executeAdmin.d.ts +99 -0
  415. package/dist/types/kernel/bootReport.d.ts +112 -0
  416. package/dist/types/kernel/checks/deep.d.ts +122 -0
  417. package/dist/types/kernel/checks/freshness.d.ts +49 -0
  418. package/dist/types/kernel/checks/gating.d.ts +134 -0
  419. package/dist/types/kernel/checks/guards.d.ts +57 -0
  420. package/dist/types/kernel/checks/lastBoot.d.ts +189 -0
  421. package/dist/types/kernel/checks/live.d.ts +110 -0
  422. package/dist/types/kernel/checks/packageDeps.d.ts +44 -0
  423. package/dist/types/kernel/checks/readiness.d.ts +97 -0
  424. package/dist/types/kernel/checks/renderReport.d.ts +107 -0
  425. package/dist/types/kernel/checks/report.d.ts +414 -0
  426. package/dist/types/kernel/checks/runDoctor.d.ts +361 -0
  427. package/dist/types/kernel/checks/surface.d.ts +148 -0
  428. package/dist/types/kernel/checks/walk.d.ts +45 -0
  429. package/dist/types/kernel/checks/wiring.d.ts +54 -0
  430. package/dist/types/kernel/commands/AiMcpCommand.d.ts +39 -0
  431. package/dist/types/kernel/commands/AiSyncCommand.d.ts +28 -0
  432. package/dist/types/kernel/commands/BuildCommand.d.ts +13 -0
  433. package/dist/types/kernel/commands/CardCommand.d.ts +34 -0
  434. package/dist/types/kernel/commands/ClusterCommand.d.ts +29 -0
  435. package/dist/types/kernel/commands/CompletionCommand.d.ts +17 -0
  436. package/dist/types/kernel/commands/CreateCommand.d.ts +16 -0
  437. package/dist/types/kernel/commands/DevCommand.d.ts +40 -0
  438. package/dist/types/kernel/commands/DoctorCommand.d.ts +55 -0
  439. package/dist/types/kernel/commands/EnvCommand.d.ts +35 -0
  440. package/dist/types/kernel/commands/GitHooksCommand.d.ts +27 -0
  441. package/dist/types/kernel/commands/InspectCommand.d.ts +59 -0
  442. package/dist/types/kernel/commands/InstallCommand.d.ts +7 -0
  443. package/dist/types/kernel/commands/MenuCommand.d.ts +58 -0
  444. package/dist/types/kernel/commands/OutdatedCommand.d.ts +47 -0
  445. package/dist/types/kernel/commands/ProdCommand.d.ts +29 -0
  446. package/dist/types/kernel/commands/StatusCommand.d.ts +17 -0
  447. package/dist/types/kernel/commands/StopCommand.d.ts +17 -0
  448. package/dist/types/kernel/commands/SymbolsCommand.d.ts +29 -0
  449. package/dist/types/kernel/commands/runtimeLauncher.d.ts +76 -0
  450. package/dist/types/kernel/decorators/kernelDecorator.d.ts +76 -0
  451. package/dist/types/kernel/injector/injector.d.ts +47 -0
  452. package/dist/types/kernel/injector/serviceOrder.d.ts +29 -0
  453. package/dist/types/kernel/inspect/adminSubjects.d.ts +169 -0
  454. package/dist/types/kernel/inspect/docOutline.d.ts +53 -0
  455. package/dist/types/kernel/lifecycleTags.d.ts +79 -0
  456. package/dist/types/kernel/moduleConfig.d.ts +35 -0
  457. package/dist/types/kernel/moduleGating.d.ts +78 -0
  458. package/dist/types/kernel/readinessRegistry.d.ts +104 -0
  459. package/dist/types/kernel/resolveModuleEntry.d.ts +46 -0
  460. package/dist/types/mcp/caller.d.ts +55 -0
  461. package/dist/types/mcp/guard.d.ts +66 -0
  462. package/dist/types/mcp/protocol.d.ts +163 -0
  463. package/dist/types/mcp/server.d.ts +70 -0
  464. package/dist/types/mcp/tools.d.ts +209 -0
  465. package/dist/types/oauth/authorizationServer.d.ts +204 -0
  466. package/dist/types/oauth/protectedResource.d.ts +339 -0
  467. package/dist/types/realtime/IRealtimeSocket.d.ts +142 -0
  468. package/dist/types/realtime/IRealtimeTransport.d.ts +50 -0
  469. package/dist/types/realtime/JsonRpcPeer.d.ts +251 -0
  470. package/dist/types/realtime/RealtimeEventMap.d.ts +269 -0
  471. package/dist/types/realtime/channelRate.d.ts +55 -0
  472. package/dist/types/realtime/platformChannels.d.ts +147 -0
  473. package/dist/types/runtime/GcScheduler.d.ts +96 -0
  474. package/dist/types/runtime/RequestContext.d.ts +152 -0
  475. package/dist/types/runtime/bearer.d.ts +88 -0
  476. package/dist/types/runtime/engineEnvironment.d.ts +115 -0
  477. package/dist/types/runtime/loadEnv.d.ts +86 -0
  478. package/dist/types/runtime/pageFacets.d.ts +104 -0
  479. package/dist/types/runtime/pageFilters.d.ts +139 -0
  480. package/dist/types/runtime/pageGuard.d.ts +78 -0
  481. package/dist/types/runtime/pageQuery.d.ts +138 -0
  482. package/dist/types/runtime/pageSort.d.ts +86 -0
  483. package/dist/types/runtime/redact.d.ts +27 -0
  484. package/dist/types/runtime/withTimeout.d.ts +51 -0
  485. package/dist/types/service/cluster/ClusterManager.d.ts +112 -0
  486. package/dist/types/service/cluster/ClusterProbeAggregator.d.ts +68 -0
  487. package/dist/types/service/cluster/ClusterRelay.d.ts +57 -0
  488. package/dist/types/service/cluster/clusterMaster.d.ts +40 -0
  489. package/dist/types/service/cluster/clusterMessage.d.ts +94 -0
  490. package/dist/types/service/cluster/cpuQuota.d.ts +37 -0
  491. package/dist/types/service/cluster/instanceProbe.d.ts +82 -0
  492. package/dist/types/service/cluster/podEnvironment.d.ts +35 -0
  493. package/dist/types/service/cluster/processProbe.d.ts +53 -0
  494. package/dist/types/service/cluster/richProcessProbe.d.ts +77 -0
  495. package/dist/types/service/cluster/topology.d.ts +64 -0
  496. package/dist/types/service/dev/BootReporter.d.ts +67 -0
  497. package/dist/types/service/dev/DevSupervisor.d.ts +117 -0
  498. package/dist/types/service/dev/bootVerdict.d.ts +99 -0
  499. package/dist/types/service/dev/detachedStart.d.ts +164 -0
  500. package/dist/types/service/dev/devProcess.d.ts +638 -0
  501. package/dist/types/service/dev/devProjects.d.ts +121 -0
  502. package/dist/types/service/dev/devStatusReport.d.ts +141 -0
  503. package/dist/types/service/dev/devStop.d.ts +57 -0
  504. package/dist/types/service/fetchService.d.ts +29 -0
  505. package/dist/types/service/gitService.d.ts +24 -0
  506. package/dist/types/syslog/Pdu.d.ts +134 -0
  507. package/dist/types/syslog/Syslog.d.ts +482 -0
  508. package/dist/types/syslog/drivers/ClusterFileLogDriver.d.ts +40 -0
  509. package/dist/types/syslog/drivers/FileLogDriver.d.ts +55 -0
  510. package/dist/types/syslog/drivers/ILogDriver.d.ts +177 -0
  511. package/dist/types/syslog/drivers/LokiLogDriver.d.ts +44 -0
  512. package/dist/types/syslog/drivers/MemoryLogDriver.d.ts +20 -0
  513. package/dist/types/syslog/drivers/OpenSearchLogDriver.d.ts +40 -0
  514. package/dist/types/syslog/drivers/builtinLogDrivers.d.ts +43 -0
  515. package/dist/types/syslog/drivers/filterPdus.d.ts +21 -0
  516. package/dist/types/syslog/drivers/logDriverRegistry.d.ts +100 -0
  517. package/dist/types/syslog/drivers/opensearchShared.d.ts +11 -0
  518. package/dist/types/syslog/drivers/pduFlow.d.ts +53 -0
  519. package/dist/types/syslog/drivers/pduProtocol.d.ts +20 -0
  520. package/dist/types/syslog/httpFetch.d.ts +48 -0
  521. package/dist/types/syslog/logColor.d.ts +59 -0
  522. package/dist/types/syslog/sinks/FileSink.d.ts +78 -0
  523. package/dist/types/syslog/transports/BatchingHttpTransport.d.ts +59 -0
  524. package/dist/types/syslog/transports/ConsoleTransport.d.ts +8 -0
  525. package/dist/types/syslog/transports/FileTransport.d.ts +13 -0
  526. package/dist/types/syslog/transports/HttpTransport.d.ts +15 -0
  527. package/dist/types/syslog/transports/LokiTransport.d.ts +45 -0
  528. package/dist/types/syslog/transports/OpenSearchTransport.d.ts +40 -0
  529. package/dist/types/syslog/transports/SyslogTransport.d.ts +9 -0
  530. package/dist/types/syslog/transports/index.d.ts +12 -0
  531. package/dist/types/testing/index.d.ts +197 -0
  532. package/dist/types/types/IAdminApi.d.ts +246 -0
  533. package/dist/types/types/ICliKernel.d.ts +28 -0
  534. package/dist/types/types/ICommand.d.ts +14 -0
  535. package/dist/types/types/IContainer.d.ts +35 -0
  536. package/dist/types/types/IIdempotencyStore.d.ts +159 -0
  537. package/dist/types/types/IKernel.d.ts +138 -0
  538. package/dist/types/types/IMcpTool.d.ts +157 -0
  539. package/dist/types/types/IModule.d.ts +73 -0
  540. package/dist/types/types/IModuleManifest.d.ts +49 -0
  541. package/dist/types/types/IPage.d.ts +119 -0
  542. package/dist/types/types/IService.d.ts +62 -0
  543. package/dist/types/types/ISyslog.d.ts +58 -0
  544. package/dist/types/types/ITransport.d.ts +5 -0
  545. package/dist/types/types/globals.d.ts +47 -0
  546. package/docs/angular-services.md +318 -0
  547. package/docs/catalogue.md +184 -0
  548. package/docs/cli.md +369 -0
  549. package/docs/client.md +616 -0
  550. package/docs/debugbar.md +218 -0
  551. package/docs/environnement.md +308 -0
  552. package/docs/index.md +388 -0
  553. package/docs/kernel.md +670 -0
  554. package/docs/progression.md +228 -0
  555. package/docs/react-hooks.md +686 -0
  556. package/docs/request-context.md +507 -0
  557. package/docs/service.md +590 -0
  558. package/docs/svelte-reactivite.md +293 -0
  559. package/docs/syslog.md +848 -0
  560. package/docs/testing.md +203 -0
  561. package/docs/vue-composables.md +255 -0
  562. package/man/nodefony.1 +152 -0
  563. package/package.json +163 -83
  564. package/templates/app/agents/AGENTS.md.tpl +967 -0
  565. package/templates/app/agents/POINTEUR.md.tpl +12 -0
  566. package/templates/app/base/Dockerfile.tpl +104 -0
  567. package/templates/app/base/README.md.tpl +314 -0
  568. package/templates/app/base/dockerignore.tpl +28 -0
  569. package/templates/app/base/env.ts.tpl +130 -0
  570. package/templates/app/base/gitattributes.tpl +28 -0
  571. package/templates/app/base/github/workflows/ci.yml.tpl +91 -0
  572. package/templates/app/base/gitignore.tpl +46 -0
  573. package/templates/app/base/gitlab-ci.yml.tpl +42 -0
  574. package/templates/app/base/index.ts.tpl +69 -0
  575. package/templates/app/base/nodefony.config.ts.tpl +342 -0
  576. package/templates/app/base/oxlintrc.json.tpl +127 -0
  577. package/templates/app/base/package.json.tpl +75 -0
  578. package/templates/app/base/prettierignore.tpl +24 -0
  579. package/templates/app/base/prettierrc.json.tpl +12 -0
  580. package/templates/app/base/rolldown.config.ts.tpl +8 -0
  581. package/templates/app/base/tests/config.test.ts.tpl +29 -0
  582. package/templates/app/base/tests/e2e.setup.ts.tpl +181 -0
  583. package/templates/app/base/tests/e2e.test.ts.tpl +105 -0
  584. package/templates/app/base/tests/migrations.e2e.test.ts.tpl +579 -0
  585. package/templates/app/base/tsconfig.json.tpl +47 -0
  586. package/templates/app/base/vitest.config.ts.tpl +88 -0
  587. package/templates/app/base/vitest.e2e.config.ts.tpl +39 -0
  588. package/templates/app/complete/compose.yaml.tpl +328 -0
  589. package/templates/app/complete/deploy/migrate-job.yaml.tpl +100 -0
  590. package/templates/app/complete/docker/db/init-nodefony-e2e.sql.tpl +30 -0
  591. package/templates/app/complete/docker/grafana/provisioning/datasources/loki.yaml.tpl +15 -0
  592. package/templates/app/complete/env.local.tpl +11 -0
  593. package/templates/app/complete/env.tpl +53 -0
  594. package/templates/app/complete/nodefony/entity/User.ts.tpl +69 -0
  595. package/templates/app/complete/nodefony/security/provisionUsers.ts.tpl +124 -0
  596. package/templates/app/complete/nodefony/service/AppBannerService.ts.tpl +98 -0
  597. package/templates/app/complete/nodefony/service/AppInfoService.ts.tpl +85 -0
  598. package/templates/app/frontend/angular/frontend/src/accent.css.tpl +34 -0
  599. package/templates/app/frontend/angular/frontend/src/app/app.component.ts.tpl +400 -0
  600. package/templates/app/frontend/react/frontend/src/App.tsx.tpl +421 -0
  601. package/templates/app/frontend/react/frontend/src/accent.css.tpl +30 -0
  602. package/templates/app/frontend/shared/frontend/src/brand.ts.tpl +3 -0
  603. package/templates/app/frontend/shared/frontend/src/showcase.css.tpl +224 -0
  604. package/templates/app/frontend/shared/nodefony/controllers/AppController.ts.tpl +45 -0
  605. package/templates/app/frontend/svelte/frontend/src/App.svelte.tpl +371 -0
  606. package/templates/app/frontend/svelte/frontend/src/accent.css.tpl +34 -0
  607. package/templates/app/frontend/vue/frontend/src/App.vue.tpl +367 -0
  608. package/templates/app/frontend/vue/frontend/src/accent.css.tpl +34 -0
  609. package/templates/app/home/nodefony/controllers/HomeController.ts.tpl +38 -0
  610. package/templates/command/nodefony/command/__NAME__.ts.tpl +132 -0
  611. package/templates/controller/duplex/nodefony/controllers/__NAME__.ts.tpl +188 -0
  612. package/templates/controller/example/nodefony/controllers/__NAME__.ts.tpl +348 -0
  613. package/templates/controller/hello/nodefony/controllers/__NAME__.ts.tpl +143 -0
  614. package/templates/controller/realtime/nodefony/controllers/__NAME__.ts.tpl +153 -0
  615. package/templates/controller/realtime/tests/__KEBAB__-realtime.test.ts.tpl +79 -0
  616. package/templates/controller/rest/nodefony/controllers/__NAME__.ts.tpl +159 -0
  617. package/templates/entity/base/nodefony/entity/__PASCAL__.schema.ts.tpl +31 -0
  618. package/templates/entity/base/nodefony/entity/__PASCAL__.ts.tpl +68 -0
  619. package/templates/entity/controller/nodefony/controllers/__PASCAL__Controller.ts.tpl +335 -0
  620. package/templates/entity/service/nodefony/service/__PASCAL__Service.ts.tpl +126 -0
  621. package/templates/entity/tests/tests/__KEBAB__.e2e.test.ts.tpl +358 -0
  622. package/templates/entity/tests/tests/__KEBAB__.test.ts.tpl +87 -0
  623. package/templates/front/angular/frontend/src/app/app.component.ts.tpl +30 -0
  624. package/templates/front/base/nodefony/controllers/__NAME__.ts.tpl +60 -0
  625. package/templates/front/react/frontend/src/App.tsx.tpl +26 -0
  626. package/templates/front/svelte/frontend/src/App.svelte.tpl +19 -0
  627. package/templates/front/vue/frontend/src/App.vue.tpl +23 -0
  628. package/templates/module/ai/AGENTS.md.tpl +71 -0
  629. package/templates/module/base/README.md.tpl +62 -0
  630. package/templates/module/base/docs/index.md.tpl +55 -0
  631. package/templates/module/base/index.ts.tpl +50 -0
  632. package/templates/module/base/nodefony/config/config.ts.tpl +60 -0
  633. package/templates/module/base/nodefony/config/defineModuleConfig.ts.tpl +55 -0
  634. package/templates/module/base/nodefony/src/errors/__PASCAL__Error.ts.tpl +20 -0
  635. package/templates/module/base/package.json.tpl +53 -0
  636. package/templates/module/base/rolldown.config.ts.tpl +14 -0
  637. package/templates/module/base/tests/__KEBAB__.test.ts.tpl +39 -0
  638. package/templates/module/base/tsconfig.json.tpl +42 -0
  639. package/templates/module/base/vitest.config.ts.tpl +27 -0
  640. package/templates/module/packages/CLAUDE.md.tpl +51 -0
  641. package/templates/module/packages/MEMORY.md.tpl +39 -0
  642. package/templates/module/packages/tsconfig.declarations.json.tpl +19 -0
  643. package/templates/module/packages/tsconfig.tests.json.tpl +11 -0
  644. package/templates/module/service/nodefony/interfaces/I__PASCAL__Service.ts.tpl +14 -0
  645. package/templates/module/service/nodefony/interfaces/index.ts.tpl +1 -0
  646. package/templates/module/service/nodefony/service/__PASCAL__Service.ts.tpl +116 -0
  647. package/templates/service/nodefony/interfaces/I__PASCAL__Service.ts.tpl +19 -0
  648. package/templates/service/nodefony/service/__PASCAL__Service.ts.tpl +136 -0
  649. package/templates/service/tests/__PASCAL__Service.test.ts.tpl +53 -0
  650. package/templates/shared/front-entry/angular/frontend/src/main.ts.tpl +17 -0
  651. package/templates/shared/front-entry/react/frontend/src/main.tsx.tpl +11 -0
  652. package/templates/shared/front-entry/svelte/frontend/src/main.ts.tpl +13 -0
  653. package/templates/shared/front-entry/vue/frontend/src/main.ts.tpl +12 -0
  654. package/templates/shared/front-registrar/nodefony/frontend/register__PASCAL__Entry.ts.tpl +40 -0
  655. package/templates/shared/front-shell/frontend/index.html.tpl +39 -0
  656. package/templates/shared/ng-app-tsconfig/frontend/tsconfig.app.json.tpl +18 -0
  657. package/templates/shared/svelte-shim/frontend/src/env.d.ts.tpl +12 -0
  658. package/templates/shared/vue-shim/frontend/src/env.d.ts.tpl +15 -0
  659. package/.yarnclean +0 -45
  660. package/CHANGELOG.md +0 -406
  661. package/autoloader.es6 +0 -253
  662. package/builder.es6 +0 -366
  663. package/cli/builder/bundles/bundle.js +0 -328
  664. package/cli/builder/bundles/controller.js +0 -73
  665. package/cli/builder/bundles/users-bundle/.yarnclean +0 -45
  666. package/cli/builder/bundles/users-bundle/Command/fixtureTask.js +0 -47
  667. package/cli/builder/bundles/users-bundle/Command/usersCommand.js +0 -105
  668. package/cli/builder/bundles/users-bundle/Entity/mongoose/userEntity.js +0 -168
  669. package/cli/builder/bundles/users-bundle/Entity/sequelize/userEntity.js +0 -231
  670. package/cli/builder/bundles/users-bundle/Fixtures/users.js +0 -117
  671. package/cli/builder/bundles/users-bundle/Fixtures/usersFixtures.js +0 -114
  672. package/cli/builder/bundles/users-bundle/Resources/config/config.js +0 -90
  673. package/cli/builder/bundles/users-bundle/Resources/config/routing.js +0 -17
  674. package/cli/builder/bundles/users-bundle/Resources/config/security.js +0 -96
  675. package/cli/builder/bundles/users-bundle/Resources/config/services.js +0 -7
  676. package/cli/builder/bundles/users-bundle/Resources/config/webpack/webpack.dev.config.js +0 -13
  677. package/cli/builder/bundles/users-bundle/Resources/config/webpack/webpack.prod.config.js +0 -31
  678. package/cli/builder/bundles/users-bundle/Resources/config/webpack.config.js +0 -146
  679. package/cli/builder/bundles/users-bundle/Resources/js/users.js +0 -153
  680. package/cli/builder/bundles/users-bundle/Resources/public/favicon.ico +0 -0
  681. package/cli/builder/bundles/users-bundle/Resources/public/images/users-logo.png +0 -0
  682. package/cli/builder/bundles/users-bundle/Resources/scss/awesome/font-awesome.config.js +0 -11
  683. package/cli/builder/bundles/users-bundle/Resources/scss/custom.scss +0 -23
  684. package/cli/builder/bundles/users-bundle/Resources/scss/users.scss +0 -59
  685. package/cli/builder/bundles/users-bundle/Resources/swagger/openapi/login.js +0 -235
  686. package/cli/builder/bundles/users-bundle/Resources/swagger/openapi/users.js +0 -232
  687. package/cli/builder/bundles/users-bundle/Resources/translations/login.en_en.yml +0 -14
  688. package/cli/builder/bundles/users-bundle/Resources/translations/login.fr_fr.yml +0 -14
  689. package/cli/builder/bundles/users-bundle/Resources/translations/users.en_en.yml +0 -35
  690. package/cli/builder/bundles/users-bundle/Resources/translations/users.fr_fr.yml +0 -36
  691. package/cli/builder/bundles/users-bundle/Resources/views/base.html.twig +0 -32
  692. package/cli/builder/bundles/users-bundle/Resources/views/footer.html.twig +0 -7
  693. package/cli/builder/bundles/users-bundle/Resources/views/header.html.twig +0 -90
  694. package/cli/builder/bundles/users-bundle/Resources/views/login/login.html.twig +0 -43
  695. package/cli/builder/bundles/users-bundle/Resources/views/users/createUser.html.twig +0 -296
  696. package/cli/builder/bundles/users-bundle/Resources/views/users/readUsers.html.twig +0 -94
  697. package/cli/builder/bundles/users-bundle/controller/api/graphql/graphqlController.js +0 -56
  698. package/cli/builder/bundles/users-bundle/controller/api/graphql/userResolver.js +0 -121
  699. package/cli/builder/bundles/users-bundle/controller/api/graphql/usertype.js +0 -58
  700. package/cli/builder/bundles/users-bundle/controller/api/openapi/loginApiController.js +0 -210
  701. package/cli/builder/bundles/users-bundle/controller/api/openapi/restUserController.js +0 -244
  702. package/cli/builder/bundles/users-bundle/controller/loginController.js +0 -91
  703. package/cli/builder/bundles/users-bundle/controller/usersController.js +0 -332
  704. package/cli/builder/bundles/users-bundle/package.json +0 -58
  705. package/cli/builder/bundles/users-bundle/readme.md +0 -360
  706. package/cli/builder/bundles/users-bundle/services/usersService.js +0 -411
  707. package/cli/builder/bundles/users-bundle/src/providers/mongoose/userProvider.js +0 -47
  708. package/cli/builder/bundles/users-bundle/src/providers/sequelize/userProvider.js +0 -48
  709. package/cli/builder/bundles/users-bundle/tests/loginTest.js +0 -182
  710. package/cli/builder/bundles/users-bundle/usersBundle.js +0 -26
  711. package/cli/builder/microService/microService.js +0 -261
  712. package/cli/builder/microService/skeletons/.editorconfig +0 -18
  713. package/cli/builder/microService/skeletons/.env-cmdrc.js +0 -32
  714. package/cli/builder/microService/skeletons/.jshintrc +0 -37
  715. package/cli/builder/microService/skeletons/README.md +0 -117
  716. package/cli/builder/microService/skeletons/bin/bash/hello.sh +0 -5
  717. package/cli/builder/microService/skeletons/bin/cli +0 -13
  718. package/cli/builder/microService/skeletons/bin/python/hello.py +0 -20
  719. package/cli/builder/microService/skeletons/config/config.js +0 -29
  720. package/cli/builder/microService/skeletons/config/pm2.config.js +0 -72
  721. package/cli/builder/microService/skeletons/config/webpack/webpack.config.dev.js +0 -8
  722. package/cli/builder/microService/skeletons/config/webpack/webpack.config.prod.js +0 -25
  723. package/cli/builder/microService/skeletons/config/webpack.config.js +0 -114
  724. package/cli/builder/microService/skeletons/package.json +0 -76
  725. package/cli/builder/microService/skeletons/src/browser/index.css +0 -3
  726. package/cli/builder/microService/skeletons/src/browser/index.js +0 -16
  727. package/cli/builder/microService/skeletons/src/browser/socketio.js +0 -46
  728. package/cli/builder/microService/skeletons/src/cli/cli.js +0 -224
  729. package/cli/builder/microService/skeletons/src/cli/menu.js +0 -28
  730. package/cli/builder/microService/skeletons/src/n-api/README.md +0 -1
  731. package/cli/builder/microService/skeletons/src/n-api/binding.gyp +0 -14
  732. package/cli/builder/microService/skeletons/src/n-api/hello.cc +0 -14
  733. package/cli/builder/microService/skeletons/src/n-api/hello.js +0 -23
  734. package/cli/builder/microService/skeletons/src/n-api/package.json +0 -15
  735. package/cli/builder/microService/skeletons/src/node/examples/index.js +0 -120
  736. package/cli/builder/microService/skeletons/src/node/index.js +0 -102
  737. package/cli/builder/microService/skeletons/src/node/services/markdown/markdown.js +0 -27
  738. package/cli/builder/microService/skeletons/src/node/services/servers/http.js +0 -45
  739. package/cli/builder/microService/skeletons/src/node/services/servers/https.js +0 -44
  740. package/cli/builder/microService/skeletons/src/node/services/socketio/socketio.js +0 -46
  741. package/cli/builder/microService/skeletons/src/node/services/syscall/syscall.js +0 -111
  742. package/cli/builder/microService/skeletons/src/node/services/worker/thread.js +0 -59
  743. package/cli/builder/microService/skeletons/src/node/services/worker/worker.js +0 -60
  744. package/cli/builder/microService/skeletons/src/templates/base.html +0 -12
  745. package/cli/builder/microService/skeletons/src/templates/socket.html +0 -23
  746. package/cli/builder/microService/skeletons/tests/unit/serviceTest.js +0 -30
  747. package/cli/builder/project/project.js +0 -474
  748. package/cli/builder/project/skeletons/README.md +0 -778
  749. package/cli/builder/project/skeletons/bin/dev-deploy.sh +0 -5
  750. package/cli/builder/project/skeletons/bin/generateCertificates.sh.skeleton +0 -180
  751. package/cli/builder/project/skeletons/bin/prod-deploy.sh +0 -3
  752. package/cli/builder/project/skeletons/config/config.js.skeleton +0 -178
  753. package/cli/builder/project/skeletons/config/openssl/ca/openssl.cnf.skeleton +0 -150
  754. package/cli/builder/project/skeletons/config/openssl/ca_intermediate/openssl.cnf.skeleton +0 -152
  755. package/cli/builder/project/skeletons/config/pm2.config.js.skeleton +0 -62
  756. package/cli/builder/project/skeletons/documentation.html.twig +0 -24
  757. package/cli/builder/project/skeletons/editorconfig.skeleton +0 -18
  758. package/cli/builder/project/skeletons/eslintignore.skeleton +0 -15
  759. package/cli/builder/project/skeletons/eslintrc.js.skeleton +0 -97
  760. package/cli/builder/project/skeletons/gitignore.skeleton +0 -32
  761. package/cli/builder/project/skeletons/migrations/2022.12.25T17.37.37.entity-user.js +0 -137
  762. package/cli/builder/project/skeletons/migrations/2022.12.25T17.37.38.entity-session.js +0 -97
  763. package/cli/builder/project/skeletons/migrations/2022.12.25T17.37.39.entity-requests.js +0 -115
  764. package/cli/builder/project/skeletons/migrations/2022.12.25T17.37.40.entity-jwts.js +0 -97
  765. package/cli/builder/project/skeletons/migrations/migrations.skeleton.js +0 -101
  766. package/cli/builder/project/skeletons/package.json.twig +0 -71
  767. package/cli/builder/react/reactBuilder.js +0 -224
  768. package/cli/builder/react/skeletons/app/Resources/databases/nodefony.db +0 -0
  769. package/cli/builder/react/skeletons/app/Resources/translations/messages.en_en.yml +0 -5
  770. package/cli/builder/react/skeletons/app/Resources/translations/messages.fr_fr.yml +0 -5
  771. package/cli/builder/react/skeletons/app/Resources/views/base.html.twig +0 -33
  772. package/cli/builder/react/skeletons/app/config/config.js +0 -392
  773. package/cli/builder/react/skeletons/app/config/routing.js +0 -10
  774. package/cli/builder/react/skeletons/app/config/security.js +0 -74
  775. package/cli/builder/react/skeletons/app/controller/appController.js +0 -22
  776. package/cli/builder/sandbox/sandBoxBuilder.js +0 -610
  777. package/cli/builder/sandbox/skeletons/Resources/css/entry.css +0 -48
  778. package/cli/builder/sandbox/skeletons/Resources/js/entry.js +0 -60
  779. package/cli/builder/sandbox/skeletons/Resources/public/manifest.json +0 -32
  780. package/cli/builder/sandbox/skeletons/Resources/translations/messages.en_en.yml +0 -5
  781. package/cli/builder/sandbox/skeletons/Resources/translations/messages.fr_fr.yml +0 -5
  782. package/cli/builder/sandbox/skeletons/Resources/views/base.html.twig +0 -46
  783. package/cli/builder/sandbox/skeletons/Resources/views/index.html.twig +0 -34
  784. package/cli/builder/sandbox/skeletons/bootstrap/custom.scss +0 -27
  785. package/cli/builder/sandbox/skeletons/bootstrap/entry.scss +0 -60
  786. package/cli/builder/sandbox/skeletons/workbox/templates/index.html.twig +0 -32
  787. package/cli/builder/sandbox/skeletons/workbox/workers/service-worker.js +0 -196
  788. package/cli/builder/skeletons/Resources/public/favicon.ico +0 -0
  789. package/cli/builder/skeletons/Resources/public/images/app-logo.png +0 -0
  790. package/cli/builder/skeletons/Resources/views/framework-bundle/views/401.html.twig +0 -1
  791. package/cli/builder/skeletons/Resources/views/framework-bundle/views/401.json.twig +0 -24
  792. package/cli/builder/skeletons/Resources/views/framework-bundle/views/403.html.twig +0 -1
  793. package/cli/builder/skeletons/Resources/views/framework-bundle/views/403.json.twig +0 -20
  794. package/cli/builder/skeletons/Resources/views/framework-bundle/views/404.html.twig +0 -1
  795. package/cli/builder/skeletons/Resources/views/framework-bundle/views/404.json.twig +0 -20
  796. package/cli/builder/skeletons/Resources/views/framework-bundle/views/500.html.twig +0 -1
  797. package/cli/builder/skeletons/Resources/views/framework-bundle/views/base.html.twig +0 -30
  798. package/cli/builder/skeletons/Resources/views/framework-bundle/views/exception.html.twig +0 -1
  799. package/cli/builder/skeletons/Resources/views/framework-bundle/views/exception.json.twig +0 -24
  800. package/cli/builder/skeletons/Resources/views/framework-bundle/views/footer.html.twig +0 -33
  801. package/cli/builder/skeletons/Resources/views/framework-bundle/views/header.html.twig +0 -6
  802. package/cli/builder/skeletons/Resources/views/framework-bundle/views/index.html.twig +0 -6
  803. package/cli/builder/skeletons/Resources/views/framework-bundle/views/index.json.twig +0 -1
  804. package/cli/builder/skeletons/Resources/views/framework-bundle/views/layout.html.twig +0 -125
  805. package/cli/builder/skeletons/Resources/views/framework-bundle/views/layout.json.twig +0 -5
  806. package/cli/builder/skeletons/Resources/views/framework-bundle/views/timeout.html.twig +0 -1
  807. package/cli/builder/skeletons/Resources/views/framework-bundle/views/timeout.json.twig +0 -24
  808. package/cli/builder/skeletons/app/appKernel.js +0 -133
  809. package/cli/builder/skeletons/binding/binding.cc +0 -26
  810. package/cli/builder/skeletons/binding/binding.skeleton +0 -8
  811. package/cli/builder/skeletons/bundle/bundleClass.js +0 -37
  812. package/cli/builder/skeletons/command/commandClass.js +0 -11
  813. package/cli/builder/skeletons/command/taskClass.js +0 -20
  814. package/cli/builder/skeletons/config/config.js +0 -138
  815. package/cli/builder/skeletons/config/nodefony/elastic-bundle.js +0 -37
  816. package/cli/builder/skeletons/config/nodefony/framework-bundle.js +0 -22
  817. package/cli/builder/skeletons/config/nodefony/http-bundle.js +0 -104
  818. package/cli/builder/skeletons/config/nodefony/mail-bundle.js +0 -44
  819. package/cli/builder/skeletons/config/nodefony/mongoose-bundle.js +0 -51
  820. package/cli/builder/skeletons/config/nodefony/monitoring-bundle.js +0 -34
  821. package/cli/builder/skeletons/config/nodefony/realtime-bundle.js +0 -15
  822. package/cli/builder/skeletons/config/nodefony/redis-bundle.js +0 -44
  823. package/cli/builder/skeletons/config/nodefony/security-bundle.js +0 -25
  824. package/cli/builder/skeletons/config/nodefony/sequelize-bundle.js +0 -156
  825. package/cli/builder/skeletons/config/routing.js +0 -55
  826. package/cli/builder/skeletons/config/security.js +0 -102
  827. package/cli/builder/skeletons/config/services.js +0 -1
  828. package/cli/builder/skeletons/config/webpack/webpack.dev.config.js +0 -13
  829. package/cli/builder/skeletons/config/webpack/webpack.prod.config.js +0 -20
  830. package/cli/builder/skeletons/config/webpack.config.js +0 -148
  831. package/cli/builder/skeletons/controller/controllerClass.js +0 -75
  832. package/cli/builder/skeletons/package.json.twig +0 -57
  833. package/cli/builder/skeletons/unittest/testFile.js +0 -46
  834. package/cli/builder/vue/skeletons/Resources/public/images/app-logo.png +0 -0
  835. package/cli/builder/vue/skeletons/Resources/translations/messages.en_en.yml +0 -5
  836. package/cli/builder/vue/skeletons/Resources/translations/messages.fr_fr.yml +0 -5
  837. package/cli/builder/vue/skeletons/vue.config.js +0 -129
  838. package/cli/builder/vue/vueBuilder.js +0 -410
  839. package/cli/generate/generate.es6 +0 -103
  840. package/cli/install/install.js +0 -190
  841. package/cli/sequelize/sequelize.js +0 -94
  842. package/cli/start.js +0 -702
  843. package/cli/tools/pm2.es6 +0 -112
  844. package/cli/tools/tools.es6 +0 -97
  845. package/cli.es6 +0 -925
  846. package/container.es6 +0 -272
  847. package/error.es6 +0 -293
  848. package/fileClass.es6 +0 -242
  849. package/finder/file.es6 +0 -33
  850. package/finder/fileResult.es6 +0 -138
  851. package/finder/finder2.es6 +0 -275
  852. package/finder.es6 +0 -427
  853. package/kernel/annotations/annotations.es6 +0 -286
  854. package/kernel/api/api.es6 +0 -61
  855. package/kernel/api/graphqlApi.es6 +0 -107
  856. package/kernel/api/jsonApi.es6 +0 -177
  857. package/kernel/api/openApi.es6 +0 -139
  858. package/kernel/api/schemas/openApiSchema.js +0 -394
  859. package/kernel/babylon.es6 +0 -162
  860. package/kernel/bundle.es6 +0 -1200
  861. package/kernel/cliKernel.es6 +0 -869
  862. package/kernel/command.es6 +0 -78
  863. package/kernel/commands/nodefonyCommand.es6 +0 -247
  864. package/kernel/commands/tasks/bundlesTask.es6 +0 -59
  865. package/kernel/controllers/controller.es6 +0 -791
  866. package/kernel/fixture.es6 +0 -19
  867. package/kernel/injections/injections.es6 +0 -424
  868. package/kernel/kernel.es6 +0 -1960
  869. package/kernel/kernelWatcher.es6 +0 -387
  870. package/kernel/orm/entity.es6 +0 -59
  871. package/kernel/orm/orm.es6 +0 -139
  872. package/kernel/reader.es6 +0 -290
  873. package/kernel/security/encoder.es6 +0 -17
  874. package/kernel/security/factories/factory.es6 +0 -111
  875. package/kernel/security/factories/passeportFactory.es6 +0 -61
  876. package/kernel/security/provider.es6 +0 -58
  877. package/kernel/security/providers/chainProvider.es6 +0 -99
  878. package/kernel/security/providers/memoryProvider.es6 +0 -55
  879. package/kernel/security/providers/providerManager.es6 +0 -67
  880. package/kernel/security/providers/userEntityProvider.es6 +0 -50
  881. package/kernel/security/role.es6 +0 -12
  882. package/kernel/security/secureArea.es6 +0 -431
  883. package/kernel/security/tokens/token.es6 +0 -171
  884. package/kernel/security/user.es6 +0 -106
  885. package/kernel/serviceRealTime.es6 +0 -21
  886. package/kernel/services/connectionsService.es6 +0 -93
  887. package/kernel/services/cron/cronService.es6 +0 -123
  888. package/kernel/task.es6 +0 -80
  889. package/kernel/templates.es6 +0 -22
  890. package/kernel/templating/twig.es6 +0 -218
  891. package/kernel/tests/cliTest.js +0 -59
  892. package/kernel/tests/containerTest.js +0 -232
  893. package/kernel/tests/kernelTest.js +0 -39
  894. package/nodefony.es6 +0 -1084
  895. package/notificationsCenter.es6 +0 -118
  896. package/protocol.es6 +0 -86
  897. package/protocols/bayeux.es6 +0 -174
  898. package/protocols/jsonRpc/jsonrpc.es6 +0 -56
  899. package/result.es6 +0 -99
  900. package/service.es6 +0 -288
  901. package/syslog/pdu.es6 +0 -141
  902. package/syslog/syslog.es6 +0 -754
  903. package/watcher.es6 +0 -113
@@ -0,0 +1,2156 @@
1
+ import { findReservedEntity } from "./reservedEntities.js";
2
+ import { findProjectRoot } from "../projectRoot.js";
3
+ import { envFileOrder } from "../../runtime/loadEnv.js";
4
+ import { pointeursInstructions } from "../agentTargets.js";
5
+ import { resolveInfra } from "../../config/infra.js";
6
+ import { resolveModuleLayout } from "./moduleLayout.js";
7
+ import { COLUMN_CASES, ENTITY_DIALECTS, ENTITY_ID_KINDS, buildEntityCodegen, describeColumnTypes, parseEntityFields, parseEntityIndexes, toSnakeCase } from "./entityFields.js";
8
+ import { assertUserFieldsAreFillable, readUserContract, userContractFields, userTableName } from "./userContractSource.js";
9
+ import { SCAFFOLD_VERSIONS, pick } from "./versions.js";
10
+ import { formatScaffoldOutput } from "./format.js";
11
+ import { ScaffoldWriter } from "./writer.js";
12
+ import { CONTROLLER_KIND_CHOICES, getScaffoldSpec } from "./spec.js";
13
+ import { randomBytes } from "node:crypto";
14
+ import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
15
+ import path from "node:path";
16
+ import { fileURLToPath } from "node:url";
17
+ import { Eta } from "eta";
18
+ //#region src/cli/scaffold/engine.ts
19
+ /**
20
+ * Options du moteur de gabarits, **écrites une seule fois** : sept rendus les
21
+ * instanciaient à l'identique, et une divergence entre deux d'entre eux serait
22
+ * passée inaperçue jusqu'au fichier produit.
23
+ *
24
+ * - `autoEscape: false` — on génère du CODE, pas du HTML : échapper les entités
25
+ * corromprait chaque fichier.
26
+ * - `useWith: false` + `varName: "it"` — accès strict par `it.`, aucune variable
27
+ * implicite.
28
+ * - `autoTrim: false` — **le défaut d'eta AVALE la ligne vide qui suit une
29
+ * balise en fin de ligne.** Un `# <%= it.appName %>` suivi d'une ligne vide et
30
+ * d'un paragraphe rendait le titre COLLÉ au paragraphe ; et en markdown, un
31
+ * paragraphe qui suit une liste sans ligne vide devient une continuation de la
32
+ * dernière puce. Le rendu n'était donc pas seulement mal formaté, il était mal
33
+ * STRUCTURÉ — et le formateur que l'application embarque le refusait. Ce que
34
+ * le gabarit écrit est ce qui sort.
35
+ */
36
+ const ETA_OPTIONS = {
37
+ useWith: false,
38
+ varName: "it",
39
+ autoEscape: false,
40
+ autoTrim: false
41
+ };
42
+ /** Fichiers dont le nom rendu diffère du template (npm strip les dotfiles publiés). */
43
+ const RENAMES = {
44
+ gitignore: ".gitignore",
45
+ dockerignore: ".dockerignore",
46
+ prettierignore: ".prettierignore",
47
+ gitattributes: ".gitattributes",
48
+ "oxlintrc.json": ".oxlintrc.json",
49
+ "prettierrc.json": ".prettierrc.json",
50
+ "gitlab-ci.yml": ".gitlab-ci.yml",
51
+ env: ".env",
52
+ "env.local": ".env.local"
53
+ };
54
+ /**
55
+ * Répertoires renommés au rendu — même raison que `RENAMES` (npm strip aussi
56
+ * les DOSSIERS pointés publiés : un `templates/…/.github/` n'arriverait jamais
57
+ * chez l'installeur). Seul le PREMIER segment du chemin relatif est mappé.
58
+ */
59
+ const DIR_RENAMES = { github: ".github" };
60
+ /**
61
+ * Paramètres frontend par framework — type registerEntry, entry, nœud de
62
+ * montage, ET les dépendances npm (SOURCE UNIQUE : consommée par le
63
+ * `package.json.tpl` de `create app` ET par `create front` qui les ajoute au
64
+ * package.json d'une cible existante — une version ne vit qu'ici).
65
+ *
66
+ * ⚠️ **Le framework front lui-même est une devDependency, et `deps` est vide.**
67
+ * Le test qui tranche est le même que pour la toolchain : *le paquet est-il
68
+ * importé au RUNTIME du serveur ?* Ici non — aucun fichier hors `frontend/`
69
+ * n'importe `react`/`vue`/`@angular/*` : Vite les inline dans le bundle
70
+ * navigateur, et c'est ce bundle que la production sert. Les déclarer en
71
+ * `dependencies` les faisait survivre au `npm prune --omit=dev` de l'image et
72
+ * embarquer un framework entier que rien ne charge (mesuré ailleurs : 26,6 Mo
73
+ * pour `vue` seul, y compris dans une app React).
74
+ *
75
+ * L'ordre du `Dockerfile` généré est ce qui rend la bascule sûre :
76
+ * `RUN npm run build && npm prune --omit=dev` — le build les a quand il en a
77
+ * besoin, le prune les retire ensuite. Toute recette qui élaguerait AVANT de
78
+ * construire casserait, et c'est vrai de n'importe quelle devDependency.
79
+ *
80
+ * `deps` reste dans le contrat : le jour où un rendu côté serveur importera
81
+ * vraiment le framework, c'est là que la déclaration devra revenir.
82
+ */
83
+ const FRONTEND_PARAMS = {
84
+ react: {
85
+ type: "react19",
86
+ entry: "./frontend/src/main.tsx",
87
+ mountNode: "<div id=\"root\"></div>",
88
+ deps: {},
89
+ devDeps: pick("react", "react-dom", "vite", "@vitejs/plugin-react", "@types/react", "@types/react-dom")
90
+ },
91
+ vue: {
92
+ type: "vue3",
93
+ entry: "./frontend/src/main.ts",
94
+ mountNode: "<div id=\"app\"></div>",
95
+ deps: {},
96
+ devDeps: pick("vue", "vite", "@vitejs/plugin-vue")
97
+ },
98
+ angular: {
99
+ type: "angular",
100
+ entry: "./frontend/src/main.ts",
101
+ mountNode: "<app-root></app-root>",
102
+ deps: {},
103
+ devDeps: pick("@angular/core", "@angular/common", "@angular/platform-browser", "vite", "@analogjs/vite-plugin-angular", "@angular/build", "@angular/compiler-cli")
104
+ },
105
+ svelte: {
106
+ type: "svelte5",
107
+ entry: "./frontend/src/main.ts",
108
+ mountNode: "<div id=\"app\"></div>",
109
+ deps: {},
110
+ devDeps: pick("svelte", "vite", "@sveltejs/vite-plugin-svelte")
111
+ }
112
+ };
113
+ /**
114
+ * Paramètres de la base SQL retenue au scaffold — SOURCE UNIQUE du service
115
+ * docker, du port publié et de l'URL de connexion, consommée par les TROIS
116
+ * gabarits qui en parlent (`compose.yaml`, `.env`, `README.md`).
117
+ *
118
+ * Pourquoi ici plutôt que recopié en eta : ces valeurs doivent coïncider EXACTEMENT
119
+ * — l'URL du `.env` joint le port que le compose publie, et le README montre la
120
+ * même. Trois littéraux séparés divergent au premier réglage, et l'écart ne se
121
+ * voit qu'à la connexion refusée.
122
+ *
123
+ * ⚠️ **MySQL est publié sur 3306, pas 3307.** Le port décalé n'existait que pour
124
+ * faire cohabiter MySQL et MariaDB dans un compose qui portait les deux ; une
125
+ * app qui retient UN dialecte n'a plus rien avec quoi cohabiter, et un port
126
+ * inattendu est une friction gratuite.
127
+ */
128
+ const DATABASE_PARAMS = {
129
+ postgres: {
130
+ service: "postgres",
131
+ label: "PostgreSQL 16",
132
+ scheme: "postgres",
133
+ port: 5432,
134
+ image: "postgres:16-alpine"
135
+ },
136
+ mariadb: {
137
+ service: "mariadb",
138
+ label: "MariaDB 11.4",
139
+ scheme: "mysql",
140
+ port: 3306,
141
+ image: "mariadb:11.4"
142
+ },
143
+ mysql: {
144
+ service: "mysql",
145
+ label: "MySQL 8.4",
146
+ scheme: "mysql",
147
+ port: 3306,
148
+ image: "mysql:8.4"
149
+ }
150
+ };
151
+ /**
152
+ * Compose le bloc `db` offert aux gabarits — `null` pour sqlite, qui n'a ni
153
+ * service ni URL (le fichier vit dans `var/databases/`, l'ORM s'en charge seul).
154
+ */
155
+ function resolveDatabase(choice, appName) {
156
+ if (choice === "sqlite") return null;
157
+ const params = DATABASE_PARAMS[choice];
158
+ const dsn = (database) => `${params.scheme}://${appName}:${appName}-dev@127.0.0.1:${params.port}/${database}`;
159
+ return {
160
+ choice,
161
+ ...params,
162
+ database: appName,
163
+ databaseE2e: `${appName}_e2e`,
164
+ databaseScratch: `${appName}_e2e_scratch`,
165
+ url: dsn(appName),
166
+ urlE2e: dsn(`${appName}_e2e`),
167
+ urlScratch: dsn(`${appName}_e2e_scratch`)
168
+ };
169
+ }
170
+ /**
171
+ * Racine du paquet `nodefony` contenant `templates/` — remontée depuis CE fichier
172
+ * (source `src/cli/scaffold/`, bundle `dist/node/cli/scaffold/` : la remontée
173
+ * marche pour les deux sans dépendre de la profondeur).
174
+ */
175
+ function findPackageRoot() {
176
+ let dir = path.dirname(fileURLToPath(import.meta.url));
177
+ for (let i = 0; i < 7; i++) {
178
+ const manifest = path.join(dir, "package.json");
179
+ if (existsSync(manifest)) try {
180
+ if (JSON.parse(readFileSync(manifest, "utf8")).name === "nodefony") return dir;
181
+ } catch {}
182
+ const parent = path.dirname(dir);
183
+ if (parent === dir) break;
184
+ dir = parent;
185
+ }
186
+ throw new Error("paquet nodefony introuvable (templates/)");
187
+ }
188
+ /**
189
+ * Workspaces nodefony d'un CHECKOUT du repo (`src/nodefony` + `src/packages/@nodefony/*`),
190
+ * résolus depuis la racine du paquet `nodefony`. Un paquet INSTALLÉ (node_modules)
191
+ * n'a pas ce voisinage → `null` (le mode link n'a de sens que sur un checkout).
192
+ */
193
+ function resolveLocalWorkspaces(packageRoot) {
194
+ const packagesDir = path.resolve(packageRoot, "..", "packages", "@nodefony");
195
+ if (!existsSync(packagesDir)) return null;
196
+ const map = { nodefony: packageRoot };
197
+ for (const entry of readdirSync(packagesDir, { withFileTypes: true })) if (entry.isDirectory()) map[`@nodefony/${entry.name}`] = path.join(packagesDir, entry.name);
198
+ return map;
199
+ }
200
+ /**
201
+ * Mode link : réécrit dans le `package.json` GÉNÉRÉ toute dep `nodefony` /
202
+ * `@nodefony/*` en `file:<workspace>` — `npm install` symlinke le checkout local
203
+ * et installe ses deps transitives. Rend l'app contrôlable AVANT toute release
204
+ * npm (dev du framework) ; les deps publiques (zod, react…) restent au registre.
205
+ *
206
+ * @returns les noms de paquets liés (triés, pour le récap)
207
+ * @throws si une dep du scope nodefony n'existe pas dans le checkout
208
+ */
209
+ function linkLocalDeps(destDir, workspaces, writer) {
210
+ const manifestPath = path.join(destDir, "package.json");
211
+ const manifest = JSON.parse(writer.read(manifestPath));
212
+ const linked = [];
213
+ for (const block of ["dependencies", "devDependencies"]) {
214
+ const deps = manifest[block];
215
+ if (!deps) continue;
216
+ for (const name of Object.keys(deps)) {
217
+ if (name !== "nodefony" && !name.startsWith("@nodefony/")) continue;
218
+ const workspace = workspaces[name];
219
+ if (!workspace) throw new Error(`link : workspace introuvable pour ${name}`);
220
+ deps[name] = `file:${workspace.split(path.sep).join("/")}`;
221
+ linked.push(name);
222
+ }
223
+ }
224
+ writer.write(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
225
+ return linked.sort();
226
+ }
227
+ /**
228
+ * Complète et VALIDE des réponses partielles contre la spec d'un type : défauts
229
+ * appliqués, patterns vérifiés, choix contraints à leur liste. Le contexte des
230
+ * templates est donc complet PAR CONSTRUCTION (eta ne rend jamais un `undefined`
231
+ * silencieux). Les questions `askIf` non satisfaites sont forcées à `false` ;
232
+ * celles dont l'`askWhen` n'est pas satisfait retombent à leur DÉFAUT — une
233
+ * réponse qui porte sur ce qui ne sera pas généré n'a rien à décider.
234
+ *
235
+ * L'ordre de la spec fait foi : une question `askWhen` lit la réponse DÉJÀ
236
+ * résolue de celle qu'elle désigne, donc cette dernière doit la précéder.
237
+ *
238
+ * @throws Error si une valeur viole la spec (message = libellé + attendu)
239
+ */
240
+ function resolveAnswers(spec, partial, caps) {
241
+ const answers = {};
242
+ for (const q of spec.questions) {
243
+ let value = partial[q.key];
244
+ if (q.askIf === "hasCheckout" && !caps.hasCheckout) value = false;
245
+ if (q.askWhen && String(answers[q.askWhen.key]) !== q.askWhen.equals) value = void 0;
246
+ if (value === void 0) value = q.default;
247
+ if (q.type === "boolean") {
248
+ answers[q.key] = value === true || value === "true";
249
+ continue;
250
+ }
251
+ if (q.type === "list") {
252
+ answers[q.key] = Array.isArray(value) ? value.map(String).filter((v) => v.length > 0) : typeof value === "string" && value.length > 0 ? [value] : [];
253
+ continue;
254
+ }
255
+ const str = String(value);
256
+ if (q.type === "choice") {
257
+ if (!q.choices?.some((c) => c.value === str)) {
258
+ const allowed = q.choices?.map((c) => c.value).join(" | ") ?? "";
259
+ throw new Error(`${q.key} invalide « ${str} » — attendu : ${allowed}`);
260
+ }
261
+ }
262
+ if (q.pattern && !new RegExp(q.pattern, "u").test(str)) throw new Error(`${q.key} invalide « ${str} » — ${q.patternHint ?? q.pattern}`);
263
+ answers[q.key] = str;
264
+ }
265
+ return answers;
266
+ }
267
+ /**
268
+ * Rend un layer de templates (`*.tpl`) dans `destDir` via eta. Les noms de
269
+ * fichiers peuvent porter des TOKENS (`__NAME__Controller.ts.tpl`) remplacés
270
+ * par `tokens` — un template ne peut pas mettre de tag eta dans un nom de
271
+ * fichier, le token est l'équivalent côté chemin.
272
+ */
273
+ function renderLayer(eta, srcDir, destDir, data, written, writer, tokens) {
274
+ const entries = readdirSync(srcDir, {
275
+ withFileTypes: true,
276
+ recursive: true
277
+ });
278
+ for (const entry of entries) {
279
+ if (!entry.isFile() || !entry.name.endsWith(".tpl")) continue;
280
+ const abs = path.join(entry.parentPath, entry.name);
281
+ let relDir = path.relative(srcDir, entry.parentPath);
282
+ const segments = relDir.split(path.sep);
283
+ const mapped = segments[0] !== void 0 && DIR_RENAMES[segments[0]];
284
+ if (mapped) relDir = path.join(mapped, ...segments.slice(1));
285
+ const base = entry.name.replace(/\.tpl$/u, "");
286
+ let rel = path.join(relDir, RENAMES[base] ?? base);
287
+ for (const [token, value] of Object.entries(tokens ?? {})) rel = rel.replaceAll(token, value);
288
+ const rendered = eta.renderString(readFileSync(abs, "utf8"), data);
289
+ if (rendered.includes("<%")) throw new Error(`tag eta résiduel dans ${rel}`);
290
+ if (rendered.trim() === "") continue;
291
+ writer.write(path.join(destDir, rel), rendered);
292
+ written.push(rel);
293
+ }
294
+ }
295
+ /** Marqueurs de la zone préservée d'`AGENTS.md` — le contrat du merge borné. */
296
+ const APP_NOTES_START = "<!-- app-notes:start -->";
297
+ const APP_NOTES_END = "<!-- app-notes:end -->";
298
+ /**
299
+ * Réinjecte la zone `app-notes` du contenu précédent dans le rendu neuf.
300
+ *
301
+ * C'est TOUT le merge que le scaffold sait faire, et c'est voulu : `AGENTS.md`
302
+ * est 100 % dérivé (réécrit en entier à chaque régénération — il ne peut pas
303
+ * mentir), SAUF cette zone, où l'humain et l'agent accumulent les leçons
304
+ * propres à l'app. Un marqueur absent ou inversé (fichier retravaillé à la
305
+ * main) → le rendu neuf part tel quel : mieux vaut perdre des notes déplacées
306
+ * qu'écrire un fichier recousu de façon imprévisible.
307
+ */
308
+ function preserveAppNotes(previous, next) {
309
+ const start = previous.indexOf(APP_NOTES_START);
310
+ const end = previous.indexOf(APP_NOTES_END);
311
+ if (start === -1 || end === -1 || end < start) return next;
312
+ const notes = previous.slice(start + 24, end);
313
+ const ns = next.indexOf(APP_NOTES_START);
314
+ const ne = next.indexOf(APP_NOTES_END);
315
+ if (ns === -1 || ne === -1 || ne < ns) return next;
316
+ return next.slice(0, ns + 24) + notes + next.slice(ne);
317
+ }
318
+ /**
319
+ * Rend `AGENTS.md` (+ le pointeur `CLAUDE.md`) à la racine du projet.
320
+ *
321
+ * Appelé par `create app` ET re-appelé par les scaffolds in-project qui
322
+ * changent l'inventaire décrit (`create module`) : régénération BORNÉE —
323
+ * réécriture complète depuis l'état RÉEL du projet, seule la zone `app-notes`
324
+ * est réinjectée (cf {@link preserveAppNotes}). Le moteur ne sait pas rejouer
325
+ * des templates sur de l'existant, et n'essaie pas : seuls ces fichiers
326
+ * 100 % dérivés sont régénérables.
327
+ *
328
+ * `CLAUDE.md` n'est écrit QUE s'il n'existe pas : c'est un pointeur d'une
329
+ * ligne, et un `CLAUDE.md` remplacé par l'utilisateur lui appartient.
330
+ */
331
+ function renderProjectAgents(eta, packageRoot, projectRoot, data, written, writer) {
332
+ const tplDir = path.join(packageRoot, "templates", "app", "agents");
333
+ const agentsPath = path.join(projectRoot, "AGENTS.md");
334
+ let rendered = eta.renderString(readFileSync(path.join(tplDir, "AGENTS.md.tpl"), "utf8"), data);
335
+ if (rendered.includes("<%")) throw new Error("tag eta résiduel dans AGENTS.md");
336
+ if (writer.exists(agentsPath)) rendered = preserveAppNotes(writer.read(agentsPath), rendered);
337
+ writer.write(agentsPath, rendered);
338
+ written.push("AGENTS.md");
339
+ const template = readFileSync(path.join(tplDir, "POINTEUR.md.tpl"), "utf8");
340
+ for (const { file, agents } of pointeursInstructions(data.agents)) {
341
+ const target = path.join(projectRoot, file);
342
+ if (writer.exists(target)) continue;
343
+ const pointer = eta.renderString(template, {
344
+ ...data,
345
+ pointeur: file,
346
+ agents: agents.join(" et ")
347
+ });
348
+ writer.write(target, pointer);
349
+ written.push(file);
350
+ }
351
+ }
352
+ /**
353
+ * Pose le fichier-POINTEUR d'instructions des agents donnés, s'il manque.
354
+ *
355
+ * Extrait de {@link renderProjectAgents} pour être appelable AILLEURS : c'est
356
+ * `nodefony ai:mcp --agent <clé>` qui câble un agent après la création de
357
+ * l'application, et c'est donc là que son pointeur doit apparaître. Sans cette
358
+ * fonction, filtrer les pointeurs à la création (pour ne plus déposer un
359
+ * `GEMINI.md` chez qui a demandé Claude) aurait privé de son fichier tout agent
360
+ * ajouté ensuite — on aurait remplacé un défaut par un autre.
361
+ *
362
+ * Un pointeur DÉJÀ présent n'est jamais réécrit : celui qu'un développeur a
363
+ * remplacé lui appartient.
364
+ *
365
+ * @param projectRoot - racine de l'application.
366
+ * @param keys - clés des agents concernés.
367
+ * @param appName - nom de l'application (rendu dans le gabarit).
368
+ * @returns les fichiers réellement écrits, relatifs à la racine.
369
+ */
370
+ function writeAgentPointers(projectRoot, keys, appName) {
371
+ const pointeurs = pointeursInstructions(keys);
372
+ if (pointeurs.length === 0) return [];
373
+ const tplDir = path.join(findPackageRoot(), "templates", "app", "agents");
374
+ const template = readFileSync(path.join(tplDir, "POINTEUR.md.tpl"), "utf8");
375
+ const eta = new Eta({ autoEscape: false });
376
+ const written = [];
377
+ for (const { file, agents } of pointeurs) {
378
+ const target = path.join(projectRoot, file);
379
+ try {
380
+ writeFileSync(target, eta.renderString(template, {
381
+ appName,
382
+ pointeur: file,
383
+ agents: agents.join(" et ")
384
+ }), {
385
+ encoding: "utf8",
386
+ flag: "wx"
387
+ });
388
+ } catch (err) {
389
+ if (err.code === "EEXIST") continue;
390
+ throw err;
391
+ }
392
+ written.push(file);
393
+ }
394
+ return written;
395
+ }
396
+ /**
397
+ * Cibles de scaffold du projet : l'app racine + les modules locaux
398
+ * (`modules/<x>/` portant `package.json` + `index.ts` — le layout produit par
399
+ * `create module`). Consommée par le CLI (validation `--module`) et par le
400
+ * futur formulaire Studio (liste de choix dynamique par projet).
401
+ */
402
+ /**
403
+ * Capacités de l'environnement, telles que le MOTEUR les voit.
404
+ *
405
+ * Sert aux questions conditionnelles (`askIf`) : `link` (créer une app branchée sur le
406
+ * checkout local du framework, au lieu des paquets npm publiés) n'a de sens que si un
407
+ * checkout est résolvable. Un front ne peut PAS le deviner — seul le serveur sait ce
408
+ * qu'il y a sur le disque. Le figer côté client à `false` reviendrait à supprimer
409
+ * l'option en silence.
410
+ *
411
+ * @returns les capacités passées à {@link resolveAnswers}.
412
+ */
413
+ function scaffoldCaps() {
414
+ return { hasCheckout: resolveLocalWorkspaces(findPackageRoot()) !== null };
415
+ }
416
+ /**
417
+ * Lit le contexte du projet contenant `dir`.
418
+ *
419
+ * @param dir - un dossier quelconque du projet (la racine est retrouvée seule).
420
+ * @param writer - accès fichiers (injectable pour les tests).
421
+ * @returns le contexte, ou `null` si `dir` n'est pas dans un projet Nodefony.
422
+ */
423
+ function getScaffoldContext(dir, writer = new ScaffoldWriter()) {
424
+ const projectRoot = findProjectRoot(dir);
425
+ if (!projectRoot) return null;
426
+ const targets = listTargets(projectRoot, writer);
427
+ const entities = {};
428
+ for (const target of targets) entities[target.name] = readEntities(target.dir, writer);
429
+ return {
430
+ targets,
431
+ connectors: readConnectors(projectRoot, writer),
432
+ columnTypes: describeColumnTypes(),
433
+ entities,
434
+ idKinds: ENTITY_ID_KINDS
435
+ };
436
+ }
437
+ function listTargets(projectRoot, writer = new ScaffoldWriter()) {
438
+ const readName = (dir) => {
439
+ try {
440
+ return JSON.parse(writer.read(path.join(dir, "package.json"))).name ?? null;
441
+ } catch {
442
+ return null;
443
+ }
444
+ };
445
+ let rootManifest = {};
446
+ try {
447
+ rootManifest = JSON.parse(writer.read(path.join(projectRoot, "package.json")));
448
+ } catch {}
449
+ const targets = [{
450
+ kind: "app",
451
+ name: rootManifest.name ?? path.basename(projectRoot),
452
+ dir: projectRoot
453
+ }];
454
+ const layout = resolveModuleLayout(rootManifest, path.basename(projectRoot));
455
+ for (const rel of layout.targetDirs) {
456
+ const modulesDir = path.join(projectRoot, ...rel.split("/"));
457
+ if (!writer.exists(modulesDir)) continue;
458
+ for (const entry of writer.listDir(modulesDir)) {
459
+ const dir = path.join(modulesDir, entry.name);
460
+ if (!entry.isDirectory || !writer.exists(path.join(dir, "index.ts"))) continue;
461
+ targets.push({
462
+ kind: "module",
463
+ name: readName(dir) ?? entry.name,
464
+ dir
465
+ });
466
+ }
467
+ }
468
+ return targets;
469
+ }
470
+ /**
471
+ * Résout la cible d'un scaffold in-project : l'application, ou le module
472
+ * désigné par `--module`.
473
+ *
474
+ * Un module a DEUX identités — son **nom npm** (`@app/blog`, celui qui vit dans
475
+ * son manifeste et dans celui de l'app) et son **dossier** (`blog`, celui qu'on
476
+ * a tapé pour le créer). N'accepter que le premier faisait échouer
477
+ * `--module blog` juste après un `create module blog` : mesuré sur un agent
478
+ * tiers, trois tentatives perdues sur cette seule asymétrie, alors que le nom
479
+ * court est celui que l'utilisateur a en tête.
480
+ *
481
+ * Le nom court est donc accepté **quand il désigne UNE cible**. Ambigu, il est
482
+ * refusé en nommant les candidats plutôt qu'en en choisissant un : un dépôt à
483
+ * plusieurs dossiers de workspaces peut porter `packages/@x/auth` ET
484
+ * `modules/auth` — deux modules distincts, même nom court. Le nom npm exact
485
+ * l'emporte toujours, et reste la façon de lever l'ambiguïté.
486
+ *
487
+ * @param projectRoot - racine de l'application (porte `nodefony.config.ts`).
488
+ * @param moduleName - valeur de `--module` ; vide = l'application elle-même.
489
+ * @param writer - accès disque injecté (simulation `--dry-run` comprise).
490
+ * @returns la cible résolue.
491
+ * @throws si le nom ne désigne aucune cible, ou s'il en désigne plusieurs.
492
+ */
493
+ function resolveScaffoldTarget(projectRoot, moduleName, writer) {
494
+ const targets = listTargets(projectRoot, writer);
495
+ const app = targets[0];
496
+ if (!moduleName) return app;
497
+ const modules = targets.filter((t) => t.kind === "module");
498
+ const exact = modules.find((t) => t.name === moduleName);
499
+ if (exact) return exact;
500
+ const short = modules.filter((t) => path.basename(t.dir) === moduleName);
501
+ if (short.length === 1) return short[0];
502
+ if (short.length > 1) throw new Error(`« ${moduleName} » désigne ${short.length} modules — reprends le nom complet : ${short.map((t) => t.name).join(" · ")}`);
503
+ const known = targets.map((t) => `${t.name} (${t.kind})`).join(" · ");
504
+ throw new Error(`module « ${moduleName} » introuvable — cibles du projet : ${known}`);
505
+ }
506
+ /** `blog-post` / `BlogPost` / `blogPost` → `BlogPost`. */
507
+ function toPascalCase(name) {
508
+ return name.split(/[-_\s]+/u).filter(Boolean).map((w) => w[0].toUpperCase() + w.slice(1)).join("");
509
+ }
510
+ /** `BlogPost` / `blog_post` → `blog-post`. */
511
+ function toKebabCase(name) {
512
+ return name.replace(/([a-z0-9])([A-Z])/gu, "$1-$2").replaceAll("_", "-").toLowerCase();
513
+ }
514
+ /**
515
+ * Câble une classe générée dans le décorateur-liste de l'`index.ts` cible
516
+ * (`@controllers([...])`, `@entities([...])`, `@services([...])`) : ajoute
517
+ * l'import après le dernier import existant + insère le nom dans le tableau du
518
+ * décorateur. Édition TEXTUELLE gardée — toute ambiguïté = throw actionnable,
519
+ * jamais un fichier corrompu (le fichier n'est écrit que si les insertions
520
+ * tiennent).
521
+ *
522
+ * `controllers`/`entities` sont TOUJOURS rendus par les scaffolds qui les
523
+ * consomment (`@controllers([])` inconditionnel dans `app`/`module` ; les
524
+ * entités passent par {@link wireEntitiesDecorator}, qui crée déjà sa propre
525
+ * liste) — le décorateur introuvable y reste un refus. `services` diffère : le
526
+ * layer `module/base` ne le rend que si la question « service » a répondu
527
+ * `true`, et la racine d'une app n'en déclare JAMAIS un — le cas nominal d'un
528
+ * `create service` est donc une cible qui n'a PAS encore de `@services([...])`.
529
+ * Pour ce seul décorateur, l'absence n'est pas un refus : on le CRÉE, juste
530
+ * au-dessus de la classe (même position que le gabarit quand il le rend lui-même),
531
+ * en import ant `services` depuis `"nodefony"` s'il ne l'est pas déjà.
532
+ *
533
+ * @throws si la classe y est déjà, si aucun import n'ancre l'insertion, ou —
534
+ * pour `controllers`/`entities` seulement — si le décorateur est introuvable
535
+ * (le message donne l'édition manuelle exacte).
536
+ */
537
+ /**
538
+ * L'expression d'un décorateur-liste RÉEL — jamais sa mention dans un commentaire.
539
+ *
540
+ * `String.replace` sans `g` réécrit la PREMIÈRE occurrence trouvée. Un `index.ts`
541
+ * dont le TSDoc cite `@services([…])` au-dessus de la classe — ce que fait le
542
+ * gabarit d'application, pour expliquer à quoi sert la liste — voyait donc sa
543
+ * PROSE réécrite : `@services([…, DiscountService]) est ce qui fait EXISTER un
544
+ * service`, phrase absurde qui s'allongeait à chaque service créé, pendant que
545
+ * le vrai décorateur, lui, était bien étendu (constaté sur un run du banc).
546
+ *
547
+ * L'ancre `^[ \t]*@` suffit à trancher : une ligne de TSDoc commence par ` * `.
548
+ * Le premier groupe capture l'indentation pour la rendre telle quelle — un
549
+ * décorateur de classe imbriquée n'a pas à être désaligné par sa réécriture.
550
+ *
551
+ * @param decorator - le décorateur-liste visé.
552
+ * @returns l'expression, multi-lignes, avec l'indentation en groupe 1 et le
553
+ * contenu de la liste en groupe 2.
554
+ */
555
+ function decoratorListRe(decorator) {
556
+ return new RegExp(`^([ \\t]*)@${decorator}\\(\\[([^\\]]*)\\]\\)`, "mu");
557
+ }
558
+ function wireDecoratorList(indexPath, decorator, className, importPath, writer) {
559
+ const source = writer.read(indexPath);
560
+ if (new RegExp(`\\b${className}\\b`, "u").test(source)) throw new Error(`${className} est déjà référencé dans ${indexPath} — choisis un autre nom`);
561
+ const importLine = `import ${className} from "${importPath}";`;
562
+ const last = [...source.matchAll(/^import [^\n]*$/gmu)].at(-1);
563
+ if (!last || last.index === void 0) throw new Error(`aucun import trouvé dans ${indexPath} — ajoute à la main :\n ${importLine}\n @${decorator}([..., ${className}])`);
564
+ const importAt = last.index + last[0].length;
565
+ const extraImport = decorator === "services" && !/\bservices\b[^\n]*from "nodefony"/u.test(source) ? `\nimport { services } from "nodefony";` : "";
566
+ const withImport = source.slice(0, importAt) + `\n${importLine}${extraImport}` + source.slice(importAt);
567
+ const decoRe = decoratorListRe(decorator);
568
+ const match = decoRe.exec(withImport);
569
+ if (match && match.index !== void 0) {
570
+ const list = match[2].trim();
571
+ const wired = withImport.replace(decoRe, `$1@${decorator}([${list ? `${list.replace(/,\s*$/u, "")}, ` : ""}${className}])`);
572
+ writer.write(indexPath, wired);
573
+ return;
574
+ }
575
+ if (decorator !== "services") throw new Error(`@${decorator}([...]) introuvable dans ${indexPath} — ajoute à la main :\n ${importLine}\n @${decorator}([${className}]) sur la classe Module`);
576
+ const classMatch = /^(?:export\s+(?:default\s+)?)?class\s+\w+\s+extends\s+Module\b/mu.exec(withImport);
577
+ if (!classMatch || classMatch.index === void 0) throw new Error(`« class … extends Module » introuvable dans ${indexPath} — ajoute à la main :\n ${importLine}\n @services([${className}]) juste au-dessus de la classe`);
578
+ const wired = withImport.slice(0, classMatch.index) + `@services([${className}])\n` + withImport.slice(classMatch.index);
579
+ writer.write(indexPath, wired);
580
+ }
581
+ /**
582
+ * Exécute un scaffold — point d'entrée UNIQUE des trois fronts (CLI rapide,
583
+ * CLI interactif, Studio), dispatché par type :
584
+ * - `app` : projet NEUF dans `request.dir` (layers base/preset/frontend) ;
585
+ * - `controller` : IN-PROJECT — `request.dir` est le point de départ de la
586
+ * détection de racine (cwd de l'appelant), la cible réelle vient de
587
+ * `answers.module` (vide = app racine).
588
+ *
589
+ * Toutes les écritures passent par une TRANSACTION ({@link ScaffoldWriter}) que
590
+ * seul le scaffold RACINE vide sur disque, une fois toutes les étapes passées :
591
+ * un refus, même tardif, ne laisse donc jamais de projet à moitié modifié. Et
592
+ * comme la simulation n'est que « la même exécution sans vidage », `dryRun`
593
+ * décrit exactement ce qui se passerait — pas ce qu'un second code de
594
+ * prévision croirait.
595
+ *
596
+ * @returns fichiers écrits + paquets liés — l'appelant décide du rendu (CLI ou JSON)
597
+ * @throws Error si réponses invalides, dossier non vide sans force, ou template cassé
598
+ */
599
+ function runScaffold(request, version, options = {}) {
600
+ const writer = options.writer ?? new ScaffoldWriter();
601
+ const result = dispatchScaffold(request, version, writer);
602
+ if (options.writer) return result;
603
+ const forme = formatScaffoldOutput(writer, result.dest);
604
+ const rendered = forme.pending > 0 ? {
605
+ ...result,
606
+ unformatted: forme.pending
607
+ } : result;
608
+ if (options.dryRun) return {
609
+ ...rendered,
610
+ changes: writer.changes()
611
+ };
612
+ writer.commit();
613
+ return rendered;
614
+ }
615
+ /** Résout la spec puis route vers le scaffold du type demandé. */
616
+ function dispatchScaffold(request, version, writer) {
617
+ const [spec] = getScaffoldSpec(request.type);
618
+ if (!spec) throw new Error(`type de scaffold inconnu : ${request.type}`);
619
+ const packageRoot = findPackageRoot();
620
+ const workspaces = resolveLocalWorkspaces(packageRoot);
621
+ const answers = resolveAnswers(spec, request.answers, { hasCheckout: workspaces !== null });
622
+ if (request.type === "module") return runModuleScaffold(request, answers, packageRoot, version, writer);
623
+ if (request.type === "controller") return runControllerScaffold(request, answers, packageRoot, writer);
624
+ if (request.type === "service") return runServiceScaffold(request, answers, packageRoot, writer);
625
+ if (request.type === "front") return runFrontScaffold(request, answers, packageRoot, writer);
626
+ if (request.type === "entity") return runEntityScaffold(request, answers, packageRoot, writer);
627
+ if (request.type === "command") return runCommandScaffold(request, answers, packageRoot, writer);
628
+ const dest = path.resolve(request.dir);
629
+ if (existsSync(dest) && readdirSync(dest).length > 0 && !request.force) throw new Error(`le dossier ${dest} existe et n'est pas vide (--force pour écraser)`);
630
+ const preset = answers.preset;
631
+ const frontend = answers.frontend;
632
+ const front = frontend !== "none" ? FRONTEND_PARAMS[frontend] : null;
633
+ const db = resolveDatabase(answers.database, String(answers.name));
634
+ const data = {
635
+ appName: answers.name,
636
+ dialect: answers.database === "postgres" ? "postgres" : answers.database === "sqlite" || !answers.database ? "sqlite" : "mysql",
637
+ pascal: toPascalCase(String(answers.name)),
638
+ entryName: answers.name,
639
+ nodefonyVersion: version,
640
+ pkg: SCAFFOLD_VERSIONS,
641
+ preset,
642
+ complete: preset === "complete",
643
+ hasSecurity: preset === "complete",
644
+ hasOrm: preset === "complete",
645
+ frontend,
646
+ front,
647
+ db,
648
+ secrets: {
649
+ NF_TOTP_KEY: randomBytes(32).toString("base64"),
650
+ NF_WEBHOOK_KEY: randomBytes(32).toString("base64"),
651
+ NF_CSRF_SECRET: randomBytes(32).toString("base64")
652
+ }
653
+ };
654
+ const eta = new Eta(ETA_OPTIONS);
655
+ const templates = path.join(packageRoot, "templates", request.type);
656
+ const written = [];
657
+ renderLayer(eta, path.join(templates, "base"), dest, data, written, writer);
658
+ if (!front) renderLayer(eta, path.join(templates, "home"), dest, data, written, writer);
659
+ renderLayer(eta, path.join(packageRoot, "templates", "controller", "hello"), dest, {
660
+ nameClass: "HelloController",
661
+ kebab: "hello",
662
+ route: "/api",
663
+ indexPath: "/hello",
664
+ helloName: answers.name,
665
+ secureRoute: preset === "complete"
666
+ }, written, writer, { __NAME__: "HelloController" });
667
+ if (preset === "complete") {
668
+ renderLayer(eta, path.join(templates, "complete"), dest, data, written, writer);
669
+ renderLayer(eta, path.join(packageRoot, "templates", "controller", "realtime"), dest, {
670
+ nameClass: "LiveController",
671
+ kebab: "live",
672
+ route: "/api/live",
673
+ channel: "live",
674
+ hasSecurity: true
675
+ }, written, writer, {
676
+ __NAME__: "LiveController",
677
+ __KEBAB__: "live"
678
+ });
679
+ }
680
+ if (front) {
681
+ renderLayer(eta, path.join(packageRoot, "templates", "shared", "front-shell"), dest, data, written, writer);
682
+ renderLayer(eta, path.join(templates, "frontend", "shared"), dest, data, written, writer);
683
+ renderLayer(eta, path.join(packageRoot, "templates", "shared", "front-entry", frontend), dest, data, written, writer);
684
+ renderLayer(eta, path.join(packageRoot, "templates", "shared", "front-registrar"), dest, data, written, writer, { __PASCAL__: toPascalCase(String(answers.name)) });
685
+ renderLayer(eta, path.join(templates, "frontend", frontend), dest, data, written, writer);
686
+ if (frontend === "angular") renderLayer(eta, path.join(packageRoot, "templates", "shared", "ng-app-tsconfig"), dest, data, written, writer);
687
+ if (frontend === "vue") renderLayer(eta, path.join(packageRoot, "templates", "shared", "vue-shim"), dest, data, written, writer);
688
+ if (frontend === "svelte") renderLayer(eta, path.join(packageRoot, "templates", "shared", "svelte-shim"), dest, data, written, writer);
689
+ }
690
+ const chosenAgents = Array.isArray(answers.agents) ? answers.agents : [];
691
+ renderProjectAgents(eta, packageRoot, dest, {
692
+ appName: String(answers.name),
693
+ nodefonyVersion: version,
694
+ hasSecurity: preset === "complete",
695
+ hasOrm: preset === "complete",
696
+ hasRealtime: preset === "complete",
697
+ hasStudio: preset === "complete",
698
+ front: front !== null,
699
+ hasMigrateRecipe: writer.exists(path.join(dest, "deploy", "migrate-job.yaml")),
700
+ modules: [],
701
+ agents: chosenAgents
702
+ }, written, writer);
703
+ let linked = [];
704
+ if (answers.link === true) {
705
+ if (!workspaces) throw new Error("link exige un CHECKOUT de nodefony-core (paquet installé détecté) — sans checkout, attends la release npm puis installe sans --link");
706
+ linked = linkLocalDeps(dest, workspaces, writer);
707
+ }
708
+ return {
709
+ dest,
710
+ files: written.sort(),
711
+ linked
712
+ };
713
+ }
714
+ /**
715
+ * Déclare `modules/*` en workspaces npm de l'app et branche les scripts sur eux.
716
+ *
717
+ * POURQUOI c'est indispensable : le Kernel charge un module par son NOM
718
+ * (`import("@app/blog")`, cf `Kernel.loadModule`) — pas par un chemin. Sans le
719
+ * symlink que npm pose pour un workspace, le boot échoue sur « Cannot find
720
+ * package ». Les scripts sont chaînés dans la foulée, sinon le module ne serait
721
+ * ni construit (le bundler de l'app ne regarde que `index.ts` + `nodefony/**`),
722
+ * ni typé, ni testé : du code mort au premier jour.
723
+ *
724
+ * Idempotent — relancé pour le 2ᵉ module, il ne touche plus à rien.
725
+ *
726
+ * @returns true si le `package.json` de l'app a été modifié.
727
+ */
728
+ function ensureWorkspaces(projectRoot, writer) {
729
+ const manifestPath = path.join(projectRoot, "package.json");
730
+ const manifest = JSON.parse(writer.read(manifestPath));
731
+ let changed = false;
732
+ const workspaces = manifest.workspaces ?? [];
733
+ if (!workspaces.includes("modules/*")) {
734
+ manifest.workspaces = [...workspaces, "modules/*"];
735
+ changed = true;
736
+ }
737
+ const scripts = manifest.scripts ??= {};
738
+ for (const [name, when] of Object.entries({
739
+ build: "before",
740
+ typecheck: "after",
741
+ test: "after"
742
+ })) {
743
+ const script = scripts[name];
744
+ if (!script || script.includes("--workspaces")) continue;
745
+ const delegated = `npm run ${name} --workspaces --if-present`;
746
+ scripts[name] = when === "before" ? `${delegated} && ${script}` : `${script} && ${delegated}`;
747
+ changed = true;
748
+ }
749
+ if (changed) writer.write(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
750
+ return changed;
751
+ }
752
+ /**
753
+ * Ajoute le module au manifeste `modules` de `nodefony.config.ts` — c'est CE
754
+ * tableau qui décide de ce qui se charge, et dans quel ordre. Un module local
755
+ * vient à la fin : le socle (http, framework, security…) doit être debout avant.
756
+ *
757
+ * Édition textuelle GARDÉE (même doctrine que `wireDecoratorList`) : on n'écrit
758
+ * que si l'ancre est trouvée sans ambiguïté, sinon on rend une note actionnable —
759
+ * jamais un `nodefony.config.ts` corrompu, qui empêcherait l'app de booter.
760
+ *
761
+ * @param moduleDir - dossier où le module a été créé, relatif à la racine (en
762
+ * `/`) — seulement pour NOMMER le bon endroit dans le commentaire inséré.
763
+ * @returns note à afficher si un geste manuel reste nécessaire, sinon `null`.
764
+ */
765
+ function wireModuleManifest(configPath, pkgName, writer, moduleDir = "modules") {
766
+ const manual = `ajoute à la main dans le manifeste modules de nodefony.config.ts :\n use("${pkgName}", {}),`;
767
+ if (!writer.exists(configPath)) return manual;
768
+ const source = writer.read(configPath);
769
+ if (source.includes(`"${pkgName}"`)) return null;
770
+ const anchor = /modules\s*:\s*\[/u.exec(source);
771
+ if (!anchor || anchor.index === void 0) return manual;
772
+ let depth = 0;
773
+ let close = -1;
774
+ for (let i = anchor.index + anchor[0].length - 1; i < source.length; i++) {
775
+ const char = source[i];
776
+ if (char === "[") depth++;
777
+ else if (char === "]") {
778
+ depth--;
779
+ if (depth === 0) {
780
+ close = i;
781
+ break;
782
+ }
783
+ }
784
+ }
785
+ if (close === -1) return manual;
786
+ const entry = /import\s*\{[^}]*\buse\b[^}]*\}\s*from\s*"nodefony"/u.test(source) ? `use("${pkgName}", {}),` : `"${pkgName}",`;
787
+ const comment = `// Module local du projet (${moduleDir}/) — créé par \`nodefony create module\`.`;
788
+ writer.write(configPath, `${source.slice(0, close).trimEnd()}\n\n ${comment}\n ${entry}\n ${source.slice(close)}`);
789
+ return null;
790
+ }
791
+ /**
792
+ * Déclare un rôle applicatif sous `ROLE_ADMIN` dans la hiérarchie de l'app.
793
+ *
794
+ * ## Pourquoi la hiérarchie, et pas une liste de rôles sur l'action
795
+ *
796
+ * Deux gestes rendent la même réponse sur la route qu'on vient d'écrire — une
797
+ * hiérarchie, ou `@IsGranted(["ROLE_X", "ROLE_ADMIN"])`. Un seul GÉNÉRALISE :
798
+ * la hiérarchie vaut pour toute route future gardée par ce rôle, la liste
799
+ * s'arrête à cette action-là et devra être recopiée à chaque fois. Le troisième
800
+ * geste — attribuer le rôle à l'administrateur au semis — est pire encore : il
801
+ * ne se voit dans aucun diff de route et se perd au premier compte recréé.
802
+ *
803
+ * ## Ce que cette fonction n'invente pas
804
+ *
805
+ * Elle n'ajoute JAMAIS une clé `roleHierarchy` absente, ni un `ROLE_ADMIN`
806
+ * absent : composer un bloc de configuration à l'aveugle dans un fichier que
807
+ * l'utilisateur a pu réorganiser produirait un manifeste qui ne boote plus.
808
+ * Quand l'ancre manque, elle rend la note à taper — même doctrine que
809
+ * {@link wireModuleManifest} et `wireDecoratorList`.
810
+ *
811
+ * @param configPath - `nodefony.config.ts` de l'application.
812
+ * @param role - le rôle à déclarer (déjà validé par le schéma de la question).
813
+ * @param writer - écrivain du scaffold (respecte `--dry-run`).
814
+ * @returns note à afficher si un geste manuel reste nécessaire, sinon `null`.
815
+ */
816
+ function wireRoleHierarchy(configPath, role, writer) {
817
+ const manual = `ajoute à la main dans roleHierarchy de nodefony.config.ts :\n ROLE_ADMIN: ["ROLE_USER", "${role}"],\n (sans quoi un administrateur devra porter « ${role} » pour accéder à ce controller)`;
818
+ if (!writer.exists(configPath)) return manual;
819
+ const source = writer.read(configPath);
820
+ const block = /roleHierarchy\s*:\s*\{/u.exec(source);
821
+ if (!block || block.index === void 0) return manual;
822
+ let depth = 0;
823
+ let close = -1;
824
+ for (let i = block.index + block[0].length - 1; i < source.length; i++) {
825
+ const char = source[i];
826
+ if (char === "{") depth++;
827
+ else if (char === "}") {
828
+ depth--;
829
+ if (depth === 0) {
830
+ close = i;
831
+ break;
832
+ }
833
+ }
834
+ }
835
+ if (close === -1) return manual;
836
+ const body = source.slice(block.index, close);
837
+ if (body.includes(`"${role}"`)) return null;
838
+ const admin = /(ROLE_ADMIN\s*:\s*\[)([^\]]*)(\])/u.exec(body);
839
+ if (!admin || admin.index === void 0) return manual;
840
+ const inner = (admin[2] ?? "").replace(/,\s*$/u, "");
841
+ const separator = inner.trim().length > 0 ? ", " : "";
842
+ const replacement = `${admin[1]}${inner}${separator}"${role}"${admin[3]}`;
843
+ const patchedBody = body.slice(0, admin.index) + replacement + body.slice(admin.index + admin[0].length);
844
+ writer.write(configPath, source.slice(0, block.index) + patchedBody + source.slice(close));
845
+ return null;
846
+ }
847
+ /**
848
+ * Noms des paquets que l'APPLICATION déclare (`dependencies` + `devDependencies`).
849
+ *
850
+ * Source unique des deux scaffolds qui doivent savoir si une brique du framework
851
+ * est installée : `create module` (refus AVANT écriture quand elle manque) et
852
+ * `create front` (qui pose la peer sur un module local quand l'app, elle, l'a).
853
+ *
854
+ * @param projectRoot - racine de l'application (porte `nodefony.config.ts`).
855
+ * @returns l'ensemble des noms déclarés — jamais les versions, seule la présence
856
+ * est une information ici.
857
+ */
858
+ function readAppDependencyNames(projectRoot, writer) {
859
+ const manifest = JSON.parse(writer.read(path.join(projectRoot, "package.json")));
860
+ return /* @__PURE__ */ new Set([...Object.keys(manifest.dependencies ?? {}), ...Object.keys(manifest.devDependencies ?? {})]);
861
+ }
862
+ /**
863
+ * Scaffold IN-PROJECT d'un module applicatif : un WORKSPACE npm sous
864
+ * `modules/<nom>/` (package.json + build rolldown + config Zod + tests + docs),
865
+ * déclaré dans les workspaces de l'app et dans le manifeste `modules`.
866
+ *
867
+ * Ce scaffold pose la COQUILLE et rien d'autre : le controller et le frontend
868
+ * éventuels sont rendus par les scaffolds `controller` et `front` EXISTANTS,
869
+ * ciblés sur le module fraîchement créé. Aucun template n'est dupliqué — la
870
+ * commande qui sait poser un controller reste la seule à savoir le faire.
871
+ *
872
+ * @throws hors projet, module déjà présent (sans `--force`), nom en collision,
873
+ * ou brique manquante dans l'app (realtime/frontend) — avec le geste exact.
874
+ */
875
+ function runModuleScaffold(request, answers, packageRoot, version, writer) {
876
+ const projectRoot = findProjectRoot(request.dir);
877
+ if (!projectRoot) throw new Error("aucun projet Nodefony ici (nodefony.config.ts introuvable en remontant) — lance la commande depuis une app (créée par `nodefony create app`)");
878
+ const name = toKebabCase(String(answers.name));
879
+ const appManifestPath = path.join(projectRoot, "package.json");
880
+ const appManifest = JSON.parse(writer.read(appManifestPath));
881
+ const appName = appManifest.name ?? path.basename(projectRoot);
882
+ const layout = resolveModuleLayout(appManifest, path.basename(projectRoot));
883
+ const dest = path.join(projectRoot, ...layout.createDir.split("/"), name);
884
+ if (writer.exists(dest) && writer.listDir(dest).length > 0 && !request.force) throw new Error(`le module ${name} existe déjà (${layout.createDir}/${name}) — choisis un autre nom, ou --force`);
885
+ const pkgName = `${layout.scope}/${name}`;
886
+ const appDeps = readAppDependencyNames(projectRoot, writer);
887
+ const controller = String(answers.controller);
888
+ const frontend = answers.frontend;
889
+ if ((controller === "realtime" || controller === "duplex") && !appDeps.has("@nodefony/realtime")) throw new Error(`le controller ${controller} exige @nodefony/realtime, absent de l'app — ajoute la dep + use("@nodefony/realtime") au manifeste, ou --controller hello`);
890
+ if (frontend !== "none" && !appDeps.has("@nodefony/frontend")) throw new Error("un frontend exige @nodefony/frontend, absent de l'app — ajoute la dep + \"@nodefony/frontend\" au manifeste, ou --frontend none");
891
+ const pascal = toPascalCase(name);
892
+ const data = {
893
+ name,
894
+ pkgName,
895
+ appName,
896
+ pascal,
897
+ camel: pascal[0].toLowerCase() + pascal.slice(1),
898
+ upper: name.replaceAll("-", "_").toUpperCase(),
899
+ description: String(answers.description) || `Module ${name} de ${appName}`,
900
+ nodefonyVersion: version,
901
+ pkg: SCAFFOLD_VERSIONS,
902
+ service: answers.service === true,
903
+ command: answers.command === true,
904
+ controller,
905
+ needsRealtime: controller === "realtime" || controller === "duplex",
906
+ frontend,
907
+ front: frontend !== "none" ? FRONTEND_PARAMS[frontend] : null,
908
+ /**
909
+ * Le module naît-il paquet PUBLIABLE (surface npm : `exports`, `types`,
910
+ * `files`, peerDependencies, `.d.ts` générés) ou module local privé ?
911
+ * Décidé par le layout du dépôt, pas par une option — c'est une propriété du
912
+ * dossier où il atterrit.
913
+ */
914
+ publishable: layout.kind === "packages",
915
+ /** Dossier où le module atterrit, relatif à la racine (en `/`, il VOYAGE). */
916
+ moduleDir: layout.createDir,
917
+ /**
918
+ * Version de départ. Un paquet d'un monorepo suit la version de SA racine
919
+ * (verrouillage naturel : tous les paquets du dépôt avancent ensemble) ; un
920
+ * module local d'application démarre à `0.1.0`, il a sa propre vie.
921
+ */
922
+ version: layout.kind === "packages" ? appManifest.version ?? "0.1.0" : "0.1.0"
923
+ };
924
+ const eta = new Eta(ETA_OPTIONS);
925
+ const templates = path.join(packageRoot, "templates", "module");
926
+ const tokens = {
927
+ __PASCAL__: pascal,
928
+ __KEBAB__: name
929
+ };
930
+ const written = [];
931
+ renderLayer(eta, path.join(templates, "base"), dest, data, written, writer, tokens);
932
+ if (data.service) renderLayer(eta, path.join(templates, "service"), dest, data, written, writer, tokens);
933
+ renderLayer(eta, path.join(templates, "ai"), dest, data, written, writer, tokens);
934
+ if (data.publishable) renderLayer(eta, path.join(templates, "packages"), dest, data, written, writer, tokens);
935
+ const notes = [];
936
+ if (data.command) {
937
+ const sub = runScaffold({
938
+ type: "command",
939
+ answers: {
940
+ name: "hello",
941
+ description: `Salue depuis le module ${name}`,
942
+ phase: "onReady",
943
+ module: pkgName,
944
+ service: data.service
945
+ },
946
+ dir: projectRoot,
947
+ force: request.force
948
+ }, version, { writer });
949
+ written.push(...sub.files);
950
+ notes.push(...sub.notes ?? []);
951
+ }
952
+ if (controller !== "none") {
953
+ const sub = runScaffold({
954
+ type: "controller",
955
+ answers: {
956
+ name,
957
+ kind: controller,
958
+ route: "",
959
+ module: pkgName
960
+ },
961
+ dir: projectRoot,
962
+ force: request.force
963
+ }, version, { writer });
964
+ written.push(...sub.files);
965
+ notes.push(...sub.notes ?? []);
966
+ }
967
+ if (frontend !== "none") {
968
+ const sub = runScaffold({
969
+ type: "front",
970
+ answers: {
971
+ name: controller === "none" ? name : `${name}-page`,
972
+ frontend,
973
+ route: `/${toKebabCase(name)}`,
974
+ module: pkgName
975
+ },
976
+ dir: projectRoot,
977
+ force: request.force
978
+ }, version, { writer });
979
+ written.push(...sub.files);
980
+ notes.push(...sub.notes ?? []);
981
+ }
982
+ if (!layout.workspaceDeclared && ensureWorkspaces(projectRoot, writer)) notes.push("package.json de l'app : workspaces modules/* + scripts build/typecheck/test chaînés");
983
+ const manifestNote = wireModuleManifest(path.join(projectRoot, "nodefony.config.ts"), pkgName, writer, layout.createDir);
984
+ notes.push(manifestNote ?? `nodefony.config.ts : use("${pkgName}", {}) ajouté au manifeste modules`);
985
+ if (!data.publishable) renderProjectAgents(eta, packageRoot, projectRoot, {
986
+ appName,
987
+ nodefonyVersion: version,
988
+ hasSecurity: appDeps.has("@nodefony/security"),
989
+ hasOrm: appDeps.has("@nodefony/orm-core"),
990
+ hasRealtime: appDeps.has("@nodefony/realtime"),
991
+ hasStudio: appDeps.has("@nodefony/studio"),
992
+ front: appDeps.has("@nodefony/frontend"),
993
+ hasMigrateRecipe: writer.exists(path.join(projectRoot, "deploy", "migrate-job.yaml")),
994
+ agents: [],
995
+ modules: listTargets(projectRoot, writer).filter((t) => t.kind === "module").map((t) => ({
996
+ name: t.name,
997
+ dir: path.relative(projectRoot, t.dir).split(path.sep).join("/")
998
+ }))
999
+ }, written, writer);
1000
+ return {
1001
+ dest,
1002
+ files: written.sort(),
1003
+ linked: [],
1004
+ notes
1005
+ };
1006
+ }
1007
+ /**
1008
+ * Scaffold IN-PROJECT d'un controller : résout la cible (app racine ou module),
1009
+ * rend le template de la saveur (`hello`/`rest`/`duplex`/`realtime`/`example`)
1010
+ * dans `<cible>/nodefony/controllers/` puis câble la classe dans le
1011
+ * `@controllers([...])` de l'`index.ts` cible.
1012
+ *
1013
+ * @throws hors projet, cible inconnue (le message liste les cibles), saveur
1014
+ * realtime/duplex sans dep `@nodefony/realtime`, ou wiring impossible
1015
+ * (actionnable).
1016
+ */
1017
+ function runControllerScaffold(request, answers, packageRoot, writer) {
1018
+ const projectRoot = findProjectRoot(request.dir);
1019
+ if (!projectRoot) throw new Error("aucun projet Nodefony ici (nodefony.config.ts introuvable en remontant) — lance la commande depuis une app (créée par `nodefony create app`)");
1020
+ const target = resolveScaffoldTarget(projectRoot, String(answers.module ?? ""), writer);
1021
+ const kind = answers.kind;
1022
+ const base = String(answers.name).replace(/[-_]?[Cc]ontroller$/u, "");
1023
+ const nameClass = `${toPascalCase(base)}Controller`;
1024
+ const kebab = toKebabCase(base);
1025
+ const route = String(answers.route) || `/api/${kebab}`;
1026
+ const manifest = JSON.parse(writer.read(path.join(target.dir, "package.json")));
1027
+ const targetDeps = new Set([
1028
+ "dependencies",
1029
+ "devDependencies",
1030
+ "peerDependencies"
1031
+ ].flatMap((b) => Object.keys(manifest[b] ?? {})));
1032
+ if (!CONTROLLER_KIND_CHOICES.includes(kind)) throw new Error(`saveur « ${String(kind)} » inconnue — choix : ${CONTROLLER_KIND_CHOICES.join(" | ")}`);
1033
+ if ((kind === "realtime" || kind === "duplex") && !targetDeps.has("@nodefony/realtime")) throw new Error(`la saveur ${kind} exige @nodefony/realtime dans ${target.name} — ajoute la dep + use("@nodefony/realtime") au manifeste, ou choisis --kind hello`);
1034
+ const role = String(answers.role ?? "").trim();
1035
+ if (role.length > 0 && !targetDeps.has("@nodefony/security")) throw new Error(`--role ${role} exige @nodefony/security dans ${target.name} — sans lui, @IsGranted n'est évalué par personne et la route resterait OUVERTE.\n → l'ajouter : npm install @nodefony/security, puis use("@nodefony/security", {}) au manifeste`);
1036
+ const eta = new Eta(ETA_OPTIONS);
1037
+ const written = [];
1038
+ const data = {
1039
+ nameClass,
1040
+ kebab,
1041
+ route,
1042
+ channel: kebab,
1043
+ hasSecurity: targetDeps.has("@nodefony/security"),
1044
+ indexPath: "",
1045
+ helloName: kebab,
1046
+ secureRoute: false,
1047
+ roleGuard: role.length > 0,
1048
+ role
1049
+ };
1050
+ renderLayer(eta, path.join(packageRoot, "templates", "controller", kind), target.dir, data, written, writer, {
1051
+ __NAME__: nameClass,
1052
+ __KEBAB__: kebab
1053
+ });
1054
+ wireDecoratorList(path.join(target.dir, "index.ts"), "controllers", nameClass, `./nodefony/controllers/${nameClass}`, writer);
1055
+ written.push("index.ts");
1056
+ const noteRole = role.length > 0 ? wireRoleHierarchy(path.join(projectRoot, "nodefony.config.ts"), role, writer) : null;
1057
+ const NOTES = {
1058
+ hello: [`GET ${route}`, `WS ${route}/echo`],
1059
+ rest: [`REST ${route} (GET/POST) · ${route}/{id} (GET/PUT/PATCH/DELETE)`],
1060
+ duplex: [`REST ${route} (GET/POST) · ${route}/{id} (GET/DELETE)`, `WS ${route}/realtime (socket Nodefony — mêmes actions via socket.request/mutate)`],
1061
+ realtime: [`WS ${route}/realtime (socket Nodefony — canal ${kebab}:ticker, action ${kebab}:ping)`, `TEST tests/${kebab}-realtime.test.ts — \`npx vitest run\` (harnais @nodefony/realtime/testing, ni serveur ni navigateur)`],
1062
+ example: [`REST ${route} (vitrine décorateurs — voir les curl du TSDoc généré)`, `WS ${route}/echo`]
1063
+ };
1064
+ return {
1065
+ dest: target.dir,
1066
+ files: written.sort(),
1067
+ linked: [],
1068
+ notes: [...NOTES[kind], ...role.length > 0 ? [`GARDE ${role} sur TOUT le controller (@IsGranted de classe) — ` + (noteRole === null ? `déclaré sous ROLE_ADMIN dans roleHierarchy : l'administrateur y a accès sans porter ce rôle` : `⚠ hiérarchie NON modifiée — ${noteRole}`)] : []]
1069
+ };
1070
+ }
1071
+ /**
1072
+ * Scaffold IN-PROJECT d'un service injectable : résout la cible (app racine ou
1073
+ * module), rend la classe (`@injectable`, `extends Service`) + son interface
1074
+ * dans `<cible>/nodefony/{service,interfaces}/`, puis câble la classe dans le
1075
+ * `@services([...])` de l'`index.ts` cible — CRÉÉ s'il n'existe pas encore
1076
+ * (cf {@link wireDecoratorList}, seul cas où l'absence du décorateur n'est pas
1077
+ * un refus).
1078
+ *
1079
+ * Volontairement AUTONOME : contrairement au service PRINCIPAL d'un module
1080
+ * (`create module`, gabarit `templates/module/service/`), celui-ci ne dépend
1081
+ * d'aucun `nodefony/config/config.ts` — une cible existante (app racine, ou
1082
+ * module créé avec `--no-service`) n'en a pas forcément un, et le résultat
1083
+ * doit compiler tel quel, sans édition manuelle.
1084
+ *
1085
+ * @throws hors projet, cible inconnue (le message liste les cibles), ou nom
1086
+ * déjà référencé dans l'`index.ts` cible.
1087
+ */
1088
+ function runServiceScaffold(request, answers, packageRoot, writer) {
1089
+ const projectRoot = findProjectRoot(request.dir);
1090
+ if (!projectRoot) throw new Error("aucun projet Nodefony ici (nodefony.config.ts introuvable en remontant) — lance la commande depuis une app (créée par `nodefony create app`)");
1091
+ const target = resolveScaffoldTarget(projectRoot, String(answers.module ?? ""), writer);
1092
+ const base = String(answers.name).replace(/[-_]?[Ss]ervice$/u, "");
1093
+ const pascal = toPascalCase(base);
1094
+ const nameClass = `${pascal}Service`;
1095
+ const camel = pascal[0].toLowerCase() + pascal.slice(1);
1096
+ const existing = listTargetServices(target.dir, writer);
1097
+ const injectName = String(answers.inject ?? "").trim();
1098
+ let inject = null;
1099
+ if (injectName !== "") {
1100
+ const wanted = `${toPascalCase(injectName.replace(/[-_]?[Ss]ervice$/u, ""))}Service`;
1101
+ const found = existing.find((s) => s.pascal === wanted);
1102
+ if (!found) {
1103
+ const known = existing.map((s) => s.pascal).join(" · ") || "aucun";
1104
+ throw new Error(`--inject : « ${injectName} » introuvable dans ${target.name} (services de la cible : ${known}) — crée-le d'abord, ou relance sans --inject`);
1105
+ }
1106
+ if (found.pascal === nameClass) throw new Error(`--inject : un service ne s'injecte pas lui-même (${nameClass})`);
1107
+ const method = findCallableMethod(found.source);
1108
+ if (method === null) throw new Error(`--inject : ${found.pascal} n'expose aucune méthode publique appelable sans argument obligatoire — le service injecté serait déclaré sans être appelé`);
1109
+ inject = {
1110
+ pascal: found.pascal,
1111
+ key: found.key,
1112
+ camel: found.key[0].toLowerCase() + found.key.slice(1),
1113
+ method
1114
+ };
1115
+ }
1116
+ const eta = new Eta(ETA_OPTIONS);
1117
+ const written = [];
1118
+ renderLayer(eta, path.join(packageRoot, "templates", "service"), target.dir, {
1119
+ pascal,
1120
+ camel,
1121
+ inject,
1122
+ description: String(answers.description) || `Service ${pascal} de ${target.name}`
1123
+ }, written, writer, { __PASCAL__: pascal });
1124
+ wireDecoratorList(path.join(target.dir, "index.ts"), "services", nameClass, `./nodefony/service/${nameClass}`, writer);
1125
+ written.push("index.ts");
1126
+ const notes = [`DI container.get("${camel}") — clé du conteneur (super("${camel}", …))`, `DI @inject("${nameClass}") — nom de classe (@injectable)`];
1127
+ if (inject) notes.push(`DI ${inject.pascal} est injecté par le CONSTRUCTEUR — dépendance déclarée, ordonnée par le conteneur`);
1128
+ else if (existing.length > 0) notes.push(`DI pour appeler ${existing[0].pascal} depuis ce service, injecte-le : nodefony create service ${base} --inject ${existing[0].pascal}`);
1129
+ return {
1130
+ dest: target.dir,
1131
+ files: written.sort(),
1132
+ linked: [],
1133
+ notes
1134
+ };
1135
+ }
1136
+ /**
1137
+ * Retire commentaires de bloc et de ligne d'une source TypeScript.
1138
+ *
1139
+ * ⚠️ Approximation assumée — une chaîne contenant `//` (une URL) perd sa fin.
1140
+ * C'est sans effet ici : les lecteurs ci-dessous cherchent une FORME de code
1141
+ * (`super("…"`, une signature de méthode), jamais le contenu d'une chaîne.
1142
+ * L'inverse, lui, s'est produit : un TSDoc qui CITE `super("autreService", …)`
1143
+ * pour expliquer la clé d'un voisin faisait lire cette clé-là. Un fichier bien
1144
+ * documenté est le cas NORMAL, pas le cas tordu.
1145
+ *
1146
+ * @param source - le contenu du fichier.
1147
+ * @returns la même source, commentaires blanchis.
1148
+ */
1149
+ function stripComments(source) {
1150
+ return source.replace(/\/\*[\s\S]*?\*\//gu, "").replace(/(^|[^:])\/\/[^\n]*/gu, "$1");
1151
+ }
1152
+ /**
1153
+ * Nom Nodefony déclaré par le premier `super("…", …)` d'un fichier — celui d'un
1154
+ * `Module` (`index.ts`) ou d'un `Service`.
1155
+ *
1156
+ * C'est la CLÉ du conteneur, et pour un module le préfixe de ses commandes CLI
1157
+ * (`<module>:<action>`). Elle ne se déduit ni du nom npm du paquet ni du nom de
1158
+ * la classe : les trois peuvent différer, et seule celle-ci existe au runtime.
1159
+ *
1160
+ * Lue sur le CODE seul : un `super("…")` cité dans un TSDoc pour documenter un
1161
+ * autre service faisait rendre la clé du voisin — la commande générée importait
1162
+ * alors une classe et résolvait l'instance d'une autre.
1163
+ *
1164
+ * @returns le nom déclaré, ou `null` si le fichier ne suit pas la forme attendue.
1165
+ */
1166
+ function readNodefonyName(file, writer) {
1167
+ try {
1168
+ return /\bsuper\(\s*"([^"]+)"/u.exec(stripComments(writer.read(file)))?.[1] ?? null;
1169
+ } catch {
1170
+ return null;
1171
+ }
1172
+ }
1173
+ /**
1174
+ * Première méthode d'un service qu'une commande peut appeler SANS argument.
1175
+ *
1176
+ * On cherche une méthode **publique** dont tous les paramètres sont facultatifs
1177
+ * (aucun, ou tous avec `?`/valeur par défaut) : c'est la seule forme dont on
1178
+ * puisse produire un appel qui compile à coup sûr. Le cycle de vie (`init`) et
1179
+ * le constructeur sont exclus — les appeler serait un contresens.
1180
+ *
1181
+ * ⚠️ Analyse TEXTUELLE, volontairement conservatrice : à la moindre signature
1182
+ * qu'on ne sait pas lire, on passe. Mieux vaut refuser `--service` que livrer un
1183
+ * appel qui ne compile pas.
1184
+ *
1185
+ * @returns le nom de la méthode, ou `null` si aucune ne convient.
1186
+ */
1187
+ function findCallableMethod(source) {
1188
+ for (const m of source.matchAll(/^ {2}(?:public\s+)?(?:async\s+)?([a-z][A-Za-z0-9_]*)\s*\(([^)]*)\)/gmu)) {
1189
+ const [, name, params] = m;
1190
+ if (name === "constructor" || name === "init" || name === "if") continue;
1191
+ const args = params.trim();
1192
+ if (args === "" || args.split(",").every((p) => p.includes("=") || /\?\s*:/u.test(p) || p.trim() === "")) return name;
1193
+ }
1194
+ return null;
1195
+ }
1196
+ /**
1197
+ * Les services d'une cible : classe, clé de conteneur, source.
1198
+ *
1199
+ * Point UNIQUE qui sait où vivent les services et comment on les nomme —
1200
+ * `--service` de `create command` et `--inject` de `create service` en
1201
+ * dépendent tous deux. Deux façons de « trouver les services » divergeraient en
1202
+ * silence, chacune passant ses propres essais.
1203
+ *
1204
+ * Un fichier dont on ne sait pas lire le `super("…", …)` est ignoré : sans clé
1205
+ * de conteneur, il n'est atteignable par personne, donc il n'est pas un
1206
+ * candidat.
1207
+ */
1208
+ function listTargetServices(targetDir, writer) {
1209
+ const dir = path.join(targetDir, "nodefony", "service");
1210
+ if (!writer.exists(dir)) return [];
1211
+ const found = [];
1212
+ for (const entry of writer.listDir(dir)) {
1213
+ if (entry.isDirectory || !entry.name.endsWith("Service.ts")) continue;
1214
+ const file = path.join(dir, entry.name);
1215
+ const key = readNodefonyName(file, writer);
1216
+ if (key === null) continue;
1217
+ found.push({
1218
+ pascal: entry.name.replace(/\.ts$/u, ""),
1219
+ key,
1220
+ source: writer.read(file)
1221
+ });
1222
+ }
1223
+ return found;
1224
+ }
1225
+ /**
1226
+ * Service appelable de la cible : sa classe, sa clé de conteneur, sa méthode.
1227
+ *
1228
+ * ⚠️ La méthode est CHERCHÉE, pas supposée. Elle a longtemps été exigée par son
1229
+ * nom (`greet`, celui du gabarit) — or ce même gabarit dit « à remplacer par la
1230
+ * vôtre » : suivre le conseil cassait `--service`, sur un message qui réclamait
1231
+ * une méthode d'exemple. Un générateur ne doit pas exiger que son propre exemple
1232
+ * soit resté intact.
1233
+ *
1234
+ * ⚠️ **Le premier venu n'est une réponse que s'il est le SEUL.** Sans `wanted`,
1235
+ * cette fonction rendait le premier service de l'ordre du disque — donc le
1236
+ * premier alphabétiquement. Tant qu'une application n'avait qu'un service
1237
+ * d'exemple, le hasard tombait juste ; dès le deuxième, la commande générée
1238
+ * appelait un service que personne n'avait demandé, et rien ne le disait. Une
1239
+ * application réelle en a dix.
1240
+ *
1241
+ * @param targetDir - racine de la cible (app ou module).
1242
+ * @param writer - accès disque injecté.
1243
+ * @param wanted - nom du service voulu : classe (`ReportService`, `Report`) ou
1244
+ * clé de conteneur (`report`). Omis, la cible ne doit en avoir qu'un.
1245
+ * @returns `{ pascal, key, method }`, ou `null` si la cible n'a aucun service
1246
+ * dont une méthode soit appelable sans argument.
1247
+ * @throws Si `wanted` ne correspond à rien, ou si plusieurs services sont
1248
+ * appelables et qu'aucun n'est désigné — dans les deux cas en NOMMANT les
1249
+ * candidats : refuser en donnant le choix coûte un essai, produire un appel
1250
+ * vers le mauvais service coûte un débogage.
1251
+ */
1252
+ function findTargetService(targetDir, writer, wanted) {
1253
+ const callables = [];
1254
+ for (const { pascal, key, source } of listTargetServices(targetDir, writer)) {
1255
+ const method = findCallableMethod(source);
1256
+ if (method !== null) callables.push({
1257
+ pascal,
1258
+ key,
1259
+ method
1260
+ });
1261
+ }
1262
+ if (callables.length === 0) return null;
1263
+ const names = callables.map((s) => `${s.pascal} (clé « ${s.key} »)`).join(", ");
1264
+ if (wanted !== void 0) {
1265
+ const needle = wanted.toLowerCase().replace(/service$/u, "");
1266
+ const found = callables.find((s) => s.pascal.toLowerCase().replace(/service$/u, "") === needle || s.key.toLowerCase() === wanted.toLowerCase());
1267
+ if (found === void 0) throw new Error(`--service ${wanted} : aucun service de ce nom dans ${targetDir} — services appelables : ${names}`);
1268
+ return found;
1269
+ }
1270
+ if (callables.length > 1) throw new Error(`--service : ${callables.length} services appelables, précise lequel (--service <Nom>) — ${names}`);
1271
+ return callables[0];
1272
+ }
1273
+ /**
1274
+ * Câble `this.addCommand(<Classe>)` dans le constructeur du Module d'un
1275
+ * `index.ts` : ajoute l'import après le dernier import, insère l'appel juste
1276
+ * après le `super(…)` du constructeur.
1277
+ *
1278
+ * L'ancre est le `super(…)` et non l'accolade du constructeur : c'est la seule
1279
+ * ligne dont l'existence est GARANTIE (un `Module` doit appeler `super`), et
1280
+ * insérer après elle vaut pour un constructeur vide comme pour un constructeur
1281
+ * déjà rempli. Édition textuelle gardée, même contrat que
1282
+ * {@link wireDecoratorList} : toute ambiguïté = throw actionnable, jamais un
1283
+ * fichier corrompu.
1284
+ *
1285
+ * @throws si la classe y est déjà, si aucun import n'ancre l'insertion, ou si
1286
+ * le `super(…)` du constructeur est introuvable (le message donne l'édition
1287
+ * manuelle exacte).
1288
+ */
1289
+ function wireCommandCall(indexPath, className, importPath, writer) {
1290
+ const source = writer.read(indexPath);
1291
+ const importLine = `import ${className} from "${importPath}";`;
1292
+ const callLine = `this.addCommand(${className});`;
1293
+ if (new RegExp(`\\b${className}\\b`, "u").test(source)) throw new Error(`${className} est déjà référencé dans ${indexPath} — choisis un autre nom`);
1294
+ const last = [...source.matchAll(/^import [^\n]*$/gmu)].at(-1);
1295
+ if (!last || last.index === void 0) throw new Error(`aucun import trouvé dans ${indexPath} — ajoute à la main :\n ${importLine}\n ${callLine} dans le constructeur`);
1296
+ const importAt = last.index + last[0].length;
1297
+ const withImport = source.slice(0, importAt) + `\n${importLine}` + source.slice(importAt);
1298
+ const match = /super\([^()]*\);/u.exec(withImport);
1299
+ if (!match || match.index === void 0) throw new Error(`super(…) du constructeur introuvable dans ${indexPath} — ajoute à la main :\n ${importLine}\n ${callLine} après le super(…) du constructeur`);
1300
+ const lineStart = withImport.lastIndexOf("\n", match.index) + 1;
1301
+ const indent = /^[ \t]*/u.exec(withImport.slice(lineStart, match.index))?.[0] ?? " ";
1302
+ const at = match.index + match[0].length;
1303
+ writer.write(indexPath, `${withImport.slice(0, at)}\n${indent}${callLine}${withImport.slice(at)}`);
1304
+ }
1305
+ /**
1306
+ * Scaffold IN-PROJECT d'une commande CLI : rend la classe dans
1307
+ * `<cible>/nodefony/command/` puis la câble dans le constructeur du Module
1308
+ * cible (`this.addCommand(…)`).
1309
+ *
1310
+ * Le nom complet est DÉRIVÉ : `<module>:<action>`, où le module est celui que
1311
+ * l'`index.ts` de la cible déclare (`super("blog", …)`) — pas le nom npm du
1312
+ * paquet, qui peut différer. Écrire le préfixe soi-même est toléré (il est
1313
+ * strippé), jamais exigé.
1314
+ *
1315
+ * @throws hors projet, cible inconnue (le message liste les cibles), action
1316
+ * vide, phase inconnue, `--service` sans service appelable, ou wiring
1317
+ * impossible (actionnable).
1318
+ */
1319
+ function runCommandScaffold(request, answers, packageRoot, writer) {
1320
+ const projectRoot = findProjectRoot(request.dir);
1321
+ if (!projectRoot) throw new Error("aucun projet Nodefony ici (nodefony.config.ts introuvable en remontant) — lance la commande depuis une app (créée par `nodefony create app`)");
1322
+ const moduleName = String(answers.module ?? "");
1323
+ const target = resolveScaffoldTarget(projectRoot, moduleName, writer);
1324
+ const indexPath = path.join(target.dir, "index.ts");
1325
+ const prefix = readNodefonyName(indexPath, writer) ?? target.name.split("/").pop();
1326
+ let action = String(answers.name).trim().toLowerCase();
1327
+ if (action === prefix) action = "";
1328
+ else if (action.startsWith(`${prefix}:`)) action = action.slice(prefix.length + 1);
1329
+ if (action === "") throw new Error(`action requise (le préfixe « ${prefix} » est ajouté seul) — ex : nodefony create command publish${moduleName ? ` --module ${moduleName}` : ""}`);
1330
+ const phase = String(answers.phase);
1331
+ const commandName = `${prefix}:${action}`;
1332
+ const nameClass = `${toPascalCase(action.replaceAll(":", "-"))}Command`;
1333
+ const serviceName = String(answers.serviceName ?? "").trim();
1334
+ const service = answers.service === true ? findTargetService(target.dir, writer, serviceName === "" ? void 0 : serviceName) : null;
1335
+ if (answers.service === true && service === null) throw new Error(`--service : aucun service appelable dans ${target.name} (attendu : nodefony/service/*Service.ts exposant une méthode publique sans argument obligatoire) — relance sans --service`);
1336
+ const eta = new Eta(ETA_OPTIONS);
1337
+ const written = [];
1338
+ renderLayer(eta, path.join(packageRoot, "templates", "command"), target.dir, {
1339
+ nameClass,
1340
+ commandName,
1341
+ phase,
1342
+ description: String(answers.description) || `Commande ${commandName} de ${prefix}`,
1343
+ service
1344
+ }, written, writer, { __NAME__: nameClass });
1345
+ wireCommandCall(indexPath, nameClass, `./nodefony/command/${nameClass}`, writer);
1346
+ written.push("index.ts");
1347
+ return {
1348
+ dest: target.dir,
1349
+ files: written.sort(),
1350
+ linked: [],
1351
+ notes: [`CLI nodefony ${commandName} [who] [-j] (phase ${phase})`, `visible dans \`nodefony --help\` une fois le module construit`]
1352
+ };
1353
+ }
1354
+ /**
1355
+ * Valeur d'exemple d'un champ — VALIDE au regard du schéma de l'entité.
1356
+ *
1357
+ * Un échantillon n'est pas une décoration : les tests générés s'en servent pour
1358
+ * insérer, et la documentation pour montrer un appel qui marche. S'il viole le
1359
+ * schéma, l'entité naît avec un test rouge, et la première impression du
1360
+ * générateur est qu'il produit du code cassé.
1361
+ *
1362
+ * Ce piège s'est présenté trois fois, sur trois types ajoutés à trois moments
1363
+ * différents — l'énumération, puis le décimal et le caractère fixe. Chaque fois,
1364
+ * la valeur générique `nom-1` passait à côté d'une contrainte que le schéma, lui,
1365
+ * faisait respecter. D'où cette fonction unique : tout type qui restreint ses
1366
+ * valeurs doit dire ici ce qu'il accepte, sinon rien ne le rappellera.
1367
+ *
1368
+ * @param field - le champ à illustrer.
1369
+ * @param id - stratégie de clé primaire (une référence vers une clé numérique
1370
+ * attend un nombre, pas une chaîne).
1371
+ * @returns `fixed` pour le JSON de la documentation, `expr` pour la fabrique
1372
+ * paramétrée des tests (une expression TypeScript où `n` varie).
1373
+ */
1374
+ /**
1375
+ * Le filtre sur lequel le test généré peut prouver qu'un filtre FILTRE — ou
1376
+ * `null` si aucun champ ne s'y prête.
1377
+ *
1378
+ * Prouver un refus (`?actf=x` → 400) et prouver un filtre sont deux choses
1379
+ * différentes, et la seconde exige une ligne qui NE matche PAS. Sans elle,
1380
+ * l'assertion « toutes les lignes rendues portent la valeur demandée » reste
1381
+ * vraie quand le filtre est ignoré : tous les échantillons d'un booléen valent
1382
+ * `true`, ceux d'une énumération valent sa première valeur ({@link sampleValue}).
1383
+ * Le test serait vert sur un filtre débranché — exactement le défaut qu'il est
1384
+ * censé attraper.
1385
+ *
1386
+ * D'où les deux seules natures retenues, celles qui offrent un contraire sûr et
1387
+ * gratuit. Une clé étrangère en est écartée : poser une seconde valeur
1388
+ * demanderait de créer l'entité visée, et le test parlerait d'autre chose.
1389
+ *
1390
+ * Deux écritures de chaque valeur, et ce n'est pas une redondance : `match` part
1391
+ * dans l'URL (`?publie=true` — tout y est du texte), `matchJson` est la
1392
+ * littérale TypeScript posée dans le corps envoyé (`publie: true`, un booléen —
1393
+ * `"true"` y serait refusé par le schéma).
1394
+ *
1395
+ * @param fields - les champs déclarés de l'entité.
1396
+ * @returns le nom du filtre et ses deux valeurs sous les deux écritures, ou
1397
+ * `null` si l'entité n'a aucun champ éprouvable.
1398
+ */
1399
+ function filterProbe(fields) {
1400
+ for (const f of fields) {
1401
+ if (f.nullable) continue;
1402
+ if (f.type === "bool") return {
1403
+ name: f.name,
1404
+ match: "true",
1405
+ matchJson: "true",
1406
+ other: "false",
1407
+ otherJson: "false"
1408
+ };
1409
+ if (f.type === "enum" && (f.values?.length ?? 0) >= 2) {
1410
+ const [a, b] = f.values;
1411
+ return {
1412
+ name: f.name,
1413
+ match: a,
1414
+ matchJson: JSON.stringify(a),
1415
+ other: b,
1416
+ otherJson: JSON.stringify(b)
1417
+ };
1418
+ }
1419
+ }
1420
+ return null;
1421
+ }
1422
+ /**
1423
+ * Le filtre sur lequel une valeur MAL FORMÉE existe, et de quoi la fabriquer —
1424
+ * ou `null` si aucun filtre de l'entité ne peut en refuser une.
1425
+ *
1426
+ * Toutes les natures ne refusent pas : un filtre `"string"` accepte n'importe
1427
+ * quelle chaîne, par construction. Le test généré visait pourtant le PREMIER
1428
+ * filtre déclaré quel qu'il soit, et attendait un `400` — sur une entité dont
1429
+ * le seul filtre est une clé étrangère à identifiant textuel
1430
+ * (`author:ref:Author` avec des `uuid`), il exigeait donc le refus d'une valeur
1431
+ * parfaitement valide, et échouait sur le générateur au lieu de l'éprouver.
1432
+ *
1433
+ * @param filters - le vocabulaire de filtre déjà calculé (nom → littérale de sa
1434
+ * nature, telle qu'elle sera écrite dans le gabarit).
1435
+ * @returns le nom du filtre et une valeur qu'il doit refuser, ou `null`.
1436
+ */
1437
+ function malformedProbe(filters) {
1438
+ for (const f of filters) {
1439
+ if (f.def === "\"boolean\"") return {
1440
+ name: f.name,
1441
+ value: "oui"
1442
+ };
1443
+ if (f.def === "\"int\"") return {
1444
+ name: f.name,
1445
+ value: "abc"
1446
+ };
1447
+ if (f.def.startsWith("[")) return {
1448
+ name: f.name,
1449
+ value: "valeur-hors-domaine"
1450
+ };
1451
+ }
1452
+ return null;
1453
+ }
1454
+ function sampleValue(field, id) {
1455
+ const { name, type } = field;
1456
+ if (type === "int" || type === "float") return {
1457
+ fixed: 1,
1458
+ expr: "n"
1459
+ };
1460
+ if (type === "bool") return {
1461
+ fixed: true,
1462
+ expr: "true"
1463
+ };
1464
+ if (type === "json") return {
1465
+ fixed: {},
1466
+ expr: "{}"
1467
+ };
1468
+ if (type === "date") return {
1469
+ fixed: "2026-01-01T00:00:00.000Z",
1470
+ expr: "new Date()"
1471
+ };
1472
+ if (type === "enum") {
1473
+ const value = field.defaultValue ?? field.values?.[0] ?? "";
1474
+ return {
1475
+ fixed: value,
1476
+ expr: JSON.stringify(value)
1477
+ };
1478
+ }
1479
+ const echantillonUuid = () => ({
1480
+ fixed: `00000000-0000-4000-8000-${"1".padStart(12, "0")}`,
1481
+ expr: `\`00000000-0000-4000-8000-\${String(n).padStart(12, "0")}\``
1482
+ });
1483
+ if (type === "ref") return id === "serial" ? {
1484
+ fixed: 1,
1485
+ expr: "n"
1486
+ } : echantillonUuid();
1487
+ if (type === "uuid") return echantillonUuid();
1488
+ if (type === "decimal") return {
1489
+ fixed: "12.34",
1490
+ expr: "`${n}.50`"
1491
+ };
1492
+ if (type === "char") {
1493
+ const size = field.length ?? 1;
1494
+ return {
1495
+ fixed: "A".repeat(size),
1496
+ expr: `String(n).padStart(${size}, "A").slice(-${size})`
1497
+ };
1498
+ }
1499
+ const max = field.length;
1500
+ if (type === "string" && max !== void 0) return {
1501
+ fixed: `${name}-1`.slice(0, max),
1502
+ expr: `\`${name}-\${n}\`.slice(0, ${max})`
1503
+ };
1504
+ return {
1505
+ fixed: `${name}-1`,
1506
+ expr: `\`${name}-\${n}\``
1507
+ };
1508
+ }
1509
+ /** Pluriel simple, suffisant pour un nom de table (`Post` → `posts`, `Story` → `stories`). */
1510
+ function pluralize(word) {
1511
+ if (/[^aeiou]y$/u.test(word)) return `${word.slice(0, -1)}ies`;
1512
+ if (/(s|x|z|ch|sh)$/u.test(word)) return `${word}es`;
1513
+ return `${word}s`;
1514
+ }
1515
+ /**
1516
+ * `BlogPost` → `blog_posts` — nom de table SQL par DÉFAUT.
1517
+ *
1518
+ * Par défaut seulement : `--table` impose un nom littéral, seul moyen d'épouser une
1519
+ * table qui existe déjà (`website`, `session`) — la pluralisation ne se devine pas à
1520
+ * l'envers, et une table réelle ne demande la permission à personne.
1521
+ *
1522
+ * La casse vient de {@link toSnakeCase}, la même que celle des colonnes : deux
1523
+ * grammaires auraient fini par diverger sur un nom composé.
1524
+ */
1525
+ function tableName(pascal) {
1526
+ return pluralize(toSnakeCase(pascal));
1527
+ }
1528
+ /**
1529
+ * Identifiant SQL acceptable — minuscules, chiffres, tiret bas.
1530
+ *
1531
+ * Volontairement plus strict que ce que les moteurs tolèrent entre guillemets : un
1532
+ * nom qui exige d'être cité voyage mal d'un dialecte à l'autre, et le générateur
1533
+ * l'écrirait sans les guillemets qui le sauvent.
1534
+ */
1535
+ const SQL_NAME_RE = /^[a-z_][a-z0-9_]*$/u;
1536
+ /** Refuse un identifiant SQL AVANT toute écriture, en nommant ce qui est attendu. */
1537
+ function assertSqlName(value, option) {
1538
+ if (!SQL_NAME_RE.test(value)) throw new Error(`${option} invalide « ${value} » — un identifiant SQL commence par une lettre minuscule ou « _ », puis minuscules, chiffres et « _ » (ex : website_id)`);
1539
+ return value;
1540
+ }
1541
+ /**
1542
+ * Dialecte SQL du connecteur visé — **l'infra déclarée d'abord**, le fichier ensuite.
1543
+ *
1544
+ * L'ordre n'est pas un détail de confort : c'est celui du RUNTIME. Une
1545
+ * application déclare sa base par URL (`NF_DATABASE_URL`, modèle « infra
1546
+ * déclarée » — docker, conteneur, CI, production), et `nodefony.config.ts` ne
1547
+ * porte alors aucun dialecte. Le scaffold, qui ne lisait que le fichier,
1548
+ * retombait sur `sqlite` et générait du Drizzle SQLite pour une application qui
1549
+ * tourne sur PostgreSQL. Le DDL de développement créait ensuite les colonnes
1550
+ * telles quelles : chaînes bornées devenues `text`, identifiants `uuid` devenus
1551
+ * `text` — et là c'est structurel, PostgreSQL refuse `text = uuid`, donc toute
1552
+ * jointure écrite ensuite échoue.
1553
+ *
1554
+ * Mesuré sur un schéma réel (banc de schéma, umami) : **18 identifiants
1555
+ * dégradés et 32 longueurs perdues** sur 83 colonnes, sans un mot.
1556
+ *
1557
+ * La déduction depuis l'URL n'est pas réécrite ici : {@link resolveInfra} en est
1558
+ * la source unique, la même que celle qu'exécute le kernel. Un scaffold qui
1559
+ * dériverait son propre scheme divergerait au premier alias ajouté.
1560
+ *
1561
+ * Le fichier reste consulté ensuite : un connecteur qui déclare explicitement
1562
+ * son dialecte l'emporte sur un défaut, et une app multi-connecteurs n'a pas
1563
+ * qu'une base.
1564
+ */
1565
+ function detectDialect(projectRoot, writer, connector = "default") {
1566
+ const connectors = readConnectors(projectRoot, writer);
1567
+ const found = connectors.find((c) => c.name === connector);
1568
+ if (found && declaresDialect(projectRoot, writer, connector)) return found.dialect;
1569
+ const declared = infraDialect(projectRoot);
1570
+ if (declared) return declared;
1571
+ if (found) return found.dialect;
1572
+ return connectors[0]?.dialect ?? "sqlite";
1573
+ }
1574
+ /**
1575
+ * Le dialecte du connecteur est-il ÉCRIT dans `nodefony.config.ts` ?
1576
+ *
1577
+ * {@link readConnectors} ne peut pas répondre : il rend `sqlite` aussi bien pour
1578
+ * un `dialect: "sqlite"` assumé que pour une absence de déclaration. Distinguer
1579
+ * les deux est tout l'enjeu — sans cela, l'infra déclarée ne pourrait jamais
1580
+ * s'appliquer, puisqu'un défaut lui ressemblerait toujours à un choix.
1581
+ */
1582
+ function declaresDialect(projectRoot, writer, connector) {
1583
+ const configPath = path.join(projectRoot, "nodefony.config.ts");
1584
+ if (!writer.exists(configPath)) return false;
1585
+ const block = extractBlock(writer.read(configPath), "connectors");
1586
+ if (block === null) return false;
1587
+ const entry = /(\w+)\s*:\s*\{([^{}]*)\}/gu;
1588
+ let match;
1589
+ while ((match = entry.exec(block)) !== null) {
1590
+ if (match[1] !== connector) continue;
1591
+ return /dialect\s*:\s*["'](\w+)["']/u.test(match[2] ?? "");
1592
+ }
1593
+ return false;
1594
+ }
1595
+ /**
1596
+ * Dialecte de l'infra DÉCLARÉE du projet, cascade `.env` comprise.
1597
+ *
1598
+ * `process.env` seul ne suffit pas : une URL posée dans `.env.local` (le cas
1599
+ * nominal en développement — le fichier est gitignoré, c'est là qu'on met sa
1600
+ * base) n'est pas dans l'environnement du terminal qui lance le scaffold. On
1601
+ * emprunte donc l'ORDRE de la cascade au runtime ({@link envFileOrder}) plutôt
1602
+ * que d'en inventer un : un ordre affiché qui différerait de l'ordre appliqué
1603
+ * serait pire que pas d'ordre du tout.
1604
+ *
1605
+ * @returns le dialecte SQL, ou `null` si aucune base n'est déclarée (ou si elle
1606
+ * n'est pas SQL — une base mongo ne dicte aucun dialecte à `create entity`).
1607
+ */
1608
+ function infraDialect(projectRoot) {
1609
+ const env = {};
1610
+ try {
1611
+ for (const name of envFileOrder()) {
1612
+ const file = path.join(projectRoot, name);
1613
+ if (!existsSync(file)) continue;
1614
+ for (const line of readFileSync(file, "utf8").split("\n")) {
1615
+ const m = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/u.exec(line);
1616
+ if (!m?.[1]) continue;
1617
+ if (env[m[1]] === void 0) env[m[1]] = m[2]?.trim().replace(/^["']|["']$/gu, "");
1618
+ }
1619
+ }
1620
+ } catch {}
1621
+ try {
1622
+ const dialect = resolveInfra({
1623
+ ...env,
1624
+ ...process.env
1625
+ }).database?.dialect;
1626
+ return dialect && ENTITY_DIALECTS.includes(dialect) ? dialect : null;
1627
+ } catch {
1628
+ return null;
1629
+ }
1630
+ }
1631
+ /**
1632
+ * Contenu de l'objet qui suit `<clé>:`, borné par son accolade APPARIÉE.
1633
+ *
1634
+ * Sans appariement, une expression régulière lâche continue de trouver des
1635
+ * entrées bien après la fin du bloc visé — elle ramasserait alors tout le reste
1636
+ * du fichier de configuration.
1637
+ *
1638
+ * @returns le corps du bloc (accolades exclues), ou `null` si la clé est absente.
1639
+ */
1640
+ function extractBlock(source, key) {
1641
+ const at = source.indexOf(key);
1642
+ if (at < 0) return null;
1643
+ const open = source.indexOf("{", at);
1644
+ if (open < 0) return null;
1645
+ let depth = 0;
1646
+ for (let i = open; i < source.length; i += 1) if (source[i] === "{") depth += 1;
1647
+ else if (source[i] === "}") {
1648
+ depth -= 1;
1649
+ if (depth === 0) return source.slice(open + 1, i);
1650
+ }
1651
+ return null;
1652
+ }
1653
+ /**
1654
+ * Connecteurs déclarés par l'application, lus SANS démarrer le noyau.
1655
+ *
1656
+ * Lecture TEXTUELLE assumée : `nodefony.config.ts` est du TypeScript, et
1657
+ * l'exécuter pour lire une clé coûterait un boot complet. On isole donc le bloc
1658
+ * `connectors: { … }` puis chaque entrée `<nom>: { … dialect: "…" }`.
1659
+ *
1660
+ * Pourquoi ne pas se contenter de chercher le premier `dialect:` du fichier :
1661
+ * une application qui déclare deux connecteurs de moteurs différents ferait
1662
+ * générer TOUTES ses entités dans le dialecte du premier, sans un mot.
1663
+ *
1664
+ * @param projectRoot - racine du projet (là où vit `nodefony.config.ts`).
1665
+ * @param writer - accès fichiers transactionnel du scaffold.
1666
+ * @returns les connecteurs trouvés, dans l'ordre de déclaration (vide si aucun).
1667
+ */
1668
+ function readConnectors(projectRoot, writer) {
1669
+ const configPath = path.join(projectRoot, "nodefony.config.ts");
1670
+ if (!writer.exists(configPath)) return [];
1671
+ const source = writer.read(configPath);
1672
+ const asDialect = (value) => ENTITY_DIALECTS.includes(value ?? "") ? value : "sqlite";
1673
+ const connectors = [];
1674
+ const block = extractBlock(source, "connectors");
1675
+ if (block !== null) {
1676
+ const entry = /(\w+)\s*:\s*\{([^{}]*)\}/gu;
1677
+ let match;
1678
+ while ((match = entry.exec(block)) !== null) {
1679
+ const [, name, body] = match;
1680
+ if (!name) continue;
1681
+ const ddl = /ddl\s*:\s*["'](\w+)["']/u.exec(body ?? "")?.[1];
1682
+ connectors.push({
1683
+ name,
1684
+ dialect: asDialect(/dialect\s*:\s*["'](\w+)["']/u.exec(body ?? "")?.[1]),
1685
+ ...ddl !== void 0 ? { ddl } : {}
1686
+ });
1687
+ }
1688
+ }
1689
+ if (connectors.length > 0) return connectors;
1690
+ return [{
1691
+ name: "default",
1692
+ dialect: asDialect(/dialect\s*:\s*["'](\w+)["']/u.exec(source)?.[1])
1693
+ }];
1694
+ }
1695
+ /**
1696
+ * Entités déjà déclarées dans une cible, lues au disque.
1697
+ *
1698
+ * Sert à transformer `ref:` d'un texte libre en un CHOIX : le formulaire de
1699
+ * Studio comme le dialogue du terminal peuvent proposer ce qui existe vraiment,
1700
+ * au lieu de laisser deviner un nom qui fera échouer le démarrage.
1701
+ */
1702
+ function readEntities(targetDir, writer) {
1703
+ const dir = path.join(targetDir, "nodefony", "entity");
1704
+ if (!writer.exists(dir)) return [];
1705
+ return writer.listDir(dir).filter((file) => !file.isDirectory && file.name.endsWith(".ts") && !file.name.endsWith(".schema.ts")).map((file) => file.name.slice(0, -3)).sort();
1706
+ }
1707
+ /**
1708
+ * Scaffold IN-PROJECT d'une entité (`create entity`) : table Drizzle native, interface
1709
+ * de ligne, schémas Zod d'entrée, service CRUD, controller REST + WebSocket, tests.
1710
+ *
1711
+ * Câble la cible : `@entities([...])` (créé au besoin) et `@controllers([...])`.
1712
+ *
1713
+ * @throws hors projet · cible inconnue · aucun module ORM dans les dépendances ·
1714
+ * entité déjà déclarée · champ mal formé.
1715
+ */
1716
+ function runEntityScaffold(request, answers, packageRoot, writer) {
1717
+ const projectRoot = findProjectRoot(request.dir);
1718
+ if (!projectRoot) throw new Error("aucun projet Nodefony ici (nodefony.config.ts introuvable en remontant) — lance la commande depuis une app (créée par `nodefony create app`)");
1719
+ const target = resolveScaffoldTarget(projectRoot, String(answers.module ?? ""), writer);
1720
+ const manifestPath = path.join(target.dir, "package.json");
1721
+ const manifest = JSON.parse(writer.read(manifestPath));
1722
+ const depsOf = (dir) => {
1723
+ const file = path.join(dir, "package.json");
1724
+ if (!writer.exists(file)) return /* @__PURE__ */ new Set();
1725
+ const pkg = JSON.parse(writer.read(file));
1726
+ return new Set([
1727
+ "dependencies",
1728
+ "devDependencies",
1729
+ "peerDependencies"
1730
+ ].flatMap((b) => Object.keys(pkg[b] ?? {})));
1731
+ };
1732
+ const targetDeps = depsOf(target.dir);
1733
+ const projectDeps = depsOf(projectRoot);
1734
+ if (!targetDeps.has("@nodefony/drizzle") && !projectDeps.has("@nodefony/drizzle")) throw new Error(`@nodefony/drizzle absent de ${target.name} — ajoute la dep + use("@nodefony/drizzle") au manifeste modules de nodefony.config.ts, puis relance`);
1735
+ const ormRuntimeNote = [];
1736
+ const depsAdded = [];
1737
+ {
1738
+ const rootManifestPath = path.join(projectRoot, "package.json");
1739
+ let rootManifest = null;
1740
+ const ensure = (section, dep, why) => {
1741
+ if (projectDeps.has(dep)) return;
1742
+ rootManifest ??= JSON.parse(writer.read(rootManifestPath));
1743
+ rootManifest[section] ??= {};
1744
+ rootManifest[section][dep] = SCAFFOLD_VERSIONS[dep];
1745
+ depsAdded.push(dep);
1746
+ ormRuntimeNote.push(`dépendance manquante ajoutée au package.json : ${dep}@${SCAFFOLD_VERSIONS[dep]} (${why})`);
1747
+ };
1748
+ ensure("dependencies", "drizzle-orm", "l'entité l'importe en direct");
1749
+ ensure("devDependencies", "drizzle-kit", "`nodefony orm:generate` le pilote pour écrire les migrations");
1750
+ if (rootManifest !== null) writer.write(rootManifestPath, `${JSON.stringify(rootManifest, null, 2)}\n`);
1751
+ }
1752
+ const base = String(answers.name).replace(/[-_]?[Ee]ntity$/u, "");
1753
+ const pascal = toPascalCase(base);
1754
+ const reserved = findReservedEntity(pascal);
1755
+ if (reserved && !reserved.appOwned) throw new Error(`create entity ${pascal} : ce nom appartient au module « ${reserved.module} » (entité « ${reserved.name} ») — deux entités du même nom ne cohabitent pas dans le registre ORM, et l'application ne démarrerait plus.\n → ${reserved.advice}`);
1756
+ const isUserEntity = reserved?.appOwned === true;
1757
+ if (isUserEntity && target.kind !== "app") throw new Error(`create entity ${pascal} : l'entité de l'utilisateur ne peut vivre que dans l'application RACINE, pas dans le module « ${target.name} ». L'ordre de chargement des modules n'est pas garanti : posée ailleurs, elle arriverait parfois APRÈS celle du framework, et le registre d'entités refuserait le doublon par une exception au démarrage.\n → relancer sans « --module »`);
1758
+ if (isUserEntity) {
1759
+ const refuse = (option, value, impose) => {
1760
+ throw new Error(`create entity ${pascal} : « ${option} ${value} » est refusé — ${impose}. Le framework écrit ces noms en dur dans ses requêtes (recherche par compte externe, listing) : une divergence ne lèverait pas, elle rendrait simplement des résultats vides.\n → relancer sans « ${option} »`);
1761
+ };
1762
+ if (String(answers.table ?? "").trim()) refuse("--table", String(answers.table), "la table s'appelle « User »");
1763
+ if (answers.columnCase !== void 0 && answers.columnCase !== "camel") refuse("--column-case", String(answers.columnCase), "les colonnes du contrat sont en camelCase");
1764
+ if (String(answers.id) === "serial") refuse("--id", String(answers.id), "l'identifiant de l'utilisateur est un UUID (un entier auto-incrémenté est devinable, donc énumérable)");
1765
+ if (answers.idName !== void 0 && String(answers.idName) !== "id") refuse("--id-name", String(answers.idName), "la clé primaire s'appelle « id »");
1766
+ }
1767
+ const camel = pascal.charAt(0).toLowerCase() + pascal.slice(1);
1768
+ const kebab = toKebabCase(base);
1769
+ const tableAnswer = String(answers.table ?? "").trim();
1770
+ const table = isUserEntity ? userTableName(target.dir) : tableAnswer ? assertSqlName(tableAnswer, "--table") : tableName(pascal);
1771
+ const columnCase = String(answers.columnCase ?? "camel");
1772
+ if (!COLUMN_CASES.includes(columnCase)) throw new Error(`--column-case invalide « ${columnCase} » — attendus : ${COLUMN_CASES.join(" | ")}`);
1773
+ const idName = assertSqlName(String(answers.idName ?? "id").trim() || "id", "--id-name");
1774
+ const connector = String(answers.connector || "default");
1775
+ const dialect = String(answers.dialect || "") || detectDialect(projectRoot, writer, connector);
1776
+ if (!ENTITY_DIALECTS.includes(dialect)) throw new Error(`dialecte invalide « ${dialect} » — attendus : ${ENTITY_DIALECTS.join(" | ")}`);
1777
+ const id = String(answers.id);
1778
+ if (!ENTITY_ID_KINDS.includes(id)) throw new Error(`--id invalide « ${id} » — attendus : ${ENTITY_ID_KINDS.join(" | ")}`);
1779
+ const requested = parseEntityFields(String(answers.fields ?? ""));
1780
+ const contract = isUserEntity ? readUserContract(target.dir) : null;
1781
+ if (isUserEntity) assertUserFieldsAreFillable(requested);
1782
+ const fields = contract ? [...userContractFields(contract), ...requested] : requested;
1783
+ if (fields.length === 0) throw new Error(`create entity ${pascal} : aucun champ déclaré — passe-les en arguments (ex : nodefony create entity ${pascal} title:string! body:text? views:int=0)`);
1784
+ const timestamps = isUserEntity ? false : answers.timestamps !== false;
1785
+ const softDelete = answers.softDelete === true;
1786
+ const toList = (value) => Array.isArray(value) ? value.map(String) : typeof value === "string" && value.length > 0 ? [value] : [];
1787
+ const indexes = [...parseEntityIndexes(toList(answers.index), fields, {
1788
+ timestamps,
1789
+ softDelete
1790
+ }), ...parseEntityIndexes(toList(answers.uniqueIndex), fields, {
1791
+ timestamps,
1792
+ softDelete
1793
+ }, true)];
1794
+ const codegen = buildEntityCodegen(fields, {
1795
+ dialect,
1796
+ id,
1797
+ timestamps,
1798
+ softDelete,
1799
+ table,
1800
+ indexes,
1801
+ columnCase,
1802
+ idName
1803
+ });
1804
+ const sortable = [
1805
+ "id",
1806
+ ...fields.filter((f) => f.type !== "json").map((f) => f.name),
1807
+ ...timestamps ? ["createdAt", "updatedAt"] : []
1808
+ ];
1809
+ const filterDef = (f) => {
1810
+ if (f.type === "bool") return "\"boolean\"";
1811
+ if (f.type === "enum" && f.values?.length) return JSON.stringify(f.values).replace(/","/g, "\", \"");
1812
+ if (f.type === "ref") return id === "serial" ? "\"int\"" : "\"string\"";
1813
+ return null;
1814
+ };
1815
+ const filters = fields.map((f) => ({
1816
+ name: f.name,
1817
+ def: filterDef(f)
1818
+ })).filter((e) => e.def !== null);
1819
+ const defaultOrder = timestamps ? "[[\"createdAt\", \"DESC\"], [\"id\", \"DESC\"]]" : "[[\"id\", \"DESC\"]]";
1820
+ for (const field of fields) {
1821
+ if (field.type !== "ref" || !field.target) continue;
1822
+ const targetFile = path.join(target.dir, "nodefony", "entity", `${field.target}.ts`);
1823
+ if (!writer.exists(targetFile)) throw new Error(`create entity ${pascal} : la relation « ${field.name}:ref:${field.target} » vise une entité qui n'existe pas dans ${target.name} — crée-la d'abord (nodefony create entity ${field.target} …), puis relance`);
1824
+ }
1825
+ const relations = fields.filter((f) => f.type === "ref" && f.target).map((f) => ({
1826
+ field: f.name,
1827
+ target: f.target,
1828
+ type: "many-to-one",
1829
+ foreignKey: f.name
1830
+ }));
1831
+ const route = String(answers.route) || `/api/${pluralize(kebab)}`;
1832
+ const sample = {};
1833
+ const factory = [];
1834
+ for (const f of fields) {
1835
+ if (f.nullable) continue;
1836
+ const { fixed, expr } = sampleValue(f, id);
1837
+ sample[f.name] = fixed;
1838
+ factory.push(`${f.name}: ${expr}`);
1839
+ }
1840
+ const comparableField = fields.find((f) => !f.nullable && f.type !== "date" && f.type !== "json" && f.type !== "ref" && f.type !== "enum")?.name ?? null;
1841
+ const eta = new Eta(ETA_OPTIONS);
1842
+ const written = [];
1843
+ const data = {
1844
+ pascal,
1845
+ camel,
1846
+ kebab,
1847
+ table,
1848
+ route,
1849
+ connector,
1850
+ dialect,
1851
+ moduleName: target.kind === "app" ? "app" : String(target.name),
1852
+ curlBody: JSON.stringify(sample),
1853
+ sampleFactory: `{ ${factory.join(", ")} }`,
1854
+ comparableField,
1855
+ hasUnique: fields.some((f) => f.unique),
1856
+ sortProbe: comparableField ?? "id",
1857
+ filterProbe: filterProbe(fields),
1858
+ malformedProbe: malformedProbe(filters),
1859
+ relationTargets: [...new Set(fields.filter((f) => f.type === "ref" && f.target).map((f) => f.target))],
1860
+ timestamps,
1861
+ softDelete,
1862
+ sortable,
1863
+ filters,
1864
+ defaultOrder,
1865
+ relations,
1866
+ hasSecurity: targetDeps.has("@nodefony/security"),
1867
+ ...codegen
1868
+ };
1869
+ const templates = path.join(packageRoot, "templates", "entity");
1870
+ const tokens = {
1871
+ __PASCAL__: pascal,
1872
+ __KEBAB__: kebab
1873
+ };
1874
+ const service = isUserEntity ? false : answers.service !== false;
1875
+ const controller = service && answers.controller !== false;
1876
+ renderLayer(eta, path.join(templates, "base"), target.dir, data, written, writer, tokens);
1877
+ if (service) renderLayer(eta, path.join(templates, "service"), target.dir, data, written, writer, tokens);
1878
+ if (controller) renderLayer(eta, path.join(templates, "controller"), target.dir, data, written, writer, tokens);
1879
+ if (!isUserEntity && answers.tests !== false) renderLayer(eta, path.join(templates, "tests"), target.dir, data, written, writer, tokens);
1880
+ if (target.kind === "module") {
1881
+ const peer = manifest["peerDependencies"] ??= {};
1882
+ let touched = false;
1883
+ for (const brick of ["@nodefony/orm-core", "@nodefony/drizzle"]) if (!targetDeps.has(brick)) {
1884
+ peer[brick] = "*";
1885
+ touched = true;
1886
+ }
1887
+ if (touched) {
1888
+ writer.write(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
1889
+ written.push("package.json");
1890
+ }
1891
+ }
1892
+ const indexPath = path.join(target.dir, "index.ts");
1893
+ wireEntitiesDecorator(indexPath, `${pascal}Entity`, `./nodefony/entity/${pascal}`, writer, isUserEntity);
1894
+ if (controller) wireDecoratorList(indexPath, "controllers", `${pascal}Controller`, `./nodefony/controllers/${pascal}Controller`, writer);
1895
+ written.push("index.ts");
1896
+ const writtenMode = readConnectors(projectRoot, writer).find((c) => c.name === connector)?.ddl;
1897
+ const schemaBuiltAtBoot = writtenMode !== "none" && writtenMode !== "migrate";
1898
+ const notes = [
1899
+ ...ormRuntimeNote,
1900
+ schemaBuiltAtBoot ? `table ${table} sur le connecteur « ${connector} » (${dialect}) — créée au prochain boot en développement` : `table ${table} sur le connecteur « ${connector} » (${dialect}, schéma : ${writtenMode}) — le démarrage ne la crée PAS`,
1901
+ ...schemaBuiltAtBoot ? [`⚠ en dev, un champ ajouté qui accepte le vide est posé au prochain boot ; un champ OBLIGATOIRE ne l'est pas — « nodefony orm:reset », ou une migration`, `⚠ production : appliquer les migrations AVANT le déploiement (« nodefony orm:migrate »), jamais depuis le processus qui sert le trafic`] : [
1902
+ `→ écrire la migration : nodefony orm:generate --name <nom>`,
1903
+ `→ l'appliquer : nodefony orm:migrate${connector === "default" ? "" : ` --connector ${connector}`}`,
1904
+ `⚠ tant qu'elle n'est pas appliquée, la colonne n'existe pas en base — les requêtes qui la lisent échoueront`
1905
+ ]
1906
+ ];
1907
+ if (controller) notes.push(`REST ${route} (GET liste paginée/POST) · ${route}/{id} (GET/PUT/PATCH/DELETE) — les lectures répondent AUSSI par la socket`);
1908
+ if (!service) notes.push("service et controller non générés (--no-service)");
1909
+ else if (!controller) notes.push("controller non généré (--no-controller)");
1910
+ return {
1911
+ dest: target.dir,
1912
+ files: written.sort(),
1913
+ linked: [],
1914
+ notes,
1915
+ depsAdded
1916
+ };
1917
+ }
1918
+ /**
1919
+ * Ajoute une entité au décorateur `@entities([...])` de la cible — **en le créant
1920
+ * s'il n'existe pas encore**.
1921
+ *
1922
+ * Différence avec `wireDecoratorList` : une app n'a aucune raison de porter un
1923
+ * `@entities([])` vide tant qu'elle n'a pas d'entité, et les apps déjà générées n'en
1924
+ * ont pas. Plutôt que d'exiger une ancre (throw) ou de l'imposer à tous les templates,
1925
+ * on **pose le décorateur au premier usage**, juste au-dessus de `@controllers` (ou de
1926
+ * la déclaration de classe s'il n'y en a pas).
1927
+ *
1928
+ * Édition textuelle conservatrice : toute ambiguïté fait échouer avec un geste à
1929
+ * appliquer à la main, plutôt que de corrompre le fichier.
1930
+ *
1931
+ * @param indexPath - `index.ts` de la cible.
1932
+ * @param className - nom du descripteur (`PostEntity`).
1933
+ * @param importPath - chemin relatif du descripteur.
1934
+ * @throws si l'entité est déjà référencée, ou si aucun point d'insertion n'est trouvé.
1935
+ */
1936
+ /**
1937
+ * Position d'insertion devant la classe `… extends Module` — décorateurs de
1938
+ * classe COMPRIS, puisque `@entities([…])` doit se poser au-dessus d'eux.
1939
+ *
1940
+ * Volontairement PAS une expression régulière. Le motif naturel
1941
+ * (`(?:@\w+\([\s\S]*?\)\s*)*class …`) est ambigu — le contenu paresseux d'un
1942
+ * décorateur peut se découper de plusieurs façons entre deux itérations — donc
1943
+ * exponentiel au backtracking : mesuré ici, 26 décorateurs sans classe à trouver
1944
+ * coûtaient 636 ms, et chaque paire supplémentaire multipliait par ~3,5. Or le
1945
+ * pire cas EST le cas d'échec prévu par l'appelant (ancre introuvable) : la
1946
+ * commande figeait au lieu de rendre son geste manuel.
1947
+ *
1948
+ * Le balayage ci-dessous remonte les décorateurs un par un en appariant les
1949
+ * parenthèses — linéaire, et correct sur un décorateur qui en contient
1950
+ * lui-même (`@entities([defineEntity(x)])`).
1951
+ *
1952
+ * @param source - contenu de l'`index.ts` de la cible.
1953
+ * @returns l'index d'insertion, ou `undefined` si aucune classe `extends Module`.
1954
+ */
1955
+ function findModuleClassAnchor(source) {
1956
+ const match = /^class\s+\w+\s+extends\s+Module\b/mu.exec(source);
1957
+ if (!match || match.index === void 0) return;
1958
+ let at = match.index;
1959
+ for (;;) {
1960
+ let end = at - 1;
1961
+ while (end >= 0 && /\s/u.test(source[end])) end--;
1962
+ if (end < 0 || source[end] !== ")") return at;
1963
+ let depth = 0;
1964
+ let open = end;
1965
+ for (; open >= 0; open--) {
1966
+ const char = source[open];
1967
+ if (char === ")") depth++;
1968
+ else if (char === "(") {
1969
+ depth--;
1970
+ if (depth === 0) break;
1971
+ }
1972
+ }
1973
+ if (open < 0) return at;
1974
+ let name = open - 1;
1975
+ while (name >= 0 && /[\w.]/u.test(source[name])) name--;
1976
+ if (name < 0 || source[name] !== "@" || name === open - 1) return at;
1977
+ at = name;
1978
+ }
1979
+ }
1980
+ /**
1981
+ * Câble un descripteur d'entité au module de la cible.
1982
+ *
1983
+ * @param indexPath - `index.ts` de la cible (app racine ou module).
1984
+ * @param className - nom exporté du descripteur.
1985
+ * @param importPath - chemin d'import, en `/`.
1986
+ * @param writer - écrivain du scaffold (respecte `--dry-run`).
1987
+ * @param alreadyWiredIsNormal - `true` quand RÉGÉNÉRER l'entité est le geste
1988
+ * attendu, et non un doublon. C'est le cas de l'utilisateur : son fichier
1989
+ * PRESCRIT « relance la commande avec tes champs », et l'entité est déjà
1990
+ * câblée depuis la naissance de l'application. Le refus reste entier pour
1991
+ * toutes les autres : deux entités du même nom dans un registre PLAT font
1992
+ * échouer le démarrage.
1993
+ * @throws Si le nom est déjà référencé hors de ce cas, ou si l'`index.ts` n'a
1994
+ * aucun import où s'accrocher.
1995
+ */
1996
+ function wireEntitiesDecorator(indexPath, className, importPath, writer, alreadyWiredIsNormal = false) {
1997
+ const source = writer.read(indexPath);
1998
+ if (new RegExp(`\\b${className}\\b`, "u").test(source)) {
1999
+ if (alreadyWiredIsNormal) return;
2000
+ throw new Error(`${className} est déjà référencé dans ${indexPath} — choisis un autre nom d'entité`);
2001
+ }
2002
+ const importLine = `import { ${className} } from "${importPath}";`;
2003
+ const manual = ` ${importLine}\n @entities([${className}]) sur la classe Module`;
2004
+ const last = [...source.matchAll(/^import [^\n]*$/gmu)].at(-1);
2005
+ if (!last || last.index === void 0) throw new Error(`aucun import trouvé dans ${indexPath} — ajoute à la main :\n${manual}`);
2006
+ const importAt = last.index + last[0].length;
2007
+ const listRe = decoratorListRe("entities");
2008
+ if (listRe.test(source)) {
2009
+ const withImport = source.slice(0, importAt) + `\n${importLine}` + source.slice(importAt);
2010
+ const current = listRe.exec(withImport)[2].trim();
2011
+ const wired = withImport.replace(listRe, `$1@entities([${current ? `${current.replace(/,\s*$/u, "")}, ` : ""}${className}])`);
2012
+ writer.write(indexPath, wired);
2013
+ return;
2014
+ }
2015
+ const anchorRe = /^@controllers\(/mu;
2016
+ if ((anchorRe.exec(source)?.index ?? findModuleClassAnchor(source)) === void 0) throw new Error(`ni @controllers ni « class … extends Module » dans ${indexPath} — ajoute à la main :\n${manual}`);
2017
+ const needsDecoratorImport = !/\bentities\b[^\n]*@nodefony\/orm-core/u.test(source);
2018
+ const injected = `\n${importLine}` + (needsDecoratorImport ? `\nimport { entities } from "@nodefony/orm-core";` : "");
2019
+ const withImports = source.slice(0, importAt) + injected + source.slice(importAt);
2020
+ const shiftedAt = anchorRe.exec(withImports)?.index ?? findModuleClassAnchor(withImports);
2021
+ if (shiftedAt === void 0) throw new Error(`point d'insertion perdu dans ${indexPath} — ajoute à la main :\n${manual}`);
2022
+ const wired = withImports.slice(0, shiftedAt) + `@entities([${className}])\n` + withImports.slice(shiftedAt);
2023
+ writer.write(indexPath, wired);
2024
+ }
2025
+ /**
2026
+ * Scaffold IN-PROJECT d'un frontend Vite (`create front`) : pose la coquille
2027
+ * HTML (la MÊME brique que `create app`), l'entry du framework choisi, le
2028
+ * controller de page (`renderDocument` + nonce CSP) et le registrar d'entry ;
2029
+ * câble `@controllers` et le hook `onKernelBoot` de la cible ; complète les
2030
+ * deps npm du framework (catalogue UNIQUE `FRONTEND_PARAMS`).
2031
+ *
2032
+ * @throws hors projet, cible inconnue, cible portant DÉJÀ un front
2033
+ * (`frontend/index.html` — ce scaffold pose l'INITIAL, il ne fusionne pas),
2034
+ * ou dep `@nodefony/frontend` absente de l'APPLICATION. Absente du seul module
2035
+ * visé, elle y est POSÉE en peer : un workspace résout depuis l'arbre de
2036
+ * l'app, et exiger une édition manuelle du `package.json` revenait à demander
2037
+ * à la main ce que ce scaffold existe pour écrire.
2038
+ */
2039
+ function runFrontScaffold(request, answers, packageRoot, writer) {
2040
+ const projectRoot = findProjectRoot(request.dir);
2041
+ if (!projectRoot) throw new Error("aucun projet Nodefony ici (nodefony.config.ts introuvable en remontant) — lance la commande depuis une app (créée par `nodefony create app`)");
2042
+ const target = resolveScaffoldTarget(projectRoot, String(answers.module ?? ""), writer);
2043
+ if (writer.exists(path.join(target.dir, "frontend", "index.html"))) throw new Error(`${target.name} porte déjà un front (frontend/index.html) — ce scaffold pose la coquille et l'entry INITIALES ; pour une page de plus, ajoute une entry au registerEntry existant et un controller de page`);
2044
+ const manifestPath = path.join(target.dir, "package.json");
2045
+ const manifest = JSON.parse(writer.read(manifestPath));
2046
+ const targetDeps = new Set([
2047
+ "dependencies",
2048
+ "devDependencies",
2049
+ "peerDependencies"
2050
+ ].flatMap((b) => Object.keys(manifest[b] ?? {})));
2051
+ let addFrontendPeer = false;
2052
+ if (!targetDeps.has("@nodefony/frontend")) {
2053
+ const appDeps = readAppDependencyNames(projectRoot, writer);
2054
+ if (target.kind === "module" && appDeps.has("@nodefony/frontend")) addFrontendPeer = true;
2055
+ else throw new Error(`@nodefony/frontend manque dans ${target.name} — ajoute la dep + "@nodefony/frontend" au manifeste modules de nodefony.config.ts`);
2056
+ }
2057
+ const frontend = answers.frontend;
2058
+ const front = FRONTEND_PARAMS[frontend];
2059
+ const base = String(answers.name);
2060
+ const pascal = toPascalCase(base);
2061
+ const nameClass = `${pascal}Controller`;
2062
+ const kebab = toKebabCase(base);
2063
+ const route = String(answers.route) || `/${kebab}`;
2064
+ const eta = new Eta(ETA_OPTIONS);
2065
+ const written = [];
2066
+ const data = {
2067
+ nameClass,
2068
+ pascal,
2069
+ kebab,
2070
+ entryName: kebab,
2071
+ route,
2072
+ frontend,
2073
+ front,
2074
+ appName: target.name
2075
+ };
2076
+ const tokens = {
2077
+ __NAME__: nameClass,
2078
+ __PASCAL__: pascal
2079
+ };
2080
+ renderLayer(eta, path.join(packageRoot, "templates", "shared", "front-shell"), target.dir, data, written, writer, tokens);
2081
+ renderLayer(eta, path.join(packageRoot, "templates", "front", "base"), target.dir, data, written, writer, tokens);
2082
+ renderLayer(eta, path.join(packageRoot, "templates", "shared", "front-registrar"), target.dir, data, written, writer, tokens);
2083
+ renderLayer(eta, path.join(packageRoot, "templates", "front", frontend), target.dir, data, written, writer, tokens);
2084
+ renderLayer(eta, path.join(packageRoot, "templates", "shared", "front-entry", frontend), target.dir, data, written, writer, tokens);
2085
+ if (frontend === "angular") renderLayer(eta, path.join(packageRoot, "templates", "shared", "ng-app-tsconfig"), target.dir, data, written, writer, tokens);
2086
+ if (frontend === "vue") renderLayer(eta, path.join(packageRoot, "templates", "shared", "vue-shim"), target.dir, data, written, writer, tokens);
2087
+ if (frontend === "svelte") renderLayer(eta, path.join(packageRoot, "templates", "shared", "svelte-shim"), target.dir, data, written, writer, tokens);
2088
+ const added = [];
2089
+ const addDeps = (block, entries) => {
2090
+ if (Object.keys(entries).length === 0) return;
2091
+ const deps = manifest[block] ??= {};
2092
+ for (const [name, version] of Object.entries(entries)) if (!targetDeps.has(name)) {
2093
+ deps[name] = version;
2094
+ added.push(name);
2095
+ }
2096
+ };
2097
+ addDeps("dependencies", front.deps);
2098
+ addDeps("devDependencies", front.devDeps);
2099
+ if (addFrontendPeer) addDeps("peerDependencies", { "@nodefony/frontend": "*" });
2100
+ if (added.length > 0) {
2101
+ writer.write(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
2102
+ written.push("package.json");
2103
+ }
2104
+ const indexPath = path.join(target.dir, "index.ts");
2105
+ wireDecoratorList(indexPath, "controllers", nameClass, `./nodefony/controllers/${nameClass}`, writer);
2106
+ const wireNote = wireKernelBootCall(indexPath, `register${pascal}Entry`, `./nodefony/frontend/register${pascal}Entry`, writer);
2107
+ written.push("index.ts");
2108
+ const notes = [
2109
+ `page ${route} (GET — controller ${nameClass}, entry Vite « ${kebab} »)`,
2110
+ ...added.length > 0 ? [`deps ajoutées : ${added.join(", ")} → lance npm install`] : [],
2111
+ ...wireNote ? [wireNote] : []
2112
+ ];
2113
+ return {
2114
+ dest: target.dir,
2115
+ files: written.sort(),
2116
+ linked: [],
2117
+ notes
2118
+ };
2119
+ }
2120
+ /**
2121
+ * Câble un appel dans le hook `onKernelBoot()` de l'`index.ts` cible : import
2122
+ * de la fonction + soit INSERTION d'un hook complet (s'il n'existe pas — le
2123
+ * layout des index générés est connu), soit note actionnable (un hook existant
2124
+ * n'est JAMAIS édité : on ne réécrit pas du code utilisateur).
2125
+ *
2126
+ * @returns note à afficher si un geste manuel reste nécessaire, sinon `null`.
2127
+ */
2128
+ function wireKernelBootCall(indexPath, fnName, importPath, writer) {
2129
+ const source = writer.read(indexPath);
2130
+ const importLine = `import { ${fnName} } from "${importPath}";`;
2131
+ const last = [...source.matchAll(/^import [^\n]*$/gmu)].at(-1);
2132
+ if (!last || last.index === void 0) return `ajoute à la main : ${importLine} + ${fnName}(this); dans onKernelBoot()`;
2133
+ const importAt = last.index + last[0].length;
2134
+ const withImport = source.slice(0, importAt) + `\n${importLine}` + source.slice(importAt);
2135
+ if (/onKernelBoot\s*\(/u.test(withImport)) {
2136
+ writer.write(indexPath, withImport);
2137
+ return `onKernelBoot() existe déjà dans index.ts — ajoute : ${fnName}(this);`;
2138
+ }
2139
+ const hook = `
2140
+ /**
2141
+ * Déclare l'entry frontend auprès du FrontendService — AVANT
2142
+ * onKernelReady pour que le superviseur Vite démarre avec elle.
2143
+ */
2144
+ override async onKernelBoot(): Promise<this> {
2145
+ ${fnName}(this);\n return this;\n }\n`;
2146
+ const closer = /\n\}\s*\n+export default /u.exec(withImport);
2147
+ if (!closer || closer.index === void 0) {
2148
+ writer.write(indexPath, withImport);
2149
+ return `fin de classe introuvable dans index.ts — ajoute à la main le hook :\n override async onKernelBoot(): Promise<this> { ${fnName}(this); return this; }`;
2150
+ }
2151
+ const wired = withImport.slice(0, closer.index) + hook + withImport.slice(closer.index + 1);
2152
+ writer.write(indexPath, wired);
2153
+ return null;
2154
+ }
2155
+ //#endregion
2156
+ export { DATABASE_PARAMS, FRONTEND_PARAMS, ensureWorkspaces, filterProbe, findModuleClassAnchor, findPackageRoot, findProjectRoot, getScaffoldContext, linkLocalDeps, listTargets, malformedProbe, resolveAnswers, resolveDatabase, resolveLocalWorkspaces, runScaffold, sampleValue, scaffoldCaps, toKebabCase, toPascalCase, wireCommandCall, wireDecoratorList, wireEntitiesDecorator, wireKernelBootCall, wireModuleManifest, wireRoleHierarchy, writeAgentPointers };