@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/stack/task.mjs ADDED
@@ -0,0 +1,42 @@
1
+ // task.mjs - `starci app stack task <name>`: runs ONE declared one-off command (deploy.tasks.<name>.command) inside the running task of
2
+ // deploy.tasks.<name>.service on the manager, through the ssh runner, and relays its stdout. Which command runs is the app's declaration, never a
3
+ // flag: the only argument is the task name. Without --execute a dry-run line says what would run. Output is relayed as the command prints it; this
4
+ // module never reads or prints environment values.
5
+ import { firstLine, lines as splitLines } from './context.mjs';
6
+ import { failure, refusal } from './errors.mjs';
7
+ import { remoteDocker, withRunner } from './remote.mjs';
8
+
9
+ const TASK_NAME = /^[a-z][a-z0-9-]{0,40}$/u;
10
+
11
+ function undeclared(config, why) {
12
+ const names = Object.keys(config.tasks ?? {});
13
+ const listing = names.length > 0 ? `declared tasks: ${names.join(', ')}` : 'the environment declares no deploy.tasks';
14
+ return refusal('STACK_TASK_UNDECLARED', `${why}; ${listing}`);
15
+ }
16
+
17
+ function runningTask(run, service) {
18
+ const result = remoteDocker(run, ['ps', '-q', '--filter', `label=com.docker.swarm.service.name=${service}`, '--filter', 'status=running']);
19
+ const task = result.status === 0 ? splitLines(result.stdout)[0] : undefined;
20
+ if (task) return task;
21
+ const reason = result.status === 0 ? 'none is running' : firstLine(result.stderr);
22
+ throw failure('STACK_TASK_NO_TASK', `no running task of service ${service} on the manager (${reason})`);
23
+ }
24
+
25
+ /** task: runs the declared one-off command `name`. */
26
+ export async function runTask(ctx, config, { name, args = [], execute = false, sshCmd = null } = {}, deps = {}) {
27
+ if (name === undefined || !TASK_NAME.test(String(name)) || !Object.hasOwn(config.tasks ?? {}, name)) {
28
+ throw undeclared(config, name === undefined ? 'task takes the name of a declared task' : `no task named ${String(name).slice(0, 41)} is declared`);
29
+ }
30
+ if (args.length > 0) throw refusal('STACK_USAGE', 'task takes only the task name; the command is the app\'s declaration');
31
+ const declared = config.tasks[name];
32
+ if (!execute) {
33
+ return { ok: true, lines: [`dry run: would run task ${name} (${declared.command.join(' ')}) in the running ${declared.service} task (add --execute)`], data: { name, status: null } };
34
+ }
35
+ return withRunner(ctx, config, { sshCmd }, deps, (run, notes) => {
36
+ const task = runningTask(run, `${config.stack}_${declared.service}`);
37
+ const result = remoteDocker(run, ['exec', task, ...declared.command]);
38
+ const lines = [...notes, ...splitLines(result.stdout)];
39
+ if (result.status !== 0) lines.push(`FAIL task ${name} exited ${result.status}: ${firstLine(result.stderr)}`);
40
+ return { ok: result.status === 0, lines, data: { name, status: result.status } };
41
+ });
42
+ }
@@ -0,0 +1,99 @@
1
+ // verify.mjs - functional verification after convergence: the declared HTTP journeys (config.verify.http) and the declared checks inside a
2
+ // running task (config.verify.exec). Probes are bounded and read-only; a POST body comes only from the declared config and nothing here
3
+ // mutates business data. HTTP goes through ctx.fetch, docker through ctx.spawn, waits through ctx.sleep and time through ctx.now.
4
+ import { isDeepStrictEqual } from 'node:util';
5
+ import { eachInOrder, repeatInOrder } from '../runtime/scripts/lib/in-order.mjs';
6
+ import { lines as splitLines, tool } from './context.mjs';
7
+ import { edgeAttachmentsOf, verifyEdgeAttachment } from './edge-alias.mjs';
8
+
9
+ const RETRY_MS = 6000;
10
+ const HTTP_TIMEOUT_MS = 8000;
11
+
12
+ const expect = (condition, message) => {
13
+ if (!condition) throw new Error(message);
14
+ };
15
+
16
+ /** Runs one check, retrying every 6 s up to `retrySeconds` when `retry` is set; records { name, ok, detail }. */
17
+ async function runCheck(ctx, results, name, run, { retry = false, retrySeconds = 0 } = {}) {
18
+ const deadline = ctx.now() + (retry ? retrySeconds * 1000 : 0);
19
+ const outcome = await repeatInOrder(async () => {
20
+ try {
21
+ return { ok: true, detail: await run() };
22
+ } catch (error) {
23
+ if (ctx.now() >= deadline) return { ok: false, detail: String(error.message) };
24
+ await ctx.sleep(RETRY_MS);
25
+ return undefined;
26
+ }
27
+ });
28
+ results.push({ name, ...outcome });
29
+ }
30
+
31
+ function checkJson(text, expected) {
32
+ let body;
33
+ try { body = JSON.parse(text); } catch { throw new Error('body is not JSON'); }
34
+ for (const [key, value] of Object.entries(expected)) expect(isDeepStrictEqual(body?.[key], value), `body.${key} is not the declared value`);
35
+ }
36
+
37
+ /** One declared HTTP journey; throws a message when the answer is not the declared one. */
38
+ async function probeHttp(ctx, check) {
39
+ const init = { method: check.method, signal: AbortSignal.timeout(HTTP_TIMEOUT_MS) };
40
+ if (check.body !== null) Object.assign(init, { body: check.body, headers: { 'content-type': 'application/json' } });
41
+ const response = await ctx.fetch(check.url, init);
42
+ expect(response.status === check.status, `HTTP ${response.status}`);
43
+ const text = check.json === null && check.contains === null ? '' : await response.text();
44
+ if (check.json !== null) checkJson(text, check.json);
45
+ if (check.contains !== null) expect(text.includes(check.contains), `no ${check.contains} in the answer`);
46
+ return String(response.status);
47
+ }
48
+
49
+ /** `docker exec` of a declared command in the running container; exit 0 and, when declared, stdout matching the regex. */
50
+ function probeExec(ctx, container, check) {
51
+ const run = tool(ctx, 'docker', ['exec', container, ...check.command]);
52
+ expect(run.status === 0, `exit ${run.status}`);
53
+ if (check.stdout === null) return 'ok';
54
+ expect(new RegExp(check.stdout, 'u').test(run.stdout), `stdout does not match ${check.stdout}`);
55
+ return run.stdout.trim();
56
+ }
57
+
58
+ /** The running-task check of one service, then its declared commands (skipped when no task runs). */
59
+ async function verifyService(ctx, config, results, service) {
60
+ const found = tool(ctx, 'docker', ['ps', '-q', '--filter', `label=com.docker.swarm.service.name=${config.stack}_${service}`, '--filter', 'status=running']);
61
+ const container = splitLines(found.stdout)[0];
62
+ await runCheck(ctx, results, `a ${service} task is running on this manager`, () => {
63
+ expect(container, `no running container of ${config.stack}_${service} on the daemon`);
64
+ return container.slice(0, 12);
65
+ });
66
+ if (!container) return;
67
+ await eachInOrder(config.verify.exec.filter((entry) => entry.service === service), (check) => runCheck(ctx, results, check.name, () => probeExec(ctx, container, check)));
68
+ }
69
+
70
+ /** Every edge-attached service carries exactly its declared aliases (never the implicit short name) on the shared external network. */
71
+ async function verifyEdge(ctx, config, results, file) {
72
+ let attachments;
73
+ try {
74
+ attachments = edgeAttachmentsOf(file);
75
+ } catch (error) {
76
+ results.push({ name: 'edge aliases: the stack file is readable', ok: false, detail: String(error.message) });
77
+ return;
78
+ }
79
+ await eachInOrder(attachments, (item) => runCheck(ctx, results, `edge aliases of ${item.service} on ${item.network} are exactly the declared ones`, () => verifyEdgeAttachment(ctx, config, item)));
80
+ }
81
+
82
+ const verifyExec = (ctx, config, results) => eachInOrder([...new Set(config.verify.exec.map((check) => check.service))], (service) => verifyService(ctx, config, results, service));
83
+
84
+ /**
85
+ * Runs the declared verification. Returns { ok, lines, data: { checks: [{ name, ok }] } }; the lines read `PASS|FAIL name detail`.
86
+ * `skipPublic` skips the HTTP journeys, `skipInternal` the checks inside a task. `stackFile` (default: the stack source) is read for the services
87
+ * attached to an external network: each must carry exactly its declared aliases, never the implicit short name.
88
+ */
89
+ export async function verifyStack(ctx, config, { retrySeconds = 180, skipPublic = false, skipInternal = false, stackFile = config.stackFile } = {}) {
90
+ const results = [];
91
+ if (!skipPublic) await eachInOrder(config.verify.http, (check) => runCheck(ctx, results, check.name, () => probeHttp(ctx, check), { retry: check.retry, retrySeconds }));
92
+ if (!skipInternal && stackFile) await verifyEdge(ctx, config, results, stackFile);
93
+ if (!skipInternal) await verifyExec(ctx, config, results);
94
+ return {
95
+ ok: results.every((entry) => entry.ok),
96
+ lines: results.map((entry) => `${entry.ok ? 'PASS' : 'FAIL'} ${entry.name} ${entry.detail}`),
97
+ data: { checks: results.map(({ name, ok }) => ({ name, ok })) },
98
+ };
99
+ }
@@ -0,0 +1,13 @@
1
+ // One command vocabulary for managed CI rendering and the runtime coverage gate.
2
+ const exampleCommand = (source, command) => source?.runtimeExample
3
+ ? 'node node_modules/@starci/hfs/runtime/scripts/hfs/example-secrets.mjs --run . -- ' + command
4
+ : command;
5
+
6
+ export function ciWorkflowCommands(source, lcovReport = 'be/coverage/lcov.info') {
7
+ return {
8
+ ciLint: exampleCommand(source, 'npm run lint'),
9
+ ciLintSonar: exampleCommand(source, 'npm run lint -- --sonar reports/lint.sonar.json'),
10
+ ciCoverageProof: exampleCommand(source, 'npx starci app check --coverage-from ' + lcovReport),
11
+ ciDatabaseLint: exampleCommand(source, 'npm run db:lint'),
12
+ };
13
+ }
package/sync/index.mjs CHANGED
@@ -4,7 +4,8 @@
4
4
  // kind app, CI, the hooks, the formatter and Sonar configuration and .starciwork; its sides be/ and fe/ hold each side's tool
5
5
  // configuration. The managed set is read from the slot manifest (knowledge/hfs/slots.yaml): every slot that names `managedBy`
6
6
  // lists, in its `path`, the whole files sync renders, and `managedBy` names the template directory they are rendered from:
7
- // templates/<scope or common>/<managedBy>/<file path with each leading dot dropped>, the scope being app for a root slot and the
7
+ // templates/<scope or common>/<managedBy>/<file path with each leading dot dropped>, with dotted skeleton paths tried when no
8
+ // undotted template exists. The scope is app for a root slot and the
8
9
  // side (be, fe) for a side slot, whose files land under that side's folder. Two targets are not whole files of a managedBy slot
9
10
  // and are listed in this module: the marked block of the root .gitignore, and .starciwork/.gitignore, which lives inside the
10
11
  // .starciwork directory slot. Every file is rendered with the app's hfs.json (both sides and their apps) and, for the Sonar
@@ -24,8 +25,12 @@ import { APP_SCOPE, SIDES, litePresenceOf, loadSlotManifest, resolveRepoDeclarat
24
25
  import { managedGroupOf } from '../runtime/scripts/hfs/edition-slots.mjs';
25
26
  import { feAppPackageName } from '../runtime/scripts/hfs/rules/monorepo.mjs';
26
27
  import { DEFAULT_PORT, NODE_IMAGE, dockerfilePath } from '../runtime/scripts/hfs/rules/docker.mjs';
28
+ import { isFile } from '../runtime/scripts/lib/fs-kind.mjs';
27
29
  import { readDeclaredSonarKey } from './sonar-key.mjs';
30
+ import { readDeclaredStack, stackWorkflowVariables } from './stack-images.mjs';
28
31
  import { byCodeUnit } from '../report/order.mjs';
32
+ import { isRuntimeExampleApp } from '../runtime/scripts/lib/runtime-example.mjs';
33
+ import { ciWorkflowCommands } from './ci-commands.mjs';
29
34
 
30
35
  export const TEMPLATES_DIR = path.join(import.meta.dirname, '..', 'templates');
31
36
  const NODE_MAJOR = 22;
@@ -34,7 +39,7 @@ const APP_BINARY = 'starci app';
34
39
  export const SCOPES = Object.freeze([APP_SCOPE, ...SIDES]);
35
40
  export const BLOCK_BEGIN = '# >>> starci app sync (managed block; do not edit) >>>';
36
41
  export const BLOCK_END = '# <<< starci app sync <<<';
37
- const HEADER = scope => `# Generated by ${APP_BINARY} sync (${scope === APP_SCOPE ? 'app root' : `${scope} side`}). Do not edit: run "npx ${APP_BINARY} sync --write".`;
42
+ const HEADER = scope => `# Generated by ${APP_BINARY} sync (${scope === APP_SCOPE ? 'app root' : scope + ' side'}). Do not edit: run "npx ${APP_BINARY} sync --write".`;
38
43
 
39
44
  export class SyncError extends Error {
40
45
  constructor(code, message) {
@@ -47,7 +52,8 @@ export class SyncError extends Error {
47
52
  // package.json. A template that can hold a comment opens with {{header}}; a file whose exact content is specified (JSON, the
48
53
  // eslint one-liner) has no header.
49
54
  const MODE_OF_GROUP = Object.freeze({ 'package-scripts': 'scripts', 'package-scripts-lite': 'scripts' });
50
- const LITERAL_FILE = /^[^*?<>{}[\]]+[^/*?<>{}[\]]$/;
55
+ const GLOB_CHARACTERS = new Set('*?<>{}[]');
56
+ const literalFile = file => file.length >= 2 && !file.endsWith('/') && ![...file].some(character => GLOB_CHARACTERS.has(character));
51
57
 
52
58
  /** The targets no managedBy slot lists as a whole file (see the header of this module); both belong to the app root. */
53
59
  const UNLISTED_TARGETS = Object.freeze([
@@ -58,32 +64,44 @@ const UNLISTED_TARGETS = Object.freeze([
58
64
  const templateName = (scope, group, file) => `${scope}/${group}/${file.split('/').map(segment => segment.replace(/^\./, '')).join('/')}`;
59
65
  const onSide = (scope, file) => (scope === APP_SCOPE ? file : `${scope}/${file}`);
60
66
 
67
+ function managedTemplate(scope, group, file, hasTemplate) {
68
+ const directories = [scope, 'common'];
69
+ const undotted = directories.map(dir => templateName(dir, group, file)).find(name => hasTemplate(name));
70
+ if (undotted || !['skeleton', 'skeleton-lite'].includes(group)) return undotted;
71
+ return directories.map(dir => `${dir}/${group}/${file}`).find(name => hasTemplate(name));
72
+ }
73
+
74
+ function slotTargets(slot, scope, edition, manifest, hasTemplate) {
75
+ if (!slot.profiles.includes(scope) || !slotInEdition(manifest, slot, edition)) return [];
76
+ if (edition === 'lite' && litePresenceOf(slot, scope) === 'forbidden') return [];
77
+ const group = managedGroupOf(slot, edition);
78
+ if (group === undefined) return [];
79
+ const effectivePath = edition === 'lite' ? (slot.lite?.path ?? slot.path) : slot.path;
80
+ return braceVariants(effectivePath).map(file => {
81
+ const managedKey = edition === 'lite' && slot.liteManagedBy !== undefined ? 'liteManagedBy' : 'managedBy';
82
+ if (!literalFile(file)) throw new SyncError('HFS_SYNC_MANIFEST_MANAGED', `slot ${slot.id} names ${managedKey} but its path ${effectivePath} is not a list of literal files (${file})`);
83
+ const template = managedTemplate(scope, group, file, hasTemplate);
84
+ if (!template) throw new SyncError('HFS_SYNC_TEMPLATE_MISSING', `slot ${slot.id} manages ${file} for ${scope}, but templates/{${scope},common}/${group} has no template for it`);
85
+ return { path: file, template, mode: MODE_OF_GROUP[group] ?? 'file', slot: slot.id };
86
+ });
87
+ }
88
+
89
+ function unlistedTargets(scope, edition) {
90
+ if (scope !== APP_SCOPE) return [];
91
+ return UNLISTED_TARGETS.map(target => {
92
+ if (edition !== 'lite') return { ...target };
93
+ return { ...target, template: target.path === '.gitignore' ? 'app/gitignore-lite' : 'app/starciwork.gitignore-lite' };
94
+ });
95
+ }
96
+
61
97
  /**
62
98
  * Every target of `scope` (app, be or fe), its path relative to that scope's folder: the files the manifest's managedBy slots
63
99
  * list, then (app root) the unlisted ones. A slot that names managedBy lists literal files only, and each has a template under
64
100
  * its scope or under common.
65
101
  */
66
102
  export function targetsOf(scope, { edition = 'full', manifest = loadSlotManifest(), hasTemplate = name => fs.existsSync(path.join(TEMPLATES_DIR, name)) } = {}) {
67
- const listed = [];
68
- for (const slot of manifest.slots) {
69
- if (!slot.profiles.includes(scope) || !slotInEdition(manifest, slot, edition)) continue;
70
- if (edition === 'lite' && litePresenceOf(slot, scope) === 'forbidden') continue;
71
- const group = managedGroupOf(slot, edition);
72
- if (group === undefined) continue;
73
- const effectivePath = edition === 'lite' ? (slot.lite?.path ?? slot.path) : slot.path;
74
- for (const file of braceVariants(effectivePath)) {
75
- if (!LITERAL_FILE.test(file)) throw new SyncError('HFS_SYNC_MANIFEST_MANAGED', `slot ${slot.id} names ${edition === 'lite' && slot.liteManagedBy !== undefined ? 'liteManagedBy' : 'managedBy'} but its path ${effectivePath} is not a list of literal files (${file})`);
76
- const template = [scope, 'common'].map(dir => templateName(dir, group, file)).find(hasTemplate);
77
- if (!template) throw new SyncError('HFS_SYNC_TEMPLATE_MISSING', `slot ${slot.id} manages ${file} for ${scope}, but templates/{${scope},common}/${group} has no template for it`);
78
- listed.push({ path: file, template, mode: MODE_OF_GROUP[group] ?? 'file', slot: slot.id });
79
- }
80
- }
81
- const unlisted = scope === APP_SCOPE
82
- ? UNLISTED_TARGETS.map(target => edition === 'lite' && target.path === '.gitignore'
83
- ? { ...target, template: 'app/gitignore-lite' }
84
- : { ...target })
85
- : [];
86
- return [...listed, ...unlisted];
103
+ const listed = manifest.slots.flatMap(slot => slotTargets(slot, scope, edition, manifest, hasTemplate));
104
+ return [...listed, ...unlistedTargets(scope, edition)];
87
105
  }
88
106
 
89
107
  const lf = text => text.replace(/\r\n/g, '\n');
@@ -111,14 +129,20 @@ function readBundled(name) {
111
129
  * so a JavaScript template stays valid source before rendering. An unknown name is an error, never an empty string.
112
130
  */
113
131
  export function render(text, vars, readTemplate = readBundled) {
114
- const withPartials = text.replace(/^(?:\/\/ )?\{\{> ([\w./-]+)\}\}\n/gm, (_, name) => render(readTemplate(name), vars, readTemplate).replace(/\n*$/, '\n'));
132
+ const withPartials = text.replace(/^(?:\/\/ )?\{\{> ([\w./-]+)\}\}\n/gm, (_, name) => trailingNewline(render(readTemplate(name), vars, readTemplate)));
115
133
  return withPartials.replace(/\{\{([A-Za-z]\w*)\}\}/g, (_, key) => {
116
134
  if (!Object.hasOwn(vars, key)) throw new SyncError('HFS_SYNC_TEMPLATE_VARIABLE', `template names {{${key}}}, which sync does not provide`);
117
135
  return vars[key];
118
136
  });
119
137
  }
120
138
 
121
- /** What the app's sync reads from the jest preset it installs for its be side: the Sonar exclusions ({ sonarExclusions }); both editions install it (a lite back end keeps full's unit run, owner ruling 2026-10-12). */
139
+ function trailingNewline(text) {
140
+ let end = text.length;
141
+ while (end > 0 && text[end - 1] === '\n') end -= 1;
142
+ return text.slice(0, end) + '\n';
143
+ }
144
+
145
+ /** What the app's sync reads from the jest preset it installs for its be side: the Sonar exclusions ({ sonarExclusions }); both editions install it (a lite back end keeps full's unit run, ruling:R212/case-1, amended by ruling:R212/case-4). */
122
146
  export async function loadPresets(root) {
123
147
  const name = '@starci/jest-preset';
124
148
  const require = createRequire(path.join(root, 'package.json'));
@@ -139,7 +163,9 @@ const SOURCE_SKIPPED = new Set(['node_modules', 'dist', 'coverage']);
139
163
 
140
164
  /**
141
165
  * The be source of the app at `root` the coverage components read: { files, read }, `files` be-relative posix paths of every `*.ts` file under
142
- * be/apps and be/src (build output and dependencies skipped) and `read` their text. An app with no be folder has no files.
166
+ * be/apps and be/src (build output and dependencies skipped) and `read` their text. `workPresent` records whether the optional
167
+ * lite brand tree exists, so sync can preserve its absence. `hasImage` identifies authored optional Dockerfiles without reading
168
+ * their content. An app with no be folder has no source files.
143
169
  */
144
170
  export function appSource(root) {
145
171
  const files = [];
@@ -152,7 +178,9 @@ export function appSource(root) {
152
178
  }
153
179
  };
154
180
  for (const dir of ['apps', 'src']) walk(dir);
155
- return { files: files.sort(byCodeUnit), read: file => fs.readFileSync(path.join(root, 'be', file), 'utf8') };
181
+ return { runtimeExample: isRuntimeExampleApp(root), files: files.sort(byCodeUnit), read: file => fs.readFileSync(path.join(root, 'be', file), 'utf8'),
182
+ workPresent: fs.existsSync(path.join(root, '.starciwork')), hasImage: file => isFile(path.join(root, file), { followLinks: false }),
183
+ stack: readDeclaredStack(root) };
156
184
  }
157
185
 
158
186
  /**
@@ -183,7 +211,7 @@ export function jestCoverageSource(coverage) {
183
211
  * `docker:build:<app>` builds the image of one app (the exact command its Dockerfile header states, tag `<project>/<app>:dev`) and
184
212
  * `docker:build` builds every image, one after the other, never pushing.
185
213
  */
186
- export function appScripts(app) {
214
+ export function appScripts(app, source) {
187
215
  const line = (name, command) => `${JSON.stringify(name)}: ${JSON.stringify(command)},`;
188
216
  const be = app.sides.be.apps;
189
217
  const fe = app.sides.fe.apps;
@@ -194,7 +222,7 @@ export function appScripts(app) {
194
222
  const workspace = entry => feAppPackageName(app.project, entry.name);
195
223
  // Both editions generate before they run (owner ruling 2026-10-13): the fe's GraphQL wire types, and under lite the Supabase types check.
196
224
  const dev = entry => `npm run codegen --silent && turbo run dev --filter=${workspace(entry)}`;
197
- const images = imageEntries(app);
225
+ const images = imageEntries(app, source);
198
226
  const imageCommand = ({ side, entry }) => `docker build -f ${dockerfilePath(side, entry.name)} -t ${app.project}/${entry.name}:dev .`;
199
227
  return [
200
228
  ...(apis.length === 1 ? [line('dev:be', watch(apis[0]))] : apis.map(entry => line(`dev:be:${entry.name}`, watch(entry)))),
@@ -219,9 +247,10 @@ const opensPackages = side => (side.optionalSlots ?? []).some(id => id === 'repo
219
247
  /** The stylesheets of the fe side the CSS canon judges, relative to fe/: every app and every workspace package (slot fe.route, fe.package.*). */
220
248
  export const STYLE_GLOB = '{apps,packages}/*/src/**/*.css';
221
249
 
222
- /** The images a declaration renders commands and CI entries for. Lite's default deployable is the back-end API image. */
223
- function imageEntries(app) {
224
- return [...app.sides.be.apps.map(entry => ({ side: 'be', entry })), ...(app.edition === 'lite' ? [] : app.sides.fe.apps.map(entry => ({ side: 'fe', entry })))];
250
+ /** Required app images plus lite front ends whose optional Dockerfile the source holds. */
251
+ function imageEntries(app, source) {
252
+ const frontend = app.sides.fe.apps.filter(entry => app.edition !== 'lite' || source?.hasImage?.(dockerfilePath('fe', entry.name)));
253
+ return [...app.sides.be.apps.map(entry => ({ side: 'be', entry })), ...frontend.map(entry => ({ side: 'fe', entry }))];
225
254
  }
226
255
 
227
256
  /**
@@ -229,9 +258,16 @@ function imageEntries(app) {
229
258
  * runs on the release tag, so every image is built there; no path filter narrows it. A YAML block mapping per entry, indented for
230
259
  * `strategy.matrix.include`.
231
260
  */
232
- export function imageMatrix(app) {
261
+ export function imageMatrix(app, source) {
233
262
  const entry = (side, item) => ` - name: ${item.name}\n file: ${dockerfilePath(side, item.name)}`;
234
- return imageEntries(app).map(image => entry(image.side, image.entry)).join('\n');
263
+ return imageEntries(app, source).map(image => entry(image.side, image.entry)).join('\n');
264
+ }
265
+
266
+ /** The Dockerfile of the app a deploy image names (be first, then fe); an app hfs.json does not declare cannot be built by the managed workflow. */
267
+ function stackDockerfile(app, name) {
268
+ const side = ['be', 'fe'].find(which => app.sides[which].apps.some(entry => entry.name === name));
269
+ if (!side) throw new SyncError('HFS_SYNC_STACK_IMAGE', `deploy.images names the app ${name}, which hfs.json does not declare under be or fe`);
270
+ return dockerfilePath(side, name);
235
271
  }
236
272
 
237
273
  const IMAGE_TEMPLATE = Object.freeze({ api: 'be/image/api', worker: 'be/image/worker', cli: 'be/image/cli', next: 'fe/image/next' });
@@ -241,14 +277,14 @@ const IMAGE_TEMPLATE = Object.freeze({ api: 'be/image/api', worker: 'be/image/wo
241
277
  * and then the app's own, judged by the docker rules (R187-R191). A Next image copies the fe packages only when the fe side opts into
242
278
  * them and the be contracts only when the fe side reads them, so each COPY names a folder the app has.
243
279
  */
244
- export function imageFiles(app) {
280
+ export function imageFiles(app, source) {
245
281
  const side = app.sides.fe;
246
282
  const inputs = [...(opensPackages(side) ? ['fe/packages'] : []), ...((side.reads ?? []).includes('be/contracts/') ? ['be/contracts'] : [])];
247
283
  const copyInputs = inputs.map(folder => `COPY ${folder} ${folder}\n`).join('');
248
284
  // The folders outside be/ that the back end declares it reads (the generated Supabase types of a lite back end, which an upgraded app keeps reading) are in every one of its images.
249
285
  const beFolders = (app.sides.be.reads ?? []).map(folder => folder.replace(/\/$/, ''));
250
286
  const copyBeInputs = beFolders.map(folder => `COPY ${folder} ${folder}\n`).join('');
251
- const entries = imageEntries(app).map(({ side: which, entry: item }) => ({ side: which, item }));
287
+ const entries = imageEntries(app, source).map(({ side: which, entry: item }) => ({ side: which, item }));
252
288
  return entries.map(({ side: which, item }) => {
253
289
  const template = IMAGE_TEMPLATE[item.kind];
254
290
  const vars = { project: app.project, app: item.name, nodeImage: NODE_IMAGE, port: DEFAULT_PORT, copyInputs, copyBeInputs };
@@ -258,14 +294,18 @@ export function imageFiles(app) {
258
294
 
259
295
  /** Every value a template of `scope` can name, derived from hfs.json and the presets. */
260
296
  export function variables(app, scope, presets, sonarKey, manifest = loadSlotManifest(), source = { files: [], read: () => '' }) {
297
+ const stack = source.stack ?? { images: [], passEnv: [] };
261
298
  const fe = app.sides.fe;
262
299
  const packages = opensPackages(fe);
263
300
  const lite = app.edition === 'lite';
264
301
  const providers = (app.sides.be.connections ?? []).map(connection => connection.provider);
265
302
  return {
266
303
  header: HEADER(scope),
304
+ project: app.project,
267
305
  appBinary: APP_BINARY,
268
- appScripts: appScripts(app),
306
+ appScripts: appScripts(app, source),
307
+ // A full upgrade keeps the app's declared Supabase connection and its local database commands.
308
+ supabaseScripts: providers.includes('supabase') ? render(readBundled('app/package-scripts/supabase.json'), { appBinary: APP_BINARY }) : '',
269
309
  // turbo builds every fe app workspace; ^build (turbo.json) builds the packages each app imports first, and caches both.
270
310
  buildFe: 'npm run codegen --silent && turbo run build --filter=./fe/apps/*',
271
311
  // The be side with its tsc; every fe workspace with its own typecheck script through turbo (the packages are built first: ^build).
@@ -276,20 +316,23 @@ export function variables(app, scope, presets, sonarKey, manifest = loadSlotMani
276
316
  sonarExclusions: [presets?.sonarExclusions, '**/.next/**', '**/node_modules/**', '**/src/messages/**', ...(lite ? ['supabase/types/**'] : [])].filter(Boolean).join(','),
277
317
  sonarSources: ['be/apps', 'be/src', 'fe/apps', ...(packages ? ['fe/packages'] : [])].join(','),
278
318
  lcovReport: LCOV_REPORT,
279
- // The coverage scope is the same in both editions: lite measures exactly what full measures (owner ruling 2026-10-12).
319
+ ...ciWorkflowCommands(source, LCOV_REPORT),
320
+ // The coverage scope is the same in both editions: lite measures exactly what full measures (ruling:R212/case-1, amended by ruling:R212/case-4).
280
321
  jestCoverage: scope === 'be' ? jestCoverageSource(jestCoverage(manifest, providers)) : '',
281
322
  coverageExclusions: scope === APP_SCOPE ? sonarCoverageExclusions(manifest, providers).join(',') : '',
282
323
  codecovPaths: scope === APP_SCOPE ? codecovPaths(manifest, providers).map(glob => ` - ${JSON.stringify(glob)}`).join('\n') : '',
283
324
  codecovComponents: scope === APP_SCOPE ? componentsYaml(coverageComponents(manifest, { ...source, apps: app.sides.be.apps, providers })) : '',
284
325
  tsconfigPaths: ['be/tsconfig.json', ...fe.apps.map(entry => `fe/apps/${entry.name}/tsconfig.json`), ...(packages ? ['fe/packages/*/tsconfig.json'] : [])].join(','),
285
326
  styleGlob: STYLE_GLOB,
286
- imageMatrix: imageMatrix(app),
327
+ imageMatrix: imageMatrix(app, source),
287
328
  imageProject: app.project,
329
+ ...stackWorkflowVariables(stack, app.project, name => stackDockerfile(app, name)),
288
330
  };
289
331
  }
290
332
 
291
333
  // The scripts of a package.json as one comparable text: a line per script, sorted by name (order is not drift).
292
- const scriptsText = scripts => `${Object.keys(scripts).sort(byCodeUnit).map(name => `${name}: ${typeof scripts[name] === 'string' ? scripts[name] : JSON.stringify(scripts[name])}`).join('\n')}\n`;
334
+ const scriptsText = scripts => Object.keys(scripts).sort(byCodeUnit)
335
+ .map(name => `${name}: ${typeof scripts[name] === 'string' ? scripts[name] : JSON.stringify(scripts[name])}`).join('\n') + '\n';
293
336
 
294
337
  function parseScripts(text) {
295
338
  try {
@@ -310,7 +353,9 @@ export function renderTargets(hfs, presets, { sonarKey, readTemplate = readBundl
310
353
  const hasTemplate = name => { try { return readTemplate(name) !== undefined; } catch { return false; } };
311
354
  return SCOPES.flatMap(scope => {
312
355
  const vars = variables(app, scope, presets, sonarKey, manifest, source);
313
- return targetsOf(scope, { edition: app.edition, manifest, hasTemplate }).map(target => {
356
+ const targets = targetsOf(scope, { edition: app.edition, manifest, hasTemplate })
357
+ .filter(target => !(app.edition === 'lite' && source?.workPresent === false && target.path === '.starciwork/.gitignore'));
358
+ return targets.map(target => {
314
359
  const body = render(readTemplate(target.template), vars, readTemplate);
315
360
  const where = onSide(scope, target.path);
316
361
  if (target.mode === 'scripts') {
@@ -319,7 +364,7 @@ export function renderTargets(hfs, presets, { sonarKey, readTemplate = readBundl
319
364
  const content = scriptsText(scripts);
320
365
  return { path: where, scope, mode: 'scripts', content, hash: hashOf(content), scripts };
321
366
  }
322
- const managed = target.mode === 'block' ? `${BLOCK_BEGIN}\n${body.replace(/\n*$/, '\n')}${BLOCK_END}\n` : body;
367
+ const managed = target.mode === 'block' ? `${BLOCK_BEGIN}\n${trailingNewline(body)}${BLOCK_END}\n` : body;
323
368
  return { path: where, scope, mode: target.mode, content: managed, hash: hashOf(managed) };
324
369
  });
325
370
  });
@@ -361,6 +406,21 @@ export function checkTargets(root, targets) {
361
406
  }
362
407
 
363
408
  /** Rewrite the missing or drifted targets; returns the paths written. */
409
+ function writtenContent(target, existing) {
410
+ if (target.mode === 'scripts') {
411
+ let pkg = {};
412
+ if (existing) {
413
+ try { pkg = JSON.parse(existing); } catch { throw new SyncError('HFS_SYNC_HFS_INVALID', `${target.path} is not valid JSON; fix it by hand before sync can set its scripts`); }
414
+ }
415
+ return `${JSON.stringify({ ...pkg, scripts: target.scripts }, null, 2)}\n`;
416
+ }
417
+ if (target.mode === 'block' && existing) {
418
+ const current = managedPart('block', existing);
419
+ return current === null ? `${target.content}\n${existing}` : existing.replace(current, () => target.content);
420
+ }
421
+ return target.content;
422
+ }
423
+
364
424
  export function writeTargets(root, targets) {
365
425
  const written = [];
366
426
  for (const result of checkTargets(root, targets)) {
@@ -368,17 +428,7 @@ export function writeTargets(root, targets) {
368
428
  const target = targets.find(candidate => candidate.path === result.path);
369
429
  const file = path.join(root, target.path);
370
430
  const existing = fs.existsSync(file) ? lf(fs.readFileSync(file, 'utf8')) : '';
371
- let next = target.content;
372
- if (target.mode === 'scripts') {
373
- let pkg = {};
374
- if (existing) {
375
- try { pkg = JSON.parse(existing); } catch { throw new SyncError('HFS_SYNC_HFS_INVALID', `${target.path} is not valid JSON; fix it by hand before sync can set its scripts`); }
376
- }
377
- next = `${JSON.stringify({ ...pkg, scripts: target.scripts }, null, 2)}\n`;
378
- } else if (target.mode === 'block' && existing) {
379
- const current = managedPart('block', existing);
380
- next = current === null ? `${target.content}\n${existing}` : existing.replace(current, () => target.content);
381
- }
431
+ const next = writtenContent(target, existing);
382
432
  fs.mkdirSync(path.dirname(file), { recursive: true });
383
433
  fs.writeFileSync(file, next);
384
434
  written.push(target.path);
@@ -404,19 +454,40 @@ export function loadHfs(root, manifest = loadSlotManifest()) {
404
454
  export async function renderRepo(root, { presets, parseYaml, manifest = loadSlotManifest() } = {}) {
405
455
  const hfs = loadHfs(root, manifest);
406
456
  const sonarKey = await readDeclaredSonarKey(root, { parseYaml, fail: message => { throw new SyncError('HFS_SYNC_SONAR_KEY', message); } });
407
- return { hfs, targets: renderTargets(hfs, presets ?? await loadPresets(root), { sonarKey, manifest, source: appSource(root) }) };
457
+ return { hfs, targets: renderTargets(hfs, presets ?? await loadPresets(root), { sonarKey, manifest, source: { ...appSource(root), stack: readDeclaredStack(root, { parseYaml }) } }) };
408
458
  }
409
459
 
410
460
  /** `starci app sync --check | --write [--cwd <dir>]` at the app root; returns 0 clean, 1 drift, 2 usage. */
411
- export async function runSync(argv, { cwd = process.cwd(), out = line => process.stdout.write(`${line}\n`), presets, parseYaml } = {}) {
461
+ function syncOptions(argv) {
412
462
  const check = argv.includes('--check'), write = argv.includes('--write');
413
463
  const cwdAt = argv.indexOf('--cwd');
414
464
  const known = new Set(['--check', '--write', '--cwd']);
415
465
  const unknown = argv.find((arg, index) => !known.has(arg) && index !== cwdAt + 1);
416
466
  if ([check, write].filter(Boolean).length !== 1 || unknown !== undefined || (cwdAt >= 0 && argv[cwdAt + 1] === undefined)) {
467
+ return null;
468
+ }
469
+ return { write, cwdAt };
470
+ }
471
+
472
+ function reportSyncCheck(root, targets, out) {
473
+ const results = checkTargets(root, targets);
474
+ const bad = results.filter(item => item.status !== 'ok');
475
+ for (const result of bad) {
476
+ const found = result.actualHash ? `, found ${result.actualHash.slice(0, 12)}` : '';
477
+ const where = result.difference ? ` (line ${result.difference.line}: expected ${JSON.stringify(result.difference.expected)}, found ${JSON.stringify(result.difference.actual)})` : '';
478
+ out(`HFS_SYNC_DRIFT ${result.path}: ${result.status}, expected sha256 ${result.expectedHash.slice(0, 12)}${found}${where}`);
479
+ }
480
+ out(`starci app sync --check: ${results.length - bad.length} of ${results.length} in sync${bad.length ? '; run "npx starci app sync --write"' : ''}`);
481
+ return bad.length ? 1 : 0;
482
+ }
483
+
484
+ export async function runSync(argv, { cwd = process.cwd(), out = line => process.stdout.write(`${line}\n`), presets, parseYaml } = {}) {
485
+ const options = syncOptions(argv);
486
+ if (!options) {
417
487
  out('usage: starci app sync (--check | --write) [--cwd <dir>]');
418
488
  return 2;
419
489
  }
490
+ const { write, cwdAt } = options;
420
491
  const root = cwdAt >= 0 ? path.resolve(cwd, argv[cwdAt + 1]) : cwd;
421
492
  try {
422
493
  const { targets } = await renderRepo(root, { presets, parseYaml });
@@ -426,15 +497,7 @@ export async function runSync(argv, { cwd = process.cwd(), out = line => process
426
497
  out(`starci app sync: ${written.length} written, ${targets.length - written.length} already in sync`);
427
498
  return 0;
428
499
  }
429
- const results = checkTargets(root, targets);
430
- const bad = results.filter(item => item.status !== 'ok');
431
- for (const result of bad) {
432
- const found = result.actualHash ? `, found ${result.actualHash.slice(0, 12)}` : '';
433
- const where = result.difference ? ` (line ${result.difference.line}: expected ${JSON.stringify(result.difference.expected)}, found ${JSON.stringify(result.difference.actual)})` : '';
434
- out(`HFS_SYNC_DRIFT ${result.path}: ${result.status}, expected sha256 ${result.expectedHash.slice(0, 12)}${found}${where}`);
435
- }
436
- out(`starci app sync --check: ${results.length - bad.length} of ${results.length} in sync${bad.length ? '; run "npx starci app sync --write"' : ''}`);
437
- return bad.length ? 1 : 0;
500
+ return reportSyncCheck(root, targets, out);
438
501
  } catch (error) {
439
502
  if (!(error instanceof SyncError)) throw error;
440
503
  out(error.message);