@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
@@ -7,7 +7,7 @@ import {
7
7
  } from "../runtime/scripts/hfs/slots.mjs";
8
8
  import { imageFiles, render, TEMPLATES_DIR } from "../sync/index.mjs";
9
9
  import { ScaffoldError, pascalOf } from "./service.mjs";
10
- import { PARTIAL_VARIABLES, coreFeApps, jsonText, packageJsonText } from "./app.mjs";
10
+ import { PARTIAL_VARIABLES, bundledPins, callbackSourceFiles, coreFeApps, hasAuthCallback, jsonText, packageJsonText } from "./app.mjs";
11
11
 
12
12
  /** The shared fe app core (modules/api, auth, toast, redux, the providers, the hooks): the base tree, then the edition overlay. */
13
13
  const CORE_DIRS = Object.freeze({
@@ -35,37 +35,8 @@ const fill = (text, values, file) =>
35
35
  return values[key];
36
36
  });
37
37
 
38
- /**
39
- * Adds one Next workspace from the edition's canonical app skeleton and the shared app core (both editions, owner ruling
40
- * 2026-10-13: lite and full have one fe structure), then registers it in hfs.json. The root workspace glob is added only when an
41
- * older app manifest does not already carry it.
42
- */
43
- export function addApp({ root, name }) {
44
- if (!NAME.test(String(name)))
45
- throw new ScaffoldError(
46
- "HFS_ADD_NAME_INVALID",
47
- `app name ${name} must be kebab-case (letters, digits and single dashes)`,
48
- );
49
- const declarationFile = path.join(root, "hfs.json");
50
- const packageFile = path.join(root, "package.json");
51
- if (!fs.existsSync(declarationFile) || !fs.existsSync(packageFile))
52
- throw new ScaffoldError(
53
- "HFS_ADD_NOT_AN_APP",
54
- "add app runs at the app root containing hfs.json and package.json",
55
- );
56
- const manifest = loadSlotManifest();
57
- const repo = readRepoDeclaration(manifest, root);
58
- if (!repo.sides?.fe)
59
- throw new ScaffoldError(
60
- "HFS_ADD_NO_FRONT_END",
61
- "this app has no front-end side",
62
- );
63
- if (repo.sides.fe.apps.some((app) => app.name === name))
64
- throw new ScaffoldError(
65
- "HFS_ADD_EXISTS",
66
- `front-end app ${name} is already declared`,
67
- );
68
-
38
+ /** Render the edition skeleton followed by its shared core and edition overlay. */
39
+ function planAppTemplates(repo, name) {
69
40
  const source = path.join(
70
41
  TEMPLATES_DIR,
71
42
  "fe",
@@ -94,8 +65,13 @@ export function addApp({ root, name }) {
94
65
  if (!fs.existsSync(dir)) continue;
95
66
  for (const relative of listFiles(dir)) byPath.set(relative, { relative: `${targetBase}/${relative}`, body: readTemplate(dir, relative) });
96
67
  }
97
- const planned = [...byPath.values()];
68
+ return callbackSourceFiles([...byPath.values()].map(file => ({ path: file.relative, content: file.body })), repo)
69
+ .map(file => ({ relative: file.path, body: file.content }));
70
+ }
98
71
 
72
+ /** The package and TypeScript manifests of a new Next workspace. */
73
+ function planWorkspaceManifests(root, repo, name) {
74
+ const targetBase = `fe/apps/${name}`;
99
75
  // The dependency source is a product app (it carries the core's fe-stack packages), else the first Next app.
100
76
  const firstApp = coreFeApps(repo.sides.fe.apps)[0] ?? repo.sides.fe.apps.find((app) => app.kind === "next");
101
77
  const firstPackage = firstApp
@@ -108,8 +84,14 @@ export function addApp({ root, name }) {
108
84
  );
109
85
  const workspacePackage = JSON.parse(fs.readFileSync(firstPackage, "utf8"));
110
86
  workspacePackage.name = `@${repo.project}/${name}`;
87
+ if (hasAuthCallback(repo)) {
88
+ const pins = bundledPins();
89
+ workspacePackage.dependencies ??= {};
90
+ for (const dependency of ["@supabase/ssr", "@supabase/supabase-js", "server-only"])
91
+ workspacePackage.dependencies[dependency] = pins[dependency].version;
92
+ }
111
93
  const appTemplates = path.join(TEMPLATES_DIR, "fe", "app");
112
- planned.push(
94
+ return [
113
95
  {
114
96
  relative: `${targetBase}/package.json`,
115
97
  body:
@@ -121,32 +103,10 @@ export function addApp({ root, name }) {
121
103
  relative: `${targetBase}/tsconfig.json`,
122
104
  body: fs.readFileSync(path.join(appTemplates, "tsconfig.json"), "utf8"),
123
105
  },
124
- );
125
- const declaration = JSON.parse(fs.readFileSync(declarationFile, "utf8"));
126
- declaration.sides.fe.apps.push({ name, kind: "next" });
127
- if (repo.edition === "full") {
128
- const image = imageFiles(
129
- resolveRepoDeclaration(manifest, declaration),
130
- ).find((file) => file.path === `${targetBase}/Dockerfile`);
131
- if (!image)
132
- throw new ScaffoldError(
133
- "HFS_ADD_TEMPLATE_MISSING",
134
- `the Dockerfile of front-end app ${name} did not render`,
135
- );
136
- planned.push({ relative: image.path, body: image.content });
137
- }
138
-
139
- const clashes = planned
140
- .filter((file) =>
141
- fs.existsSync(path.join(root, ...file.relative.split("/"))),
142
- )
143
- .map((file) => file.relative);
144
- if (clashes.length)
145
- throw new ScaffoldError(
146
- "HFS_ADD_EXISTS",
147
- `front-end app ${name} already exists: ${clashes.join(", ")}`,
148
- );
106
+ ];
107
+ }
149
108
 
109
+ function persistAppFiles({ root, name, planned, declaration, declarationFile, packageFile }) {
150
110
  const rootPackage = JSON.parse(fs.readFileSync(packageFile, "utf8"));
151
111
  const oldDeclaration = fs.readFileSync(declarationFile, "utf8");
152
112
  const oldPackage = fs.readFileSync(packageFile, "utf8");
@@ -173,6 +133,64 @@ export function addApp({ root, name }) {
173
133
  fs.writeFileSync(packageFile, oldPackage);
174
134
  throw error;
175
135
  }
136
+ }
137
+
138
+ /**
139
+ * Adds one Next workspace from the edition's canonical app skeleton and the shared app core (both editions, owner ruling
140
+ * 2026-10-13: lite and full have one fe structure), then registers it in hfs.json. The root workspace glob is added only when an
141
+ * older app manifest does not already carry it.
142
+ */
143
+ export function addApp({ root, name }) {
144
+ if (!NAME.test(String(name)))
145
+ throw new ScaffoldError(
146
+ "HFS_ADD_NAME_INVALID",
147
+ `app name ${name} must be kebab-case (letters, digits and single dashes)`,
148
+ );
149
+ const declarationFile = path.join(root, "hfs.json");
150
+ const packageFile = path.join(root, "package.json");
151
+ if (!fs.existsSync(declarationFile) || !fs.existsSync(packageFile))
152
+ throw new ScaffoldError(
153
+ "HFS_ADD_NOT_AN_APP",
154
+ "add app runs at the app root containing hfs.json and package.json",
155
+ );
156
+ const manifest = loadSlotManifest();
157
+ const repo = readRepoDeclaration(manifest, root);
158
+ if (!repo.sides?.fe)
159
+ throw new ScaffoldError(
160
+ "HFS_ADD_NO_FRONT_END",
161
+ "this app has no front-end side",
162
+ );
163
+ if (repo.sides.fe.apps.some((app) => app.name === name))
164
+ throw new ScaffoldError(
165
+ "HFS_ADD_EXISTS",
166
+ `front-end app ${name} is already declared`,
167
+ );
168
+ const targetBase = `fe/apps/${name}`;
169
+ const planned = [...planAppTemplates(repo, name), ...planWorkspaceManifests(root, repo, name)];
170
+ const declaration = JSON.parse(fs.readFileSync(declarationFile, "utf8"));
171
+ declaration.sides.fe.apps.push({ name, kind: "next" });
172
+ const image = imageFiles(
173
+ resolveRepoDeclaration(manifest, declaration),
174
+ ).find((file) => file.path === `${targetBase}/Dockerfile`);
175
+ if (!image && repo.edition !== "lite")
176
+ throw new ScaffoldError(
177
+ "HFS_ADD_TEMPLATE_MISSING",
178
+ `the Dockerfile of front-end app ${name} did not render`,
179
+ );
180
+ if (image) planned.push({ relative: image.path, body: image.content });
181
+
182
+ const clashes = planned
183
+ .filter((file) =>
184
+ fs.existsSync(path.join(root, ...file.relative.split("/"))),
185
+ )
186
+ .map((file) => file.relative);
187
+ if (clashes.length)
188
+ throw new ScaffoldError(
189
+ "HFS_ADD_EXISTS",
190
+ `front-end app ${name} already exists: ${clashes.join(", ")}`,
191
+ );
192
+
193
+ persistAppFiles({ root, name, planned, declaration, declarationFile, packageFile });
176
194
  return {
177
195
  created: planned.map((file) => file.relative),
178
196
  registered: { app: name, workspace: "fe/apps/*" },
@@ -2,7 +2,7 @@ import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { resolveRepoDeclaration } from "../runtime/scripts/hfs/slots.mjs";
4
4
  import { parseYaml } from "../runtime/engine/yaml.mjs";
5
- import { appScripts, imageFiles, TEMPLATES_DIR } from "../sync/index.mjs";
5
+ import { appScripts, appSource, imageFiles, TEMPLATES_DIR } from "../sync/index.mjs";
6
6
  import { ScaffoldError } from "./service.mjs";
7
7
  import { jsonText, packageJsonText } from "./app.mjs";
8
8
  import { registerLiteExports } from "./lite-exports.mjs";
@@ -15,14 +15,12 @@ const read = (relative) =>
15
15
  const PINS_FILE = path.join(import.meta.dirname, "..", "runtime", "knowledge", "hfs", "canon-pins.yaml");
16
16
 
17
17
 
18
- const managedScripts = (repo) => {
19
- const fragment = appScripts(repo).replace(/,\s*$/, "");
18
+ const managedScripts = (repo, source) => {
19
+ const fragment = appScripts(repo, source).replace(/,\s*$/, "");
20
20
  return fragment === "" ? {} : JSON.parse(`{${fragment}}`);
21
21
  };
22
22
 
23
- const CLI_OPTIONS = `import type { EnvSource } from "@modules/platform/config"
24
- import { parsePrimaryDatabaseConfig } from "@modules/platform/database"
25
- import type { DatabaseConnectionOptions } from "@modules/platform/database"
23
+ const CLI_OPTIONS = `import type { DatabaseConnectionOptions } from "@modules/platform/database"
26
24
 
27
25
  /** Everything the lite cli needs: the least-privilege Supabase PostgreSQL connection. */
28
26
  export interface CliAppOptions {
@@ -30,9 +28,9 @@ export interface CliAppOptions {
30
28
  readonly connections: ReadonlyArray<DatabaseConnectionOptions>
31
29
  }
32
30
 
33
- /** Reads the one declared connection without adding entities or TypeORM migrations. */
34
- export const parseCliAppOptions = (env: EnvSource): CliAppOptions => ({
35
- connections: [parsePrimaryDatabaseConfig(env)],
31
+ /** Uses the parsed connection without adding entities or TypeORM migrations. */
32
+ export const createCliAppOptions = (primary: DatabaseConnectionOptions): CliAppOptions => ({
33
+ connections: [primary],
36
34
  })
37
35
  `;
38
36
 
@@ -111,10 +109,15 @@ function enableLiteCliDatabase(root) {
111
109
  text = text.replace(anchor, `${anchor}\nimport { MigrationRunnerService } from "./migration-runner.service"`);
112
110
  }
113
111
  if (!text.includes('import { SeedRunnerService } from "./seed-runner.service"')) {
114
- const anchor = 'import { PRIMARY_ENTITY_MANAGER } from "./primary.decorators"';
115
- if (!text.includes(anchor)) throw new ScaffoldError("HFS_ADD_CLI_DATABASE", `${relative} has an unknown import shape`);
112
+ // The import of the entity-manager token, with or without the probe token beside it.
113
+ const anchor = /^import \{[^}]*\bPRIMARY_ENTITY_MANAGER\b[^}]*\} from "\.\/primary\.decorators"$/m.exec(text)?.[0];
114
+ if (!anchor) throw new ScaffoldError("HFS_ADD_CLI_DATABASE", `${relative} has an unknown import shape`);
116
115
  text = text.replace(anchor, `${anchor}\nimport { SeedRunnerService } from "./seed-runner.service"`);
117
116
  }
117
+ const probeProviders = "providers: [...(base.providers ?? []), ...managers, ...probes]";
118
+ if (text.includes(probeProviders)) {
119
+ text = text.replace(probeProviders, "providers: [...(base.providers ?? []), ...managers, ...probes, MigrationRunnerService, SeedRunnerService]");
120
+ }
118
121
  const providers = "providers: [...(base.providers ?? []), ...managers]";
119
122
  if (text.includes(providers)) {
120
123
  text = text.replace(providers, "providers: [...(base.providers ?? []), ...managers, MigrationRunnerService, SeedRunnerService]");
@@ -142,7 +145,7 @@ function enableLiteCliDatabase(root) {
142
145
  }
143
146
 
144
147
  /**
145
- * Creates the optional lite cli app and its Supabase migrate/seed groups on first `add cli`.
148
+ * Creates the optional lite cli app, its typed CliEnv/readCliEnv boundary and Supabase migrate/seed groups on first `add cli`.
146
149
  * Existing cli trees are never repaired or overwritten: drift remains a check finding.
147
150
  */
148
151
  export function ensureLiteCli({ root, manifest, repo }) {
@@ -182,6 +185,10 @@ export function ensureLiteCli({ root, manifest, repo }) {
182
185
  "be/apps/cli/src/app.module.ts",
183
186
  read("be/skeleton/apps/cli/src/app.module.ts"),
184
187
  ],
188
+ ["be/apps/cli/src/compose/platform.ts", read("be/skeleton-lite/apps/cli/src/compose/platform.ts")],
189
+ ["be/apps/cli/src/compose/cli.ts", read("be/skeleton/apps/cli/src/compose/cli.ts")],
190
+ ["be/src/tests/fixtures/builders/cli.builder.ts", read("be/skeleton-lite/src/tests/fixtures/builders/cli.builder.ts")],
191
+ ["be/apps/cli/src/cli.env.ts", read("be/skeleton-lite/apps/cli/src/cli.env.ts")],
185
192
  ["be/apps/cli/src/cli.options.ts", CLI_OPTIONS],
186
193
  [
187
194
  "be/src/features/cli/index.ts",
@@ -204,7 +211,7 @@ export function ensureLiteCli({ root, manifest, repo }) {
204
211
  read("be/skeleton-lite/src/modules/platform/database/migration-runner.service.ts"),
205
212
  ],
206
213
  ["be/src/features/cli/migrate/subs/run.cli.ts", MIGRATE_RUN],
207
- // Owner ruling 2026-10-12: each cli command of a lite back end owes its unit spec, as in full (ruleParams.be.unitRoles).
214
+ // ruling:R212/case-1, amended by ruling:R212/case-4: each cli command of a lite back end owes its unit spec, as in full (ruleParams.be.unitRoles).
208
215
  [
209
216
  "be/src/features/cli/migrate/migrate.cli.spec.ts",
210
217
  read("be/skeleton/src/features/cli/migrate/migrate.cli.spec.ts"),
@@ -256,7 +263,7 @@ export function ensureLiteCli({ root, manifest, repo }) {
256
263
  );
257
264
  const nest = JSON.parse(fs.readFileSync(nestFile, "utf8"));
258
265
  nest.projects = {
259
- ...(nest.projects ?? {}),
266
+ ...nest.projects,
260
267
  cli: {
261
268
  type: "application",
262
269
  root: "apps/cli",
@@ -270,10 +277,10 @@ export function ensureLiteCli({ root, manifest, repo }) {
270
277
  const pin = parseYaml(fs.readFileSync(PINS_FILE, "utf8")).pins?.["nest-commander"]?.version;
271
278
  if (typeof pin !== "string" || pin === "")
272
279
  throw new ScaffoldError("HFS_ADD_CANON_PIN_MISSING", "add cli needs the nest-commander canon pin");
273
- packageManifest.dependencies = { ...(packageManifest.dependencies ?? {}), "nest-commander": pin };
280
+ packageManifest.dependencies = { ...packageManifest.dependencies, "nest-commander": pin };
274
281
  packageManifest.scripts = {
275
- ...(packageManifest.scripts ?? {}),
276
- ...managedScripts(resolved),
282
+ ...packageManifest.scripts,
283
+ ...managedScripts(resolved, appSource(root)),
277
284
  };
278
285
  for (const [relative, body] of files) {
279
286
  const target = path.join(root, ...relative.split("/"));
@@ -0,0 +1,79 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { TEMPLATES_DIR } from '../sync/index.mjs';
4
+ import { ScaffoldError } from './service.mjs';
5
+
6
+ /** Adds `importLine` and one `symbol` entry of the `imports` array of a module file's lines, once. */
7
+ export function addModuleImport(lines, { importLine, entry, relative, noun }) {
8
+ const active = compositionCode(lines.join('\n')).split('\n');
9
+ const imported = active.some(line => line.replace(/;\s*$/, '') === importLine);
10
+ const registered = active.some(line => line.trim() === `${entry},` ||
11
+ (/\bimports:\s*\[/.test(line) && !line.trim().startsWith('//') && line.replace(/\s+/g, '').includes(entry.replace(/\s+/g, ''))));
12
+ const lastImport = active.reduce((last, line, index) => line.startsWith('import ') ? index : last, -1);
13
+ const importsLine = line => /\bimports:\s*\[/.test(line) && !line.trim().startsWith('//');
14
+ if (lastImport < 0 || active.findIndex(importsLine) < 0) throw new ScaffoldError('HFS_ADD_WIRE_INVALID', `add ${noun} could not find the imports array in ${relative}`);
15
+ if (imported && registered) return;
16
+ if (!imported) lines.splice(lastImport + 1, 0, importLine);
17
+ if (registered) return;
18
+ const imports = compositionCode(lines.join('\n')).split('\n').findIndex(importsLine);
19
+ const indent = /^(\s*)/.exec(lines[imports])?.[1] ?? '';
20
+ if (/^\s*imports: \[$/.test(lines[imports])) lines.splice(imports + 1, 0, `${indent} ${entry},`);
21
+ else lines[imports] = lines[imports].replace(/(\bimports:\s*\[)\s*/, `$1${entry}, `);
22
+ }
23
+
24
+ /** Masks comments without changing offsets or treating quoted module specifiers as comments. */
25
+ export function compositionCode(text) {
26
+ return text.replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`|\/\*[\s\S]*?\*\/|\/\/[^\n]*/g,
27
+ value => value.startsWith('/') ? value.replace(/[^\r\n]/g, ' ') : value);
28
+ }
29
+
30
+
31
+ /** Reads the explicitly typed expression body of a group without treating comments as active declarations. */
32
+ export function readComposeGroup({ repoRoot, repo, app, area, noun }) {
33
+ const relative = `be/apps/${app}/src/compose/${area}.ts`;
34
+ const file = path.join(repoRoot, ...relative.split('/'));
35
+ const exists = fs.existsSync(file);
36
+ const skeleton = repo.edition === 'lite' ? 'skeleton-lite/apps/api' : 'skeleton/apps/__app__';
37
+ const template = path.join(TEMPLATES_DIR, 'be', ...skeleton.split('/'), 'src', 'compose', `${area}.ts`);
38
+ const text = fs.readFileSync(exists ? file : template, 'utf8').replace(/\r\n/g, '\n');
39
+ const argument = area === 'domain' && repo.edition === 'lite' ? 'verifyAccessToken: VerifySupabaseAccessToken' : '';
40
+ const marker = `export const ${area}Modules = (${argument}): Array<Type<unknown> | DynamicModule> => [`;
41
+ const declarationAt = compositionCode(text).indexOf(`\n${marker}`);
42
+ const active = declarationAt >= 0;
43
+ const tail = active ? text.slice(declarationAt + 1 + marker.length).trimEnd().replace(/;$/, '').trimEnd() : '';
44
+ const body = tail.endsWith(']') ? tail.slice(0, -1) : undefined;
45
+ if (body === undefined) throw new ScaffoldError('HFS_ADD_WIRE_INVALID', `add ${noun} needs the typed ${area}Modules group in ${relative}`);
46
+ return { relative, file, exists, text, marker, body };
47
+ }
48
+
49
+ /** Creates a typed group from the edition's template, or adds members without duplicating inline registrations or semicolon imports. */
50
+ export function planComposeGroup(input) {
51
+ const { relative, file, exists, marker, body, text: original } = readComposeGroup(input);
52
+ let text = original;
53
+ let compactBody = compositionCode(body).replace(/\s+/g, '');
54
+ for (const { importLine, entry } of input.entries) {
55
+ const lines = text.split('\n');
56
+ if (!lines.some(line => line.replace(/;\s*$/, '') === importLine)) {
57
+ const lastImport = lines.reduce((last, line, index) => line.startsWith('import ') ? index : last, -1);
58
+ lines.splice(lastImport + 1, 0, importLine);
59
+ text = lines.join('\n');
60
+ }
61
+ const compactEntry = entry.replace(/\s+/g, '');
62
+ const members = `,${compactBody},`;
63
+ if (!members.includes(`,${compactEntry},`)) {
64
+ const afterMarker = compositionCode(text).indexOf(marker) + marker.length;
65
+ const empty = text.slice(afterMarker).startsWith(']');
66
+ text = `${text.slice(0, afterMarker)}\n ${entry},${empty ? '\n' : ''}${text.slice(afterMarker)}`;
67
+ compactBody = `${compactEntry},${compactBody}`;
68
+ }
69
+ }
70
+ return { relative, file, exists, text };
71
+ }
72
+
73
+ /** Writes the planned group and reports a newly created composition file. */
74
+ export function writeComposeGroup(input) {
75
+ const { relative, file, exists, text } = planComposeGroup(input);
76
+ fs.mkdirSync(path.dirname(file), { recursive: true });
77
+ fs.writeFileSync(file, text);
78
+ return exists ? [] : [relative];
79
+ }
@@ -0,0 +1,14 @@
1
+ import { ScaffoldError } from './service.mjs';
2
+
3
+ /** Enables the lazy SQL identifier guard's error family when a lite generator installs database.sql.ts. */
4
+ export function withSqlIdentifierError(text) {
5
+ const code = ' IdentifierRejected = "DATABASE_IDENTIFIER_REJECTED",';
6
+ if (text.includes(code)) return text;
7
+ const enumMarker = 'export enum DatabaseErrorCode {';
8
+ const kindsMarker = 'export const DATABASE_ERROR_KINDS: Record<DatabaseErrorCode, ErrorKind> = {';
9
+ if (!text.includes(enumMarker) || !text.includes(kindsMarker)) {
10
+ throw new ScaffoldError('HFS_ADD_DATABASE_ERROR', 'platform/database has an unknown error-family shape');
11
+ }
12
+ return text.replace(enumMarker, enumMarker + '\n /** A dynamic SQL identifier was not on the names its caller allows. */\n' + code)
13
+ .replace(kindsMarker, kindsMarker + '\n [DatabaseErrorCode.IdentifierRejected]: "internal",');
14
+ }
@@ -0,0 +1,122 @@
1
+ import { createRequire } from "node:module";
2
+ import path from "node:path";
3
+ import { ScaffoldError, pascalOf } from "./service.mjs";
4
+ import { feName, readFeContext, readFeFile, requireNewFeOwner, planFeCatalogs, writeFePlan } from "./add-fe-page.mjs";
5
+
6
+ function loadTypeScript(root) {
7
+ try { return createRequire(path.join(root, "package.json"))("typescript"); } catch {
8
+ throw new ScaffoldError("HFS_ADD_TYPESCRIPT_MISSING", "add overlay reads the host page with its installed TypeScript compiler");
9
+ }
10
+ }
11
+
12
+ function pageArrow(ts, source, symbol) {
13
+ for (const statement of source.statements) {
14
+ if (!ts.isVariableStatement(statement) || !statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)) continue;
15
+ const candidate = statement.declarationList.declarations.find((declaration) => declaration.name.getText(source) === symbol);
16
+ if (candidate?.initializer && ts.isArrowFunction(candidate.initializer)) return candidate.initializer;
17
+ }
18
+ return null;
19
+ }
20
+
21
+ function pageRender(ts, arrow) {
22
+ if (!arrow) return null;
23
+ if (!ts.isBlock(arrow.body)) {
24
+ let body = arrow.body;
25
+ while (ts.isParenthesizedExpression(body)) body = body.expression;
26
+ return body;
27
+ }
28
+ const returns = arrow.body.statements.filter(ts.isReturnStatement);
29
+ const unconditional = arrow.body.statements.every((statement) => ts.isVariableStatement(statement) || ts.isExpressionStatement(statement) || ts.isReturnStatement(statement));
30
+ if (!unconditional || returns.length !== 1) return null;
31
+ let render = returns[0].expression;
32
+ while (render && ts.isParenthesizedExpression(render)) render = render.expression;
33
+ return render;
34
+ }
35
+
36
+ function mountOverlay({ ts, before, relative, pageSymbol, symbol }) {
37
+ const source = ts.createSourceFile(relative, before, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
38
+ const client = source.statements.some((statement) => ts.isExpressionStatement(statement) && ts.isStringLiteral(statement.expression) && statement.expression.text === "use client");
39
+ const render = pageRender(ts, pageArrow(ts, source, pageSymbol));
40
+ const isJsx = render && (ts.isJsxElement(render) || ts.isJsxSelfClosingElement(render) || ts.isJsxFragment(render));
41
+ if (source.parseDiagnostics.length || client || !isJsx) {
42
+ throw new ScaffoldError("HFS_ADD_WIRE_INVALID", `${relative} must export the server ${pageSymbol} arrow with one unconditional JSX return`);
43
+ }
44
+ if (before.split(/[^A-Za-z0-9_]/).includes(symbol)) {
45
+ throw new ScaffoldError("HFS_ADD_EXISTS", `${relative} already references ${symbol}`);
46
+ }
47
+ const from = render.getStart(source);
48
+ const wrapped = ["(", " <>", ` ${before.slice(from, render.end)}`, ` <${symbol} />`, " </>", " )"].join("\n");
49
+ const body = before.slice(0, from) + wrapped + before.slice(render.end);
50
+ const imports = source.statements.filter(ts.isImportDeclaration);
51
+ const last = imports.at(-1)?.end ?? 0;
52
+ const importLine = `import { ${symbol} } from "@/features/overlays/${symbol}"`;
53
+ return `${body.slice(0, last)}\n${importLine}${body.slice(last)}`;
54
+ }
55
+
56
+ function overlaySources(symbol, namespace) {
57
+ return {
58
+ entry: `"use client"
59
+
60
+ import { useTranslations } from "next-intl"
61
+ import { ${symbol}Base } from "./component"
62
+
63
+ /** Resolve the overlay's copy at its client interaction boundary. */
64
+ export const ${symbol} = () => {
65
+ const t = useTranslations(${JSON.stringify(namespace)})
66
+ return <${symbol}Base props={{ title: t("title"), open: t("open"), close: t("close"), description: t("description") }} />
67
+ }
68
+ `,
69
+ component: `import { Button, Dialog, Text } from "@starci/grammar/common"
70
+
71
+ /** Settled labels and content for the overlay's accessible dialog. */
72
+ export interface ${symbol}BaseProps {
73
+ readonly props: {
74
+ readonly title: string
75
+ readonly open: string
76
+ readonly close: string
77
+ readonly description: string
78
+ }
79
+ }
80
+
81
+ /** Draw an accessible dialog with an intrinsic trigger and dismissal. */
82
+ export const ${symbol}Base = (props: ${symbol}BaseProps) => (
83
+ <Dialog
84
+ title={props.props.title}
85
+ closeLabel={props.props.close}
86
+ trigger={<Button>{props.props.open}</Button>}
87
+ >
88
+ <Text>{props.props.description}</Text>
89
+ </Dialog>
90
+ )
91
+ `,
92
+ };
93
+ }
94
+
95
+ /** Add a localized client overlay and mount its public entry in a server feature page. */
96
+ export function addFeOverlay({ root, app, name, page, messages, ts }) {
97
+ feName(name, "overlay name");
98
+ if (page === undefined) throw new ScaffoldError("HFS_ADD_OPTION_MISSING", "add overlay needs --page naming its server feature page");
99
+ feName(page, "host page name");
100
+ const context = readFeContext({ root, app });
101
+ const symbol = `${pascalOf(name)}Overlay`;
102
+ const owner = `${context.source}/features/overlays/${symbol}`;
103
+ requireNewFeOwner(context, owner);
104
+ const pageSymbol = `${pascalOf(page)}Page`;
105
+ const relative = `${context.source}/features/pages/${pageSymbol}/index.tsx`;
106
+ const before = readFeFile(context, relative);
107
+ const body = mountOverlay({ ts: ts ?? loadTypeScript(context.root), before, relative, pageSymbol, symbol });
108
+ const catalogs = planFeCatalogs(context, {
109
+ group: "overlays", name, messages,
110
+ defaults: {
111
+ vi: { title: "Th\u00f4ng tin", open: "M\u1edf th\u00f4ng tin", close: "\u0110\u00f3ng", description: "Xem th\u00f4ng tin c\u1ee7a trang n\u00e0y." },
112
+ en: { title: "Information", open: "Open information", close: "Close", description: "View information about this page." },
113
+ },
114
+ });
115
+ const sources = overlaySources(symbol, catalogs.namespace);
116
+ const created = [
117
+ { relative: `${owner}/index.tsx`, body: sources.entry },
118
+ { relative: `${owner}/component.tsx`, body: sources.component },
119
+ ];
120
+ const updates = [...catalogs.updates, { relative, before, body }];
121
+ return { ...writeFePlan(context, created, updates), registered: { app: context.app, overlay: symbol, page: pageSymbol } };
122
+ }