@starci/hfs 4.2.17 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (564) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +7 -0
  3. package/db/migrate.mjs +361 -0
  4. package/db/post-cutover.mjs +136 -0
  5. package/emit/db-types.mjs +10 -1
  6. package/lint/run.mjs +1 -1
  7. package/package.json +4 -1
  8. package/release/gate.mjs +298 -0
  9. package/release/index.mjs +103 -0
  10. package/release/push.mjs +110 -0
  11. package/runtime/knowledge/hfs/canon-pins.yaml +11 -11
  12. package/runtime/knowledge/hfs/fe-stack.yaml +22 -1
  13. package/runtime/knowledge/hfs/rules.yaml +3437 -160
  14. package/runtime/knowledge/hfs/slots.yaml +430 -92
  15. package/runtime/knowledge/patterns/be/api.yaml +92 -2
  16. package/runtime/knowledge/patterns/be/cli.yaml +37 -9
  17. package/runtime/knowledge/patterns/be/event-bus.yaml +3 -3
  18. package/runtime/knowledge/patterns/be/jobs.yaml +20 -2
  19. package/runtime/knowledge/patterns/be/projections.yaml +4 -2
  20. package/runtime/knowledge/patterns/be/queues.yaml +11 -3
  21. package/runtime/knowledge/patterns/be/reactors.yaml +20 -1
  22. package/runtime/knowledge/patterns/be/realtime.yaml +25 -3
  23. package/runtime/knowledge/patterns/be/saga.yaml +20 -3
  24. package/runtime/knowledge/patterns/be/webhooks.yaml +210 -36
  25. package/runtime/knowledge/patterns/fe/folder.yaml +4 -0
  26. package/runtime/knowledge/sonar-gate.yaml +1 -1
  27. package/runtime/modules/kernel/failure-codes.yaml +3128 -47
  28. package/runtime/modules/models/runtimes.yaml +1 -1
  29. package/runtime/modules/schemas/application-stacks.schema.yaml +663 -0
  30. package/runtime/packages/cli/src/catalog.generated.mjs +14104 -0
  31. package/runtime/packages/cli/src/validate-args.mjs +213 -0
  32. package/runtime/scripts/api/git/add.mjs +5 -0
  33. package/runtime/scripts/api/git/cat-file.mjs +5 -0
  34. package/runtime/scripts/api/git/init.mjs +7 -0
  35. package/runtime/scripts/api/git/lib.mjs +3 -0
  36. package/runtime/scripts/api/git/tag.mjs +5 -0
  37. package/runtime/scripts/api/node/lib.mjs +20 -0
  38. package/runtime/scripts/api/node/run-node.mjs +6 -0
  39. package/runtime/scripts/api/sops/lib.mjs +41 -0
  40. package/runtime/scripts/api/sops/resolve-sops.mjs +7 -0
  41. package/runtime/scripts/api/sops/seal.mjs +1 -1
  42. package/runtime/scripts/api/sops/selected-age-recipient.mjs +7 -0
  43. package/runtime/scripts/api/supabase/database-dump.mjs +5 -0
  44. package/runtime/scripts/api/supabase/database-query.mjs +5 -0
  45. package/runtime/scripts/api/supabase/lib.mjs +44 -0
  46. package/runtime/scripts/api/supabase/migration-repair.mjs +5 -0
  47. package/runtime/scripts/hfs/app-wiring.mjs +201 -0
  48. package/runtime/scripts/hfs/architecture/backend-app-files.mjs +103 -0
  49. package/runtime/scripts/hfs/architecture/backend.mjs +22 -10
  50. package/runtime/scripts/hfs/architecture/background-unowned.mjs +157 -41
  51. package/runtime/scripts/hfs/architecture/canon-clones.mjs +240 -0
  52. package/runtime/scripts/hfs/architecture/canon-jsx-clones.mjs +143 -0
  53. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +6 -3
  54. package/runtime/scripts/hfs/architecture/clones.mjs +17 -8
  55. package/runtime/scripts/hfs/architecture/connection-map.mjs +14 -12
  56. package/runtime/scripts/hfs/architecture/context-map.mjs +25 -14
  57. package/runtime/scripts/hfs/architecture/context-owner.mjs +1 -2
  58. package/runtime/scripts/hfs/architecture/dead-exports-bindings.mjs +149 -0
  59. package/runtime/scripts/hfs/architecture/dead-exports.mjs +23 -50
  60. package/runtime/scripts/hfs/architecture/default-deny.mjs +8 -6
  61. package/runtime/scripts/hfs/architecture/frontend-block-render.mjs +104 -0
  62. package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +30 -11
  63. package/runtime/scripts/hfs/architecture/frontend-world.mjs +1 -1
  64. package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +153 -0
  65. package/runtime/scripts/hfs/architecture/hfs.mjs +3 -3
  66. package/runtime/scripts/hfs/architecture/i18n-keys.mjs +16 -3
  67. package/runtime/scripts/hfs/architecture/machine-ast.mjs +17 -0
  68. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +49 -25
  69. package/runtime/scripts/hfs/architecture/next-data-inference.mjs +176 -0
  70. package/runtime/scripts/hfs/architecture/next-data-symbols.mjs +26 -0
  71. package/runtime/scripts/hfs/architecture/next-data.mjs +31 -23
  72. package/runtime/scripts/hfs/architecture/reachability.mjs +14 -0
  73. package/runtime/scripts/hfs/architecture/register-once.mjs +15 -7
  74. package/runtime/scripts/hfs/architecture/required-files.mjs +2 -1
  75. package/runtime/scripts/hfs/architecture/source-names.mjs +48 -3
  76. package/runtime/scripts/hfs/architecture/staged-consumers.mjs +37 -0
  77. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +214 -17
  78. package/runtime/scripts/hfs/architecture/supabase-be.mjs +1 -6
  79. package/runtime/scripts/hfs/architecture/supabase.mjs +59 -21
  80. package/runtime/scripts/hfs/architecture/surface.mjs +2 -1
  81. package/runtime/scripts/hfs/architecture/tiers.mjs +11 -4
  82. package/runtime/scripts/hfs/architecture/typescript.mjs +1 -8
  83. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +9 -13
  84. package/runtime/scripts/hfs/check-results.mjs +28 -0
  85. package/runtime/scripts/hfs/check.mjs +63 -72
  86. package/runtime/scripts/hfs/coverage-from.mjs +70 -0
  87. package/runtime/scripts/hfs/db-baseline-verb.mjs +276 -0
  88. package/runtime/scripts/hfs/db-plan-verb.mjs +254 -0
  89. package/runtime/scripts/hfs/db-restamp-verb.mjs +60 -0
  90. package/runtime/scripts/hfs/declaration-shape.mjs +39 -3
  91. package/runtime/scripts/hfs/doc-commands.mjs +367 -0
  92. package/runtime/scripts/hfs/edition-slots.mjs +2 -2
  93. package/runtime/scripts/hfs/example-secrets.mjs +158 -0
  94. package/runtime/scripts/hfs/manifest-shape.mjs +91 -6
  95. package/runtime/scripts/hfs/path-findings.mjs +32 -2
  96. package/runtime/scripts/hfs/refusal-codes.mjs +6 -0
  97. package/runtime/scripts/hfs/rule-catalog.mjs +156 -11
  98. package/runtime/scripts/hfs/rule-params-shape.mjs +22 -10
  99. package/runtime/scripts/hfs/rules/a15-findings.mjs +88 -0
  100. package/runtime/scripts/hfs/rules/app-env-required.mjs +20 -0
  101. package/runtime/scripts/hfs/rules/app-root.mjs +2 -2
  102. package/runtime/scripts/hfs/rules/assets.mjs +79 -0
  103. package/runtime/scripts/hfs/rules/boot-proof.mjs +16 -0
  104. package/runtime/scripts/hfs/rules/canon-copy-drift.mjs +59 -0
  105. package/runtime/scripts/hfs/rules/contract.mjs +66 -5
  106. package/runtime/scripts/hfs/rules/coverage-below.mjs +230 -0
  107. package/runtime/scripts/hfs/rules/database-access.mjs +343 -0
  108. package/runtime/scripts/hfs/rules/database-baseline.mjs +142 -0
  109. package/runtime/scripts/hfs/rules/database-config.mjs +250 -2
  110. package/runtime/scripts/hfs/rules/database-constants.mjs +9 -0
  111. package/runtime/scripts/hfs/rules/database-migrations.mjs +317 -37
  112. package/runtime/scripts/hfs/rules/database-plpgsql.mjs +119 -9
  113. package/runtime/scripts/hfs/rules/database-queue-sources.mjs +176 -0
  114. package/runtime/scripts/hfs/rules/database-queues.mjs +361 -0
  115. package/runtime/scripts/hfs/rules/database-sql.mjs +91 -9
  116. package/runtime/scripts/hfs/rules/database.mjs +11 -2
  117. package/runtime/scripts/hfs/rules/deploy-scan.mjs +192 -0
  118. package/runtime/scripts/hfs/rules/deploy-workflow.mjs +219 -0
  119. package/runtime/scripts/hfs/rules/doc-fresh.mjs +286 -0
  120. package/runtime/scripts/hfs/rules/docker-health.mjs +249 -0
  121. package/runtime/scripts/hfs/rules/docker.mjs +24 -7
  122. package/runtime/scripts/hfs/rules/edition.mjs +11 -10
  123. package/runtime/scripts/hfs/rules/env-parity.mjs +373 -0
  124. package/runtime/scripts/hfs/rules/env-value-shape.mjs +113 -0
  125. package/runtime/scripts/hfs/rules/event-bus.mjs +3 -2
  126. package/runtime/scripts/hfs/rules/event-tables.mjs +100 -0
  127. package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +115 -15
  128. package/runtime/scripts/hfs/rules/fe-stack.mjs +57 -0
  129. package/runtime/scripts/hfs/rules/feature-files.mjs +28 -0
  130. package/runtime/scripts/hfs/rules/frontend-tree.mjs +54 -35
  131. package/runtime/scripts/hfs/rules/integration-specs.mjs +3 -2
  132. package/runtime/scripts/hfs/rules/lexicon.mjs +62 -0
  133. package/runtime/scripts/hfs/rules/lite-proof-lane.mjs +89 -0
  134. package/runtime/scripts/hfs/rules/monorepo.mjs +14 -1
  135. package/runtime/scripts/hfs/rules/pipeline.mjs +8 -1
  136. package/runtime/scripts/hfs/rules/proof-commands.mjs +2 -0
  137. package/runtime/scripts/hfs/rules/release-provenance.mjs +211 -0
  138. package/runtime/scripts/hfs/rules/saga-delivery.mjs +66 -0
  139. package/runtime/scripts/hfs/rules/saga.mjs +8 -16
  140. package/runtime/scripts/hfs/rules/script-hygiene.mjs +151 -0
  141. package/runtime/scripts/hfs/rules/secret-custody-only.mjs +65 -0
  142. package/runtime/scripts/hfs/rules/secrets.mjs +26 -0
  143. package/runtime/scripts/hfs/rules/services.mjs +4 -4
  144. package/runtime/scripts/hfs/rules/stack-tooling.mjs +93 -0
  145. package/runtime/scripts/hfs/rules/stacks-deploy.mjs +76 -0
  146. package/runtime/scripts/hfs/rules/stacks-ingress.mjs +87 -0
  147. package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +111 -0
  148. package/runtime/scripts/hfs/rules/stacks-release.mjs +366 -0
  149. package/runtime/scripts/hfs/rules/stacks-runtime.mjs +168 -0
  150. package/runtime/scripts/hfs/rules/stacks-swarm.mjs +417 -0
  151. package/runtime/scripts/hfs/rules/stacks.mjs +13 -8
  152. package/runtime/scripts/hfs/rules/supabase-secrets.mjs +1 -1
  153. package/runtime/scripts/hfs/rules/supply-chain.mjs +167 -0
  154. package/runtime/scripts/hfs/rules/unused-symbol-flow.mjs +101 -0
  155. package/runtime/scripts/hfs/rules/unused-symbol-ledger.mjs +77 -0
  156. package/runtime/scripts/hfs/rules/unused-symbol-translators.mjs +165 -0
  157. package/runtime/scripts/hfs/rules/unused-symbol-values.mjs +103 -0
  158. package/runtime/scripts/hfs/rules/unused-symbol.mjs +320 -0
  159. package/runtime/scripts/hfs/runtime-rules/source-ast.mjs +185 -0
  160. package/runtime/scripts/hfs/runtime-rules/workflow-source.mjs +19 -0
  161. package/runtime/scripts/hfs/sealed-members.mjs +101 -0
  162. package/runtime/scripts/hfs/secret-bootstrap-crypto.mjs +200 -0
  163. package/runtime/scripts/hfs/secret-document.mjs +97 -0
  164. package/runtime/scripts/hfs/secret-edit.mjs +170 -0
  165. package/runtime/scripts/hfs/secret-errors.mjs +20 -0
  166. package/runtime/scripts/hfs/secret-files.mjs +124 -0
  167. package/runtime/scripts/hfs/secret-init.mjs +300 -0
  168. package/runtime/scripts/hfs/secret-output.mjs +100 -0
  169. package/runtime/scripts/hfs/secret-sops.mjs +130 -0
  170. package/runtime/scripts/hfs/secret-store.mjs +163 -0
  171. package/runtime/scripts/hfs/secret.mjs +162 -193
  172. package/runtime/scripts/hfs/slot-imports.mjs +18 -3
  173. package/runtime/scripts/hfs/slot-required.mjs +13 -0
  174. package/runtime/scripts/hfs/slot-semantic-problems.mjs +2 -1
  175. package/runtime/scripts/hfs/slots.mjs +1 -0
  176. package/runtime/scripts/hfs/stack-config-kit.mjs +34 -0
  177. package/runtime/scripts/hfs/stack-deploy-config.mjs +313 -0
  178. package/runtime/scripts/hfs/stack-ingress-config.mjs +237 -0
  179. package/runtime/scripts/hfs/stacks-gate.mjs +414 -0
  180. package/runtime/scripts/hfs/starcistacks-runtime.mjs +61 -0
  181. package/runtime/scripts/hfs/tree-findings.mjs +22 -0
  182. package/runtime/scripts/lib/command-tokens.mjs +126 -0
  183. package/runtime/scripts/lib/compose-values.mjs +11 -0
  184. package/runtime/scripts/lib/deployment-findings.mjs +7 -0
  185. package/runtime/scripts/lib/json-clone.mjs +61 -0
  186. package/runtime/scripts/lib/list.mjs +4 -0
  187. package/runtime/scripts/lib/roots.mjs +59 -0
  188. package/runtime/scripts/lib/runtime-example.mjs +13 -0
  189. package/runtime/scripts/lib/secret-patterns.mjs +60 -5
  190. package/runtime/scripts/lib/sql-ast.mjs +32 -0
  191. package/runtime/scripts/lib/stack-services.mjs +1 -1
  192. package/runtime/scripts/lib/swarm-stack.mjs +126 -0
  193. package/runtime/scripts/lib/trim-end.mjs +8 -0
  194. package/runtime/scripts/lib/ts-analysis.mjs +23 -0
  195. package/runtime/scripts/lib/ts-ast.mjs +27 -2
  196. package/runtime/scripts/lib/workflow-events.mjs +8 -0
  197. package/scaffold/add-app.mjs +77 -59
  198. package/scaffold/add-cli-lite.mjs +24 -17
  199. package/scaffold/add-compose.mjs +79 -0
  200. package/scaffold/add-database-lite.mjs +14 -0
  201. package/scaffold/add-fe-overlay.mjs +122 -0
  202. package/scaffold/add-fe-page.mjs +269 -0
  203. package/scaffold/add-inbox-lite.mjs +87 -124
  204. package/scaffold/add-plan.mjs +183 -0
  205. package/scaffold/add-table.mjs +132 -70
  206. package/scaffold/add.mjs +186 -204
  207. package/scaffold/app.mjs +128 -43
  208. package/scaffold/edition-gate.mjs +3 -3
  209. package/scaffold/image.mjs +2 -2
  210. package/scaffold/lite-exports.mjs +2 -1
  211. package/scaffold/service.mjs +2 -2
  212. package/src/main.mjs +238 -119
  213. package/stack/age-install.mjs +37 -0
  214. package/stack/backup.mjs +100 -0
  215. package/stack/build-args.mjs +42 -0
  216. package/stack/build.mjs +173 -0
  217. package/stack/cli.mjs +130 -0
  218. package/stack/config.mjs +57 -0
  219. package/stack/context.mjs +57 -0
  220. package/stack/converge.mjs +107 -0
  221. package/stack/deploy.mjs +301 -0
  222. package/stack/digest.mjs +16 -0
  223. package/stack/docker-transport.mjs +53 -0
  224. package/stack/edge-alias.mjs +118 -0
  225. package/stack/errors.mjs +21 -0
  226. package/stack/image-scan.mjs +152 -0
  227. package/stack/ingress-create.mjs +114 -0
  228. package/stack/ingress-publish.mjs +273 -0
  229. package/stack/ingress.mjs +706 -0
  230. package/stack/jobs.mjs +51 -0
  231. package/stack/live-restore.mjs +177 -0
  232. package/stack/prepare.mjs +266 -0
  233. package/stack/private-file.mjs +22 -0
  234. package/stack/provision.mjs +90 -0
  235. package/stack/prune.mjs +46 -0
  236. package/stack/release-store.mjs +87 -0
  237. package/stack/remote.mjs +205 -0
  238. package/stack/render.mjs +149 -0
  239. package/stack/retire.mjs +47 -0
  240. package/stack/sealed.mjs +149 -0
  241. package/stack/task.mjs +42 -0
  242. package/stack/verify.mjs +99 -0
  243. package/sync/ci-commands.mjs +13 -0
  244. package/sync/index.mjs +127 -64
  245. package/sync/stack-images.mjs +127 -0
  246. package/templates/app/ci-workflows/github/workflows/ci.yml +5 -1
  247. package/templates/app/ci-workflows/github/workflows/images.yml +15 -3
  248. package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +148 -0
  249. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +7 -3
  250. package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +38 -6
  251. package/templates/app/ci-workflows-lite/github/workflows/images.yml +14 -2
  252. package/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml +148 -0
  253. package/templates/app/codegen-graphql.mjs.partial +13 -2
  254. package/templates/app/codegen-lite/scripts/codegen.mjs +6 -1
  255. package/templates/app/codegen-prelude.mjs.partial +8 -0
  256. package/templates/app/gitignore +4 -0
  257. package/templates/app/gitignore-lite +4 -0
  258. package/templates/app/hooks/husky/pre-commit +7 -12
  259. package/templates/app/package-scripts/package.json +1 -0
  260. package/templates/app/package-scripts/supabase.json +7 -0
  261. package/templates/app/package-scripts-lite/package.json +1 -7
  262. package/templates/app/skeleton/.editorconfig +1 -0
  263. package/templates/app/skeleton/.gitattributes +7 -1
  264. package/templates/app/skeleton/.github/dependabot.yml +17 -0
  265. package/templates/app/skeleton/.nvmrc +1 -1
  266. package/templates/app/skeleton/.sops.yaml +1 -1
  267. package/templates/app/skeleton/.starcistacks/application-stacks.yaml +134 -1
  268. package/templates/app/skeleton/.starcistacks/dev/README.md +16 -0
  269. package/templates/app/skeleton/.starcistacks/dev/environment.json +5 -0
  270. package/templates/app/skeleton/.starcistacks/dev/infra/compose/stack.yaml +71 -0
  271. package/templates/app/skeleton/.starcistacks/dev/runtime/env/KEYS.md +14 -0
  272. package/templates/app/skeleton/.starcistacks/production/README.md +37 -0
  273. package/templates/app/skeleton/.starcistacks/production/environment.json +5 -0
  274. package/templates/app/skeleton/.starcistacks/production/infra/swarm/Caddyfile +15 -0
  275. package/templates/app/skeleton/.starcistacks/production/infra/swarm/owner-actions.md +11 -0
  276. package/templates/app/skeleton/.starcistacks/production/infra/swarm/recovery/restore-proof.md +7 -0
  277. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/current.yaml +6 -0
  278. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/images.lock.yaml +9 -0
  279. package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/previous.yaml +6 -0
  280. package/templates/app/skeleton/.starcistacks/production/infra/swarm/stack.yaml +142 -0
  281. package/templates/app/skeleton/.starcistacks/production/runtime/env/KEYS.md +14 -0
  282. package/templates/app/skeleton/README.md +6 -6
  283. package/templates/app/skeleton/scripts/codegen.mjs +1 -2
  284. package/templates/app/skeleton-lite/.editorconfig +16 -0
  285. package/templates/app/skeleton-lite/.gitattributes +8 -0
  286. package/templates/app/skeleton-lite/.github/dependabot.yml +17 -0
  287. package/templates/app/skeleton-lite/.nvmrc +1 -0
  288. package/templates/app/skeleton-lite/.sops.yaml +3 -2
  289. package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +193 -0
  290. package/templates/app/skeleton-lite/.starcistacks/dev/README.md +20 -0
  291. package/templates/app/skeleton-lite/.starcistacks/dev/environment.json +2 -1
  292. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/Caddyfile +16 -0
  293. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/owner-actions.md +11 -0
  294. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/recovery/restore-proof.md +7 -0
  295. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/current.yaml +6 -0
  296. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/images.lock.yaml +8 -0
  297. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/releases/previous.yaml +6 -0
  298. package/templates/app/skeleton-lite/.starcistacks/dev/infra/swarm/stack.yaml +120 -0
  299. package/templates/app/skeleton-lite/.starcistacks/dev/runtime/env/KEYS.md +18 -0
  300. package/templates/app/skeleton-lite/.starcistacks/production/README.md +38 -0
  301. package/templates/app/skeleton-lite/.starcistacks/production/environment.json +6 -0
  302. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/Caddyfile +11 -0
  303. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/owner-actions.md +11 -0
  304. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/recovery/restore-proof.md +7 -0
  305. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/current.yaml +6 -0
  306. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/images.lock.yaml +8 -0
  307. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/releases/previous.yaml +6 -0
  308. package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +121 -0
  309. package/templates/app/skeleton-lite/.starcistacks/production/runtime/env/KEYS.md +21 -0
  310. package/templates/app/skeleton-lite/.starciwork/brand/index.yaml +12 -0
  311. package/templates/app/skeleton-lite/.starciwork/workspace.yaml +9 -0
  312. package/templates/app/skeleton-lite/README.md +8 -4
  313. package/templates/app/skeleton-lite/supabase/config.toml +8 -1
  314. package/templates/app/skeleton-lite/supabase/migrations/__timestamp___baseline.sql +15 -6
  315. package/templates/app/starciwork.gitignore-lite +6 -0
  316. package/templates/app/upgrade-full/.starcistacks/dev/README.md +13 -7
  317. package/templates/be/image/worker/Dockerfile +1 -1
  318. package/templates/be/inbox/database-client.spec.ts.tpl +22 -28
  319. package/templates/be/inbox/database-client.ts.tpl +14 -28
  320. package/templates/be/inbox/delivery-recorded.event.ts.tpl +36 -0
  321. package/templates/be/inbox/event-index.ts.tpl +1 -0
  322. package/templates/be/inbox/events.json.tpl +10 -0
  323. package/templates/be/inbox/migration.sql.tpl +1 -0
  324. package/templates/be/inbox/platform/event-bus/event-bus.contracts.ts.tpl +42 -0
  325. package/templates/be/inbox/platform/event-bus/event-bus.decorators.ts.tpl +9 -0
  326. package/templates/be/inbox/platform/event-bus/event-bus.module-definition.ts.tpl +6 -0
  327. package/templates/be/inbox/platform/event-bus/event-bus.module.ts.tpl +19 -0
  328. package/templates/be/inbox/platform/event-bus/event-bus.options.ts.tpl +2 -0
  329. package/templates/be/inbox/platform/event-bus/event-bus.port.ts.tpl +16 -0
  330. package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +45 -0
  331. package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +15 -0
  332. package/templates/be/inbox/platform/event-bus/index.ts.tpl +5 -0
  333. package/templates/be/inbox/platform/event-bus/persistence/event-bus.sql.ts.tpl +5 -0
  334. package/templates/be/inbox/platform/inbox/inbox.decorators.ts.tpl +9 -0
  335. package/templates/be/inbox/platform/inbox/inbox.module-definition.ts.tpl +6 -0
  336. package/templates/be/inbox/platform/inbox/inbox.module.ts.tpl +19 -0
  337. package/templates/be/inbox/platform/inbox/inbox.options.ts.tpl +2 -0
  338. package/templates/be/inbox/platform/inbox/inbox.port.ts.tpl +10 -0
  339. package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +41 -0
  340. package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +19 -0
  341. package/templates/be/inbox/platform/inbox/index.ts.tpl +3 -0
  342. package/templates/be/inbox/platform/inbox/persistence/inbox.sql.ts.tpl +7 -0
  343. package/templates/be/inbox/platform/migration.sql.tpl +48 -0
  344. package/templates/be/inbox/service.spec.ts.tpl +49 -20
  345. package/templates/be/inbox/service.ts.tpl +27 -16
  346. package/templates/be/inbox/sql.ts.tpl +2 -2
  347. package/templates/be/inbox/webhook-door.spec.ts.tpl +3 -3
  348. package/templates/be/inbox/webhook-door.ts.tpl +4 -3
  349. package/templates/be/patterns/api/feature.module-definition.ts.tpl +6 -0
  350. package/templates/be/patterns/api/feature.module.ts.tpl +2 -1
  351. package/templates/be/patterns/api/feature.options.ts.tpl +2 -0
  352. package/templates/be/patterns/api/graphql.module-definition.ts.tpl +6 -0
  353. package/templates/be/patterns/api/graphql.module.ts.tpl +3 -2
  354. package/templates/be/patterns/api/graphql.options.ts.tpl +2 -0
  355. package/templates/be/patterns/cli/group.module-definition.ts.tpl +6 -0
  356. package/templates/be/patterns/cli/group.module.ts.tpl +2 -1
  357. package/templates/be/patterns/cli/group.options.ts.tpl +2 -0
  358. package/templates/be/patterns/event-bus/platform/errors/event-bus.error.ts.tpl +0 -3
  359. package/templates/be/patterns/event-bus/platform/messages/event-bus.messages.ts.tpl +0 -2
  360. package/templates/be/patterns/jobs/job.module-definition.ts.tpl +6 -0
  361. package/templates/be/patterns/jobs/job.module.ts.tpl +2 -1
  362. package/templates/be/patterns/jobs/job.options.ts.tpl +2 -0
  363. package/templates/be/patterns/jobs/job.queue.module-definition.ts.tpl +6 -0
  364. package/templates/be/patterns/jobs/job.queue.module.ts.tpl +6 -3
  365. package/templates/be/patterns/jobs/job.queue.options.ts.tpl +2 -0
  366. package/templates/be/patterns/jobs/platform/fenced.processor.spec.ts.tpl +3 -1
  367. package/templates/be/patterns/outbox/platform/outbox-relay.policy.spec.ts.tpl +25 -9
  368. package/templates/be/patterns/queues/platform/queue.config.ts.tpl +1 -1
  369. package/templates/be/patterns/queues/platform/queue.index.ts.tpl +1 -1
  370. package/templates/be/patterns/queues/platform/queue.module-definition.ts.tpl +2 -2
  371. package/templates/be/patterns/queues/platform/queue.module.ts.tpl +11 -0
  372. package/templates/be/patterns/queues/platform/queue.options.ts.tpl +10 -0
  373. package/templates/be/patterns/queues/queue.module-definition.ts.tpl +6 -0
  374. package/templates/be/patterns/queues/queue.module.ts.tpl +2 -1
  375. package/templates/be/patterns/queues/queue.options.ts.tpl +2 -0
  376. package/templates/be/patterns/reactors/message.module-definition.ts.tpl +6 -0
  377. package/templates/be/patterns/reactors/message.module.ts.tpl +6 -3
  378. package/templates/be/patterns/reactors/message.options.ts.tpl +2 -0
  379. package/templates/be/patterns/reactors/reactor.module-definition.ts.tpl +6 -0
  380. package/templates/be/patterns/reactors/reactor.module.ts.tpl +2 -1
  381. package/templates/be/patterns/reactors/reactor.options.ts.tpl +2 -0
  382. package/templates/be/patterns/realtime/channel.gateway.spec.ts.tpl +188 -0
  383. package/templates/be/patterns/realtime/channel.gateway.ts.tpl +22 -0
  384. package/templates/be/patterns/realtime/channel.graphql.module-definition.ts.tpl +6 -0
  385. package/templates/be/patterns/realtime/channel.graphql.module.ts.tpl +2 -1
  386. package/templates/be/patterns/realtime/channel.graphql.options.ts.tpl +2 -0
  387. package/templates/be/patterns/realtime/channel.websocket.module-definition.ts.tpl +6 -0
  388. package/templates/be/patterns/realtime/channel.websocket.module.ts.tpl +110 -0
  389. package/templates/be/patterns/realtime/channel.websocket.options.ts.tpl +7 -0
  390. package/templates/be/patterns/saga/saga.message.module-definition.ts.tpl +6 -0
  391. package/templates/be/patterns/saga/saga.message.module.ts.tpl +6 -3
  392. package/templates/be/patterns/saga/saga.message.options.ts.tpl +2 -0
  393. package/templates/be/patterns/saga/saga.module-definition.ts.tpl +6 -0
  394. package/templates/be/patterns/saga/saga.module.ts.tpl +2 -1
  395. package/templates/be/patterns/saga/saga.options.ts.tpl +2 -0
  396. package/templates/be/patterns/webhooks/webhook.door.spec.ts.tpl +3 -3
  397. package/templates/be/patterns/webhooks/webhook.door.ts.tpl +3 -3
  398. package/templates/be/patterns/webhooks/webhook.http.module-definition.ts.tpl +6 -0
  399. package/templates/be/patterns/webhooks/webhook.http.module.ts.tpl +2 -1
  400. package/templates/be/patterns/webhooks/webhook.http.options.ts.tpl +2 -0
  401. package/templates/be/platform/graphql/localize-errors.mapper.spec.ts +80 -26
  402. package/templates/be/platform/graphql/localize-errors.mapper.ts +2 -5
  403. package/templates/be/skeleton/apps/__app__/src/__app__.env.ts +21 -0
  404. package/templates/be/skeleton/apps/__app__/src/__app__.options.ts +1 -1
  405. package/templates/be/skeleton/apps/__app__/src/app.module.ts +15 -45
  406. package/templates/be/skeleton/apps/__app__/src/compose/api.ts +4 -0
  407. package/templates/be/skeleton/apps/__app__/src/compose/domain.ts +11 -0
  408. package/templates/be/skeleton/apps/__app__/src/compose/platform.ts +44 -0
  409. package/templates/be/skeleton/apps/__app__/src/compose/system.ts +5 -0
  410. package/templates/be/skeleton/apps/__app__/src/compose/webhooks.ts +4 -0
  411. package/templates/be/skeleton/apps/__app__/src/main.ts +18 -10
  412. package/templates/be/skeleton/apps/cli/src/app.module.ts +3 -13
  413. package/templates/be/skeleton/apps/cli/src/cli.env.ts +13 -0
  414. package/templates/be/skeleton/apps/cli/src/cli.options.ts +7 -17
  415. package/templates/be/skeleton/apps/cli/src/compose/cli.ts +5 -0
  416. package/templates/be/skeleton/apps/cli/src/compose/platform.ts +24 -0
  417. package/templates/be/skeleton/apps/cli/src/main.ts +3 -2
  418. package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.contracts.ts +2 -0
  419. package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.handler.ts +22 -0
  420. package/templates/be/skeleton/src/features/api/system-health/application/check-readiness.query.ts +11 -0
  421. package/templates/be/skeleton/src/features/api/system-health/system-health.module-definition.ts +6 -0
  422. package/templates/be/skeleton/src/features/api/system-health/system-health.module.ts +5 -3
  423. package/templates/be/skeleton/src/features/api/system-health/system-health.options.ts +2 -0
  424. package/templates/be/skeleton/src/features/api/system-health/transport/http/ready.controller.ts +19 -0
  425. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module-definition.ts +6 -0
  426. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module.ts +8 -3
  427. package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.options.ts +2 -0
  428. package/templates/be/skeleton/src/features/cli/cli.module-definition.ts +6 -0
  429. package/templates/be/skeleton/src/features/cli/cli.module.ts +3 -2
  430. package/templates/be/skeleton/src/features/cli/cli.options.ts +2 -0
  431. package/templates/be/skeleton/src/features/cli/migrate/migrate.module-definition.ts +6 -0
  432. package/templates/be/skeleton/src/features/cli/migrate/migrate.module.ts +2 -1
  433. package/templates/be/skeleton/src/features/cli/migrate/migrate.options.ts +2 -0
  434. package/templates/be/skeleton/src/features/cli/seed/seed.module-definition.ts +6 -0
  435. package/templates/be/skeleton/src/features/cli/seed/seed.module.ts +2 -1
  436. package/templates/be/skeleton/src/features/cli/seed/seed.options.ts +2 -0
  437. package/templates/be/skeleton/src/features/cli/seed/subs/run.cli.spec.ts +5 -6
  438. package/templates/be/skeleton/src/modules/domain/identity/admission.policy.spec.ts +56 -4
  439. package/templates/be/skeleton/src/modules/domain/identity/admission.policy.ts +36 -5
  440. package/templates/be/skeleton/src/modules/domain/identity/auth.guard.spec.ts +39 -8
  441. package/templates/be/skeleton/src/modules/domain/identity/auth.guard.ts +14 -8
  442. package/templates/be/skeleton/src/modules/domain/identity/credential.contracts.ts +26 -0
  443. package/templates/be/skeleton/src/modules/domain/identity/credential.guard.spec.ts +119 -0
  444. package/templates/be/skeleton/src/modules/domain/identity/credential.guard.ts +43 -0
  445. package/templates/be/skeleton/src/modules/domain/identity/identity.contracts.ts +16 -0
  446. package/templates/be/skeleton/src/modules/domain/identity/identity.decorators.ts +18 -1
  447. package/templates/be/skeleton/src/modules/domain/identity/identity.module.ts +22 -4
  448. package/templates/be/skeleton/src/modules/domain/identity/identity.options.ts +8 -2
  449. package/templates/be/skeleton/src/modules/domain/identity/index.ts +1 -0
  450. package/templates/be/skeleton/src/modules/platform/config/env-source.config.ts +6 -6
  451. package/templates/be/skeleton/src/modules/platform/database/connection-source.client.spec.ts +11 -4
  452. package/templates/be/skeleton/src/modules/platform/database/database-probe.service.spec.ts +32 -0
  453. package/templates/be/skeleton/src/modules/platform/database/database-probe.service.ts +14 -0
  454. package/templates/be/skeleton/src/modules/platform/database/database.module.ts +7 -1
  455. package/templates/be/skeleton/src/modules/platform/database/index.ts +2 -0
  456. package/templates/be/skeleton/src/modules/platform/database/primary.decorators.ts +8 -0
  457. package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.spec.ts +25 -6
  458. package/templates/be/skeleton/src/modules/platform/errors/errors.filter.spec.ts +16 -6
  459. package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.spec.ts +130 -0
  460. package/templates/be/skeleton/src/modules/platform/http-security/edge-security.service.ts +58 -0
  461. package/templates/be/skeleton/src/modules/platform/http-security/errors/http-security.error.ts +3 -0
  462. package/templates/be/skeleton/src/modules/platform/http-security/execution-request.mapper.spec.ts +16 -4
  463. package/templates/be/skeleton/src/modules/platform/http-security/http-security.config.ts +16 -10
  464. package/templates/be/skeleton/src/modules/platform/http-security/http-security.module.ts +3 -1
  465. package/templates/be/skeleton/src/modules/platform/http-security/http-security.options.ts +2 -0
  466. package/templates/be/skeleton/src/modules/platform/http-security/index.ts +5 -0
  467. package/templates/be/skeleton/src/modules/platform/http-security/messages/http-security.messages.ts +12 -5
  468. package/templates/be/skeleton/src/modules/platform/http-security/rate-limit.guard.spec.ts +13 -8
  469. package/templates/be/skeleton/src/modules/platform/http-security/readiness.contracts.ts +5 -0
  470. package/templates/be/skeleton/src/modules/platform/http-security/readiness.decorators.ts +7 -0
  471. package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.spec.ts +86 -0
  472. package/templates/be/skeleton/src/modules/platform/http-security/readiness.service.ts +39 -0
  473. package/templates/be/skeleton/src/modules/platform/http-security/webhook-signature.service.spec.ts +139 -131
  474. package/templates/be/skeleton/src/modules/platform/i18n/index.ts +1 -1
  475. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +91 -67
  476. package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +5 -4
  477. package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +0 -2
  478. package/templates/be/skeleton/src/modules/platform/primitives/outcome.mapper.spec.ts +20 -6
  479. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +16 -4
  480. package/templates/be/skeleton/src/modules/platform/retry/index.ts +3 -0
  481. package/templates/be/skeleton/src/modules/platform/retry/retry.decorators.ts +9 -0
  482. package/templates/be/skeleton/src/modules/platform/retry/retry.module-definition.ts +7 -0
  483. package/templates/be/skeleton/src/modules/platform/retry/retry.module.ts +19 -0
  484. package/templates/be/skeleton/src/modules/platform/retry/retry.options.ts +2 -0
  485. package/templates/be/skeleton/src/modules/platform/retry/retry.port.ts +11 -0
  486. package/templates/be/skeleton/src/modules/platform/retry/system-timer.service.spec.ts +36 -0
  487. package/templates/be/skeleton/src/modules/platform/retry/system-timer.service.ts +10 -0
  488. package/templates/be/skeleton/src/tests/contract/http-security/http-security.contract-spec.ts +54 -0
  489. package/templates/be/skeleton-lite/apps/api/src/api.env.ts +25 -0
  490. package/templates/be/skeleton-lite/apps/api/src/api.options.ts +1 -1
  491. package/templates/be/skeleton-lite/apps/api/src/app.module.ts +18 -58
  492. package/templates/be/skeleton-lite/apps/api/src/compose/api.ts +4 -0
  493. package/templates/be/skeleton-lite/apps/api/src/compose/domain.ts +10 -0
  494. package/templates/be/skeleton-lite/apps/api/src/compose/integrations.ts +16 -0
  495. package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +47 -0
  496. package/templates/be/skeleton-lite/apps/api/src/compose/system.ts +5 -0
  497. package/templates/be/skeleton-lite/apps/api/src/compose/webhooks.ts +4 -0
  498. package/templates/be/skeleton-lite/apps/api/src/main.ts +5 -13
  499. package/templates/be/skeleton-lite/apps/cli/src/cli.env.ts +17 -0
  500. package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +12 -0
  501. package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.spec.ts +71 -5
  502. package/templates/be/skeleton-lite/src/modules/domain/identity/admission.policy.ts +29 -2
  503. package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.spec.ts +68 -15
  504. package/templates/be/skeleton-lite/src/modules/domain/identity/auth.guard.ts +14 -9
  505. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.contracts.ts +9 -2
  506. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.decorators.ts +16 -1
  507. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.module.ts +19 -4
  508. package/templates/be/skeleton-lite/src/modules/domain/identity/identity.options.ts +5 -1
  509. package/templates/be/skeleton-lite/src/modules/domain/identity/index.ts +2 -1
  510. package/templates/be/skeleton-lite/src/modules/integrations/supabase/errors/supabase.error.ts +0 -3
  511. package/templates/be/skeleton-lite/src/modules/integrations/supabase/supabase.client.spec.ts +17 -5
  512. package/templates/be/skeleton-lite/src/modules/platform/database/database.module.ts +24 -13
  513. package/templates/be/skeleton-lite/src/modules/platform/database/database.port.ts +5 -0
  514. package/templates/be/skeleton-lite/src/modules/platform/database/errors/database.error.ts +16 -0
  515. package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +3 -0
  516. package/templates/be/skeleton-lite/src/modules/platform/http-security/http-security.module.ts +5 -2
  517. package/templates/be/skeleton-lite/src/modules/platform/http-security/index.ts +4 -1
  518. package/templates/be/skeleton-lite/src/modules/platform/i18n/index.ts +1 -1
  519. package/templates/be/skeleton-lite/src/modules/platform/logging/logging.log-events.ts +7 -0
  520. package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +29 -0
  521. package/templates/be/skeleton-lite/src/tests/fixtures/builders/cli.builder.ts +16 -0
  522. package/templates/be/table/api/feature.module-definition.ts.tpl +6 -0
  523. package/templates/be/table/api/feature.module.ts.tpl +2 -1
  524. package/templates/be/table/api/feature.options.ts.tpl +2 -0
  525. package/templates/be/table/api/graphql.module-definition.ts.tpl +6 -0
  526. package/templates/be/table/api/graphql.module.ts.tpl +3 -2
  527. package/templates/be/table/api/graphql.options.ts.tpl +2 -0
  528. package/templates/be/table/migration.sql.tpl +1 -0
  529. package/templates/be/upgrade-full/apps/cli/src/app.module.ts +4 -6
  530. package/templates/be/upgrade-full/apps/cli/src/cli.env.ts +7 -0
  531. package/templates/be/upgrade-full/apps/cli/src/compose/cli.ts +5 -0
  532. package/templates/be/upgrade-full/apps/cli/src/compose/platform.ts +9 -0
  533. package/templates/be/upgrade-full/apps/cli/src/main.ts +3 -0
  534. package/templates/be/upgrade-full/src/features/cli/cli.module.ts +3 -2
  535. package/templates/fe/auth/supabase-client.ts.tpl +34 -0
  536. package/templates/fe/skeleton/apps/__app__/src/app/auth/callback/route.ts +11 -0
  537. package/templates/fe/skeleton/apps/__app__/src/components/blocks/SignInForm/index.tsx +6 -1
  538. package/templates/fe/skeleton/apps/__app__/src/components/leaves/Icon/index.tsx +4 -0
  539. package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/component.tsx +31 -0
  540. package/templates/fe/skeleton/apps/__app__/src/features/overlays/AppGuideOverlay/index.tsx +24 -0
  541. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSession.ts +6 -1
  542. package/templates/fe/skeleton/apps/__app__/src/hooks/auth/useSignOut.ts +3 -4
  543. package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/index.ts +1 -0
  544. package/templates/fe/skeleton/apps/__app__/src/hooks/zustand/appGuide/store.ts +15 -0
  545. package/templates/fe/skeleton/apps/__app__/src/modules/auth/pkce-client.ts +18 -0
  546. package/templates/fe/skeleton/apps/__app__/src/modules/auth/server.ts +19 -0
  547. package/templates/fe/skeleton/apps/app/src/features/pages/AppHomePage/index.tsx +7 -1
  548. package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/en.json +57 -0
  549. package/templates/fe/skeleton/apps/app/src/modules/i18n/messages/vi.json +6 -0
  550. package/templates/fe/skeleton/apps/landing/src/modules/i18n/messages/en.json +31 -0
  551. package/templates/fe/skeleton/packages/__project__-i18n/src/routing.ts +1 -1
  552. package/templates/fe/skeleton/packages/__project__-ui/src/index.ts +1 -0
  553. package/templates/fe/skeleton-lite/apps/__app__/src/modules/auth/client.ts +1 -34
  554. package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +7 -1
  555. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/en.json +57 -0
  556. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/vi.json +30 -24
  557. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/routing.ts +1 -1
  558. package/templates/fe/transport/graphql/clients/clients/auth.ts +1 -1
  559. package/templates/fe/transport/graphql/clients/links/locale.ts +2 -2
  560. package/upgrade/check.mjs +3 -1
  561. package/upgrade/file-state.mjs +68 -0
  562. package/upgrade/index.mjs +58 -94
  563. package/runtime/scripts/api/git/log.mjs +0 -5
  564. package/templates/app/skeleton-lite/.starciwork/shell/index.yaml +0 -55
package/scaffold/add.mjs CHANGED
@@ -16,101 +16,18 @@
16
16
  // `@@vCamel@@` its camelCase, `@@vUpper@@` its UPPER_SNAKE and `@@vSnake@@` its snake_case; the option `--service Class=module` gives `@@service@@` (the class) and `@@serviceModule@@`.
17
17
  import fs from 'node:fs';
18
18
  import path from 'node:path';
19
- import { createSlotResolver, loadSlotManifest, readRepoDeclaration, ruleParams } from '../runtime/scripts/hfs/slots.mjs';
20
- import { parseYaml } from '../runtime/engine/yaml.mjs';
21
- import { TEMPLATES_DIR } from '../sync/index.mjs';
22
19
  import { ScaffoldError, pascalOf } from './service.mjs';
23
- import { refuseFullAdapterInLite, refuseInEdition } from './edition-gate.mjs';
24
20
  import { addTable, graphqlPlatformFiles } from './add-table.mjs';
21
+ import { addModuleImport, compositionCode, readComposeGroup, writeComposeGroup } from './add-compose.mjs';
25
22
  import { addApp } from './add-app.mjs';
26
23
  import { ensureLiteCli } from './add-cli-lite.mjs';
27
- import { addLiteWebhookInbox, selectLiteWebhookEntries } from './add-inbox-lite.mjs';
24
+ import { addLiteWebhookInbox } from './add-inbox-lite.mjs';
25
+ import { addBuiltinLiteCli, fillKnownForms, formsOf, planKind, readAddContext } from './add-plan.mjs';
26
+ export { liteApiTemplates } from './add-plan.mjs';
28
27
  import { bundledPins, jsonText, packageJsonText, peerDependencies } from './app.mjs';
29
28
  import { byCodeUnit } from '../report/order.mjs';
30
29
  import { registerLiteDomainService, registerLiteExports } from './lite-exports.mjs';
31
30
 
32
- const KEBAB = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
33
- const VARIABLE = /<([A-Za-z][A-Za-z0-9]*)>/g;
34
- const PLACEHOLDER = /@@([A-Za-z][A-Za-z0-9]*)@@/g;
35
- const PLATFORM_PREFIX = 'src/modules/platform/';
36
- const TOPICS_DIR = path.join(import.meta.dirname, '..', 'runtime', 'knowledge', 'patterns', 'be');
37
- const PATTERN_TEMPLATES = path.join(TEMPLATES_DIR, 'be', 'patterns');
38
-
39
- const camelOf = (name) => { const pascal = pascalOf(name); return pascal[0].toLowerCase() + pascal.slice(1); };
40
-
41
- /** The `files:` tree of the topic `topic`, read from the knowledge file the package carries. */
42
- function readTree(topic) {
43
- const file = path.join(TOPICS_DIR, `${topic}.yaml`);
44
- if (!fs.existsSync(file)) throw new ScaffoldError('HFS_ADD_TOPIC_MISSING', `the pattern topic ${topic} (knowledge/patterns/be/${topic}.yaml) is not carried by this package`);
45
- const doc = parseYaml(fs.readFileSync(file, 'utf8'));
46
- if (!Array.isArray(doc?.files)) throw new ScaffoldError('HFS_ADD_TREE_MISSING', `knowledge/patterns/be/${topic}.yaml has no files tree`);
47
- return doc.files;
48
- }
49
-
50
- /** The parsed document of the topic `topic` (knowledge/patterns/be/<topic>.yaml) the package carries. */
51
- const readTopic = (topic) => ({ id: topic, doc: parseYaml(fs.readFileSync(path.join(TOPICS_DIR, `${topic}.yaml`), 'utf8')) });
52
-
53
- /**
54
- * The lite api tree: the full tree, GraphQL door included (owner ruling 2026-10-13: lite and full have one code structure), with
55
- * the command and handler that hand the verified Supabase principal to the domain service (lite's identity adapter).
56
- */
57
- const selectLiteApiEntries = entries => entries.map(entry => {
58
- if (entry.path === 'src/features/api/<feature>/application/<action>.command.ts') return { ...entry, template: 'api/application.command.principal.ts.tpl' };
59
- if (entry.path === 'src/features/api/<feature>/application/<action>.handler.ts') return { ...entry, template: 'api/application.handler.principal.ts.tpl' };
60
- return entry;
61
- });
62
-
63
- /** The templates the lite api tree names beyond the pattern topic's own files tree (the principal-aware command and handler): the template set stays one set with the topics' (tests/packages-hfs/hfs-add.spec.mjs). */
64
- export const liteApiTemplates = () => selectLiteApiEntries(readTree('api')).map(entry => entry.template).filter(Boolean);
65
-
66
- /** Fills the `<name>` variables of a path; returns null when a variable has no value. */
67
- const fillPath = (text, values) => {
68
- let missing = false;
69
- const filled = text.replace(VARIABLE, (whole, name) => (values[name] === undefined ? ((missing = true), whole) : values[name]));
70
- return missing ? null : filled;
71
- };
72
-
73
- /** The body of a template with its placeholders filled; an unknown placeholder is a refusal. */
74
- const renderBody = (text, values, forms, template) => text.replace(PLACEHOLDER, (whole, name) => {
75
- if (forms[name] === undefined) throw new ScaffoldError('HFS_ADD_PLACEHOLDER_UNKNOWN', `template ${template} uses ${whole}, which no variable of this noun provides`);
76
- return forms[name];
77
- });
78
-
79
- /** The spellings of every variable: kebab (`job`), Pascal (`Job`), camel (`jobCamel`), UPPER_SNAKE (`jobUpper`) and snake_case (`jobSnake`). */
80
- function formsOf(values) {
81
- const forms = {};
82
- for (const [name, value] of Object.entries(values)) {
83
- const kebab = KEBAB.test(value);
84
- forms[name] = value;
85
- forms[name[0].toUpperCase() + name.slice(1)] = kebab ? pascalOf(value) : value[0].toUpperCase() + value.slice(1);
86
- forms[`${name}Camel`] = kebab ? camelOf(value) : value[0].toLowerCase() + value.slice(1);
87
- forms[`${name}Upper`] = kebab ? value.replaceAll('-', '_').toUpperCase() : value;
88
- forms[`${name}Snake`] = kebab ? value.replaceAll('-', '_') : value;
89
- }
90
- return forms;
91
- }
92
-
93
- /** The variables of the noun: the instance name, the options it needs and the defaults of the others. */
94
- function variablesOf({ spec, noun, name, options }) {
95
- if (!KEBAB.test(name)) throw new ScaffoldError('HFS_ADD_NAME_INVALID', `${noun} name ${name} must be kebab-case (letters, digits and single dashes)`);
96
- const values = { [spec.variable]: name };
97
- for (const need of spec.needs ?? []) {
98
- const value = options[need];
99
- if (typeof value !== 'string' || !value) throw new ScaffoldError('HFS_ADD_OPTION_MISSING', `starci app add ${noun} needs --${need}`);
100
- if (need === 'service') {
101
- const [className, module] = value.split('=');
102
- if (!className || !module) throw new ScaffoldError('HFS_ADD_OPTION_INVALID', '--service takes <ServiceClass>=<module specifier>');
103
- values.service = className;
104
- values.serviceModule = module;
105
- } else {
106
- if (!KEBAB.test(value)) throw new ScaffoldError('HFS_ADD_OPTION_INVALID', `--${need} ${value} must be kebab-case`);
107
- values[need] = value;
108
- }
109
- }
110
- for (const [key, rule] of Object.entries(spec.defaults ?? {})) if (values[key] === undefined) values[key] = rule === '@name' ? name : String(rule);
111
- return values;
112
- }
113
-
114
31
  /**
115
32
  * Generates the tree of one noun in the app at `repoRoot` (the folder of hfs.json).
116
33
  *
@@ -126,80 +43,32 @@ export function addKind({ repoRoot, noun, name, options = {}, now = Date.now })
126
43
  const result = addApp({ root: repoRoot, name });
127
44
  return { ...result, registered: { patterns: [], kinds: [] } };
128
45
  }
129
- const manifest = loadSlotManifest();
130
- const params = ruleParams(manifest, 'be');
131
- const spec = params.addKinds[noun];
132
- if (!spec) throw new ScaffoldError('HFS_ADD_NOUN_UNKNOWN', `starci app add knows ${Object.keys(params.addKinds).join(', ')}; ${noun} is none of them`);
133
- const declarationFile = path.join(repoRoot, 'hfs.json');
134
- if (!fs.existsSync(declarationFile)) throw new ScaffoldError('HFS_ADD_NOT_AN_APP', 'starci app add runs at the app root (the folder of hfs.json)');
135
- const repo = readRepoDeclaration(manifest, repoRoot);
136
- if (!repo.sides?.be) throw new ScaffoldError('HFS_ADD_NO_BACK_END', 'this app has no back-end side');
137
- const liteCliBuiltIn = noun === 'cli' && repo.edition === 'lite' && (name === 'migrate' || name === 'seed');
138
- if (liteCliBuiltIn) {
139
- if (repo.sides.be.apps.some(app => app.kind === 'cli'))
140
- throw new ScaffoldError('HFS_ADD_EXISTS', `the lite cli already carries its built-in ${name} group`);
141
- return { created: ensureLiteCli({ root: repoRoot, manifest, repo }), registered: { patterns: [], kinds: ['cli'] } };
142
- }
143
- const nouns = [noun, ...(spec.also ?? [])];
144
- const specs = nouns.map((each) => [each, params.addKinds[each]]);
145
- const patterns = [...new Set(specs.flatMap(([, eachSpec]) => eachSpec.patterns))];
146
- const topics = new Set([...specs.map(([, eachSpec]) => eachSpec.topic), ...topicsOfPatterns(patterns)]);
147
- const liteCli = noun === 'cli' && repo.edition === 'lite';
148
- const liteApi = noun === 'api' && repo.edition === 'lite';
149
- // Lite has every pattern (owner ruling 2026-10-12), but a topic with a liteAdapter is generated only from full templates.
150
- refuseFullAdapterInLite({ edition: repo.edition, topics: [...topics].map(readTopic), command: `add ${noun}` });
151
- const trees = new Map([...topics].map((topic) => {
152
- // Lite keeps every colocated unit spec of a tree (owner ruling 2026-10-12); only the webhook door and its spec are lite's own.
153
- const entries = repo.edition === 'lite' ? selectLiteWebhookEntries(readTree(topic)) : readTree(topic);
154
- return [topic, liteApi && topic === spec.topic ? selectLiteApiEntries(entries) : entries];
155
- }));
156
- // The slots the noun's trees name decide whether the edition has the noun (a slot lite does not carry or forbids
157
- // makes it a full-edition capability); never the noun's name. Optional entries are never generated, so they gate nothing.
158
- const writtenSlots = new Set();
159
- for (const [each, eachSpec] of specs) for (const entry of trees.get(eachSpec.topic)) if (!entry.path.startsWith(PLATFORM_PREFIX) && entry.optional !== true && entry.slot) writtenSlots.add(entry.slot);
160
- for (const topic of topics) for (const entry of trees.get(topic)) if (entry.path.startsWith(PLATFORM_PREFIX) && entry.optional !== true && entry.slot) writtenSlots.add(entry.slot);
161
- refuseInEdition({ resolver: createSlotResolver(manifest, repo), slotIds: [...writtenSlots], command: `add ${noun}` });
162
- const values = Object.assign({}, ...specs.map(([each, eachSpec]) => variablesOf({ spec: eachSpec, noun: each, name, options })), variablesOf({ spec, noun, name, options }));
163
- const stamp = String(now()).padStart(13, '0').slice(0, 13);
164
- const withStamp = { ...values, epochMs13: stamp };
165
- const forms = formsOf(withStamp);
166
- const beRoot = path.join(repoRoot, 'be');
167
-
168
- const planned = [];
169
- const consider = (entry, topic, instance, shared = false) => {
170
- if (entry.optional === true) return;
171
- const target = fillPath(entry.path, withStamp);
172
- if (target === null) return;
173
- const absolute = path.join(beRoot, ...target.split('/'));
174
- // A platform capability is whole or absent: it exists when its index.ts does, and then none of its files is written again.
175
- if (!instance && fs.existsSync(path.join(beRoot, ...target.split('/').slice(0, 4), 'index.ts'))) return;
176
- if ((!instance || shared) && fs.existsSync(absolute)) return;
177
- if (!entry.template) throw new ScaffoldError('HFS_ADD_TEMPLATE_MISSING', `${topic}.yaml lists ${entry.path} without a template, so it cannot be generated`);
178
- const templateFile = path.join(PATTERN_TEMPLATES, ...entry.template.split('/'));
179
- if (!fs.existsSync(templateFile)) throw new ScaffoldError('HFS_ADD_TEMPLATE_MISSING', `${topic}.yaml names template ${entry.template} for ${entry.path}, which the package does not carry`);
180
- planned.push({ target, absolute, body: renderBody(fs.readFileSync(templateFile, 'utf8'), withStamp, forms, entry.template), instance });
181
- };
182
-
183
- for (const [each, eachSpec] of specs) for (const entry of trees.get(eachSpec.topic)) if (!entry.path.startsWith(PLATFORM_PREFIX)) consider(entry, eachSpec.topic, true, each !== noun);
184
- for (const topic of topics) for (const entry of trees.get(topic)) if (entry.path.startsWith(PLATFORM_PREFIX)) consider(entry, topic, false);
185
-
186
- const clash = planned.filter((file) => file.instance && fs.existsSync(file.absolute)).map((file) => file.target);
187
- if (clash.length) throw new ScaffoldError('HFS_ADD_EXISTS', `${noun} ${name} already exists: ${clash.join(', ')}`);
46
+ const context = readAddContext({ repoRoot, noun, name, options, now });
47
+ const builtin = addBuiltinLiteCli(context);
48
+ if (builtin) return builtin;
49
+ return writeKind({ ...context, ...planKind(context) });
50
+ }
188
51
 
189
- const bootstrapCreated = liteCli ? ensureLiteCli({ root: repoRoot, manifest, repo }) : [];
190
- for (const file of planned) {
191
- fs.mkdirSync(path.dirname(file.absolute), { recursive: true });
192
- fs.writeFileSync(file.absolute, file.body);
193
- }
52
+ function registerFeatureExports({ repoRoot, repo, noun, values }) {
194
53
  if (noun === 'webhook' || (repo.edition === 'lite' && noun === 'api')) {
195
54
  registerLiteExports({ root: repoRoot, generator: repo.edition === 'lite' ? noun : 'webhook-full' });
196
55
  }
197
- if (repo.edition === 'lite' && (noun === 'api' || noun === 'webhook')) {
56
+ if (repo.edition === 'lite' && noun === 'api') {
198
57
  registerLiteDomainService({ root: repoRoot, moduleSpecifier: values.serviceModule, service: values.service });
199
58
  }
59
+ }
60
+
61
+ function writeKind({ repoRoot, noun, name, now, manifest, repo, spec, declarationFile, values, forms, beRoot, patterns, planned }) {
62
+ if (noun === 'api' || noun === 'webhook') preflightFeatureComposition({ repoRoot, repo, noun, name });
63
+ const bootstrapCreated = noun === 'cli' && repo.edition === 'lite' ? ensureLiteCli({ root: repoRoot, manifest, repo }) : [];
64
+ for (const file of planned) {
65
+ fs.mkdirSync(path.dirname(file.absolute), { recursive: true });
66
+ fs.writeFileSync(file.absolute, file.body);
67
+ }
68
+ registerFeatureExports({ repoRoot, repo, noun, values });
200
69
  if (spec.wire) wire({ beRoot, wire: spec.wire, forms, noun, name });
201
- const platformCreated = repo.edition === 'lite' && (noun === 'api' || noun === 'webhook') ? wireLiteFeature({ repoRoot, repo, noun, name }) : [];
202
70
  const inboxCreated = addLiteWebhookInbox({ root: repoRoot, repo, noun, provider: name, values, now });
71
+ const platformCreated = noun === 'api' || noun === 'webhook' ? wireFeature({ repoRoot, repo, noun, name }) : [];
203
72
  const graphqlDoor = noun === 'api' ? ensureGraphqlPeers(repoRoot) : [];
204
73
  const registered = register({ declarationFile, spec: { patterns, trigger: spec.trigger, optionalSlots: noun === 'api' ? [GRAPHQL_CONTRACT_SLOT] : [] } });
205
74
  return { created: [...bootstrapCreated, ...planned.map((file) => `be/${file.target}`), ...platformCreated, ...inboxCreated, ...graphqlDoor], registered };
@@ -223,15 +92,59 @@ function ensureGraphqlPeers(repoRoot) {
223
92
  return ['package.json'];
224
93
  }
225
94
 
226
- /** Adds `importLine` and one `symbol` entry of the `imports` array of a module file's lines, once. */
227
- function addModuleImport(lines, { importLine, entry, relative, noun }) {
228
- if (lines.includes(importLine)) return;
229
- const lastImport = lines.reduce((last, line, index) => line.startsWith('import ') ? index : last, -1);
230
- if (lastImport < 0 || lines.findIndex(line => /^\s*imports: \[$/.test(line)) < 0) throw new ScaffoldError('HFS_ADD_WIRE_INVALID', `add ${noun} could not find the imports array in ${relative}`);
231
- lines.splice(lastImport + 1, 0, importLine);
232
- const imports = lines.findIndex(line => /^\s*imports: \[$/.test(line));
233
- const indent = /^(\s*)/.exec(lines[imports])?.[1] ?? '';
234
- lines.splice(imports + 1, 0, `${indent} ${entry},`);
95
+ /** The skeleton registrations that move into area groups when the first transport is added. */
96
+ function skeletonComposition(lines, edition) {
97
+ const active = compositionCode(lines.join('\n')).split('\n');
98
+ const candidates = [
99
+ { area: 'domain', symbol: 'IdentityModule', source: '@modules/domain/identity', entry: edition === 'lite' ? 'IdentityModule.register({ isGlobal: true, verifyAccessToken })' : 'IdentityModule.register({ isGlobal: true })' },
100
+ { area: 'domain', symbol: 'LivenessModule', source: '@modules/domain/liveness', entry: 'LivenessModule.register({ isGlobal: true })' },
101
+ { area: 'api', symbol: 'SystemHealthHttpModule', source: '@features/api/system-health', entry: 'SystemHealthHttpModule.register({})' },
102
+ { area: 'api', symbol: 'GraphqlModule', source: '@modules/platform/graphql', entry: 'GraphqlModule.register({ isGlobal: true })' },
103
+ ];
104
+ if (edition === 'full') candidates.push({ area: 'domain', symbol: 'NoteModule', source: '@modules/domain/note', entry: 'NoteModule.register({ isGlobal: true })' });
105
+ return candidates.filter(({ entry }) => active.some(line => line.trim() === `${entry},`));
106
+ }
107
+
108
+ /** Existing groups that the root spreads must survive when another area is generated. */
109
+ function referencedComposeAreas(lines) {
110
+ const code = compositionCode(lines.join('\n'));
111
+ return ['domain', 'system', 'api', 'webhooks'].filter((area) => code.includes(`...${area}Modules(`));
112
+ }
113
+
114
+ function featureCompositionRoot({ repoRoot, repo, noun }) {
115
+ const app = repo.sides.be.apps.find(entry => entry.kind === 'api');
116
+ if (!app) throw new ScaffoldError('HFS_ADD_NO_API_OWNER', `add ${noun} needs a declared api app`);
117
+ const relative = `be/apps/${app.name}/src/app.module.ts`;
118
+ const file = path.join(repoRoot, ...relative.split('/'));
119
+ if (!fs.existsSync(file)) throw new ScaffoldError('HFS_ADD_WIRE_MISSING', `add ${noun} registers its transport in ${relative}, which does not exist`);
120
+ return { app, relative, file, lines: fs.readFileSync(file, 'utf8').replace(/\r\n/g, '\n').split('\n') };
121
+ }
122
+
123
+ /** Refuse an incompatible root or area group before generating the feature or changing its capability exports. */
124
+ function preflightFeatureComposition({ repoRoot, repo, noun, name }) {
125
+ const { app, relative, lines } = featureCompositionRoot({ repoRoot, repo, noun });
126
+ if (noun === 'webhook' && repo.edition === 'lite') {
127
+ if (!compositionCode(lines.join('\n')).split('\n').some(line => /^\s*imports: \[$/.test(line)))
128
+ throw new ScaffoldError('HFS_ADD_WIRE_INVALID', `add webhook needs a multiline imports array in ${relative}`);
129
+ planLiteWebhookConfig({ repoRoot, app: app.name, provider: name });
130
+ }
131
+ const areas = new Set([noun === 'api' ? 'api' : 'webhooks', ...referencedComposeAreas(lines), ...skeletonComposition(lines, repo.edition).map(entry => entry.area)]);
132
+ for (const area of areas) {
133
+ readComposeGroup({ repoRoot, repo, app: app.name, area, noun });
134
+ const argument = area === 'domain' && repo.edition === 'lite' ? 'verifyAccessToken' : '';
135
+ addModuleImport(lines, { importLine: `import { ${area}Modules } from "./compose/${area}"`, entry: `...${area}Modules(${argument})`, relative, noun });
136
+ }
137
+ }
138
+
139
+ /** Remove only a moved skeleton registration and its named import, keeping guards imported from the same barrel. */
140
+ function removeSkeletonRegistration(lines, { symbol, source, entry }) {
141
+ const entryAt = compositionCode(lines.join('\n')).split('\n').findIndex(line => line.trim() === `${entry},`);
142
+ lines.splice(entryAt, 1);
143
+ const importAt = lines.findIndex(line => line.startsWith('import { ') && line.replace(/;\s*$/, '').endsWith(` } from "${source}"`));
144
+ if (importAt < 0) return;
145
+ const symbols = lines[importAt].slice('import { '.length, lines[importAt].indexOf(' } from ')).split(',').map(value => value.trim()).filter(value => value !== symbol);
146
+ if (symbols.length) lines[importAt] = `import { ${symbols.join(', ')} } from "${source}"`;
147
+ else lines.splice(importAt, 1);
235
148
  }
236
149
 
237
150
  /** The GraphQL server capability (be/src/modules/platform/graphql), written whole by the first GraphQL feature of an app that has none. */
@@ -249,38 +162,62 @@ function ensureGraphqlPlatform(repoRoot) {
249
162
  return written;
250
163
  }
251
164
 
252
- /**
253
- * Lite has one API app; compose each generated transport into that app immediately: an api feature's GraphQL module (with the
254
- * GraphQL server on first use), a webhook's HTTP door.
255
- */
256
- function wireLiteFeature({ repoRoot, repo, noun, name }) {
257
- const app = repo.sides.be.apps.find(entry => entry.kind === 'api');
258
- if (!app) throw new ScaffoldError('HFS_ADD_NO_API_OWNER', `add ${noun} needs a declared lite api app`);
259
- const relative = `be/apps/${app.name}/src/app.module.ts`;
260
- const file = path.join(repoRoot, ...relative.split('/'));
261
- if (!fs.existsSync(file)) throw new ScaffoldError('HFS_ADD_WIRE_MISSING', `add ${noun} registers its transport in ${relative}, which does not exist`);
262
- const lines = fs.readFileSync(file, 'utf8').replace(/\r\n/g, '\n').split('\n');
263
- const created = [];
165
+ /** The typed area members of one transport, including lite transactional handoff capabilities. */
166
+ function transportComposition(noun, name, edition) {
167
+ const entries = [];
264
168
  if (noun === 'api') {
265
- created.push(...ensureGraphqlPlatform(repoRoot));
266
- addModuleImport(lines, { importLine: 'import { GraphqlModule } from "@modules/platform/graphql"', entry: 'GraphqlModule.register({ isGlobal: true })', relative, noun });
169
+ entries.push({ importLine: 'import { GraphqlModule } from "@modules/platform/graphql"', entry: 'GraphqlModule.register({ isGlobal: true })' });
267
170
  const symbol = `${pascalOf(name)}GraphqlModule`;
268
- addModuleImport(lines, { importLine: `import { ${symbol} } from "@features/api/${name}"`, entry: symbol, relative, noun });
171
+ entries.push({ importLine: `import { ${symbol} } from "@features/api/${name}"`, entry: symbol });
269
172
  } else {
270
173
  const symbol = `${pascalOf(name)}HttpModule`;
271
- addModuleImport(lines, { importLine: `import { ${symbol} } from "@features/webhooks/${name}"`, entry: symbol, relative, noun });
174
+ entries.push({ importLine: `import { ${symbol} } from "@features/webhooks/${name}"`, entry: symbol });
175
+ if (edition === 'lite') {
176
+ const inbox = `${pascalOf(name)}InboxModule`;
177
+ const importLine = `import { ${inbox} } from "@modules/domain/${name}-inbox"`;
178
+ const entry = `${inbox}.register({ isGlobal: true })`;
179
+ entries.push({ importLine, entry });
180
+ for (const platform of ['InboxModule', 'EventBusModule']) {
181
+ const capability = platform === 'InboxModule' ? 'inbox' : 'event-bus';
182
+ entries.push({ importLine: `import { ${platform} } from "@modules/platform/${capability}"`, entry: `${platform}.register({ isGlobal: true })` });
183
+ }
184
+ }
185
+ }
186
+ return entries;
187
+ }
188
+
189
+ /**
190
+ * Compose each generated transport in its declared API owner through an explicitly typed area group. The root only spreads
191
+ * the group; it never acquires a second feature list. Lite's inbox capability belongs to its webhook group as well.
192
+ */
193
+ function wireFeature({ repoRoot, repo, noun, name }) {
194
+ const { app, relative, file, lines } = featureCompositionRoot({ repoRoot, repo, noun });
195
+ const created = [];
196
+ const area = noun === 'api' ? 'api' : 'webhooks';
197
+ const groups = new Map([...referencedComposeAreas(lines), area].map((group) => [group, []]));
198
+ for (const migration of skeletonComposition(lines, repo.edition)) {
199
+ const entries = groups.get(migration.area) ?? [];
200
+ entries.push({ importLine: `import { ${migration.symbol} } from "${migration.source}"`, entry: migration.entry });
201
+ groups.set(migration.area, entries);
202
+ removeSkeletonRegistration(lines, migration);
203
+ }
204
+ const entries = groups.get(area);
205
+ if (noun === 'api') created.push(...ensureGraphqlPlatform(repoRoot));
206
+ entries.push(...transportComposition(noun, name, repo.edition));
207
+ for (const [group, members] of groups) {
208
+ const symbol = `${group}Modules`;
209
+ const argument = group === 'domain' && repo.edition === 'lite' ? 'verifyAccessToken' : '';
210
+ addModuleImport(lines, { importLine: `import { ${symbol} } from "./compose/${group}"`, entry: `...${symbol}(${argument})`, relative, noun });
211
+ created.push(...writeComposeGroup({ repoRoot, repo, app: app.name, area: group, entries: members, noun }));
272
212
  }
273
- fs.writeFileSync(file, `${lines.join('\n').replace(/\n+$/, '')}\n`);
274
- if (noun === 'webhook') wireLiteWebhookConfig({ repoRoot, app: app.name, provider: name });
213
+ fs.writeFileSync(file, `${lines.join('\n').trimEnd()}\n`);
214
+ if (noun === 'webhook' && repo.edition === 'lite') wireLiteWebhookConfig({ repoRoot, app: app.name, provider: name });
275
215
  return created;
276
216
  }
277
217
 
278
- /** Adds one generated webhook provider to the lite API's parsed options and enables exact raw-body verification. */
279
- function wireLiteWebhookConfig({ repoRoot, app, provider }) {
280
- const relative = `be/apps/${app}/src/main.ts`;
281
- const file = path.join(repoRoot, ...relative.split('/'));
282
- let text = fs.readFileSync(file, 'utf8').replace(/\r\n/g, '\n');
283
- const httpSecurityImport = /^import \{ ([^}]+) \} from "@modules\/platform\/http-security"$/m;
218
+ /** Adds the provider config reader to the existing typed HTTP security import. */
219
+ function webhookConfigImport(text, relative) {
220
+ const httpSecurityImport = /^import \{ ([^}]+) \} from "@modules\/platform\/http-security";?$/m;
284
221
  const importMatch = httpSecurityImport.exec(text);
285
222
  if (!importMatch) throw new ScaffoldError('HFS_ADD_WIRE_INVALID', `add webhook could not find the HTTP security import in ${relative}`);
286
223
  const imported = importMatch[1].split(',').map(symbol => symbol.trim()).filter(Boolean);
@@ -289,53 +226,98 @@ function wireLiteWebhookConfig({ repoRoot, app, provider }) {
289
226
  imported.splice(configIndex < 0 ? imported.length : configIndex + 1, 0, 'parseWebhookProviderConfig');
290
227
  text = text.replace(httpSecurityImport, `import { ${imported.join(', ')} } from "@modules/platform/http-security"`);
291
228
  }
229
+ return text;
230
+ }
231
+
232
+ /** Declares a signing key by name and leaves custody/provisioning to the environment owner. */
233
+ function webhookRosterPatches(repoRoot, provider) {
234
+ const stacks = path.join(repoRoot, '.starcistacks');
235
+ if (!fs.existsSync(stacks)) return [];
236
+ const key = formsOf({ provider }).providerUpper + '_WEBHOOK_SECRET';
237
+ const patches = [];
238
+ for (const entry of fs.readdirSync(stacks, { withFileTypes: true })) {
239
+ if (!entry.isDirectory()) continue;
240
+ const file = path.join(stacks, entry.name, 'runtime', 'env', 'KEYS.md');
241
+ if (!fs.existsSync(file)) continue;
242
+ const before = fs.readFileSync(file, 'utf8');
243
+ if (before.includes('\x60' + key + '\x60')) continue;
244
+ const lines = before.replace(/\r\n/g, '\n').split('\n');
245
+ const lastRow = lines.reduce((last, line, index) => line.startsWith('|') ? index : last, -1);
246
+ const row = '| \x60' + key + '\x60 | API signed ' + provider + ' webhook; owner must seal this key and provision its versioned external runtime secret before deployment |';
247
+ lines.splice(lastRow + 1, 0, row);
248
+ patches.push({ file, text: lines.join('\n') });
249
+ }
250
+ return patches;
251
+ }
252
+
253
+ /** Adds one generated webhook provider to the lite API's typed env reader (or an existing inline bootstrap) and enables exact raw-body verification. */
254
+ function planLiteWebhookConfig({ repoRoot, app, provider }) {
255
+ const envRelative = `be/apps/${app}/src/${app}.env.ts`;
256
+ const relative = fs.existsSync(path.join(repoRoot, ...envRelative.split('/')))
257
+ ? envRelative
258
+ : `be/apps/${app}/src/main.ts`;
259
+ const file = path.join(repoRoot, ...relative.split('/'));
260
+ if (!fs.existsSync(file)) throw new ScaffoldError('HFS_ADD_WIRE_MISSING', `add webhook needs ${relative}`);
261
+ let text = fs.readFileSync(file, 'utf8').replace(/\r\n/g, '\n');
262
+ text = webhookConfigImport(text, relative);
292
263
  const providerKey = /^[a-z][a-z0-9]*$/.test(provider) ? provider : JSON.stringify(provider);
293
- const providerLine = ` ${providerKey}: parseWebhookProviderConfig(env, ${JSON.stringify(formsOf({ provider }).providerUpper)}),`;
264
+ const indent = /^([ \t]*)httpSecurity:/m.exec(text)?.[1] ?? ' ';
265
+ const providerLine = `${indent} ${providerKey}: parseWebhookProviderConfig(env, ${JSON.stringify(formsOf({ provider }).providerUpper)}),`;
294
266
  if (!text.includes(providerLine)) {
295
267
  const empty = /httpSecurity:\s*parseHttpSecurityConfig\(env\)/;
296
268
  if (empty.test(text)) {
297
- text = text.replace(empty, ['httpSecurity: {', ' ...parseHttpSecurityConfig(env),', ' webhooks: {', providerLine, ' },', ' }'].join('\n'));
269
+ text = text.replace(empty, ['httpSecurity: {', `${indent} ...parseHttpSecurityConfig(env),`, `${indent} webhooks: {`, providerLine, `${indent} },`, `${indent}}`].join('\n'));
298
270
  } else {
299
- const found = /^\s*webhooks: \{$/m.exec(text);
271
+ const found = /^[ \t]*webhooks: \{$/m.exec(text);
300
272
  if (!found) throw new ScaffoldError('HFS_ADD_WIRE_INVALID', `add webhook could not find httpSecurity options in ${relative}`);
301
273
  const insertAt = found.index + found[0].length;
302
274
  text = `${text.slice(0, insertAt)}\n${providerLine}${text.slice(insertAt)}`;
303
275
  }
304
276
  }
305
277
  text = text.replace('NestFactory.create(AppModule.register(options))', 'NestFactory.create(AppModule.register(options), { rawBody: true })');
306
- fs.writeFileSync(file, text);
278
+ const planned = [{ file, text }];
279
+ if (relative === envRelative) {
280
+ const main = path.join(repoRoot, 'be', 'apps', app, 'src', 'main.ts');
281
+ if (!fs.existsSync(main)) throw new ScaffoldError('HFS_ADD_WIRE_MISSING', `add webhook needs be/apps/${app}/src/main.ts`);
282
+ const entry = fs.readFileSync(main, 'utf8').replace('NestFactory.create(AppModule.register(options))', 'NestFactory.create(AppModule.register(options), { rawBody: true })');
283
+ planned.push({ file: main, text: entry });
284
+ }
285
+ planned.push(...webhookRosterPatches(repoRoot, provider));
286
+ return planned;
287
+ }
288
+
289
+ /** Applies the preflighted provider configuration and exact raw-body bootstrap option. */
290
+ function wireLiteWebhookConfig(input) {
291
+ const patches = planLiteWebhookConfig(input);
292
+ const originals = new Map(patches.map(({ file }) => [file, fs.readFileSync(file)]));
293
+ try {
294
+ for (const { file, text } of patches) fs.writeFileSync(file, text);
295
+ } catch (error) {
296
+ for (const [file, text] of originals) fs.writeFileSync(file, text);
297
+ throw error;
298
+ }
307
299
  }
308
300
 
309
301
  /**
310
302
  * Registers the generated member in the static module that lists its siblings (the cli feature root lists each group module): one import
311
- * line after the last import and one more entry of the `imports` array of its `@Module`. Nothing happens when the member is already listed.
303
+ * line after the last import and one more entry of the `imports` array of its `@Module`, written `Symbol.register({})` (every module has a module definition). Nothing happens when the member is already listed.
312
304
  */
313
305
  function wire({ beRoot, wire: target, forms, noun, name }) {
314
306
  const file = path.join(beRoot, ...target.file.split('/'));
315
307
  if (!fs.existsSync(file)) throw new ScaffoldError('HFS_ADD_WIRE_MISSING', `starci app add ${noun} ${name} registers the member in ${target.file}, which does not exist`);
316
- const fill = (text) => text.replace(PLACEHOLDER, (whole, key) => forms[key] ?? whole);
308
+ const fill = (text) => fillKnownForms(text, forms);
317
309
  const symbol = fill(target.symbol);
318
310
  let text = fs.readFileSync(file, 'utf8');
319
- if (new RegExp(`\\b${symbol}\\b`).test(text)) return;
311
+ if (new RegExp(String.raw`\b${symbol}\b`).test(text)) return;
320
312
  const array = /@Module\(\{ imports: \[([^\]]*)\] \}\)/.exec(text);
321
313
  const lines = text.split('\n');
322
314
  const lastImport = lines.map((line) => line.startsWith('import ')).lastIndexOf(true);
323
315
  if (!array || lastImport < 0) throw new ScaffoldError('HFS_ADD_WIRE_FAILED', `${target.file} has no \`@Module({ imports: [...] })\` to register ${symbol} in`);
324
316
  lines.splice(lastImport + 1, 0, `import { ${symbol} } from "${fill(target.import)}"`);
325
- text = lines.join('\n').replace(array[0], `@Module({ imports: [${[array[1].trim(), symbol].filter(Boolean).join(', ')}] })`);
317
+ text = lines.join('\n').replace(array[0], `@Module({ imports: [${[array[1].trim(), `${symbol}.register({})`].filter(Boolean).join(', ')}] })`);
326
318
  fs.writeFileSync(file, text);
327
319
  }
328
320
 
329
- /** The topics that carry the platform capability of each pattern: the topic whose `pattern` key names it. */
330
- function topicsOfPatterns(patterns) {
331
- const topics = new Set();
332
- for (const file of fs.readdirSync(TOPICS_DIR).filter((entry) => entry.endsWith('.yaml'))) {
333
- const doc = parseYaml(fs.readFileSync(path.join(TOPICS_DIR, file), 'utf8'));
334
- if (patterns.includes(doc?.pattern)) topics.add(file.slice(0, -'.yaml'.length));
335
- }
336
- return topics;
337
- }
338
-
339
321
  /** Adds the patterns, the trigger kind and the opt-in slots of the noun to sides.be of hfs.json, keeping its other content and key order. */
340
322
  function register({ declarationFile, spec }) {
341
323
  const declaration = JSON.parse(fs.readFileSync(declarationFile, 'utf8'));